汇编(函数调用堆栈图)
本文介绍了x86架构下函数调用的关键机制。首先对比了JMP和CALL指令的区别,指出CALL在跳转时会将返回地址压栈,实现函数的多处调用。其次讲解了参数传递方法,当参数超过8个时需使用堆栈传参。然后详细阐述了堆栈平衡的概念,强调函数返回前要调整ESP指针恢复堆栈状态,可通过外平栈(add esp)或内平栈(ret n)实现。最后介绍了更安全的EBP寻址方式,通过保存/恢复EBP寄存器维护栈帧结构,
函数调用
我们知道函数本身其实就是多条指令的合集,只不过这多个指令的合集可以被多次重复调用
我们学过两个指令 JMP和CALL指令,都是跳转到对应目标地址继续执行下一条指令,但JMP和CALl的区别是什么?
jmp只能修改eip的值,要跳回去还要再加一个jmp来实现回跳的功能,但如果有多个地方要使用这个函数,那这时,jmp就无法使用了
call是在同时修改eip的同时,还把call指令的下一条地址压入栈中,再ret返回时,取出栈中的地址放入eip中,这时不管什么地方调用都不会出现问题
参数传递
在x86模式下,我们要给一个函数传递参数可以用到8个通用寄存器,但是如果我们要传递的参数不止8个呢?
这时,就要使用我们的堆栈传参
// 使用堆栈完成10个数相加
_asm{
push 1
push 2
push 3
push 4
push 5
push 6
push 7
push 8
push 10
push 11
call 0x4183E1
add eax,DWORD PTR DS:[esp+28] // 第十个参数
add eax,DWORD PTR DS:[esp+24] // 第九个参数
add eax,DWORD PTR DS:[esp+20] // 第八个参数
add eax,DWORD PTR DS:[esp+1c] // 第七个参数
add eax,DWORD PTR DS:[esp+18] // 第六个参数
add eax,DWORD PTR DS:[esp+14] // 第五个参数
add eax,DWORD PTR DS:[esp+10] // 第四个参数
add eax,DWORD PTR DS:[esp+c] // 第三个参数
add eax,DWORD PTR DS:[esp+8] // 第二个参数
add eax,DWORD PTR DS:[esp+4] // 第一个参数
ret
}
堆栈平衡
什么是堆栈平衡?
如果通过堆栈传递参数了,那么在函数执行完毕后,要平衡参数导致的堆栈变化,要在RET这条指令之前,ESP指向的是我们压入栈中的地址
_asm{
...
call 0x12345678
...
// 函数调用后有使用堆栈的操作
mov eax,1
push eax
ret // 堆栈不平衡
}
修改后
// 在函数执行完成后(外平栈)
_asm{
...
call 0x12345678
add esp,4 // 堆栈平衡
// 函数调用后有使用堆栈的操作
mov eax,1
push eax
ret
...
}
// // 在函数执行里面(内平栈)
_asm{
...
call 0x12345678
...
// 函数调用后有使用堆栈的操作
mov eax,1
push eax
ret 4 // 堆栈平衡
...
}
EBP寻址
esp寻址的弊端
我们在往堆栈中存入参数时,可以根据esp来加上偏移来取出参数,但是这种通过esp来寻址的方式并不友好,前面有几个push就要加多个对应的偏移来获取,可以采取另一种寻址方式EBP寻址

_asm{
...
push 1
push 2
call 0x12345678
...
push ebp // 保留原来ebp的值
mov ebp,esp
sub esp,1c // 提升esp
mov eax,dword ptr ss:[ebp + 8] // 第二个参数
add eax,dword ptr ss:[ebp + c] // 第一个参数
mov esp,ebp // 把ebp的值给到esp (还原esp)
pop ebp // 还原ebp
ret
...
}
代码测试
// 测试函数调用对应的堆栈关系
#include <iostream>
int test(int a, int b, int c) {
int e = 0;
int d = 2;
e = a + 1;
d = c + 1;
return e;
}
int main() {
printf("begin\n");
int a = 0;
a = test(1, 2, 3);
printf("end\n");
return 0;
}
编译生成.exe程序后,我们放到x64dbg中进行分析

