汇编原理深入了解 > c语言程序执行原理
从内存和寄存器中深入分析函数调用原理

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中。

以上内容来自https://blog.csdn.net/theLostLamb/article/details/79950815 感谢博主分享

下面我结合一个实际例子

int add(int a, int b) {
    int c = 0;
    c = a + b;
    return c;
}

int main() {
    add(1, 2);
    return 0;
}

/***
 ## 下面是main的汇编代码
   0x0040162d <+0>:    push   %ebp
   0x0040162e <+1>:    mov    %esp,%ebp
   0x00401630 <+3>:    and    $0xfffffff0,%esp
   0x00401633 <+6>:    sub    $0x10,%esp
   0x00401636 <+9>:    call   0x401f40 <__main>       [CS和IP 压入栈中 sep 下移8个字节]
1 =>  0x0040163b <+14>:    movl   $0x2,0x4(%esp)      [ebp 0x60feb8 esp 0x60fea0]
   0x00401643 <+22>:   movl   $0x1,(%esp)
   0x0040164a <+29>:   call   0x401610 <add>     [CS和IP 压入栈中 sep 下移8个字节]
6 =>  0x0040164f <+34>:    mov    $0x0,%eax          [ebp 0x60feb8  esp 0x60fea0]
   0x00401654 <+39>:   leave
   0x00401655 <+40>:   ret

 ## 下面是add函数的汇编代码
   0x00401610 <+0>:    push   %ebp     [ebp 0x60feb8 esp 0x60fe98]
   0x00401611 <+1>:        mov    %esp,%ebp
   0x00401613 <+3>:    sub    $0x10,%esp
2 =>   0x00401616 <+6>:        movl   $0x0,-0x4(%ebp) [ebp  0x60fe98  esp  0x60fe88]
3 =>   0x0040161d <+13>:   mov    0x8(%ebp),%edx  [ebp  0x60fe98  esp  0x60fe88]
   0x00401620 <+16>:   mov    0xc(%ebp),%eax
   0x00401623 <+19>:   add    %edx,%eax
   0x00401625 <+21>:   mov    %eax,-0x4(%ebp)
4=>   0x00401628 <+24>:    mov    -0x4(%ebp),%eax    [ebp  0x60fe98   esp  0x60fe88]
5=>   0x0040162b <+27>:    leave           [ebp  0x60fe98 esp  0x60fe88]
   0x0040162c <+28>:   ret

 */