函数调用堆栈
hello.c
#include
int add(int a, int b) {
a = a + 1;
b = b + 2;
int c = a + b;
return c;
}
int main() {
int i = add(1,2);
return 0;
}
gcc -S -masm=intel hello.c -o hello_intel.s
add:
push ebp
mov ebp, esp
sub esp, 16
add DWORD PTR [ebp+8], 1
add DWORD PTR [ebp+12], 2
mov edx, DWORD PTR [ebp+8]
mov eax, DWORD PTR [ebp+12]
add eax, edx
mov DWORD PTR [ebp-4], eax
mov eax, DWORD PTR [ebp-4]
leave
ret
main:
push ebp
mov ebp, esp
sub esp, 16
push 2 ;参数从右到左入栈
push 1
call add
add esp, 8
mov DWORD PTR [ebp-4], eax
mov eax, 0
leave
ret
函数调用堆栈顺序
- 参数从右到左入栈
- Call(eip入栈)
- ebp入栈
- 保存用到的寄存器
- 业务ebp作为基址访问局部变量[ebp-n]和参数[ebp+n]
- 恢复用到的寄存器
- leave (mov esp,ebp; pop ebp)
- ret (eip出栈)
- 调用方esp+n(调用参数平栈. 8 9等价ret n)
ret; eip出栈 | ||
leave;等价于 mov esp,ebp;pop ebp | ||
返回参数EAX | ||
局部变量 ebp-4n访问 | esp-16 | |
ebp-4(局部变量c) | ||
push ebp | ebp=esp | |
call eip 入栈 | ||
入参 ebp+8+4n访问 | push 1(入参a) | |
push 2(入参b) | ||
调用方esp+8, 参数a和b堆栈平衡 |
调用约定
参数压栈 | 堆栈平衡 | ||
__cdecl | 从右至左入栈 | 调用者 | c语言默认 |
__stdcall | 从右至左入栈, this指针做为最后一个参数入栈 | 自身 | c++ |
__fastcall | ECX EDX传递前2个 剩下从右至左入栈 | 自身 | 部门win32API |
__thiscall | 从右至左入栈, ECX传递this指针 | 自身 |
变量
全局变量
全局变量是直接生成并存储在PE文件中的,因此有固定的空间和大小
全局变量的内存地址不变. 游戏外挂中的找基址, 其实就是找全局变量.
char 如何存储和显示中文
?: if_else
c语言源代码
int calculate(int calc, int a, int b) {
return calc <= 0 ? a + b : b - a;
}
int calculate(int calc, int a, int b) {
if (calc <= 0) {
return a + b;
}
else {
return b - a;
}
}
汇编解析
calculate:
push ebp
mov ebp, esp
cmp DWORD PTR [ebp+8], 0 ;比较
jg .L2
mov edx, DWORD PTR [ebp+12] ;?逻辑
mov eax, DWORD PTR [ebp+16]
add eax, edx
jmp .L4
.L2:
mov eax, DWORD PTR [ebp+16] ;:逻辑
sub eax, DWORD PTR [ebp+12]
.L4:
pop ebp
ret
if_elseif_else
calculate:
cmp DWORD PTR [ebp+8], 0
jne .L2
;if
jmp .L3
.L2:
cmp DWORD PTR [ebp+8], 1
jne .L4
; else if
jmp .L3
.L4:
; else
.L3:
pop ebp
Switch
void test_switch(int n) {
switch (n)
{
case 1: printf("1"); break;
case 2: printf("2"); break;
case 3: printf("3"); break;
case 4: printf("4"); break;
default:
printf("default");
break;
}
}
;switch (n)
00414035 mov eax,dword ptr [n] ;参数n
00414038 mov dword ptr [ebp-0C4h],eax ;局部变量[ebp-0C4h]
0041403E mov ecx,dword ptr [ebp-0C4h]
00414044 sub ecx,1 ;局部变量[ebp-0C4h]-1
00414047 mov dword ptr [ebp-0C4h],ecx
0041404D cmp dword ptr [ebp-0C4h],3
00414054 ja $LN7+0Fh (041409Fh) ;如果局部变量[ebp-0C4h]>3(参数n>4)则跳转default
00414056 mov edx,dword ptr [ebp-0C4h]
0041405C jmp dword ptr [edx*4+4140C0h] ;最终跳转算法 (n-1)*4+case表地址
;case 1: printf("1"); break;
00414063 push offset string "1" (042BC30h)
00414068 call _printf (0411285h)
0041406D add esp,4
00414070 jmp $LN7+1Ch (04140ACh) ;break跳出switch
;case 2: printf("2"); break;
00414072 push offset string "2" (042BC34h)
00414077 call _printf (0411285h)
0041407C add esp,4
0041407F jmp $LN7+1Ch (04140ACh) ;break跳出switch
;case 3: printf("3"); break;
00414081 push offset string "3" (042BC38h)
00414086 call _printf (0411285h)
0041408B add esp,4
0041408E jmp $LN7+1Ch (04140ACh) ;break跳出switch
;case 4: printf("4"); break;
00414090 push offset string "4" (042BC3Ch)
00414095 call _printf (0411285h)
0041409A add esp,4
0041409D jmp $LN7+1Ch (04140ACh) ;break跳出switch
;default: printf("default");
0041409F push offset string "default" (042BC40h)
004140A4 call _printf (0411285h)
004140A9 add esp,4
004140AC
查看Case表的数据(4140C0h)
当case项不连续时
void test_switch(int n) {
switch (n)
{
case 1: printf("1"); break;
case 3: printf("3"); break;
case 9: printf("9"); break;
case 2: printf("2"); break;
case 8: printf("8"); break;
default:
printf("default");
break;
}
}
case表的数据
case 1的地址 |
case 2的地址 |
case 3的地址 |
default的地址 |
default的地址 |
default的地址 |
default的地址 |
case 8的地址 |
case 9的地址 |
结论: switch通过构建case表, case项的值为表的索引直接内存地址调用,因此效率高于if else
推荐用法: case项>=4且case项尽可能连续(节省case项表的大小)
while循环
int add(int a, int b) {
int c = 0;
do{
c += a;
a++;
} while (a < b);
return c;
}
add:
push ebp
mov ebp, esp
sub esp, 16
mov DWORD PTR [ebp-4], 0
jmp .L2 ;去掉变成do while循环
.L3:
mov eax, DWORD PTR [ebp+8] ;循环体
add DWORD PTR [ebp-4], eax
add DWORD PTR [ebp+8], 1
.L2:
mov eax, DWORD PTR [ebp+8] ;while判断
cmp eax, DWORD PTR [ebp+12]
jl .L3
mov eax, DWORD PTR [ebp-4]
leave
ret
for 等价于 while
int add(int a, int b) {
int sum;
for (sum = 0; a < b; a++) {
sum += a;
}
return sum;
}
add:
push ebp
mov ebp, esp
sub esp, 16
mov DWORD PTR [ebp-4], 0 ;for第一个分号初始化
jmp .L2
.L3:
mov eax, DWORD PTR [ebp+8] ;for循环体
add DWORD PTR [ebp-4], eax
add DWORD PTR [ebp+8], 1 ;for最后一个分号计数器变化
.L2:
mov eax, DWORD PTR [ebp+8] ;for循环判断,和while循环完全等价
cmp eax, DWORD PTR [ebp+12]
jl .L3
mov eax, DWORD PTR [ebp-4]
leave
ret
C++多态实现分析
普通成员函数重写,不构成多态
#include
using namespace std;
class HuMan {
public:
void show() {
cout << "HuMan" << endl;
}
};
class Person : HuMan {
public:
//非虚函数重写,不构成多态
void show() {
cout << "Person" << endl void test_showperson person person->show();
}
int main() {
Person p;
test_show(&p); //调用的是HuMan::show, 输出HuMan
return 0;
}
person.show(); 对应汇编实现,可以看出是函数的直接调用
mov ecx,dword ptr [person]
call Person::show (0411B0Eh)
虚函数重写,构成多态
#include
using namespace std;
class HuMan {
public:
virtual void show() {
cout << "HuMan" << endl;
}
};
class Person : HuMan {
public:
//虚函数重写,构成多态
void show() {
cout << "Person" << endl void test_showperson person person->show();
}
int main() {
Person p;
test_show(&p); //调用的是Person:show, 输出Person
return 0;
}
对应汇编实现
mov eax,dword ptr [person] //person对象函数首地址,即虚表地址
mov edx,dword ptr [eax]
mov esi,esp
mov ecx,dword ptr [person] //ecx存放this指针
mov eax,dword ptr [edx] //虚表第一项,第二项为[edx+4]
call eax
可以看到, p对象的第一个成员(0地址偏移)是_vfptr(虚函数表指针)
该虚函数表的第一项就是 call Person:show的跳转
多态(虚函数重写)就是通过虚函数表间接调用实现的
C++异常机制的实现方式和开销分析
参考:
https://www.baiy.cn/doc/cpp/inside_exception.htm