这里就是我们main函数的执行位置了
现在我们来逐行分析一下每条c++对应的汇编指令都作了什么
0000000140011710 <testfunc.main> | push rbp | 保存rbp的位置
0000000140011712 | sub rsp,70 | 提升堆栈
0000000140011716 | lea rbp,qword ptr ss:[rsp+20] | [ss:[rsp+20]]:__scrt_release_startup_lock+D
000000014001171B | lea rcx,qword ptr ds:[<"begin\n"...>] | ds:[0000000140018BB0]:"begin\n"获取数据段中的字符串
0000000140011722 | call testfunc.14001114A | 执行printf函数
0000000140011727 | nop | 字节对齐
0000000140011728 | mov dword ptr ss:[rbp],0 | 向栈中rbp地址位置存入0
000000014001172F | mov r8d,3 | main.cpp:16, r8d:&"ALLUSERSPROFILE=C:\\ProgramData"
0000000140011735 | mov edx,2 | edx:&"C:\\Users\\BananaLi\\Desktop\\testfunc.exe"
000000014001173A | mov ecx,1 |
000000014001173F | call testfunc.140011104 | test()
0000000140011744 | mov dword ptr ss:[rbp],eax |
0000000140011747 | lea rcx,qword ptr ds:[<"end\n"...>] | ds:[0000000140018BB8]:"end\n"获取数据段中的字符串
000000014001174E | call testfunc.14001114A | 执行printf函数
0000000140011753 | nop |
0000000140011754 | xor eax,eax | main.cpp:18
0000000140011756 | lea rsp,qword ptr ss:[rbp+50] | main.cpp:19
000000014001175A | pop rbp |
000000014001175B | ret |
test()函数
注:在64位环境中,函数的参数由rcx,rdx,r8,r9,这4个通过寄存器传递,剩余的参数用栈
0000000140011640 <testfunc.int __cd | mov dword ptr ss:[rsp+18],r8d | main.cpp:3, [dword ptr ss:[rsp+18]]:&"ALLUSERSPROFILE=C:\\ProgramData"
0000000140011645 | mov dword ptr ss:[rsp+10],edx | [dword ptr ss:[rsp+10]]:&"C:\\Users\\BananaLi\\Desktop\\testfunc.exe"
0000000140011649 | mov dword ptr ss:[rsp+8],ecx |
000000014001164D | push rbp | 保存栈底
000000014001164E | sub rsp,50 | 提升栈顶
0000000140011652 | mov rbp,rsp | 把原先栈顶的位置变成栈底
0000000140011655 | mov dword ptr ss:[rbp],0 | main.cpp:4
000000014001165C | mov dword ptr ss:[rbp+4],2 | main.cpp:5
0000000140011663 | mov eax,dword ptr ss:[rbp+60] | main.cpp:6
0000000140011666 | inc eax |
0000000140011668 | mov dword ptr ss:[rbp],eax |
000000014001166B | mov eax,dword ptr ss:[rbp+70] | main.cpp:7
000000014001166E | inc eax |
0000000140011670 | mov dword ptr ss:[rbp+4],eax |
0000000140011673 | mov eax,dword ptr ss:[rbp] | main.cpp:9
0000000140011676 | lea rsp,qword ptr ss:[rbp+50] | main.cpp:10
000000014001167A | pop rbp |
000000014001167B | ret |
堆栈图
1.原始堆栈
2.对应test传入的3个参数传入后
3.保存栈底指针
4.提升栈空间,给test()函数局部变量使用

5.使rsp=rbp

6.保存局部变量
int e = 0;
int d = 2;

7.取第一个参数1,并赋值给eax
8.eax自增1,并在局部变量e的位置保存
e = a + 1;

9.取test()第3个参数,自增1后,存入d的局部变量位置
d = c + 1;

10.eax存入要返回的值(e),还原rsp,rbp
return e;

更多推荐


所有评论(0)