函数调用的汇编语言详解

一、基本概念

想要彻底的理解函数调用过程,先要明白一下几个概念。

1、栈

这里说的栈不是数据结构中的栈,而是计算机内存中的一块存储区,它的访问方式是“先进后出”。大多数情况下,栈是从高地址向低地址增长的。

栈有很多单元格,通常情况下每个单元格是8位的(即可以存8个0或1),称为数据宽度,是用来存放数据的。每个单元格都会对应一个地址,地址一般是无符号32位的整数,因此可以表示4294967295(32位无符号整数可以表示的最大值)个单元格。

关于栈的操作涉及到两个寄存器,即ESP和EBP(什么是寄存器这里就不多说了),另外还有两个指令,POP和PUSH。

首先说一下两个寄存器,这两个寄存器分别称为栈指针寄存器和基址指针寄存器,因此这两个寄存器中存的是指针,即地址(这个地址就是前面说到的单元格对应的地址),更确切的说应该是栈帧顶部的地址(ESP)和栈帧底部的地址(EBP)(栈帧是什么后面会讲到)。

下面是一个栈的结构图:

函数调用的汇编语言详解

可以看到,栈是从高地址向低地址增长的,高地址对应栈底,低地址对应栈顶,每个存放数据的单元格都有一个对应的地址,每个单元格的宽度为8位。

下面说一下POP和PUSH指令,这两个指令都只能操作栈顶,PUSH是向栈中推入一个数放在栈顶,POP是将栈顶的数据弹出。无论是推入新的数据还是弹出数据,ESP始终指向栈顶,也就意味着ESP会不断改变。

注意到上面我写了个栈帧,下面解释一下栈帧是什么。

2、栈帧

函数调用的汇编语言详解

栈帧是一块连续的栈区(注意看上面的图),每个函数都有自己的栈区,这个栈区就称为栈帧。注意,函数调用所占用的栈区才称为栈帧,并且每个函数都会有自己的栈帧,包括main函数。当前栈帧的范围在EBP和ESP指向的区域之间。

当然栈帧不是固定不变的,由于推入新的数据和弹出数据ESP都会改变,因此栈帧的大小也会随之改变,但是EBP一般不会改变。

那么什么时候会改变EBP?

前面说到,每个函数都有自己的栈帧,也就是有自己的EBP和ESP,那么当一个函数调用另外一个函数的时候,第二个函数也应该有自己的栈帧,也有自己的EBP和ESP,但是系统中只有一个相应的寄存器(即只有一个ESP寄存器和一个EBP寄存器)。因此这个时候就要改变EBP的值,在改变EBP的值之前把旧的EBP的值保存一下就可以了(这个后面还会讲到)。

3、保存寄存器

为什么要保存寄存器?

首先,寄存器数量有限,因此寄存器是被所有的函数共享的。假设现在有A、B两个函数,A函数调用了B函数。A在调用过程中往EAX中存放了数据x,B在调用过程中往EAX存放了新的数据y,那么当B函数调用结束返回到A函数后,A继续使用EAX中的值,但这个时候EAX中的值已经不是最初存的x了。

怎么保护?

方法很简单,在B函数使用EAX寄存器之前,在准备阶段(刚进入B函数时)先将寄存器中的值保存到栈中,用完以后,在结束阶段再从栈中将值重新写入EAX中。

注:并不是所有通用寄存器中的值都由被调用函数保存,通常调用函数保存一部分,被调用函数保存一部分。IA-32规定,寄存器EAX、ECX、EDX是调用者保存寄存器,寄存器EBX、ESI、EDI是被调用者保存寄存器。

二、函数调用大体流程

假设有两个函数A和B,A函数调用B函数,调用B函数需要传入两个参数x和y。

1:在A调用B函数之前,A先把需要传入B函数中的参数x,y推入栈中。(注意x,y被放在了A的栈帧中)

注:上面一句步骤没有说先把调用者保存寄存器推入栈中,事实上并不是每次调用函数都要把调用者保存寄存器推入栈中,只有在必要的时候才会进行保护(编译器自己决定)。

2:在执行函数调用的时候(即执行call指令),A把B函数的返回地址推入栈中(还是在A的栈帧中)。

3:在进入B函数后,推入旧的EBP的值,这时ESP指向的单元格中的内容是旧的EBP,然后令EBP等于当前的ESP,则这个EBP即为B函数的栈帧的栈底,EBP指向的单元格中的数据是旧的EBP。这里比较抽象,下面有一个图可以帮助理解。

函数调用的汇编语言详解

(上图有个地方写错了,EBP应该是B函数栈帧的栈底)

4:开辟一块相对合适的空间用来存放非静态局部变量(存放非静态局部变量前会先置成cc)。

5:将被调用者保存寄存器中的内容推入栈中进行保护。

6:执行函数中的内容。

7:函数调用完毕,return。返回的时候将EDI、ESI、EBX依次弹出,然后让ESP指向EBP,将ESP指向的单元格中的内容(即旧的EBP)弹到EBP中,这样EBP又重新变成了A的栈帧的栈底,执行ret指令,弹出B的返回地址,然后ESP根据参数的个数加上相应的数使ESP指向原来A的栈帧的栈顶(一个参数加4,两个加8,以此类推)。这里有一点需要注意,ESP的值虽然改变了,但是栈中B的参数还存在,但是无所谓,在推入新的数据的时候就会被覆盖掉。

注意上图ESP = ESP+8后ESP指向了原来A栈帧的栈顶,但是B的参数还存在,不过当有新数据进来的时候就会被覆盖掉。

这里有个问题,B函数的参数在A函数的栈帧中,B函数应该怎么去读取使用呢?

注意推入参数和函数返回地址后就改变了EBP,因此EBP和函数的参数紧挨着(也就是说参数在B能够访问到的地方),那么怎么去访问呢?拿上面的例子讲,参数x在EBP+8中,参数y在EBP+12中。

博主最近比较忙,最后三部分抽时间再更。

三、用VC++6.0查看反汇编详解函数调用

四、用VC++6.0调试讲解函数递归调用

五、微软的内存保护机制

相关推荐