函数调⽤堆栈的汇编解析
⼤家可能都会做过这个的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);
return0;
}
⼤家会发现,这个程序竟然没有头⽂件,是的我们没有使⽤到c库函数,也没⽤到⼀些函数没有定义的⼀些符号(⼀些变量名或函数名)。程序实现的功能很简单:只是在主函数中调⽤,add()函数完成简单的加法运算。现在我们将这个程序进⾏汇编(gcc -S test.c -o test.s ):得到汇编代码如下:
.file"test.c"
.text
.globl add
.type add, @function
add:offset指令是什么意思
.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, @function
main:
.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"
.GNU-stack,"",@progbits
这段汇编代码看似很多的汇编指令,实际是很多并不是真正的汇编指令,将⼀些伪指令等删除后得到:
add:
pushl %ebp
movl %esp, %ebp
movl 12(%ebp), %eax
movl 8(%ebp), %edx
addl %edx, %eax
popl %ebp
ret
main:
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返回到调⽤函数的的下⼀条指令处执⾏。
以上就是函数调⽤堆栈的汇编分析,也是本⼈对于调⽤规则的粗鄙理解,望提出宝贵意见。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论