main函数及其子函数之间的栈
1 工具及实验程序
本文的实验在一个虚拟机中进行,虚拟机模拟的cpu是x86-64(Intel(R) Core(TM) i7-6820HQ CPU @ 2.70GHz),运行的是64bit ubuntu,安装了ARM64的工具链:
$sudo apt-get install gcc-aarch64-linux-gnu
$sudo apt-get install gcc-arm-linux-gnueabi
实验使用的程序为:
#include
#include
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;
}
2 x86-64
2.1 main函数的栈
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:
(gdb) disas main
Dump of assembler code for function main:
0x0000000000000790 : push %rbp
0x0000000000000791 : mov %rsp,%rbp
0x0000000000000794 : sub $0x40,%rsp
0x0000000000000798 : mov %edi,-0x24(%rbp)
0x000000000000079b : mov %rsi,-0x30(%rbp)
0x000000000000079f : mov %rdx,-0x38(%rbp)
0x00000000000007a3 : mov -0x24(%rbp),%eax
0x00000000000007a6 : mov %eax,-0x4(%rbp)
0x00000000000007a9 : mov -0x30(%rbp),%rax
0x00000000000007ad : mov %rax,-0x10(%rbp)
0x00000000000007b1 : mov -0x38(%rbp),%rax
0x00000000000007b5 : mov %rax,-0x18(%rbp)
0x00000000000007b9 : callq 0x6fd
0x00000000000007be : mov $0x0,%eax
0x00000000000007c3 : leaveq
0x00000000000007c4 : retq
End of assembler dump.
从上面对main函数栈的分析可以知道,一个函数栈大致会做以下几件事:
保存上一个栈帧的信息。
0x0000000000000790 : push %rbp
0x0000000000000791 : 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 : sub $0x40,%rsp
也就是开辟当前栈帧的空间,开辟的栈帧空间主要用于接收参数、存放局部变量以及运算的场所,下面的汇编开辟0x40个字节的空间。
接收参数。
0x0000000000000798 : mov %edi,-0x24(%rbp)
0x000000000000079b : mov %rsi,-0x30(%rbp)
0x000000000000079f : mov %rdx,-0x38(%rbp)
前面也说过,x86-64参数传递的规则是rdi传递第一个参数、rsi传递第二个参数、rdx传递第三个参数....。mian函数的一个形参是argc,第二个形参是argv,第三个形参是envp。从下面的汇编也可以看出,实参在rdi、rsi和rdx中,然后分别放到rbp - 0x24 、rbp - 0x30和rbp -0x38形参的位置处。
栈上运算。
0x00000000000007a3 : mov -0x24(%rbp),%eax
0x00000000000007a6 : mov %eax,-0x4(%rbp)
0x00000000000007a9 : mov -0x30(%rbp),%rax
0x00000000000007ad : mov %rax,-0x10(%rbp)
0x00000000000007b1 : mov -0x38(%rbp),%rax
0x00000000000007b5 : mov %rax,-0x18(%rbp)
0x00000000000007b9 : callq 0x6fd
栈的最大作用就是作为运算场所,main的栈上并没有安排太多的运算,仅仅做了三次赋值,然后主要工作就就转给子函数了。下面的一二两句的作用是将argc的值赋值给count;三四句的作用是将argv的值赋给p_argv;五六两句的作用是将envp赋值给p_envl;最后一句就是调用子函数,我们也把他看做是运算的一部分。
设置返回值。x86-64的函数返回值使用rax传递。从return 0可知函数的返回值是0,因此将0赋给eax。
0x00000000000007be : mov $0x0,%eax
恢复上一个栈帧。先看一下相反的操作保存栈帧的两个步骤:一是将帧指针(rbp)压栈;二是将栈指针(rsp)的值赋值给帧指针(rbp),可知,上一个栈帧结构可以由当前的帧指针rbp推导出,也即下面汇编语句leaveq的作用,该语句的作用有两个:一是将帧指针(rbp)的值赋值给栈指针(rsp),即mov %rbp, %rsp;二是将帧指针(rbp)出栈,即pop %rbp。正好和保存上一个栈帧结构的操作相反。leaveq指令执行完后,帧指针(rbp)已经切换回caller的栈了,也即数据存储区已经切换完成,只差函数控制切换。
0x00000000000007c3 : 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干的。
函数控制转移。完成函数控制切换,说白了也就是让CPU接着执行caller函数中callee函数后面的指令。下面的指令使用栈指针指向的值恢复rip,功能可以按照mov (%sp), %rip; addq $8,%rsp或pop %rip来理解。该指令执行完后,函数的控制以及栈指针(rsp)切换完成。 retq指令改变了rsp 和 rip的值。
0x00000000000007c4 : retq
2. 2 子函数的栈
2.2.1 func_A的栈
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 : push %rbp
0x00000000000006fe : mov %rsp,%rbp
0x0000000000000701 : sub $0x30,%rsp
0x0000000000000705 : movl $0x0,-0x4(%rbp)
0x000000000000070c : movl $0x1,-0x8(%rbp)
0x0000000000000713 : movl $0x2,-0xc(%rbp)
0x000000000000071a : movl $0x3,-0x10(%rbp)
0x0000000000000721 : movl $0x4,-0x14(%rbp)
0x0000000000000728 : movl $0x5,-0x18(%rbp)
0x000000000000072f : movl $0x6,-0x1c(%rbp)
0x0000000000000736 : movl $0x7,-0x20(%rbp)
0x000000000000073d : movl $0x8,-0x24(%rbp)
0x0000000000000744 : movb $0x63,-0x25(%rbp)
0x0000000000000748 : movsbl -0x25(%rbp),%edi
0x000000000000074c : mov -0x1c(%rbp),%r9d
0x0000000000000750 : mov -0x18(%rbp),%r8d
0x0000000000000754 : mov -0x14(%rbp),%ecx
0x0000000000000757 : mov -0x10(%rbp),%edx
0x000000000000075a : mov -0xc(%rbp),%esi
0x000000000000075d : mov -0x8(%rbp),%eax
0x0000000000000760 : push %rdi
0x0000000000000761 : mov -0x24(%rbp),%edi
0x0000000000000764 : push %rdi
0x0000000000000765 : mov -0x20(%rbp),%edi
0x0000000000000768 : push %rdi
0x0000000000000769 : mov %eax,%edi
0x000000000000076b : callq 0x699
0x0000000000000770 : add $0x18,%rsp
0x0000000000000774 : mov %eax,-0x4(%rbp)
0x0000000000000777 : mov -0x4(%rbp),%eax
0x000000000000077a : mov %eax,%esi
0x000000000000077c : lea 0xd1(%rip),%rdi # 0x854
0x0000000000000783 : mov $0x0,%eax
0x0000000000000788 : callq 0x520
0x000000000000078d : nop
0x000000000000078e : leaveq
0x000000000000078f : retq
End of assembler dump.
上述汇编做的事情也是那几个,保存上一个栈帧的信息、开栈、接受参数、栈上运算....,下面将进行分析。
保存上一个栈帧的信息
0x00000000000006fd : push %rbp
0x00000000000006fe : mov %rsp,%rbp
保存上一个栈帧的作用就是为了该函数被调用完成后还能再回到caller函数的栈帧继续执行,需要把caller的栈帧保存起来,保存地点就是callee的栈帧上。上述汇编的第一句就是把帧指针rbp入栈保存在栈上,第二句把栈指针rsp保存在帧指针rbp中。上述两句执行完后,func_C的栈布局如下图所示。
开栈
0x0000000000000701 : sub $0x30,%rsp
开栈的操作比较简单,就是把rsp的值减小,开辟出一片连续的内存区域用作接收参数,存放局部变量以及栈上运算。不过func_C没有参数和栈上运算。执行完上述汇编后,栈的布局如下图所示。
接受参数。func_A不涉及。
栈上运算
0x0000000000000705 : movl $0x0,-0x4(%rbp)
0x000000000000070c : movl $0x1,-0x8(%rbp)
0x0000000000000713 : movl $0x2,-0xc(%rbp)
0x000000000000071a : movl $0x3,-0x10(%rbp)
0x0000000000000721 : movl $0x4,-0x14(%rbp)
0x0000000000000728 : movl $0x5,-0x18(%rbp)
0x000000000000072f : movl $0x6,-0x1c(%rbp)
0x0000000000000736 : movl $0x7,-0x20(%rbp)
0x000000000000073d : movl $0x8,-0x24(%rbp)
0x0000000000000744 : movb $0x63,-0x25(%rbp)
这里的栈上运算比较简单,只是对局部变量进行赋值。局部变量赋值过后的栈空间如下图所示:
参数传递
0x0000000000000748 : movsbl -0x25(%rbp),%edi
0x000000000000074c : mov -0x1c(%rbp),%r9d
0x0000000000000750 : mov -0x18(%rbp),%r8d
0x0000000000000754 : mov -0x14(%rbp),%ecx
0x0000000000000757 : mov -0x10(%rbp),%edx
0x000000000000075a : mov -0xc(%rbp),%esi
0x000000000000075d : mov -0x8(%rbp),%eax
0x0000000000000760 : push %rdi
0x0000000000000761 : mov -0x24(%rbp),%edi
0x0000000000000764 : push %rdi
0x0000000000000765 : mov -0x20(%rbp),%edi
0x0000000000000768 : push %rdi
0x0000000000000769 : 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使用栈传递。
2.2.2 func_B的栈
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 : push %rbp
0x000000000000069a : mov %rsp,%rbp
0x000000000000069d : sub $0x30,%rsp
0x00000000000006a1 : mov %edi,-0x14(%rbp)
0x00000000000006a4 : mov %esi,-0x18(%rbp)
0x00000000000006a7 : mov %edx,-0x1c(%rbp)
0x00000000000006aa : mov %ecx,-0x20(%rbp)
0x00000000000006ad : mov %r8d,-0x24(%rbp)
0x00000000000006b1 : mov %r9d,-0x28(%rbp)
0x00000000000006b5 : mov 0x20(%rbp),%eax
0x00000000000006b8 : mov %al,-0x2c(%rbp)
0x00000000000006bb : movl $0x0,-0x4(%rbp)
0x00000000000006c2 : mov -0x28(%rbp),%r8d
0x00000000000006c6 : mov -0x24(%rbp),%edi
0x00000000000006c9 : mov -0x20(%rbp),%ecx
0x00000000000006cc : mov -0x1c(%rbp),%edx
0x00000000000006cf : mov -0x18(%rbp),%esi
0x00000000000006d2 : mov -0x14(%rbp),%eax
0x00000000000006d5 : mov %r8d,%r9d
0x00000000000006d8 : mov %edi,%r8d
0x00000000000006db : mov %eax,%edi
0x00000000000006dd : callq 0x64a
0x00000000000006e2 : mov %eax,-0x4(%rbp)
0x00000000000006e5 : mov 0x10(%rbp),%eax
0x00000000000006e8 : add %eax,-0x4(%rbp)
0x00000000000006eb : mov 0x18(%rbp),%eax
0x00000000000006ee : add %eax,-0x4(%rbp)
0x00000000000006f1 : movsbl -0x2c(%rbp),%eax
0x00000000000006f5 : add %eax,-0x4(%rbp)
0x00000000000006f8 : mov -0x4(%rbp),%eax
0x00000000000006fb : leaveq
0x00000000000006fc : retq
End of assembler dump.
一个函数的汇编做的还是那几件事,下面分析:
保存上一个栈帧信息
0x0000000000000699 : push %rbp
0x000000000000069a : mov %rsp,%rbp
上一个栈帧的帧指针rbp保存在当前栈帧上, 上一个栈帧的栈指针rsp保存在当前栈帧的帧指针rbp寄存器中。
开栈
0x000000000000069d : sub $0x30,%rsp
在栈上开辟一块连续的地址用于接收参数,局部变量,栈上运算。
接收参数
0x00000000000006a4 : mov %esi,-0x18(%rbp)
0x00000000000006a7 : mov %edx,-0x1c(%rbp)
0x00000000000006aa : mov %ecx,-0x20(%rbp)
0x00000000000006ad : mov %r8d,-0x24(%rbp)
0x00000000000006b1 : mov %r9d,-0x28(%rbp)
0x00000000000006b5 : mov 0x20(%rbp),%eax
0x00000000000006b8 : mov %al,-0x2c(%rbp)
函数总共有9个参数,寄存器传递6个参数,栈传递三个参数。
传递参数
0x00000000000006c2 : mov -0x28(%rbp),%r8d
0x00000000000006c6 : mov -0x24(%rbp),%edi
0x00000000000006c9 : mov -0x20(%rbp),%ecx
0x00000000000006cc : mov -0x1c(%rbp),%edx
0x00000000000006cf : mov -0x18(%rbp),%esi
0x00000000000006d2 : mov -0x14(%rbp),%eax
0x00000000000006d5 : mov %r8d,%r9d
0x00000000000006d8 : mov %edi,%r8d
0x00000000000006db : mov %eax,%edi
调用函数有6个参数,这6个参数都使用寄存器传递。
栈上运算
0x00000000000006dd : callq 0x64a
0x00000000000006e2 : mov %eax,-0x4(%rbp)
0x00000000000006e5 : mov 0x10(%rbp),%eax
0x00000000000006e8 : add %eax,-0x4(%rbp)
0x00000000000006eb : mov 0x18(%rbp),%eax
0x00000000000006ee : add %eax,-0x4(%rbp)
0x00000000000006f1 : movsbl -0x2c(%rbp),%eax
0x00000000000006f5 : add %eax,-0x4(%rbp)
运算第一步就是将func_A的返回值%eax赋值给sum;第二步是将x7的值和sum相加放在sum中;第三步是将x8的和sum相加结果放在sum中;第三步是将x9的值扩展成32bit,和sum相加结果放在sum中。
2.2.3 func_C的栈
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 : push %rbp
0x000000000000064b : mov %rsp,%rbp
0x000000000000064e : mov %edi,-0x14(%rbp)
0x0000000000000651 : mov %esi,-0x18(%rbp)
0x0000000000000654 : mov %edx,-0x1c(%rbp)
0x0000000000000657 : mov %ecx,-0x20(%rbp)
0x000000000000065a : mov %r8d,-0x24(%rbp)
0x000000000000065e : mov %r9d,-0x28(%rbp)
0x0000000000000662 : movl $0x0,-0x4(%rbp)
0x0000000000000669 : mov -0x14(%rbp),%edx
0x000000000000066c : mov -0x18(%rbp),%eax
0x000000000000066f : add %edx,%eax
0x0000000000000671 : mov %eax,-0x4(%rbp)
0x0000000000000674 : mov -0x4(%rbp),%edx
0x0000000000000677 : mov -0x1c(%rbp),%eax
0x000000000000067a : add %eax,%edx
0x000000000000067c : mov -0x20(%rbp),%eax
0x000000000000067f : add %edx,%eax
0x0000000000000681 : mov %eax,-0x4(%rbp)
0x0000000000000684 : mov -0x4(%rbp),%edx
0x0000000000000687 : mov -0x24(%rbp),%eax
0x000000000000068a : add %eax,%edx
0x000000000000068c : mov -0x28(%rbp),%eax
0x000000000000068f : add %edx,%eax
0x0000000000000691 : mov %eax,-0x4(%rbp)
0x0000000000000694 : mov -0x4(%rbp),%eax
0x0000000000000697 : pop %rbp
0x0000000000000698 : retq
End of assembler dump.
func_A的栈结构和前几个函数类型,但是编译器识别到该函数时叶子函数(leaf function),其栈指针rsp不再被使用,就会少一修改rsp的指令和一个恢复rsp的指令。
保存上一个栈帧信息
0x0000000000000699 : push %rbp
0x000000000000069a : mov %rsp,%rbp
不会有开栈的操作,即不会修改rsp指针。
接收参数
0x000000000000064e : mov %edi,-0x14(%rbp)
0x0000000000000651 : mov %esi,-0x18(%rbp)
0x0000000000000654 : mov %edx,-0x1c(%rbp)
0x0000000000000657 : mov %ecx,-0x20(%rbp)
0x000000000000065a : mov %r8d,-0x24(%rbp)
0x000000000000065e : mov %r9d,-0x28(%rbp)
栈上运算
0x0000000000000662 : movl $0x0,-0x4(%rbp)
0x0000000000000669 : mov -0x14(%rbp),%edx
0x000000000000066c : mov -0x18(%rbp),%eax
0x000000000000066f : add %edx,%eax
0x0000000000000671 : mov %eax,-0x4(%rbp)
0x0000000000000674 : mov -0x4(%rbp),%edx
0x0000000000000677 : mov -0x1c(%rbp),%eax
0x000000000000067a : add %eax,%edx
0x000000000000067c : mov -0x20(%rbp),%eax
0x000000000000067f : add %edx,%eax
0x0000000000000681 : mov %eax,-0x4(%rbp)
0x0000000000000684 : mov -0x4(%rbp),%edx
0x0000000000000687 : mov -0x24(%rbp),%eax
0x000000000000068a : add %eax,%edx
0x000000000000068c : mov -0x28(%rbp),%eax
0x000000000000068f : add %edx,%eax
0x0000000000000691 : mov %eax,-0x4(%rbp)
汇编语言描述的和C语言一致。
2.3.4 调用关系以及红区