本文的实验在一个虚拟机中进行,虚拟机模拟的cpu是x86-64(Intel(R) Core(TM) i7-6820HQ CPU @ 2.70GHz),运行的是64bit ubuntu,安装了ARM64的工具链:linux
$sudo apt-get install gcc-aarch64-linux-gnu $sudo apt-get install gcc-arm-linux-gnueabi
实验使用的程序为:ubuntu
#include<stdio.h> #include <stdlib.h> int func_C(int x1, int x2, int x3, int x4, int x5, int x6){ int sum = 0; sum = x1 + x2; sum = sum + x3 + x4; sum = sum + x5 + x6; return sum; } int func_B(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8, char x9){ int sum = 0; sum = func_C(x1, x2, x3, x4, x5,x6); sum = sum + x7; sum = sum + x8; sum += x9; return sum; } void func_A(void){ int sum = 0; int x1 = 1; int x2 = 2; int x3 = 3; int x4 = 4; int x5 = 5; int x6 = 6; int x7 = 7; int x8 = 8; char x9 = 'c'; sum = func_B(x1, x2, x3, x4, x5, x6, x7, x8, x9); printf("sum = %d\n", sum); } int main(int argc, char *argv[], char *envp[]) { int count = argc; char **p_argv = argv; char **p_env = envp; func_A(); return 0; }
int main(int argc, char *argv[], char *envp[]) { int count = argc; char **p_argv = argv; char **p_env = envp; func_A(); return 0; }
首先,编译源程序 $gcc test.c -o test -fno-stack-protector
,而后反汇编出main函数 $gdb test
:ide
(gdb) disas main Dump of assembler code for function main: 0x0000000000000790 <+0>: push %rbp 0x0000000000000791 <+1>: mov %rsp,%rbp 0x0000000000000794 <+4>: sub $0x40,%rsp 0x0000000000000798 <+8>: mov %edi,-0x24(%rbp) 0x000000000000079b <+11>: mov %rsi,-0x30(%rbp) 0x000000000000079f <+15>: mov %rdx,-0x38(%rbp) 0x00000000000007a3 <+19>: mov -0x24(%rbp),%eax 0x00000000000007a6 <+22>: mov %eax,-0x4(%rbp) 0x00000000000007a9 <+25>: mov -0x30(%rbp),%rax 0x00000000000007ad <+29>: mov %rax,-0x10(%rbp) 0x00000000000007b1 <+33>: mov -0x38(%rbp),%rax 0x00000000000007b5 <+37>: mov %rax,-0x18(%rbp) 0x00000000000007b9 <+41>: callq 0x6fd <func_A> 0x00000000000007be <+46>: mov $0x0,%eax 0x00000000000007c3 <+51>: leaveq 0x00000000000007c4 <+52>: retq End of assembler dump.
从上面对main
函数栈的分析能够知道,一个函数栈大体会作如下几件事:函数
0x0000000000000790 <+0>: push %rbp 0x0000000000000791 <+1>: mov %rsp,%rbp
在x86-64上rbp和rsp彻底能够描绘出一个栈帧,rbp被称为帧指针(frame pointer),rsp被称为栈指针(stack pointer);rbp+0x08指向当前栈帧的底部,rsp指向栈帧的顶部。下面的第一句是将上一个栈帧的帧指针rbp压栈保存起来;rbp保存起来后,紧接着下一句汇编就为rbp赋予一个新值,将栈指针rsp的值赋给帧指针rbp,让rbp指向当前栈帧的底部,其实rbp+0x08才是当前栈帧的底部,只不过rbp+0x08处是上一个函数运行call指令时硬件自动存放的rip,对软件不可见。工具
0x0000000000000794 <+4>: sub $0x40,%rsp
也就是开辟当前栈帧的空间,开辟的栈帧空间主要用于接收参数、存放局部变量以及运算的场所,下面的汇编开辟0x40个字节的空间。布局
0x0000000000000798 <+8>: mov %edi,-0x24(%rbp) 0x000000000000079b <+11>: mov %rsi,-0x30(%rbp) 0x000000000000079f <+15>: mov %rdx,-0x38(%rbp)
前面也说过,x86-64参数传递的规则是rdi传递第一个参数、rsi传递第二个参数、rdx传递第三个参数....。mian
函数的一个形参是argc,第二个形参是argv,第三个形参是envp。从下面的汇编也能够看出,实参在rdi、rsi和rdx中,而后分别放到rbp - 0x24 、rbp - 0x30和rbp -0x38形参的位置处。3d
0x00000000000007a3 <+19>: mov -0x24(%rbp),%eax 0x00000000000007a6 <+22>: mov %eax,-0x4(%rbp) 0x00000000000007a9 <+25>: mov -0x30(%rbp),%rax 0x00000000000007ad <+29>: mov %rax,-0x10(%rbp) 0x00000000000007b1 <+33>: mov -0x38(%rbp),%rax 0x00000000000007b5 <+37>: mov %rax,-0x18(%rbp) 0x00000000000007b9 <+41>: callq 0x6fd <func_A>
栈的最大做用就是做为运算场所,main
的栈上并无安排太多的运算,仅仅作了三次赋值,而后主要工做就就转给子函数了。下面的一二两句的做用是将argc的值赋值给count;三四句的做用是将argv的值赋给p_argv;五六两句的做用是将envp赋值给p_envl;最后一句就是调用子函数,咱们也把他看作是运算的一部分。指针
设置返回值。x86-64的函数返回值使用rax传递。从return 0
可知函数的返回值是0,所以将0赋给eax。code
0x00000000000007be <+46>: mov $0x0,%eax
leaveq
的做用,该语句的做用有两个:一是将帧指针(rbp)的值赋值给栈指针(rsp),即mov %rbp, %rsp
;二是将帧指针(rbp)出栈,即pop %rbp
。正好和保存上一个栈帧结构的操做相反。leaveq
指令执行完后,帧指针(rbp)已经切换回caller的栈了,也即数据存储区已经切换完成,只差函数控制切换。 0x00000000000007c3 <+51>: leaveq
为了更加深刻的了解如何恢复栈帧,画了一个以下所示的图,rsp和rbp指向了current frame,也就是说寄存器rbp和rsp并无指向任何"previous frame",恢复上一个栈帧的核心问题在于如何让rbp和rsp指向上一个栈帧,答案的钥匙就是rbp寄存器。rbp指向的就是上一个rbp,rbp-16就是上一个rsp, 直觉上恢复上一个栈帧就是将rbp-16赋值给rsp,并将rbp指向的值赋值给rbp,显示上述方法能够恢复rsp和rbp,可是事实上并无使用上述方法,而是利用栈天然而然的恢复上一个栈帧,所谓的天然而然就是怎么来怎么回去。首先让rbp赋值给rsp,而后pop一次栈便可恢复rbp,再pop栈一次便可恢复rsp。“首先让rbp赋值给rsp,而后pop一次栈便可恢复rbp”是人类发明的指令leaveq
干的,"再pop栈一次便可恢复rsp"是人类发明的指令ret
干的。ip
mov (%sp), %rip; addq $8,%rsp
或pop %rip
来理解。该指令执行完后,函数的控制以及栈指针(rsp)切换完成。 retq
指令改变了rsp 和 rip的值。 0x00000000000007c4 <+52>: retq
void func_A(void){ int sum = 0; int x1 = 1; int x2 = 2; int x3 = 3; int x4 = 4; int x5 = 5; int x6 = 6; int x7 = 7; int x8 = 8; char x9 = 'c'; sum = func_B(x1, x2, x3, x4, x5, x6, x7, x8, x9); printf("sum = %d\n", sum); }
首先,编译源程序 $gcc test.c -o test -fno-stack-protector
,而后反汇编出func_A函数 $gdb test
:
(gdb) disas func_A Dump of assembler code for function func_A: 0x00000000000006fd <+0>: push %rbp 0x00000000000006fe <+1>: mov %rsp,%rbp 0x0000000000000701 <+4>: sub $0x30,%rsp 0x0000000000000705 <+8>: movl $0x0,-0x4(%rbp) 0x000000000000070c <+15>: movl $0x1,-0x8(%rbp) 0x0000000000000713 <+22>: movl $0x2,-0xc(%rbp) 0x000000000000071a <+29>: movl $0x3,-0x10(%rbp) 0x0000000000000721 <+36>: movl $0x4,-0x14(%rbp) 0x0000000000000728 <+43>: movl $0x5,-0x18(%rbp) 0x000000000000072f <+50>: movl $0x6,-0x1c(%rbp) 0x0000000000000736 <+57>: movl $0x7,-0x20(%rbp) 0x000000000000073d <+64>: movl $0x8,-0x24(%rbp) 0x0000000000000744 <+71>: movb $0x63,-0x25(%rbp) 0x0000000000000748 <+75>: movsbl -0x25(%rbp),%edi 0x000000000000074c <+79>: mov -0x1c(%rbp),%r9d 0x0000000000000750 <+83>: mov -0x18(%rbp),%r8d 0x0000000000000754 <+87>: mov -0x14(%rbp),%ecx 0x0000000000000757 <+90>: mov -0x10(%rbp),%edx 0x000000000000075a <+93>: mov -0xc(%rbp),%esi 0x000000000000075d <+96>: mov -0x8(%rbp),%eax 0x0000000000000760 <+99>: push %rdi 0x0000000000000761 <+100>: mov -0x24(%rbp),%edi 0x0000000000000764 <+103>: push %rdi 0x0000000000000765 <+104>: mov -0x20(%rbp),%edi 0x0000000000000768 <+107>: push %rdi 0x0000000000000769 <+108>: mov %eax,%edi 0x000000000000076b <+110>: callq 0x699 <func_B> 0x0000000000000770 <+115>: add $0x18,%rsp 0x0000000000000774 <+119>: mov %eax,-0x4(%rbp) 0x0000000000000777 <+122>: mov -0x4(%rbp),%eax 0x000000000000077a <+125>: mov %eax,%esi 0x000000000000077c <+127>: lea 0xd1(%rip),%rdi # 0x854 0x0000000000000783 <+134>: mov $0x0,%eax 0x0000000000000788 <+139>: callq 0x520 <printf@plt> 0x000000000000078d <+144>: nop 0x000000000000078e <+145>: leaveq 0x000000000000078f <+146>: retq End of assembler dump.
上述汇编作的事情也是那几个,保存上一个栈帧的信息、开栈、接受参数、栈上运算....,下面将进行分析。
0x00000000000006fd <+0>: push %rbp 0x00000000000006fe <+1>: mov %rsp,%rbp
保存上一个栈帧的做用就是为了该函数被调用完成后还能再回到caller函数的栈帧继续执行,须要把caller的栈帧保存起来,保存地点就是callee的栈帧上。上述汇编的第一句就是把帧指针rbp入栈保存在栈上,第二句把栈指针rsp保存在帧指针rbp中。上述两句执行完后,func_C的栈布局以下图所示。
0x0000000000000701 <+4>: sub $0x30,%rsp
开栈的操做比较简单,就是把rsp的值减少,开辟出一片连续的内存区域用做接收参数,存放局部变量以及栈上运算。不过func_C没有参数和栈上运算。执行完上述汇编后,栈的布局以下图所示。
0x0000000000000705 <+8>: movl $0x0,-0x4(%rbp) 0x000000000000070c <+15>: movl $0x1,-0x8(%rbp) 0x0000000000000713 <+22>: movl $0x2,-0xc(%rbp) 0x000000000000071a <+29>: movl $0x3,-0x10(%rbp) 0x0000000000000721 <+36>: movl $0x4,-0x14(%rbp) 0x0000000000000728 <+43>: movl $0x5,-0x18(%rbp) 0x000000000000072f <+50>: movl $0x6,-0x1c(%rbp) 0x0000000000000736 <+57>: movl $0x7,-0x20(%rbp) 0x000000000000073d <+64>: movl $0x8,-0x24(%rbp) 0x0000000000000744 <+71>: movb $0x63,-0x25(%rbp)
这里的栈上运算比较简单,只是对局部变量进行赋值。局部变量赋值事后的栈空间以下图所示:
0x0000000000000748 <+75>: movsbl -0x25(%rbp),%edi 0x000000000000074c <+79>: mov -0x1c(%rbp),%r9d 0x0000000000000750 <+83>: mov -0x18(%rbp),%r8d 0x0000000000000754 <+87>: mov -0x14(%rbp),%ecx 0x0000000000000757 <+90>: mov -0x10(%rbp),%edx 0x000000000000075a <+93>: mov -0xc(%rbp),%esi 0x000000000000075d <+96>: mov -0x8(%rbp),%eax 0x0000000000000760 <+99>: push %rdi 0x0000000000000761 <+100>: mov -0x24(%rbp),%edi 0x0000000000000764 <+103>: push %rdi 0x0000000000000765 <+104>: mov -0x20(%rbp),%edi 0x0000000000000768 <+107>: push %rdi 0x0000000000000769 <+108>: mov %eax,%edi
前面也说过,在X86-64平台,当参数小于7个时使用寄存器传参 。当参数个数大于等于7时,参数arg1~arg6分别使用寄存器rdi,rsi, rdx, rcx, r8 and r9传参,其他参数使用栈传递。以下图所示,实参x1~x6使用寄存器rdi,rsi, rdx, rcx, r8 and r9传参,实参x7~x9使用栈传递。
int func_B(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8, char x9){ int sum = 0; sum = func_C(x1, x2, x3, x4, x5,x6); sum = sum + x7; sum = sum + x8; sum += x9; return sum; }
首先,编译源程序 $gcc test.c -o test -fno-stack-protector
,而后反汇编出func_B函数 $gdb test
:
(gdb) disas func_B Dump of assembler code for function func_B: 0x0000000000000699 <+0>: push %rbp 0x000000000000069a <+1>: mov %rsp,%rbp 0x000000000000069d <+4>: sub $0x30,%rsp 0x00000000000006a1 <+8>: mov %edi,-0x14(%rbp) 0x00000000000006a4 <+11>: mov %esi,-0x18(%rbp) 0x00000000000006a7 <+14>: mov %edx,-0x1c(%rbp) 0x00000000000006aa <+17>: mov %ecx,-0x20(%rbp) 0x00000000000006ad <+20>: mov %r8d,-0x24(%rbp) 0x00000000000006b1 <+24>: mov %r9d,-0x28(%rbp) 0x00000000000006b5 <+28>: mov 0x20(%rbp),%eax 0x00000000000006b8 <+31>: mov %al,-0x2c(%rbp) 0x00000000000006bb <+34>: movl $0x0,-0x4(%rbp) 0x00000000000006c2 <+41>: mov -0x28(%rbp),%r8d 0x00000000000006c6 <+45>: mov -0x24(%rbp),%edi 0x00000000000006c9 <+48>: mov -0x20(%rbp),%ecx 0x00000000000006cc <+51>: mov -0x1c(%rbp),%edx 0x00000000000006cf <+54>: mov -0x18(%rbp),%esi 0x00000000000006d2 <+57>: mov -0x14(%rbp),%eax 0x00000000000006d5 <+60>: mov %r8d,%r9d 0x00000000000006d8 <+63>: mov %edi,%r8d 0x00000000000006db <+66>: mov %eax,%edi 0x00000000000006dd <+68>: callq 0x64a <func_C> 0x00000000000006e2 <+73>: mov %eax,-0x4(%rbp) 0x00000000000006e5 <+76>: mov 0x10(%rbp),%eax 0x00000000000006e8 <+79>: add %eax,-0x4(%rbp) 0x00000000000006eb <+82>: mov 0x18(%rbp),%eax 0x00000000000006ee <+85>: add %eax,-0x4(%rbp) 0x00000000000006f1 <+88>: movsbl -0x2c(%rbp),%eax 0x00000000000006f5 <+92>: add %eax,-0x4(%rbp) 0x00000000000006f8 <+95>: mov -0x4(%rbp),%eax 0x00000000000006fb <+98>: leaveq 0x00000000000006fc <+99>: retq End of assembler dump.
一个函数的汇编作的仍是那几件事,下面分析:
0x0000000000000699 <+0>: push %rbp 0x000000000000069a <+1>: mov %rsp,%rbp
上一个栈帧的帧指针rbp保存在当前栈帧上, 上一个栈帧的栈指针rsp保存在当前栈帧的帧指针rbp寄存器中。
0x000000000000069d <+4>: sub $0x30,%rsp
在栈上开辟一块连续的地址用于接收参数,局部变量,栈上运算。
0x00000000000006a4 <+11>: mov %esi,-0x18(%rbp) 0x00000000000006a7 <+14>: mov %edx,-0x1c(%rbp) 0x00000000000006aa <+17>: mov %ecx,-0x20(%rbp) 0x00000000000006ad <+20>: mov %r8d,-0x24(%rbp) 0x00000000000006b1 <+24>: mov %r9d,-0x28(%rbp) 0x00000000000006b5 <+28>: mov 0x20(%rbp),%eax 0x00000000000006b8 <+31>: mov %al,-0x2c(%rbp)
函数总共有9个参数,寄存器传递6个参数,栈传递三个参数。
0x00000000000006c2 <+41>: mov -0x28(%rbp),%r8d 0x00000000000006c6 <+45>: mov -0x24(%rbp),%edi 0x00000000000006c9 <+48>: mov -0x20(%rbp),%ecx 0x00000000000006cc <+51>: mov -0x1c(%rbp),%edx 0x00000000000006cf <+54>: mov -0x18(%rbp),%esi 0x00000000000006d2 <+57>: mov -0x14(%rbp),%eax 0x00000000000006d5 <+60>: mov %r8d,%r9d 0x00000000000006d8 <+63>: mov %edi,%r8d 0x00000000000006db <+66>: mov %eax,%edi
调用函数有6个参数,这6个参数都使用寄存器传递。
0x00000000000006dd <+68>: callq 0x64a <func_C> 0x00000000000006e2 <+73>: mov %eax,-0x4(%rbp) 0x00000000000006e5 <+76>: mov 0x10(%rbp),%eax 0x00000000000006e8 <+79>: add %eax,-0x4(%rbp) 0x00000000000006eb <+82>: mov 0x18(%rbp),%eax 0x00000000000006ee <+85>: add %eax,-0x4(%rbp) 0x00000000000006f1 <+88>: movsbl -0x2c(%rbp),%eax 0x00000000000006f5 <+92>: add %eax,-0x4(%rbp)
运算第一步就是将func_A的返回值%eax赋值给sum;第二步是将x7的值和sum相加放在sum中;第三步是将x8的和sum相加结果放在sum中;第三步是将x9的值扩展成32bit,和sum相加结果放在sum中。
int func_C(int x1, int x2, int x3, int x4, int x5, int x6){ int sum = 0; sum = x1 + x2; sum = sum + x3 + x4; sum = sum + x5 + x6; return sum; }
首先,编译源程序 $gcc test.c -o test -fno-stack-protector
,而后反汇编出func_A函数 $gdb test
:
(gdb) disas func_C Dump of assembler code for function func_C: 0x000000000000064a <+0>: push %rbp 0x000000000000064b <+1>: mov %rsp,%rbp 0x000000000000064e <+4>: mov %edi,-0x14(%rbp) 0x0000000000000651 <+7>: mov %esi,-0x18(%rbp) 0x0000000000000654 <+10>: mov %edx,-0x1c(%rbp) 0x0000000000000657 <+13>: mov %ecx,-0x20(%rbp) 0x000000000000065a <+16>: mov %r8d,-0x24(%rbp) 0x000000000000065e <+20>: mov %r9d,-0x28(%rbp) 0x0000000000000662 <+24>: movl $0x0,-0x4(%rbp) 0x0000000000000669 <+31>: mov -0x14(%rbp),%edx 0x000000000000066c <+34>: mov -0x18(%rbp),%eax 0x000000000000066f <+37>: add %edx,%eax 0x0000000000000671 <+39>: mov %eax,-0x4(%rbp) 0x0000000000000674 <+42>: mov -0x4(%rbp),%edx 0x0000000000000677 <+45>: mov -0x1c(%rbp),%eax 0x000000000000067a <+48>: add %eax,%edx 0x000000000000067c <+50>: mov -0x20(%rbp),%eax 0x000000000000067f <+53>: add %edx,%eax 0x0000000000000681 <+55>: mov %eax,-0x4(%rbp) 0x0000000000000684 <+58>: mov -0x4(%rbp),%edx 0x0000000000000687 <+61>: mov -0x24(%rbp),%eax 0x000000000000068a <+64>: add %eax,%edx 0x000000000000068c <+66>: mov -0x28(%rbp),%eax 0x000000000000068f <+69>: add %edx,%eax 0x0000000000000691 <+71>: mov %eax,-0x4(%rbp) 0x0000000000000694 <+74>: mov -0x4(%rbp),%eax 0x0000000000000697 <+77>: pop %rbp 0x0000000000000698 <+78>: retq End of assembler dump.
func_A的栈结构和前几个函数类型,可是编译器识别到该函数时叶子函数(leaf function),其栈指针rsp再也不被使用,就会少一修改rsp的指令和一个恢复rsp的指令。
0x0000000000000699 <+0>: push %rbp 0x000000000000069a <+1>: mov %rsp,%rbp
不会有开栈的操做,即不会修改rsp指针。
0x000000000000064e <+4>: mov %edi,-0x14(%rbp) 0x0000000000000651 <+7>: mov %esi,-0x18(%rbp) 0x0000000000000654 <+10>: mov %edx,-0x1c(%rbp) 0x0000000000000657 <+13>: mov %ecx,-0x20(%rbp) 0x000000000000065a <+16>: mov %r8d,-0x24(%rbp) 0x000000000000065e <+20>: mov %r9d,-0x28(%rbp)
0x0000000000000662 <+24>: movl $0x0,-0x4(%rbp) 0x0000000000000669 <+31>: mov -0x14(%rbp),%edx 0x000000000000066c <+34>: mov -0x18(%rbp),%eax 0x000000000000066f <+37>: add %edx,%eax 0x0000000000000671 <+39>: mov %eax,-0x4(%rbp) 0x0000000000000674 <+42>: mov -0x4(%rbp),%edx 0x0000000000000677 <+45>: mov -0x1c(%rbp),%eax 0x000000000000067a <+48>: add %eax,%edx 0x000000000000067c <+50>: mov -0x20(%rbp),%eax 0x000000000000067f <+53>: add %edx,%eax 0x0000000000000681 <+55>: mov %eax,-0x4(%rbp) 0x0000000000000684 <+58>: mov -0x4(%rbp),%edx 0x0000000000000687 <+61>: mov -0x24(%rbp),%eax 0x000000000000068a <+64>: add %eax,%edx 0x000000000000068c <+66>: mov -0x28(%rbp),%eax 0x000000000000068f <+69>: add %edx,%eax 0x0000000000000691 <+71>: mov %eax,-0x4(%rbp)
汇编语言描述的和C语言一致。