五千字从反汇编一步步详细讲述函数栈帧的创建与销毁
这篇文章带你深入了解函数参数的传递和销毁过程,形参实参的关系,和局部变量的创建等等。
------------------------------------------------------------文章较长,满满干货---------------------------------------------------------------
文章所用环境:windows x64 VS2013
文章目录
预备知识
1 . 内存四区
在介绍函数栈帧开辟之前先草草画一个内存四区的图方便后续理解:
我们知道堆区和栈区是相对着使用空间的,堆区从低地址到高地址,而栈区从高地址到低地址
2 . 栈帧-----摘自百度百科
栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。
从逻辑上讲,栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等。
---------------------------------说白了就是用来记录的。
3 . 寄存器
在我们了解了栈区的性质之后,接下来需要大概说明一下计算机至关重要的元件:寄存器
这里想要具体了解的可以去看看百度百科https://baike.baidu.com/item/%E5%AF%84%E5%AD%98%E5%99%A8
以下都是寄存器的一些种类:
其中最重要的就是ebp和esp,
重要的事情说三遍!!
ebp和esp这两个寄存器是存放函数地址的 , 专门来维护栈帧
ebp和esp这两个寄存器是存放函数地址的 , 专门来维护栈帧
ebp和esp这两个寄存器是存放函数地址的 , 专门来维护栈帧
而栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。
寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。
那么接下来我们通过一段代码和对应的汇编语言来说明
#include<stdio.h>
int add(int a, int b);
int main()
{
int a = 10;
int b = 20;
int ret = add(a, b);
printf("%d", ret);
return 0;
}
int add(int a, int b)
{
return a + b;
}
int main()
{
00C61400 push ebp
00C61401 mov ebp,esp
00C61403 sub esp,0E4h
00C61409 push ebx
00C6140A push esi
00C6140B push edi
00C6140C lea edi,[ebp+FFFFFF1Ch]
00C61412 mov ecx,39h
00C61417 mov eax,0CCCCCCCCh
00C6141C rep stos dword ptr es:[edi]
int a = 10;
00C6141E mov dword ptr [ebp-8],0Ah
int b = 20;
00C61425 mov dword ptr [ebp-14h],14h
int c = add(a, b);
00C6142C mov eax,dword ptr [ebp-14h]
00C6142F push eax
00C61430 mov ecx,dword ptr [ebp-8]
00C61433 push ecx
00C61434 call 00C61096
00C61439 add esp,8
00C6143C mov dword ptr [ebp-20h],eax
printf("%d", c);
00C6143F mov esi,esp
00C61441 mov eax,dword ptr [ebp-20h]
00C61444 push eax
00C61445 push 0C65858h
00C6144A call dword ptr ds:[00C69114h]
00C61450 add esp,8
00C61453 cmp esi,esp
00C61455 call 00C6113B
return 0;
00C6145A xor eax,eax
}
00C6145C pop edi
}
00C6145D pop esi
00C6145E pop ebx
00C6145F add esp,0E4h
00C61465 cmp ebp,esp
00C61467 call 00C6113B
00C6146C mov esp,ebp
00C6146E pop ebp
00C6146F ret
这里想要了解汇编指令可以了解这篇文章:
https://blog.csdn.net/sinat_27382047/article/details/72810788反汇编指令
一、main函数栈帧的开辟
1) 地基:main函数也是被其他函数调用的
- c语言中的main函数是被_start函数调用的,
- 函数都是在栈上开辟栈帧的, 那么有栈帧必然需要ebp和esp去维护栈帧,
ebp指向栈帧底部(高地址),esp指向栈帧顶部(低地址)
2) 压栈:从已有地基打起从高地址往低地址盖大楼
008C1400 push ebp
008C1401 mov ebp,esp
008C1403 sub esp,0E4h
008C1409 push ebx
008C140A push esi
008C140B push edi
008C140C lea edi,[ebp-0E4h]
008C1412 mov ecx,39h
008C1417 mov eax,0CCCCCCCCh
008C141C rep stos dword ptr es:[edi]
PUSH 指令:首先减少ESP的值,再将源操作数复制到堆栈。操作数是16位的,则ESP减2(字节),操作数是32位的,则 ESP减4(字节),esp通常是指向栈顶的(这里要指出的是:学过单片机的同学请注意单片机种的堆栈与Windows下的堆栈是不同的,请参考相应资料),这里顶部是地址小的区域,那么,压入堆栈的数据越多,esp也就越来越小;
- 首先看第一段进行push指令压栈操作,从高地址往低地址把ebp压栈
并且esp调整指向栈帧顶部(也就是减少ESP的值,16位的,则ESP减2字节,32位的,则 ESP减4字节)
接下来是move指令:mov ebp,esp , 就是把第二个参数拷贝到第一个参数。
即:把ebp的地址指向改为esp的地址,esp,ebp指向同一块
sub esp,0E4h
(这里sub是减,0E4h 是十六进制E4,也就是esp — E4,esp存放的地址减E4)
那么此时esp和ebp之间就分配了一定空间,即main函数的栈帧.
push ebx
push esi
push edi
这三个都是寄存器,依然压栈操作,并且esp减少指向栈顶(低地址)
lea:取得第二个参数地址后放入到前面的寄存器(第一个参数)中。
然而lea也同样可以实现mov的操作,例如:lea edi,[ebp-24h]
方括号表示存储单元,也就是提取方括号中的数据所指向的内容,然而lea提取内容的地址,这样就实现了把地址(ebp-24h)放入到了edi
lea edi,[ebp-E4h]
008C140C lea edi,[ebp-0E4h]
008C1412 mov ecx,39h
008C1417 mov eax,0CCCCCCCCh
008C141C rep stos dword ptres:[edi]
- 这段的作用就是把ebp - 0E4h的地址赋给edi,39(十六进制)赋给ecx,把CCCCCCCC(十六进制)赋给eax
并且从edi开始的以下39h个4字节都变成eax也就是CCCCCCCC
注意这里39h = 57,57个4字节也就是57 × 4 = 228转化成十六进制就是0E4h,也就是从ebp到ebp - 0E4h的那段空间全都变成CCCCCCCC
而CCCCCCCC也就是烫所对应的乱码
int a = 10;
008C141E mov dword ptr [a],0Ah
int b = 20;
008C1425 mov dword ptr [b],14h
那么我们关闭反汇编中的符号显示:
int a = 10;
008C141E mov dword ptr [ebp-8],0Ah
int b = 20;
008C1425 mov dword ptr [ebp-14h],14h
接下来把十六进制0A也就是10拷贝给a,十六进制14也就是16+4=20拷贝给b
而a 的地址就是ebp - 8h,b的地址就是ebp - 14h也就是ebp - 20
二、代码中add函数栈帧的开辟
int ret = add(a, b);
00951943 mov eax,dword ptr [b]
00951946 push eax
00951947 mov ecx,dword ptr [a]
0095194A push ecx
0095194B call 009513B1
00951950 add esp,8
00951953 mov dword ptr [ebp-20h],eax
1 ) 传参
00951943 mov eax,dword ptr [b]
00951946 push eax
00951947 mov ecx,dword ptr [a]
0095194A push ecx
方括号表示提取方括号中的数据所指向的内容.
把b的值拷贝给eax,并且把eax压栈,同理也把a的值拷贝给ecx,并压栈
> 008C1434 call 008C1096
> call指令进行调用函数并保存地址
2 ) add函数:
代码如下(示例):
int add(int a,int b)
{
00C613C0 push ebp
00C613C1 mov ebp,esp
00C613C3 sub esp,0C0h
00C613C9 push ebx
00C613CA push esi
00C613CB push edi
00C613CC lea edi,[ebp+FFFFFF40h]
00C613D2 mov ecx,30h
00C613D7 mov eax,0CCCCCCCCh
00C613DC rep stos dword ptr es:[edi]
return a + b;
00C613DE mov eax,dword ptr [ebp+8]
00C613E1 add eax,dword ptr [ebp+0Ch]
}
00C613E4 pop edi
00C613E5 pop esi
00C613E6 pop ebx
00C613E7 mov esp,ebp
00C613E9 pop ebp
00C613EA ret
- 还是一样进行add函数栈帧的开辟
00C613C0 push ebp
00C613C1 mov ebp,esp
00C613C3 sub esp,0C0h
00C613C9 push ebx
00C613CA push esi
00C613CB push edi
00C613CC lea edi,[ebp+FFFFFF40h]
00C613D2 mov ecx,30h
00C613D7 mov eax,0CCCCCCCCh
00C613DC rep stos dword ptr es:[edi]
同main函数一样不再赘述,这里注意存放ebp的是main的栈帧底部,并且此时esp ebp用来维护add函数的栈帧
return a + b;
00C613DE mov eax,dword ptr [ebp+8]
00C613E1 add eax,dword ptr [ebp+0Ch]
- 接下来把ebp+ 8 那块地址所对应的值(如图也就是10)拷贝给寄存器eax
- 并且把ebp +0Ch 的地址所对应 的值(也就是20)加到 eax
三、add函数栈帧的销毁
00C613E4 pop edi
00C613E5 pop esi
00C613E6 pop ebx
00C613E7 mov esp,ebp
00C613E9 pop ebp
00C613EA ret
pop:与push相反,esp每次加4(字节),数据出栈并放到后边寄存器里。
这里依次弹出也就是edi放到edi里,esi放到esi,ebx放到ebx
- 此时edi,esi,ebx出栈,并把ebp拷贝给esp
- 接着继续出栈,把栈顶弹出放到ebp里边。而此时的栈顶就是之前存放的Main函数的ebp
00C613E9 pop bp
- pop之后ebp指向main函数的栈底
ret:跳转会调用函数的地方。对应于call,返回到对应的call调用的下一条指令,若有返回值,则放入eax中
此时弹出栈顶(call指令下一条地址)并返回到该地址
形参的销毁
00C61439 add esp,8
- 紧接着esp往下加8个字节
- 此刻形参销毁