ret 与 ret 10之间的区别
当然可以。ret 和 ret 10 都是函数返回指令,但它们之间有一个非常重要且根本的区别。
最核心的区别在于由谁来负责清理函数调用时压入栈的参数。
ret (常规返回)
动作:
- 从栈顶弹出一个地址,放入指令指针寄存器 (
EIP或RIP) 中。 - CPU 跳转到该地址继续执行。
参数清理:
ret 指令本身不负责清理调用者压入栈的参数。它假定**调用者(Caller)**会在函数返回后自己清理栈。
对应的调用约定:
这种方式通常与 __cdecl 调用约定相关,这是 C 和 C++ 语言默认的调用约定。
示例:
1; ----- 调用者的代码 (Caller) -----
2push arg2 ; 压入第2个参数 (4字节)
3push arg1 ; 压入第1个参数 (4字节)
4call MyFunction ; 调用函数
5add esp, 8 ; <--- 由调用者负责清理 2 * 4 = 8 字节的参数
6
7; ----- 被调用函数 MyFunction 的代码 (Callee) -----
8MyFunction:
9 ; ... 函数体 ...
10 ret ; <--- 简单返回,不关心参数
优点:
因为它不关心参数数量,所以支持可变参数函数,比如 printf(format, ...)。被调用的 printf 函数不知道有多少个参数,所以它无法清理,必须由调用者来完成。
ret 10 (带参数清理的返回)
10 的含义:
这里的 10 是十六进制的 10h,代表十进制的 16。它指示了要清理的字节数。
动作:
- 从栈顶弹出一个地址,放入指令指针寄存器 (
EIP或RIP) 中。 - CPU 跳转到该地址继续执行。
- 在跳转后,CPU 会自动将栈顶指针
ESP的值增加 16 字节(相当于执行了add esp, 16)。
参数清理:
ret 10 指令在返回的同时,也清理了栈上的参数。它假定被调用者(Callee),也就是这个函数自己,负责清理栈。16 字节通常意味着 4 个参数(每个参数4字节)。
对应的调用约定:
这种方式通常与 __stdcall 调用约定相关,Windows API 函数广泛使用此约定。
示例:
1; ----- 调用者的代码 (Caller) -----
2push arg4 ; 压入第4个参数
3push arg3 ; 压入第3个参数
4push arg2 ; 压入第2个参数
5push arg1 ; 压入第1个参数
6call MyFunction ; 调用函数
7 ; <--- 调用者在这里什么都不用做!
8
9; ----- 被调用函数 MyFunction 的代码 (Callee) -----
10MyFunction:
11 ; ... 函数体 ...
12 ret 10 ; <--- 返回,并清理 4 * 4 = 16 字节的参数
优点:
代码可以更紧凑一些,因为清理栈的指令 (add esp, ...) 只需要在函数内部出现一次,而不是在每个调用它的地方都出现一次。
总结对比
| 特性 | ret | ret 10 |
|---|---|---|
| 参数清理者 | 调用者 (Caller) | 被调用者 (Callee) |
| 指令动作 | 弹出返回地址并跳转 | 弹出返回地址、跳转,并给 ESP 加上一个立即数 |
| 常见调用约定 | __cdecl | __stdcall |
| 是否支持可变参数 | 是 | 否(因为清理的字节数是固定的) |
| 常见用途 | C/C++ 默认函数 | Windows API 函数 |
为什么这个区别很重要?
如果一个函数是按 __stdcall 编译的(使用 ret 10),而调用者却按 __cdecl 的方式去调用它(在 call 之后尝试自己清理参数),就会导致栈被清理两次,从而破坏栈的平衡,引发程序崩溃。因此,在进行混合语言编程或调用外部库时,正确匹配调用约定至关重要。