用汇编写一个带参数的函数需要注意:
- 汇编层面是体现不出来有没有带参数
- 如果带参数,记得写函数原型
; void add(int a, int b);
------------------------------ ;汇编函数框架
global add
add:
push ebp
mov ebp, esp
leave
ret
-------------------------------
以具体得函数举例子
#include <iostream>
int Print(int a, int b)
{
int c = 10;
return a + b + c;
}
int main()
{
int a = 10;
Print(1, 2);
return 0;
}
;对应cpp汇编
16: Print(1, 2);
001C258F push 2
001C2591 push 1 ;从右向左
001C2593 call 001C1384
001C2598 add esp,8 ;外平栈 堆栈平衡
6: int Print(int a, int b)
7: {
001C16F0 push ebp
001C16F1 mov ebp,esp ;创建栈帧
001C16F3 sub esp,0CCh
-------------------------------------------
001C16F9 push ebx
001C16FA push esi ;保存上下文
001C16FB push edi
-------------------------------------------
001C16FC lea edi,[ebp+FFFFFF34h]
001C1702 mov ecx,33h
001C1707 mov eax,0CCCCCCCCh ;初始化栈
001C170C rep stos dword ptr es:[edi]
-------------------------------------------
8: int c = 10;
001C1718 mov dword ptr [ebp-8],0Ah
9:
10: return a + b + c; ;业务逻辑
001C171F mov eax,dword ptr [ebp+8]
001C1722 add eax,dword ptr [ebp+0Ch]
001C1725 add eax,dword ptr [ebp-8]
-------------------------------------------
11: }
001C1728 pop edi
001C1729 pop esi ;恢复上下文
001C172A pop ebx
-------------------------------------------
001C1738 mov esp,ebp
001C173A pop ebp ;释放栈帧
-------------------------------------------
001C173B ret
根据对应代码回答几个问题:
MVC
编译时,虽然没有写调用约定,但是默认的调用约定是__cdecl
_tmain
(调用者)还是print
(被调用者)?
_tmain
print函数
中怎么取到参数?
ebp
寄存器ebp+8
ebp + 0xc
ebp + 4 * 2
(非常重要!!!)rbp + 8 * 2
ret 4 * N
,其中N
为参数个数上文描述的汇编执行流图:
__cdecl
__stdcall
__fastcall
- 对于
x86架构(32位)
,GCC
默认使用__cdecl
作为其调用约定。cdecl(C declaration)
是最常见的调用约定,特别是在Unix、Linux
和其他POSIX
系统上。- 于
32位Windows(x86架构)
,许多Windows API
函数使用stdcall
作为其默认调用约定。
__cdecl
- 传参方式及传参顺序
- 只借助栈
- 自右向左
- 平栈的方式
- 外平栈
__stdcall
- 传参方式及传参顺序
- 只借助栈
- 自右向左
- 平栈的方式
- 内平栈
// 采用__stdcall调用约定
#include <iostream>
int __stdcall Print(int a, int b)
{
int c = 10;
return a + b + c;
}
int main()
{
int a = 10;
Print(1, 2);
return 0;
}
; 对比__cdecle调用约定的主要代码
16: Print(1, 2);
0005258F 6A 02 push 2
00052591 6A 01 push 1
00052593 E8 F1 ED FF FF call 00051389
;没有了 add esp,8的外平栈
6: int __stdcall Print(int a, int b)
{....
0005173B C2 08 00 ret 8 ;内平栈
__fastcall
- 传参方式及传参顺序
- 会借助寄存器传参 总计6 + 8 = 14个寄存器
- 当
参数 <= 2
时纯寄存器
传参- 当
参数 > 2
时寄存器 + 栈
的方式传参,用了两个寄存器:ecx
、edx
- 自右向左,
edx
是第二个
参数,ecx
是第一个
参数- 平栈的方式
- 当
纯寄存器
传参时不需要平栈
- 当
寄存器 + 栈
传参时采用内平栈
只有两个参数使用纯寄存器传参:
// 采用__fastcall调用约定
#include <iostream>
int __fastcall Print(int a, int b)
{
int c = 10;
return a + b + c;
}
int main()
{
int a = 10;
Print(1, 2);
return 0;
}
; 对比__cdecle调用约定的主要代码
16: Print(1, 2);
0101258F BA 02 00 00 00 mov edx,2 ;使用寄存器传参
01012594 B9 01 00 00 00 mov ecx,1
01012599 E8 F0 ED FF FF call 0101138E
;没有了 add esp,8的外平栈
int __fastcall Print(int a, int b)
7: { .....
01013D13 C3 ret ;不需要平栈
超过两个参数使用寄存器 + 栈的方式传参:
- 用了两个寄存器:
ecx、edx
- 自右向左
edx
是第二个参数,ecx
第一个参数- 前两个参数用寄存器传参,后两个参数用栈传参
- 内平栈
// 采用__fastcall调用约定
#include <iostream>
int __fastcall Print(int a, int b)
{
int c = 10;
return a + b + c;
}
int main()
{
int a = 10;
Print(1, 2);
return 0;
}
; 对比只有两个参数时使用__fastcall调用约定的主要代码
16: Print(1, 2, 3, 4);
0055258F 6A 04 push 4
00552591 6A 03 push 3 ;前两个参数用寄存器传参,后两个参数用栈传参
00552593 BA 02 00 00 00 mov edx,2
00552598 B9 01 00 00 00 mov ecx,1
0055259D E8 F1 ED FF FF call 00551393
int __fastcall Print(int a, int b)
7: { .....
00553D13 C2 08 00 ret 8 ;内平栈
对于
x64架构
,情况有所不同。x64
平台基本上统一了函数调用约定
,不同于x86有多种调用约定。在Unix-like系统(如Linux)
上,x64
使用System V ABI
,而在Windows
上使用x64 calling convention
。这意味着在x64
上,不论是GCC
还是其他编译器,都使用相同的调用约定
。
__fastcall
- 传参方式及传参顺序
- 会借助寄存器传参
- 当
参数 <= 6
时纯寄存器
传参- 当
参数 > 6
时寄存器 + 栈
的方式传参- 前六个整数或指针参数传递给
RDI
,RSI
,RDX
,RCX
,R8
,R9
。- 前八个浮点参数传递给
XMM0
到XMM7
。- 超过这些限制的参数通过堆栈传递。
在x64架构下,与x86相比,函数调用约定已经得到了简化。在Windows和Unix-based系统(如Linux)下的x64函数调用约定有所不同。以下是两者的简要概述:
### Windows x64 Calling Convention (__fastcall):
1. **寄存器传参**:
- 前四个整数或指针参数传递给`RCX`, `RDX`, `R8`, `R9`。
- 前四个浮点参数传递给`XMM0`, `XMM1`, `XMM2`, `XMM3`。
- 如果有更多的参数,它们将通过堆栈传递。
2. **调用者保存寄存器**:
- 调用者负责保存`RAX`, `RCX`, `RDX`, `R8`, `R9`, `R10`, `R11`以及`XMM0`到`XMM5`。
3. **被调用者保存寄存器**:
- 被调用函数(如果它们被修改)负责保存`RBX`, `RSI`, `RDI`, `RSP`, `RBP`, `R12`到`R15`,以及`XMM6`到`XMM15`。
4. **堆栈对齐**:
- 必须保证堆栈(RSP)在函数调用前是16字节对齐的。
### System V ABI (Unix-based, e.g., Linux) x64 Calling Convention:
1. **寄存器传参**:
- 前六个整数或指针参数传递给`RDI`, `RSI`, `RDX`, `RCX`, `R8`, `R9`。
- 前八个浮点参数传递给`XMM0`到`XMM7`。
- 超过这些限制的参数通过堆栈传递。
2. **调用者保存寄存器**:
- 调用者负责保存`RAX`, `RCX`, `RDX`, `RSI`, `RDI`, `R8`, `R9`, `R10`, `R11`以及`XMM0`到`XMM15`。
3. **被调用者保存寄存器**:
- 被调用函数(如果它们被修改)负责保存`RBX`, `RSP`, `RBP`, `R12`到`R15`。
4. **堆栈对齐**:
- 与Windows一样,堆栈(RSP)在函数调用前也必须是16字节对齐的。
以上仅仅是函数调用约定的基本点。