C语言函数反汇编
从反汇编的角度,看C是怎么创建函数的。在创建函数的过程中系统做了哪些准备,数据被存放在那里,又是如何完成函数操作的,参数与局部变量在底层硬件中又是怎么流动的?
函数的创建与堆栈操作密切相关
- 空函数
- 主函数
- 带参函数
- 无参、有局部变量的函数
- 带参且有局部变量的函数
分别从这些函数入手,查看反汇编代码,发现异同,从而加深C语言函数定义的正向理解,以及函数的反汇编代码。
空函数与主函数:
//空函数与主函数
#include "stdio.h"
void Plus(){
}
int main()
{
Plus();
}
对应的汇编代码:
//主函数部分
int main ()
{
//保存栈底
005C13E0 push ebp
//提升堆栈
005C13E1 mov ebp,esp
005C13E3 sub esp,0C0h
//保存现场
005C13E9 push ebx
005C13EA push esi
005C13EB push edi
005C13EC lea edi,[ebp-0C0h]
//填充堆栈
005C13F2 mov ecx,30h
005C13F7 mov eax,0CCCCCCCCh
005C13FC rep stos dword ptr es:[edi]
//调用函数Plus
Plus();
005C13FE call @ILT+265(_Plus) (5C110Eh)
} //到这里主函数大括号包起来的部分就已经结束了
//清空eax
005C1403 xor eax,eax
//恢复现场
005C1405 pop edi
005C1406 pop esi
005C1407 pop ebx
//恢复堆栈
005C1408 add esp,0C0h
005C140E cmp ebp,esp //用于比较栈底(ebp)和栈底(esp)两个存储的地址是否相同,并生成一个结果
005C1410 call @ILT+305(__RTC_CheckEsp) (5C1136h) //看上面的结果是否是相同的,如果不是相同的,则针对这一问题进行下一步操作
005C1415 mov esp,ebp
005C1417 pop ebp
005C1418 ret
005C1410 call @ILT+305(__RTC_CheckEsp) (5C1136h)
#下面是上面语句的跳转
005C1490 jne esperror (5C1493h) //如果是一样的,那就不跳转到处理程序部分了
005C1492 ret
#如果栈顶与栈底不一样的处理代码部分
esperror:
005C1493 push ebp
005C1494 mov ebp,esp
005C1496 sub esp,0
005C1499 push eax
005C149A push edx
005C149B push ebx
005C149C push esi
005C149D push edi
005C149E mov eax,dword ptr [ebp+4]
005C14A1 push 0
005C14A3 push eax
005C14A4 call _RTC_Failure (5C11AEh)
005C14A9 add esp,8
005C14AC pop edi
005C14AD pop esi
005C14AE pop ebx
005C14AF pop edx
005C14B0 pop eax
005C14B1 mov esp,ebp
005C14B3 pop ebp
005C14B4 ret
在函数体中实际操作部分只有调用Plus函数这么一句 Plus():call @ILT+265(_Plus) (5C110Eh),其余部分都是在为创建这个函数做准备工作:保存栈底,提升堆栈,保存现场,填充堆栈,完成函数内容,回复现场,恢复堆栈。
为函数需要创建空间 收尾工作 上面是主函数的完整过程。
下面是主函数中调用的空函数:
void Plus(){
005C13B0 push ebp
005C13B1 mov ebp,esp
005C13B3 sub esp,0C0h
005C13B9 push ebx
005C13BA push esi
005C13BB push edi
005C13BC lea edi,[ebp-0C0h]
005C13C2 mov ecx,30h
005C13C7 mov eax,0CCCCCCCCh
005C13CC rep stos dword ptr es:[edi]
}
005C13CE pop edi
005C13CF pop esi
005C13D0 pop ebx
005C13D1 mov esp,ebp
005C13D3 pop ebp
005C13D4 ret
其实空函数与我们的主函数是一样的。函数调用前和函数调用后是一摸一样。
推测出相同之处:调用函数的基本步骤是一样的。
这里的主函数和主函数中调用的函数都没有写东西,所以看一下写了东西的函数是什么样。
有局部变量的函数:
#include "stdio.h"
int Plus1(){
int x=1;
int y=2;
return x+y;
}
int main (){
Plus1();
}
对应的汇编代码:
int main (){
007F1450 push ebp
007F1451 mov ebp,esp
007F1453 sub esp,0C0h
007F1459 push ebx
007F145A push esi
007F145B push edi
007F145C lea edi,[ebp-0C0h]
007F1462 mov ecx,30h
007F1467 mov eax,0CCCCCCCCh
007F146C rep stos dword ptr es:[edi]
Plus1();
007F146E call @ILT+250(_Plus1) (7F10FFh)
}
007F1473 xor eax,eax
007F1475 pop edi
007F1476 pop esi
007F1477 pop ebx
007F1478 add esp,0C0h
007F147E cmp ebp,esp
007F1480 call @ILT+305(__RTC_CheckEsp) (7F1136h)
007F1485 mov esp,ebp
007F1487 pop ebp
007F1488 ret
看主函数其实还是没有什么不同。但是跳转到Plus1这个函数时就发生了些不同。
int Plus1(){
//为函数需要创建空间
008713B0 push ebp
008713B1 mov ebp,esp
008713B3 sub esp,0F0h
008713B9 push ebx
008713BA push esi
008713BB push edi
008713BC lea edi,[ebp-0F0h]
008713C2 mov ecx,3Ch
008713C7 mov eax,0CCCCCCCCh
008713CC rep stos dword ptr es:[edi]
int x=1;
//把1存入x代表的内存地址中
008713CE mov dword ptr [x],1
int y=2;
//把2存入y代表的内存地址中
008713D5 mov dword ptr [y],2
return x+y;
//先将x这个地址的值给到eax这个寄存器中
008713DC mov eax,dword ptr [x]
//再把y这个地址的值加到eax寄存器里面
008713DF add eax,dword ptr [y] //就这样就完成了两个局部变量相加的过程,结果被存到了eax中
}
//收尾工作
008713E2 pop edi
008713E3 pop esi
008713E4 pop ebx
008713E5 mov esp,ebp
008713E7 pop ebp
008713E8 ret
其实这里并没有讲清楚局部变量x和y存放的位置。
所以我们打开了x32dbg。这里就很清楚的写出来了,局部变量x=1是存放在堆栈的[ebp-8]这个位置,局部变量y=2是存放在堆栈的[ebp-14]这个位置的。也就是栈底的上面。
缓冲区溢出。
带参数的局部变量:
#include "stdio.h"
int Plus2(int x , int y){
return x+y;
}
int main (){
Plus2(1,2);
}
对应的汇编代码
int main (){
//为函数需要创建空间
004313F0 push ebp
004313F1 mov ebp,esp
004313F3 sub esp,0C0h
004313F9 push ebx
004313FA push esi
004313FB push edi
004313FC lea edi,[ebp-0C0h]
00431402 mov ecx,30h
00431407 mov eax,0CCCCCCCCh
0043140C rep stos dword ptr es:[edi]
Plus2(1,2);
//这里直接将1和2压入堆栈中
0043140E push 2
00431410 push 1
00431412 call @ILT+255(_Plus2) (431104h)
//堆栈平衡,外平衡
00431417 add esp,8
}
//收尾工作
0043141A xor eax,eax
0043141C pop edi
0043141D pop esi
0043141E pop ebx
0043141F add esp,0C0h
00431425 cmp ebp,esp
00431427 call @ILT+305(__RTC_CheckEsp) (431136h)
0043142C mov esp,ebp
0043142E pop ebp
0043142F ret
我们可以看到这时主函数里面有了不同,它将参数直接压入堆栈中,然后再调用Plus2函数,最后还多了一个平衡堆栈的步骤。
int Plus2(int x , int y){
004313B0 push ebp
004313B1 mov ebp,esp
004313B3 sub esp,0C0h
004313B9 push ebx
004313BA push esi
004313BB push edi
004313BC lea edi,[ebp-0C0h]
004313C2 mov ecx,30h
004313C7 mov eax,0CCCCCCCCh
004313CC rep stos dword ptr es:[edi]
return x+y;
//在eax中进行运算
004313CE mov eax,dword ptr [x]
004313D1 add eax,dword ptr [y]
}
004313D4 pop edi
004313D5 pop esi
004313D6 pop ebx
004313D7 mov esp,ebp
004313D9 pop ebp
004313DA ret
同样,只看编译器上是显示不出存储位置的。
同理,我们现在可以看到它是存放在堆栈的[ebp+8]和[ebp+c]这里面的。 堆栈平衡
带参且有局部变量的函数:
#include "stdio.h"
int Plus3(int x , int y){
int a=1;
int b=2;
return x+y+a+b;
}
int main (){
Plus3(1,2);
}
对应汇编代码:
主函数部分:
int main (){
003E1400 push ebp
003E1401 mov ebp,esp
003E1403 sub esp,0C0h
003E1409 push ebx
003E140A push esi
003E140B push edi
003E140C lea edi,[ebp-0C0h]
003E1412 mov ecx,30h
003E1417 mov eax,0CCCCCCCCh
003E141C rep stos dword ptr es:[edi]
Plus3(1,2);
003E141E push 2
003E1420 push 1
003E1422 call @ILT+255(_Plus3) (3E1104h)
003E1427 add esp,8
}
003E142A xor eax,eax
003E142C pop edi
003E142D pop esi
003E142E pop ebx
003E142F add esp,0C0h
003E1435 cmp ebp,esp
003E1437 call @ILT+305(__RTC_CheckEsp) (3E1136h)
003E143C mov esp,ebp
003E143E pop ebp
003E143F ret
调用函数Plus3部分:
int Plus3(int x , int y){
003E13B0 push ebp
003E13B1 mov ebp,esp
003E13B3 sub esp,0D8h
003E13B9 push ebx
003E13BA push esi
003E13BB push edi
003E13BC lea edi,[ebp-0D8h]
003E13C2 mov ecx,36h
003E13C7 mov eax,0CCCCCCCCh
003E13CC rep stos dword ptr es:[edi]
int a=1;
003E13CE mov dword ptr [a],1
int b=2;
003E13D5 mov dword ptr [b],2
return x+y+a+b;
003E13DC mov eax,dword ptr [x]
003E13DF add eax,dword ptr [y]
003E13E2 add eax,dword ptr [a]
003E13E5 add eax,dword ptr [b]
}
003E13E8 pop edi
003E13E9 pop esi
003E13EA pop ebx
003E13EB mov esp,ebp
003E13ED pop ebp
003E13EE ret
其实可以发现就是带参和带局部变量的结合,没有什么其他变化。