汇编 Call 指令运行原理详解:从跳转机制到堆栈操作
函数参数传递
参数传递一般有三种方式:
- 通过内存(一般是堆栈)传递
- 整形参数可以通过寄存器传递
- 浮点数参数可以通过浮点寄存器传递
堆栈传递
所谓通过堆栈传递参数,就是调用函数的一方,将参数逐个压入堆栈中,然后由函数从堆栈中取出使用。
使用堆栈的好处是不用污染寄存器,而且可以传递的参数个数基本不限。但缺点是需要读写内存。众所周知,读写内存比读写寄存器要慢的多,这就使人想到用寄存器进行传递参数会大大提高效率。在windows 内核中,大多使用快读调用协议,第一个参数使用 ecx 传递,第二个参数使用 edx 传递,其他的参数继续使用堆栈。
而普通的 C 调用方式则是全部使用堆栈来进行参数传递的。
x64平台下,通过寄存器传参。前4个参数通过rcx、rdx、r8、r9传参,多余的参数通过栈从右向左入栈。
函数的返回
一般编译器都是使用 ret 或者 ret n 指令来返回。最早和最常见的指令是 ret,这个指令总是从对战中弹出一个地址,然后让处理器跳转到这个地址。这条指令说起来非常的简单,不过他也带来了一些缺点。ret 先要弹出堆栈中记录的函数返回地址,然后才能平衡堆栈。但实际情况是,先将参数入栈,然后call 函数时候保存的函数返回地址。这个时候就有另一条指令 ret n,表示先调回函数返回地址,然后在将 esp + n(平衡堆栈)。例如:ret 8,实际会从堆栈中弹出一个返回地址,还有8个字节的堆栈大小,总共是 8+ 4(32位机器)。
三种常见的调用约定
C 调用方式(__cdecl
)
用堆栈来传递参数。将参数倒序要入堆栈,最后由调用者来平衡堆栈的方式,被称为 C 调用。
标砖调用方式(__stdcall
)
参数传递方式同上,最后由函数本身来平衡堆栈的方式被称为标准调用(stdcall
)。标准调用是 windows API 常见的调用方式。
快速调用(__fastcall
)
Windows 内核 32 位版本中,最常见的是快速调用(fastcall
)方式。其特点是,ecx
传递第一个参数,edx
传递第二个参数,其他的参数依然用堆栈传递。堆栈平衡方式和 stdcall
相同(被调用方平衡堆栈)。
(__thiscall
)
C++ 类方法调用方式,ecx 为this 指针的值。构造函数有返回值,就是 this 指针。
调用栈示例
c 语言示例代码如下:
int function(int x, int y) {return x + y;
}
int main(){function(1, 2);
}
相关的汇编示例代码如下:
; ... ...
; function(1, 2);
push 0x02h ; 将参数 2 放入堆栈
push 0x01h ; 将参数 1 放入堆栈
call function ; 将 call function 下一句代码的地址放入堆栈相当于 push function + 4
add esp, 0x8h ; 将上面入栈的两个参数给丢弃,平衡堆栈
; ......; funciton(int x, int y) 内部代码
push ebp ; 将 ebp 进行备份
mov ebp, esp ; 将 esp 备份到 ebp
sub esp, xxxx ; 函数局部变量开辟空间
; ... ... 相关操作
mov esp, ebp ; 将当前栈顶还原回当前栈基
pop ebp ; 还原栈基到上一个栈帧的栈基
ret ; 相当于 pop eip 操作,将栈内保存的函数后一个字节的地址放到 eip(下一条指令)
堆栈示意图:
地址 | 数据 | 汇编代码 |
---|---|---|
0x00000004 | 局部变量 | |
0x00000008 | 局部变量 | |
0x0000000c | 局部变量 | |
0x00000010 | bp 地址 | push ebp |
0x00000014 | 调用地址(function + 4) | call function |
0x00000018 | 实参(1) | push 1 |
0x0000001c | 实参(2) | push 2 |
0x00000020 | … | |
0x00000024 | … |
更多精彩文章请访问:技术那些事,更多精彩等着您!