`
liyiwen007
  • 浏览: 107711 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

函数调用栈一例

阅读更多

  前几天和柯柯交流一个小问题,说是如何在一个函数内得到调用该函数的函数地址。有点拗口,就是说如果有一个函数A(当然我们在这个问题中并不知道它是哪个函数)调用了B函数,现在希望用个什么办法得到A函数的地址。  

  我首先联想到的是,一般调试器都能给出嵌套的函数调用关系。那么肯定是有什么办法解决这个问题。上网查了一通之后只找到一些debug用的API和一些开发环境提供的调整宏等等,感觉不是很适用。后来想想,函数调用都涉及到“函数调用栈”(call stack),也许这里可以得到些什么信息。隐约回想起以前汇编课里老师讲过的一些函数调用时要“压栈”、“要保存现场”等,但已经记得不太清楚了,于是就又上网找了些函数调用栈的知识,发现了一些有意思的信息(上网时看到ChinaUnix上的一篇,也是转的,原地址和作者不详,如果你知道请告诉我):

  1.一个函数调用动作可分解为:零到多个PUSH指令(用于参数入栈),一个CALL指令。CALL指令内部其实还暗含了一个将返回地址(即CALL指令下一条指令的地址)压栈的动作。

    2.几乎任何本地编译器都会在每个函数体之前插入类似如下指令:PUSH EBP; MOV EBP ESP;即,在程式执行到一个函数的真正函数体时,已有以下数据顺序入栈:参数,返回地址,EBP。 

  这里我最关心的是:函数调用时,会在栈里压入返回地址,和EBP。

  因为函数调用的返回地址,正是调用指令Call的下一个指令的地址,那么,有了返回地址,就可以得到Call指令的位置了。有Call指令的位置又能干什么呢?幸好汇编课里的知识还记得一点点:Call指令就是一个跳转指令,它可以让IP(instruction point[Thanks to RednaxelaFX])指向要跳转的指令的地址,从那里开始执行。对于函数调用来说,就是让IP指向被调用的函数的地址。Call指令的操作数其实和被调用函数的地址有非常重要的关系。有了Call指令的操作数,就可以计算出被调用函数的地址。

  但仅仅有这个还不够,比如,A调用了B,那么在A函数中肯定有一个Call指令,但这个Call指令中的操作数是和B函数地址相关的,与A的函数地址直接关系不大(至少在没有其它信息的情况下,不能计算出A的地址)。而我们要得到的却是A函数的地址。所以,得向上再找一层,找到调用A函数的地方,那个地方的Call指令里的操作数才和A函数地址有关。也就是说,Z函数调用了A函数,A函数调用了B函数。现在要得到A函数的地址,我们得在Z函数里找Call指令的操作数。这时候EBP就派上用场了。本地编译器在每个函数体之前插入的指令(PUSH EBP; MOV EBP ESP)构造了一个巧妙的结构,使得我们可以顺着函数调用栈一层一层向上,找到所有调用关系。

  如何向上查找呢?我们看看函数调用时栈、EBP的值的情况就知道了。

  假设现在函数在正Z函数内执行,那么此时栈和EBP的值可能是像下图这样的:


 

  我们先不管现在EBP指向的内存(0x000f)中的内容XXX是什么(要不然会是鸡生蛋生鸡的问题),总之目前在栈中的着色块中的内容是属于函数Z的参数,Z执行结束后应该返回的地址以及Z函数的局部变量值。

  现在Z函数调用A函数,会先将传给A的参数压栈,然后将现在这个指令(就是"Call A"啦)的下一个指令的地址压入栈中,以便A函数完后返回到Z中继续执行。然后进入A函数的内存空间,首先就是调用PUSH EBP,也就是将Z的EPB的内容(地址0x000f)压入栈中,然后再MOV EBP ESP,让EBP有一个新的栈顶(此时栈顶中的内容不就是Z函数时EBP的内容么?),然后再将A函数的局部变量压入栈中,开始执行A函数的代码。这时,栈和EBP的情况就像如图所示了: 

   哈,这样就很清楚了,原来现在的EBP中的内容,正是上一级函数的EBP中的内容。而每一个函数的EBP指向的位置,向栈顶可以得到该函数的局部变量,向栈底可以得到函数的返回地址和参数。于是我们就可以根据这个结构层层向上,找到任何一层我们想找的函数EBP,从而也就能得到相应的返回地址了。  

  好,从B函数中得到Z函数对A函数调用点的返回地址的问题也就解决了。现在就是处理Call指令的问题了。

  我在Visual Studio 2003的Debug版中进行反汇编调试,发现Call指令对应的机器指令都是5个byte,第一个byte(E8)是指令的器码,猜想后面4个byte应该就是它的转移的目标地址了。结果按这个地址去找,发现根本不对,想想汇编也忘得差不多了,于是又去找了教程看看,才记起原来Call的操作数并不是绝对地址,而是偏移地址(跳转目标地址-Call指令地址-sizeof(Call指令)),这样就好办了,我有返回地址,于是就有了向上5个byte就是Call的地址,再从这个地址中取出Call指令机器码的后四个字节,加上返回地址,就得到了目标地址。

  原以为已经搞定了。不过还有一个小插曲,就是在VS的Debug版中,Call并不直接跳到一函数中去(不知道为什么),而是跳到一块代码区,这块区域内排布了很多的Jmp指令用于各种跳转(不知道为什么这么搞,也许是为调试的功能而设计的吧,谁知道?还请不吝赐教),不过没关系,也就是多走一点路而已,Jmp指令的操作数和Call指令的意义是一样的,最终Jmp是跳到函数代码块中去的。于是也就得到了想要的结果。

  

  下面是代码:

 

#include "stdafx.h"

#include <string>

unsigned int GetCallerAddress(void)
{
	unsigned int _ebp;
	__asm mov _ebp, ebp

	for (int i=2; i != 0; --i) {
		_ebp = *(unsigned int *)(_ebp);
	}
	unsigned int* ipAddress = (unsigned int*)(*(unsigned int *)(_ebp + 4));

	ipAddress = (unsigned int*)((unsigned char *)ipAddress - 5);
	unsigned int callInstructAddress = (unsigned int)ipAddress;
	ipAddress = (unsigned int*)((unsigned char *)ipAddress + 1);
	int funcAddrOffset = *ipAddress;
	unsigned int *jumAddr = (unsigned int*)(callInstructAddress + funcAddrOffset + 5); 
	callInstructAddress = (unsigned int)jumAddr;
	jumAddr = (unsigned int*)((unsigned char *)jumAddr + 1);
	funcAddrOffset = *jumAddr;
	
	return funcAddrOffset + callInstructAddress + 5;    
}

void fun1();

void fun2()
{
	fun1();
}

void fun3()
{
	fun1();
}


void fun1()
{
	unsigned int _ebp;
	__asm mov _ebp, ebp // 取当前EBP
	unsigned int _preEbp = *(unsigned int *)(_ebp);   //得到上层函数的EBP
	unsigned int* ipAddress = (unsigned int*)(*(unsigned int *)(_preEbp + 4)); // 取得返回地址
	ipAddress = (unsigned int*)((unsigned char *)ipAddress - 5); // 得到Call指令地址  
	unsigned int callInstructAddress = (unsigned int)ipAddress;	 // 保存Call指令地址  
	ipAddress = (unsigned int*)((unsigned char *)ipAddress + 1); 
	int funcAddrOffset = *ipAddress; // 得到Call指令操作数
	unsigned int *jumAddr = (unsigned int*)(callInstructAddress + funcAddrOffset + 5); // 找到Jmp指令
	callInstructAddress = (unsigned int)jumAddr; // 保存jmp指令地址
	jumAddr = (unsigned int*)((unsigned char *)jumAddr + 1); 
	funcAddrOffset = *jumAddr; // 得到jmp指令操作数
	unsigned int addr = funcAddrOffset + callInstructAddress + 5; //得到函数地址

	// 或者:unsigned int addr = GetCallerAddress();
	printf("fun1 said : Caller Addres is 0x%08x\n", addr);
}

int _tmain(int argc, _TCHAR* argv[])
{
	fun1();
	fun2();
	fun3();

	return 0;
}

 

  PS:后经柯柯验证,只有VC6、2003、2008的Debug版里才有效。Release版中不行,具体原因未细查(没时间,毕竟不是"正务",呵呵)。以后再遇到时再细究吧。至少,现在对函数调用栈有了一些新的认识。很开心,呵呵呵。 

 

 

后记:

  这两天翻看《Windows95编程大奥秘》(候捷译)中,作者在分析PE格式的时候提到了,Call指令并不直接将程序控制转到目标函数,而是转入一个Jmp的代码块中,由Jmp来最终将控制权交给函数。为什么这么做呢?作者给出的结论是这样做可以使得载入器的行为变得简单。因为Jmp的操作数是存放在idata区的一个“变量”,载入器只需要将被调用的DLL的地址一次写入这个“变量”中就可以了。如果不这么做,那么需要在每个Call指令中的位置对函数地址进行Fixup,这样会有更多的工作量。

  OK,你不要笑话我说还看Win95的书哦。是的,我承认我不知道上面这段话中内容在现在的XP或是Vista或是2000中是否依然有效(因为我没有去验证过),但我看到了解决的方向。另外,这本书真的像候捷先生所说,“仍然极具技术价值”。我很认同!

  鉴于RednaxelaFX的提示和本书给的信息,我下一步将偿试从PE文件来找这个问题的解决之道,并顺带学习一下PE格式。读完《Win95》后,也可能会写篇读后感,敬请留意,哈哈。

  

  • 大小: 30.5 KB
  • 大小: 21.4 KB
分享到:
评论
2 楼 liyiwen007 2009-03-09  
RednaxelaFX 写道

IP是instruction pointer。代码分析器一般的做法似乎是先对整个可执行文件做扫描,记录下所有操作数为常量call指令的信息,以便后续分析;可以将call的目标记录为entry point,配合ret的分析来判断函数块的范围。call的操作数如果是指针的话,分析起来就会变得困难许多。对Win32 PE文件,RVA到实际地址的转换,也可以通过分析PE文件头来了解。Win32 EXE一般是从0x00400000开始装载的。说来,如果可执行文件被做过花指令处理的话,有可能会见到用push+ret的组合来做jmp。

哦~~ Thank you for "instruction pointer"!
其实这方面唐某很外行~偶有所得就禁不住写了,
原来想过分析PE文件头,不过实在是太不熟了,感觉要用很多时间,猜想调用栈可以走近路,呵呵,多谢指点哪~
1 楼 RednaxelaFX 2009-03-08  
IP是instruction pointer。
代码分析器一般的做法似乎是先对整个可执行文件做扫描,记录下所有操作数为常量call指令的信息,以便后续分析;可以将call的目标记录为entry point,配合ret的分析来判断函数块的范围。call的操作数如果是指针的话,分析起来就会变得困难许多。
对Win32 PE文件,RVA到实际地址的转换,也可以通过分析PE文件头来了解。Win32 EXE一般是从0x00400000开始装载的。

说来,如果可执行文件被做过花指令处理的话,有可能会见到用push+ret的组合来做jmp。

相关推荐

    class3-函数与栈

    在本课程“class3-函数与栈”中,我们将深入探讨函数的声明、汇编语言中的函数调用以及栈在其中的作用。这门逆向基础课程旨在帮助学习者理解底层计算机工作原理,尤其是当涉及到函数调用时的内存管理和控制流程。 ...

    C++箴言:避免析构函数调用虚函数

    ### C++箴言:避免析构函数调用虚函数 #### 概述 在C++编程中,理解和遵循良好的设计模式对于确保程序的稳定性和可维护性至关重要。其中一个经常被提及的原则是“避免在析构函数中调用虚函数”。这一原则在C++语言...

    cdecl函数调用,了解printf这样的函数调用,对比stdcall会更清楚.zip

    在编程世界中,函数调用约定(Calling Convention)是至关重要的一个概念,它定义了函数参数如何被压入栈中以及谁负责清理栈。在C和C++编程中,有几种常见的函数调用约定,其中两种最为流行的是cdecl和stdcall。在本...

    MySQL数据库:存储函数调用.pptx

    【例】 创建一个存储函数,返回Book表中某本书的作者姓名。 存储函数举例 DELIMITER $$ CREATE FUNCTION author_book(b_name CHAR(20)) RETURNS CHAR(8) BEGIN RETURN (SELECT 作者 FROM Book WHERE 书名= b_name); ...

    微信小程序Page中data数据操作和函数调用方法

    在微信小程序的开发过程中,Page()函数起到了注册页面的作用,它接受一个对象作为参数,用于指定页面的初始数据、生命周期函数、事件处理函数等,而页面的数据操作和函数调用是小程序开发中非常重要的部分。...

    (杨辉三角)(C语言)(函数调用)

    ### 杨辉三角的实现与函数调用在C语言中...通过以上分析,我们可以看到,虽然给定的代码实现并不是最优的,但它提供了一个很好的学习函数调用和组合数学的机会。对于初学者来说,理解和实践这样的代码是非常有价值的。

    C/C++语言程序中函数调用解决办法

    函数调用是指在一个程序中调用另一个函数的过程。它可以简化程序结构,提高代码的重用性和可读性。根据调用方式的不同,函数调用可以分为: - **函数作为语句调用**:此时被调用的函数通常不返回任何值,其类型通常...

    关于函数的调用约定的一些知识

    在函数调用过程中,有两个核心问题需要解决:一是参数如何被压入栈中,二是栈应该如何被清理以保持函数调用前后的状态一致性。不同的函数调用约定提供了不同的解决方案。 ##### 3. 常见的函数调用约定 常见的函数...

    db2调自定义函数(小例)

    本文将深入探讨如何在DB2中创建和调用自定义函数,通过具体的示例代码,展示这一过程的关键步骤。 ### 创建自定义函数:理论与实践 在DB2中创建自定义函数涉及以下几个关键步骤: 1. **编写源代码**:首先,你...

    C语言函数调用规定[文].pdf

    在C语言中,函数调用是一项基础且至关重要的概念,涉及到程序执行流程和参数传递机制。函数调用规定是确保程序正确运行的关键因素之一。在本文中,我们将深入探讨两种常见的函数调用约定:stdcall和cdecl。 首先,...

    JavaScript函数调用堆栈loader

    在本例中,"JavaScript函数调用堆栈loader"是一个特定的Webpack loader,它的主要功能是捕获JavaScript函数调用的堆栈信息,并将其转化为字符串存储在`window.dxj`全局变量中。这样做的好处在于,开发人员可以在...

    函数的定义和调用

    接下来,我们讨论**函数调用**。在程序的其他地方,你可以通过函数名和传递实际参数来调用已定义的函数。调用格式如下: ```cpp 返回值 变量名 = 函数名(实际参数, 实际参数, ...); ``` 实际参数的值会赋给形参,...

    修改函数调用返回地址,改变程序执行流程

    在C语言中,当一个函数调用结束后,程序会跳转到存储在栈上的返回地址继续执行。这个地址通常位于调用函数的栈帧中,是函数返回后的下一条指令的内存地址。攻击者可以通过溢出输入数据填充栈空间,覆盖这个返回地址...

    3.7 函数的递归调用(ppt).pdf

    递归调用在解决某些问题时非常有效,但需要注意的是,由于每次递归调用都会增加函数调用栈的深度,因此可能导致栈溢出或性能下降。在编写递归函数时,应确保递归深度可控,且有明确的基线条件以防止无限递归。在...

    钩子函数调用实例

    在这个"钩子函数调用实例"中,我们将深入探讨如何利用钩子函数来实现一个功能:在用户修改屏幕分辨率之前保存并恢复桌面图标的布局。 首先,我们需要理解Windows消息机制。Windows操作系统使用消息队列来传递事件...

    C# Csharp 调用 C++的DLL中的回调函数

    在本例中,C++ DLL可能定义了一个接受回调函数作为参数的接口,这样在DLL内部执行到特定逻辑时,就可以通过这个回调函数通知C#代码。 接下来,让我们转向C#(Csharp)部分。C#不能直接调用C++的函数,但可以通过...

    C语言函数调用参数压栈的相关问题

    函数调用时,参数入栈的顺序是一个经常被问到的问题。很多人认为参数入栈的顺序是从右向左,但是这只是部分正确的。事实上,入栈的顺序是与体系架构相关的。 在32位Ubuntu系统中,参数入栈的顺序是从右向左的,而在...

    函数调用约定

    本文将深入探讨几种常见的函数调用约定,包括`stdcall`、`cdecl`、`fastcall`、`thiscall`和`nakedcall`,并以C/C++为例进行具体分析。 ### 1. `stdcall` 调用约定 `stdcall`调用约定通常被称为Pascal调用约定,这...

    c调用C++函数

    本文将深入探讨如何实现C调用C++函数,并以QT框架为例,提供一种实现方法。 首先,了解C与C++的差异是必要的。C++是C语言的超集,它扩展了C语言的功能,引入了类、对象、模板等面向对象特性。由于C++支持名称空间和...

    linux启动的函数调用关系

    本文将着重分析从`start_kernel()`函数开始直到`rest_init()`函数结束这一过程中的关键函数调用及其作用。 #### 一、`start_kernel()`函数 `start_kernel()`函数是Linux内核启动过程中的入口点之一,主要负责进行...

Global site tag (gtag.js) - Google Analytics