思考
- 如何用汇编编写带参数的执行流?
- C语言的传参,汇编层面是如何实现的?
- 函数参数过多,汇编层面是如何实现的?
如何用汇编编写带参数的执行流
- 考虑通过寄存器传参
- 考虑通过栈传参
- 考虑通过寄存器+栈传参
- 基本结构实现还是精简结构实现?
汇编实现一个带参函数
用汇编写一个带参数的函数需要注意:
- 汇编层面是体现不出来有没有带参数
- 如果带参数,记得写函数原型
; 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
32位:ebp + 4 * 2
(非常重要!!!)
64位:rbp + 8 * 2
- 因为传参破坏了栈平衡,由谁来平栈
- 内平栈:被调用函数自己来平衡栈,如
ret 4 * N
,其中N
为参数个数 - 外平栈:调用者来平衡栈
上文描述的汇编执行流图:
关于传参
- 用者传参,汇编层面是如何实现的?
- 被调用者如何拿到参数,汇编层面是如何实现的
- 传参与局部变量,汇编层面的实现,是否是一样的?
函数调用约定
__cdecl
__stdcall
__fastcall
x86模式下
对于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架构,情况有所不同。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字节对齐的。 以上仅仅是函数调用约定的基本点。