X86C++反汇编13.函数
### 函数的调用约定1. 栈参数传递(传递方向)
2. 返回值
3. 栈平衡(参数平衡 由谁平衡)
函数的调用约定系统说了算
系统的api是标准调用约定,由此编译器也不得不提供这种调用约定
在**非window系统**上,当我们把函数调用约定用 stdcall 时,,并不代表编译过后还是stdcall ,可能会根据系统的api转为其他的调用约定
#### 栈
```
内存的资源是有限且应用程序共享的, 就会产生一个抢资源的问题,但是内存资源是有限的,因此我们希望能用重复的利用一段内存,这就是栈的意义
```
```
栈的本质是一段可以重复利用的的内存,(为了节省资源),跟全局变相相对(不共享,进程退出才会收)
```
```
只用全局变量不用局部变量,会导致内存消耗居高不下,并且可能还会出不够用的情况
```
```
进入函数时会打开栈空间,退出函数时会还原栈空间。
```
```
栈就是开始申请一段指定的内存,记录在pe里面
```
```
栈平衡:申请多少,释放多少,栈的不平衡可能并不会马上出问题
```
```
通常使用栈来传递函数参数,因此传递函数的栈也属于被调用函数栈空间中的一部分。
```
```
平衡细节:汇编过程中通常使用“ret xxxx”来平衡参数所使用的栈空间,当函数的参数为不定参数时,函数自身无法确定参数所使用的栈空间的大小,因此无法由函数自身执行平衡操作,需要此函数的调用者执行平衡操作。为了确定参数的平衡者,以及参数的传递方式,于是有了函数的调用约定
```
#### 调用约定
有时候编译器会为了适应各种操作平台,可能会转换调用约定
windows api 大部分都是stdcall 调用约定 ,但是如果是不定参,则会用 默认调用约定(c调用约定 cdecl)
##### 1.c调用约定 cdecl (默认)
```
int __cdecl MyAdd(int n1, int n2) {
int nNum1 = 0;
scanf_s("%d", &nNum1);
printf("%d\n", n1 + n2 + nNum1);
return n1 + n2;
}
```
1. 参数从右到左入栈
2. 函数调用者平衡参数栈(release平衡可能会一起做,debug会分开)
###### 栈结构
寄存器环境(env)
局部变量空间(local)
栈底(ebp)
返回地址(ret)
参数(从右往左入栈)
###### 参数和变量访问
```
参数访问: ebp +偏移(一般第一个参数在 ebp + 8 位置 ,后面参数要看前面参数大小)
```
局部变量访问: ebp -偏移 (offset)
relese 版还可以可以用esp +偏移 (offset)
参数访问: esp +偏移(offset)
局部变量访问: esp +偏移(offset)
用ebp 访问存在缺点:
- 浪费了一个寄存器
- 函数的头和尾必须保存和访问ebp,增加了内存访问次数,会浪费时间,因此ebp效率没有esp高
禁止使用ebp,采用esp ,把该项设为是即可
!(./notesimg/1658560891846-6c0fdc45-b30d-44cc-a3e5-ad676f9b8945.png)
用esp的效率高于ebp,那么为什么还要用ebp呢
因为当多层函数调用时,ebp 可以定位每层函数的信息,而esp不可以
调用该函数地址 调用该函数的参数
调用该函数的上层函数地址 [ + 4] 调用该函数的上层函数参数 [ + 8]
.............
............
这样,在函数崩了,没有源码的情况下,可以打印出调用堆栈,也可以打印出每个函数的参数(一般一个正常的数的参数不超过四个),这样就可以快速定位问题和出问题的函数(调用堆栈)
但是 esp 就不行,而且用汇编时,用 esp 看变量 和参数 时,偏移不是固定的,每次esp变化时需要重新计算偏移
用 esp访问参数和局部变量
int __cdecl MyAdd1(int n1, int n2) {
```
int nNum1 = 0;
```
```
scanf_s("%d", &nNum1);
```
```
printf("%d\n", n1 + n2 + nNum1);
```
```
return n1 + n2;
```
}
int main()
{
```
printf("10+20=%d\n", MyAdd1(10, 20));
```
```
return 0;
```
}
反汇编代码:
mov , eax
push esi
push edi
lea eax,
mov dword ptr , 0
push eax
push offset _Format; "%d"
call _scanf_s
mov eax,
mov edi,
add eax, edi
mov esi,
add eax, esi
push eax
push offset aD ; "%d\n"
call _printf
mov ecx,
lea eax,
add esp, 10h
栈列表:
esp:
+0 esi
+4 edi
+8 esi
+c edi
+10 local
+14
+18 ret
+1c 10
+20 20
每一行看的代码 栈都不一样,因此需要栈列表配合来看
没次看esp 的位置一直在变,这就代表参数和局部变量的位置不固定,而且每次平栈不一定会一次平完,中间平四个字节,后面再平完
在IDA中 arg_0表示第一个参数 , var_4 表示第一个局部变量,他会模拟列出栈
!(./notesimg/1658564111158-eeb4cfd8-5cbe-4b48-a771-3ffe49dc5910.png)!(./notesimg/1658564195375-74ae62af-1f62-445e-b13e-c0b625e4353b.png)
禁止内联函数:
!(./notesimg/1658560757371-98db03e7-abf9-48c4-886b-8760056aaa47.png)
int __cdecl MyAdd(int n1, int n2) {
```
int nNum1 = 0;
```
```
scanf_s("%d", &nNum1);
```
```
printf("%d\n", n1 + n2 + nNum1);
```
```
return n1 + n2;
```
}
int main()
{
```
printf("%d\n", MyAdd(4,1));
```
```
return 0;
```
}
反汇编:
push 1
push 4
call sub_401000
push eax ; ArgList
push offset Format ; "%d\n"
call sub_401100
add esp, 10h
xor eax, eax
retn
函数
push ebp
mov ebp, esp
lea eax,
push eax ; ArgList
push 0 ; Locale
push ; Format
push 1 ; Ix
call ds:__acrt_iob_func
add esp, 4
push eax ; Stream
call sub_401080
add esp, 10h
pop ebp
retn
##### 2.标准调用约定stdcall
```
int __stdcall MyAdd(int n1, int n2) {
return n1 + n2;
}
```
1. 参数从右到左入栈
2. 函数本身平衡参数栈
一般通过 retn 来平,自己是无法平栈的,需要平栈的一瞬间返回
```
int __stdcall MyAdd(int n1, int n2) {
return n1 + n2;
}
int main()
{
printf("10+20=%d\n", MyAdd(10, 20));
return 0;
}
反汇编:
.text:00401000 push ebp
.text:00401001 mov ebp, esp
.text:00401003 mov eax,
.text:00401006 add eax,
.text:00401009 pop ebp
.text:0040100A retn 8 ;平栈,返回的过程中平栈
自己平栈的话 可以先 pop 返回地址到寄存器 平栈后在 push 寄存器 在返回 ,但是这样会破坏寄存器环境,如果用 eax,就没了返回值因此如果有参数,自己是没办法平栈的
```
##### 3.快速调用约定fastcall
参数不入栈,通过寄存器访问
1. 参数1:ecx
```
参数2:edx
```
```
参数从右到左入栈
```
1. 函数本身平衡参数栈
跟stdcall 最大的区别就是 前2个参数用寄存器,还原时,因为流水线优化, ecx和edx 可能在前面使用,从而导致被误认为stdcall调用约定,那么如何快速去分呢:
第一步: 调用该函数之前有没有调用ecx 和 edx,
第二步: 如果有的话 然后进函数看看有没有push ecx 和 edx ,没有的话说明是参数,不然会破坏寄存器环境
```
int __fastcall MyAdd3(int n1, int n2, int n3, int n4) {
return n1 + n2 + n3 + n4;
}
```
```
int __fastcall MyAdd(int n1, int n2, int n3, int n4) {
return n1 + n2 + n3+ n4;
}
int main()
{
printf("%d\n", MyAdd(10, 20,30,40));
return 0;
}
反汇编代码:
push ebp
mov ebp, esp
lea eax,
add eax, ;此时这里 arg_0 是第 3 个参数, ecx,edx分别是第一个和第二个
add eax,
pop ebp
retn 8
```
##### 4.不定参的调用约定
不定参的调用约定是 c调用约定 cdecl,因此不确定每个参数个数,自己没办法平栈
```
int MyAdd4(int count, ...) {
//esp
int sum = 0;
va_list argList;
va_start(argList , count); //获得不定参栈位置
for (int i = 0; i < count; i++)
sum += va_arg(argList, int); //esp + 4
va_end(argList); //结束
return sum; //retn xxx
}
```
```
int stdcall MyAdd(int count, ...) {
//esp
int sum = 0;
va_list argList;
va_start(argList, count); //获得不定参栈位置
for (int i = 0; i < count; i++)
sum += va_arg(argList, int); //esp + 4
va_end(argList); //结束
return sum; //retn xxx
}
int main()
{
printf("......%d\n", MyAdd(6, 1, 2, 3, 4,5,6));
printf("......%d\n", MyAdd(4,1, 2,3,4));
return 0;
}
反汇编代码:
push 6
push 5
push 4
push 3
push 2
push 1
push 6
call sub_401000
push eax ; ArgList
push offset Format ; "......%d\n"
call sub_4010A0
push 4
push 3
push 2
push 1
push 4
call sub_401000
push eax ; ArgList
push offset Format ; "......%d\n"
call sub_4010A0
add esp, 40h
xor eax, eax
push ebp
mov ebp, esp
lea eax,
push eax ; ArgList
push 0 ; Locale
push ; Format
push 1 ; Ix
call ds:__acrt_iob_func
add esp, 4
push eax ; Stream
call sub_401030
add esp, 10h
pop ebp
retn
从右往左入栈 ,虽然我们写的是stdcall 但是 函数本身是没有平栈,而变成了C调用约定,因此所有的不定参不管你设置成什么调用约定都会转为c调用约定
```
区分c调用约定函数和变参函数 ,如果参数是通过栈去算的,大概率是变参,如果这个函数有多次调用,且每次参数数量不一样,那么绝对是变参函数,如果函数只用一次,那么变参函数可以还原成 c调用约定函数
#### 函数工作原理小结
##### 1 函数调用的一般工作流程
###### (1)参数传递
通过栈或寄存器方式传递参数。
###### (2)函数调用,将返回地址压栈
使用call指令调用参数,并将返回地址压入栈中。
###### (3)保存栈底
使用栈空间保存调用方的栈底寄存器ebp。
###### (4)申请栈空间和保存寄存器环境
根据函数内局部变量的大小抬高栈顶让出对应的栈空间,并且将即将修改的寄存器保存在栈内。
###### (5)函数实现代码
函数实现过程的代码。
###### (6)还原环境
还原栈中保存的寄存器信息。
###### (7)平衡栈空间
平衡局部变量使用的栈空间。
###### (8)ret返回,结束函数调用
从栈顶取出第(2)步保存的返回地址,更新EIP。
在非__cdecl调用方式下,平衡参数占用栈空间。
###### (9)调整esp,平衡栈顶
此处为__cdecl特有的方式,用于平衡参数占用的栈顶。
##### 2 两种编译选项下的函数识别
- Debug编译选项组下的函数识别非常简单,由于其注重调试的特性,其汇编代码基本上就是函数的原貌,只需对照汇编代码逐条分析即可将其还原成高级代码。
```
// Debug编译选项组下的函数识别
push reg/mem/imm;根据调用函数查看参数使用,可确定是否为参数
……
call reg/mem/imm;调用函数
add esp, xxxx;如果Debug编译选项组下是_cdecl调用方式,由调用方平衡栈顶
jmp FUN_ADDR;call指令调用处,可能存在使用跳转指令执行到函数
FUN_ADDR:
push ebp;保存栈底
……
mov eax,0CCCCCCCCh
rep stos dword ptr;初始化局部变量
……
pop ebp;还原栈底
ret;查看ret是否平衡栈
```
- 在O2选项下,_cdecl调用方式的函数调用结束后,并不一定会马上平衡栈顶,极有可能会复写传播并与其他函数一起平衡栈。由于函数实现改用了esp寻址,因此需要注意函数执行过程是否对esp进行了修改,如进行了修改,在平衡栈顶esp时,考察是否有对esp进行平衡恢复。当函数有参数时,检查参数是否在函数实现中被平衡,以确定其调用方式。
```
// Release版函数识别
push reg/mem/imm;根据调用函数查看参数使用,可确定是否为参数
……
call reg/mem/imm;调用函数
add esp, xxxx;在Release版下调用__cdecl方式的函数,栈平衡可能会复写传播,请注意
;函数实现内没有将局部变量初始化为0CCCCCCCCh
;若在函数体内不存在内联汇编或异常处理等代码,则使用esp寻址
```
### 变参函数支持不同类型参数
```
#include <stdio.h>
#include <stdarg.h>
#include <Windows.h>
int MyAdd1(int count, ...) {
int sum = 0;
va_list argList;
va_start(argList , count);
for (int i = 0; i < count; i++)
sum += va_arg(argList, int);
va_end(argList);
return sum;
}
int MyAdd(const char* fmt, ...) {
char buf;
int sum = 0;
va_list argList;
va_start(argList, fmt);
_vsnprintf_s(buf, sizeof(buf), fmt, argList);
va_end(argList);
puts(buf);
return sum; //retn xxx
}
//vararg
int main()
{
MyAdd5("%d %f %c", 1, 3.5f, 'a');
return 0;
}
```
感谢分享!! 啥也不说了,楼主就是给力! 楼主,不论什么情况你一定要hold住!hold住就是胜利! 楼主,不论什么情况你一定要hold住!hold住就是胜利!
页:
[1]