函数调用堆栈的汇编解析

来源:互联网 发布:网络服务商地址 编辑:程序博客网 时间:2024/06/05 07:17

大家可能都会做过这个的gcc编译过程:gcc -S test.c -o test.s ,通过这样的编译得到的是我们的汇编代码,打开test.s文件会发现都是我们看不懂的汇编指令。也许我们都想过去看看这些汇编代码是什么意思,可是这些晦涩难懂的汇编代码,又让我们望洋兴叹。我们都知道函数的形参是放在栈区的,函数调用必须需要栈,可是编译器究竟是怎样为我们分配栈区的呢?今天我们就来通过一个简单的C语言程序来认识一下编译后所得到的汇编,揭开程序底层函数调用堆栈的实现。
首先,我们编写一个简单的c语言程序:实现最简单的函数调用。

int add(int a,int b){    return a+b;}int main(int argc, const char *argv[]){    add(3,4);    return 0;}

大家会发现,这个程序竟然没有头文件,是的我们没有使用到c库函数,也没用到一些函数没有定义的一些符号(一些变量名或函数名)。程序实现的功能很简单:只是在主函数中调用,add()函数完成简单的加法运算。现在我们将这个程序进行汇编(gcc -S test.c -o test.s ):得到汇编代码如下:

    .file   "test.c"    .text    .globl  add    .type   add, @functionadd:.LFB0:    .cfi_startproc    pushl   %ebp    .cfi_def_cfa_offset 8    .cfi_offset 5, -8    movl    %esp, %ebp    .cfi_def_cfa_register 5    movl    12(%ebp), %eax    movl    8(%ebp), %edx    addl    %edx, %eax    popl    %ebp    .cfi_def_cfa 4, 4    .cfi_restore 5    ret    .cfi_endproc.LFE0:    .size   add, .-add    .globl  main    .type   main, @functionmain:.LFB1:    .cfi_startproc    pushl   %ebp    .cfi_def_cfa_offset 8    .cfi_offset 5, -8    movl    %esp, %ebp    .cfi_def_cfa_register 5    subl    $8, %esp    movl    $4, 4(%esp)    movl    $3, (%esp)    call    add    movl    $0, %eax    leave    .cfi_restore 5    .cfi_def_cfa 4, 4    ret    .cfi_endproc.LFE1:    .size   main, .-main    .ident  "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"    .section    .note.GNU-stack,"",@progbits

这段汇编代码看似很多的汇编指令,实际是很多并不是真正的汇编指令,将一些伪指令等删除后得到:

add:    pushl   %ebp    movl    %esp, %ebp    movl    12(%ebp), %eax    movl    8(%ebp), %edx    addl    %edx, %eax    popl    %ebp    retmain:    pushl   %ebp    movl    %esp, %ebp    subl    $8, %esp    movl    $4, 4(%esp)    movl    $3, (%esp)    call    add    movl    $0, %eax    leave    ret

得到的这段汇编代码才是真正要转化为计算机能识别的机器语言(即是0和1),我们注意是分析这段代码:
首先介绍几个常用x86的寄存器:
eip:程序计数器,指向下一条将要执行的指令
ebp:栈底寄存器,指向栈底
esp:栈顶寄存器,指向栈顶
eax,ebx,ecx,edx:一些通用的寄存器,做数据的搬移使用
首先,在man函数中,pushl %ebp 将ebp压栈,接下来movl %esp, %ebp 是将ebp指向当前的esp位置,subl $8, %esp是为man函数分配栈区 大小为8B(分析:将esp减8,由于栈是向下增长,所以是sub指令)。
栈区分配好之后:movl $4, 4(%esp) 和 movl $3 (%esp), 做了两次数据的搬移,即是4和3分别放在esp增加4的位置和esp的位置,这是所谓的实参在栈区的存放。接下来,call add 是函数的调用指令,实际上这条指令相当于两条指令:将下一条指令的地址即是eip入栈,然后跳转到add的函数处。
进入add后,首先执行两条指令: pushl %ebp ,movl %esp %ebp 这两条指令和man函数中的头两条一样,所在的工作是是重新调整当前函数的栈区,这时栈底和栈顶指向相同的位置6,接下来就是分配合适的栈区,由于这里面没有涉及到局部变量,编译器没有分配新的栈区,只是把原来实参的数据取出放入通用寄存器(eax和edx)而已(为何?):
movl 12(%ebp), %eax movl 8(%ebp), %edx
下面就进入了add函数的核心部分:运算,指令为addl %edx, %eax 发现这是一条加法指令,将eax和edx寄存器的值取出来进行加法运算,然后在把运算结果放入到eax中,我们发现内存中的数据都是必须放入cpu的寄存器中才能进行运算的。add中返回的是a+b也就是eax的值,实际上函数的返回值都会放在eax中被返回。然后后面是一条弹栈指令:popl %ebp 这是ebp指向了原来栈底的位置,即是2的位置,esp加4指向5位置。add中最后一条ret指令,这是函数调用的返回指令实际上是等价于pop指令,即是弹出eip,这是我们前面压栈的main函数 中 call add的下条指令movl $0, %eax的地址,弹出后程序计数器eip就指向了这条指令,开始执行此指令,当然esp也加4到达4位置。这条指令是将eax清零,这是main函数的返回值0。接下来,leave指令,实际上这条指令等价于:movl %ebp, %esp 和 popl %ebp即是做清栈操作,相当于回收栈区分配的资源,这时ebp就指向了刚开始调用main函数时栈底的位置,esp就指向了刚开始调用main函数时栈顶的位置,最后一条ret指令使得程序计数器eip指向了刚开始调用main函数的下一条指令处,程序执行到这里,程序执行结束,分析也到此结束。堆栈建立过程堆栈释放过程
最后总结一下(这也是函数的调用规则):
1.函数在调用的时候都会提前保存下调用指令的下一条指令的地址,保证函数调用结束能够回到下一条指令继续执行。
2.进入被调用的函数中的时候,首先都会执行pushl %ebp 和
movl %esp, %ebp两条指令,来保存原来调用函数的栈底指针,以及重新定位下栈底指针到栈顶指针。
3.栈区的增长都是向下增长,即是向地址减小的方向增长。
4.都会为函数分配合适的栈区为函数中的局部变量使用。
5.数据运算时候都会从内存中将数据搬移到cpu的寄存器中才能运算。
6.函数的形参都是从右向左保存到通用寄存器中,然后进行运算。
7.函数调用结束,如果有返回值都会保存在eax寄存器中。
8.函数调用结束,如果原来有栈区的分配,都会调用leave来“释放”栈区。
9.函数调用结束,释放完栈区,都会通过ret返回到调用函数的的下一条指令处执行。
以上就是函数调用堆栈的汇编分析,也是本人对于调用规则的粗鄙理解,望提出宝贵意见。

0 0
原创粉丝点击