函数栈帧(用汇编来剖析)
好久没写博客了,学校开始实习,找了一个极破的地方,站公交将近两个小时,一天来回就要死了,说是实习,就是变着样的培训,一点实习的意思都没有,辣鸡。
这次讲解一下C++函数调用,学了这么久C语言,肯定听说过栈(数据结构啊,地址空间的栈啊之类的),函数调用就和栈密切相关。
因为地址空间内的栈是从高地址向低地址生长的,也就是说压栈顺序靠后的反而地址比较低,栈底的地址高于栈顶的地址,下面贴上一段测试代码
1 #include<stdio.h>
2
3 #include<stdlib.h>
4
5 void bug()
6 {
7 printf("haha I ma a bug!!");
8 exit(100);
9 }
10 int func(int x, int y)
11 {
12 int *p = &x;
13 p--;
14 *p = (int)bug;
15 printf("x:%d,y:%d\n", x, y);
16 int c = 0xcccc;
17 return c;
18 }
19
20
21
22 int main()
23 {
24
25 printf("I am main\n");
26 int a = 0xaaaa;
27 int b = 0xbbbb;
28 func(a, b);
29 printf("I should run here\n");
30 return 0;
31 }
这段代码的运行结果,并没有执行main函数的第二个printf,而是跑到了bug函数中执行,这是因为我修改了函数栈帧中的返回地址部分
本来是打算通过linux系统来看的,但是centos7的栈帧实现似乎有些不同,同样的代码在centos7上面跑不通。
下面是反汇编
1 int main()
2 {
3 00A118E0 push ebp
4 00A118E1 mov ebp,esp
5 00A118E3 sub esp,0D8h
6 00A118E9 push ebx
7 00A118EA push esi
8 00A118EB push edi
9 00A118EC lea edi,[ebp-0D8h]
10 00A118F2 mov ecx,36h
11 00A118F7 mov eax,0CCCCCCCCh
12 00A118FC rep stos dword ptr es:[edi]
13
14 printf("I am main\n");
15 00A118FE push offset string "I am main\n" (0A16CF0h)
16 00A11903 call _printf (0A1132Ah)
17 00A11908 add esp,4
18 int a = 0xaaaa;
19 00A1190B mov dword ptr [a],0AAAAh
20 int b = 0xbbbb;
21 00A11912 mov dword ptr [b],0BBBBh
22 func(a, b);
23 00A11919 mov eax,dword ptr [b]
24 00A1191C push eax
25 00A1191D mov ecx,dword ptr [a]
26 00A11920 push ecx
27 00A11921 call func (0A11366h)
28 00A11926 add esp,8
29 printf("I should run here\n");
30 00A11929 push offset string "I should run here\n" (0A16CFCh)
31 00A1192E call _printf (0A1132Ah)
32 00A11933 add esp,4
33 return 0;
34 00A11936 xor eax,eax
35 }
因为main函数本身真的是个函数!所以在执行我们编写的程序之前操作系统需要保存当前它运行的状态,就跟函数调用很类似
1 00A118E0 push ebp 这句话就是把操作系统的状态压栈
2 00A118E1 mov ebp,esp 然后把栈底指针挪到新的位置
3 00A118E3 sub esp,0D8h 扩展新的栈帧,你总不能让新的栈底和栈顶挨在一起吧?
过程图我会在讲到func函数的时候给出来,更容易理解,之后的push之类的就是为了保存现场和执行前准备
1 printf("I am main\n");
2 00A118FE push offset string "I am main\n" (0A16CF0h)
3 00A11903 call _printf (0A1132Ah)
4 00A11908 add esp,4
这部分就是调用printf的系统调用,因为库函数更多是对操作系统调用的再一次调用(封装?的说法也可以),因为我不是很懂这部分,也就不详细解释其中_printf的系统调用究竟怎么工作了
int a = 0xaaaa;
00A1190B mov dword ptr [a],0AAAAh
int b = 0xbbbb;
00A11912 mov dword ptr [b],0BBBBh
赋值阶段,这里给了双字,所以是dword 通过指针赋值~,ptr就是指针,mov dst src就是把后面的给前面的,就是dst=src这样的
1 func(a, b);
2 00A11919 mov eax,dword ptr [b]
3 00A1191C push eax 联合上一句的赋值语句构成参数压栈 y=b
4 00A1191D mov ecx,dword ptr [a]
5 00A11920 push ecx 联合上一句的赋值语句构成参数压栈 x=a
6 00A11921 call func (0A11366h) call函数调用,把fun函数的地址call一下
7 00A11926 add esp,8 push了这么多不得把栈顶指针挪一挪?
重头戏来了,这就是这次要讲述的主要部分,函数调用时候的栈帧!令人惊讶的是传的实参是放在main函数栈帧中的。我们来结合func的汇编看一下
1 int func(int x, int y)
2 {
3 00A11770 push ebp
4 00A11771 mov ebp,esp
5 00A11773 sub esp,0D8h
6 00A11779 push ebx
7 00A1177A push esi
8 00A1177B push edi
9 00A1177C lea edi,[ebp-0D8h]
10 00A11782 mov ecx,36h
11 00A11787 mov eax,0CCCCCCCCh
12 00A1178C rep stos dword ptr es:[edi]
13 int *p = &x;
14 00A1178E lea eax,[x]
15 00A11791 mov dword ptr [p],eax
16 p--;
17 00A11794 mov eax,dword ptr [p]
18 00A11797 sub eax,4
19 00A1179A mov dword ptr [p],eax
20 *p = (int)bug;
21 00A1179D mov eax,dword ptr [p]
22 00A117A0 mov dword ptr [eax],offset bug (0A1127Bh)
23 printf("x:%d,y:%d\n", x, y);
24 00A117A6 mov eax,dword ptr [y]
25 00A117A9 push eax
26 00A117AA mov ecx,dword ptr [x]
27 00A117AD push ecx
28 00A117AE push offset string "x:%d,y:%d\n" (0A16B3Ch)
29 00A117B3 call _printf (0A1132Ah)
30 00A117B8 add esp,0Ch
31 int c = 0xcccc;
32 00A117BB mov dword ptr [c],0CCCCh
33 return c;
34 00A117C2 mov eax,dword ptr [c]
35 }
1 int func(int x, int y)
2 {
3 00A11770 push ebp
4 00A11771 mov ebp,esp
5 00A11773 sub esp,0D8h
6 00A11779 push ebx
7 00A1177A push esi
8 00A1177B push edi
9 00A1177C lea edi,[ebp-0D8h]
10 00A11782 mov ecx,36h
11 00A11787 mov eax,0CCCCCCCCh
12 00A1178C rep stos dword ptr es:[edi]
没错了这一部分就是保存main函数的状态了,至于它保存了哪些main函数的状态,通过哪些寄存器保存的这里就不详细说明了(使用push命令的一般都是保存状态用的),刚才说的在这里上图,按步骤阅读更佳
- 这是func头两步的汇编指令
1 00A11770 push ebp 2 00A11771 mov ebp,esp
分别是把返回main函数的地址就是push ebp啦,压栈!,然后把栈顶指针赋值给栈底指针,就把栈底挪过来了,这就是新的栈底了!!因为main栈帧已经告一段落了
- 这就是扩展函数栈帧的方式啦,将栈顶指针往后挪动一定的位置1 00A11773 sub esp,0D8h ,这里挪动了D8(16进制),剩下的部分就是保存寄存器状态了,我就不讲了
简单来说,两个栈帧的大概情况就是这样的
所以很简单,我们不必通过y=100这样的语句就可以对y进行赋值改下代码就好
1 int func(int x, int y)
2 {
3 int *p = &x;
4 p++;
5 *p = 100;
6 printf("x:%d,y:%d\n", x, y);
7 int c = 0xcccc;
8 return c;
9 }
别着急!还没结束!汇编解释来了!
1 int *p = &x;
2 0009178E lea eax,[x] 这就是取偏移地址,取得x对于当前ebp的偏移地址
3 00091791 mov dword ptr [p],eax 简单赋值
4 p--;
5 00091794 mov eax,dword ptr [p] 看他把寄存器来回赋值的,其实就是将把地址减个4
6 00091797 sub eax,4
7 0009179A mov dword ptr [p],eax
8 *p = (int)bug;
9 0009179D mov eax,dword ptr [p] 把函数bug的地址传过来赋值
10 000917A0 mov dword ptr [eax],offset bug (09127Bh) offset也是取偏移的作用还是和lea有些不同的
11 printf("x:%d,y:%d\n", x, y);
12 000917A6 mov eax,dword ptr [y] 这就不说了是个系统调用,因为我也不是很懂
13 000917A9 push eax
14 000917AA mov ecx,dword ptr [x]
15 000917AD push ecx
16 000917AE push offset string "x:%d,y:%d\n" (096B3Ch)
17 000917B3 call _printf (09132Ah)
18 000917B8 add esp,0Ch
19 int c = 0xcccc;
20 000917BB mov dword ptr [c],0CCCCh 创建的局部变量位置在ebp下面~看图!
21 return c;
22 000917C2 mov eax,dword ptr [c]
没看到形参对不对?就两个实参,写完了不就改了么?不对哦~
x = 10;
000A178E mov dword ptr [x],0Ah
y = 10;
000A1795 mov dword ptr [y],0Ah
我把代码改成这样看汇编,这里并没有更改之前保存的寄存器里的东西,是取得了新的部分哦
dword ptr [x]这个已经不是之前的eax或者是ebx了~