《软件调试分析技术》学习笔记(十)

本文通过学习《软件调试分析技术》,探讨了程序设计语言中的函数概念,将其与数学函数进行对比,并详细解释了在汇编语言中如何使用call和retn指令进行函数调用和返回。以C语言代码为例,展示了编译后的汇编代码,并利用调试工具分析了函数调用过程中的栈操作,揭示了函数返回地址的保存和恢复机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

函数调用 

    许多程序设计语言中,可以将一段经常需要使用的代码封装起来,在需要使用时可以直接调用,这就是程序中的函数(也被称为过程)。其实程序中的函数和数学中所说的函数是很相似的,都是通过输入自变量(有些函数可能不需要自变量),然后经过一系列的运算,最后得出函数的值。 

    在汇编语言中对于函数的调用一般用一个call指令来完成,当过程返回时用retn指令来完成。call指令有一个操作数,指向被调用过程的地址,当程序需要调用一个过程的时候,程序会跳转到被调用过程的地址处去执行代码。当过程代码执行完毕的时候,程序需要返回到原调用地址处,这个时候就需要使用到retn指令。retn指令是没有操作数的,那么程序怎么知道函数要返回的原调用处的地址?显然,在调用一个过程的时候应该保存返回的原调用处的地址,具体怎么操作,先看一段C语言代码: 

#include <stdio.h> 
void function() 
{ 
  return; 
} 
int main() 
{ 
  function(); 
  return 0; 
}

这里定义了一个空函数function(),然后在主函数里调用了这个空函数function(),这里需要修
改一下编译器的选项,在对语言的优化处理中把对内联函数展开的选项修改为“只使用
__inline”,如图2.3.1.1,

否则编译器会把代码比较少的函数当作inline函数处理。然后进行编译,编译完成后使用IDA进行分析,主函数反汇编代码如下:

text:00401010    push    ebp 
.text:00401011    mov     ebp, esp 
.text:00401013    call    ?function@@YAXXZ ; function(void) 
.text:00401018    xor     eax, eax 
.text:0040101A    pop     ebp 
.text:0040101B    retn 

这里可以看到0x00401013处使用了一个call指令调用调用过程,call指令的操作数说明了被调用过程,IDA已经分析出这里被调用的过程为function(void)。再看看函数function(void)的代码:

.text:00401000 ; Attributes: bp-based frame 
.text:00401000 
.text:00401000 ; void __cdecl function() 
.text:00401000 ?function@@YAXXZ proc near  ; CODE XREF: _main+3 p 
.text:00401000    push    ebp 
.text:00401001    mov     ebp, esp 
.text:00401003    pop     ebp 
.text:00401004    retn 
.text:00401004 ?function@@YAXXZ endp 
.text:00401004

这里是IDA对function(void)的整个分析结果。在C语言中定义的函数function()是一个空函数,什么都没有做就直接返回了,但是在汇编语言中这里除了retn指令还有另外的三句。这三句代码的作用是保护栈桢。ebp是栈基址寄存器,它通常用来对局部变量进行寻址,那么在调用一个过程后,要使用过程内的全局变量,就要重新定义ebp寄存器的值,一般用这两句代码来完成:

push    ebp 
mov     ebp, esp 

这两句指令,把原ebp的值压入栈中,然后把ebp的值修改为当前栈顶。一般把这两句指令作为函数开头。那么函数的结尾就是这样的:

pop     ebp 
retn 

一般把这两句指令作为函数的结尾,从栈中弹出原来保存的ebp寄存器的值,恢复ebp后调用retn指令返回原调用处。 
    用OD加载这个程序,调试看看:

00851010  /$  55            push    ebp 
00851011  |.  8BEC          mov     ebp, esp

按F8步过这两句代码,然后注意OD的寄存器窗口,如图2.3.1.2,可以看到这时ebp寄存哭已经被赋于esp寄存器的值,这时它们都指向0x0029F910。



00851013  |.  E8 E8FFFFFF   call    00851000


 这里使用call指令调用函数function(),OD就没有IDA那么智能了,这里没有分析出被调用过程的名称。按F7跟进这个过程: 

00851000  /$  55            push    ebp 
00851001  |.  8BEC          mov     ebp, esp 
00851003  |.  5D            pop     ebp 
00851004  \.  C3            retn 

跟进这个过程后不要执行任何代码,先看看当前的状态。寄存器窗口如图2.3.1.3,很明显这里栈顶esp所指向的地址比刚
才低了4个字节,值为0x0029F90C,那么说明栈里被写入了4字节的数据,看看现在栈里的0x0029F90C处的数据,如图
2.3.1.4。0x0029F90C里的数据为0x00851018,这到底是什
么东西,先待定,按F8步过函数function()中的代码。 
00851004  \.  C3            retn 
    执行到这里的时候停住,再看看这时候寄存器的状态,如图2.3.1.5,

这时eip寄存器指向00851004,也就是retn指令的地址,ebp的值为0x0029F910,esp的值为0x0029F90C。看栈里的数据: 


0029F90C      00851018    返回到 acm.00851018  来自 acm.00851000 


 0x0029F90C的值既然为0x00851018。再按一次F8步过


retn指令,再看寄存器信息,如图2.3.1.6,现在eip寄存器指向0x00851018,而esp寄存器的值变回了0x0029F910。栈顶esp抬高了四个字节,原来栈顶0x0029F90C指向的数据0x00851018被弹出,变成了现在eip寄存器的值。看这时的代码:

00851013  |.  E8 E8FFFFFF   call    00851000 
00851018  |.  33C0          xor     eax, eax 
0085101A  |.  5D            pop     ebp 
0085101B  \.  C3            retn

0x00851018为原调用处的下一句指令的地址。经过这样的分析,过程的调用和返回过程就很明显了,当执行call指令时会把call指令的下一句指令的地址压入栈中当作返回址,然后再执行过程的代码,过程代码执行结束以后使用retn指令返回,当执行retn指令时弹出保存在栈中的原调用指令的下一句指令的地址,把它放到eip中使程序转而执行原调用指令的下一句指令。 
    再来看看使用指针调用函数的方法:

 

#include <stdio.h> 
void function() 
{ 
  return; 
} 
int main() 
{ 
  void (*fn)(); 
  fn = function; 
  fn(); 
  return 0; 
}

在这里function()不变,在主函数main()中定义了一个函数指针fn,把function()函数的地址传给函数指针fn,然后通过函数指针fn调用函数function(),使用IDA分析:

.text:00401010    push    ebp 
.text:00401011    mov     ebp, esp 
.text:00401013    push    ecx

指针变量里装的是一个32位的地址,它用占用的内存空间为4字节,因此这里向栈中压入一个32位寄存器ecx为指针变量fn开辟空间。 

.text:00401014    mov     [ebp+fn], offset ?function@@YAXXZ ; function(void) 
.text:0040101B    call    [ebp+fn] 

这里获取函数function(void)的地址放到函数指针fn里,然后通过函数指针fn调用函数。这种调用叫做间接调用,通过动态计算函数地址,然后调用。这种调用方式很像C++中的虚函数,关于虚函数的内容,会在后面的章节中讲到。 

.text:0040101E    xor     eax, eax 
.text:00401020    mov     esp, ebp 
.text:00401022    pop     ebp 
.text:00401023    retn

这种间接调用的方式要比前面看到的直接调用方式麻烦很多。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值