Register |
Status |
Use |
RAX | Volatile | Return value register |
RCX | Volatile | First integer argument |
RDX | Volatile | Second integer argument |
R8 | Volatile | Third integer argument |
R9 | Volatile | Fourth integer argument |
R10:R11 | Volatile | Must be preserved as needed by caller; used in syscall/sysret instructions |
R12:R15 | Nonvolatile | Must be preserved by callee |
RDI | Nonvolatile | Must be preserved by callee |
RSI | Nonvolatile | Must be preserved by callee |
RBX | Nonvolatile | Must be preserved by callee |
RBP | Nonvolatile | May be used as a frame pointer; must be preserved by callee |
RSP | Nonvolatile | Stack pointer |
XMM0 | Volatile | First FP argument |
XMM1 | Volatile | Second FP argument |
XMM2 | Volatile | Third FP argument |
XMM3 | Volatile | Fourth FP argument |
XMM4:XMM5 | Volatile | Must be preserved as needed by caller |
XMM6:XMM15 | Nonvolatile | Must be preserved as needed by callee. |
1. 传递参数
在 Win64 里使用下面寄存器来传递参数:
- rcx - 第 1 个参数
- rdx - 第 2 个参数
- r8 - 第 3 个参数
- r9 - 第 4 个参数
其它多出来的参数通过 stack 传递。
使用下面寄存器来传递浮数数:
- xmm0 - 第 1 个参数
- xmm1 - 第 2 个参数
- xmm2 - 第 3 个参数
- xmm3 - 第 4 个参数
下面的代码:
CreateFile() 的参数有 7 个,那么看看 VC 是怎样安排参数传递:
上面已经对 7 个参数的传递进行了标注,前 4 个参数通过 rcx,rdx,r8 以及 r9 寄存器传递,后 3 个参数确实通过 stack 传递。
可是,事情并没有这么简单:
在 Win64 下,会为每个参数保留一份用来传递的 stack 空间,以便回写 caller 的 stack |
在上面的例子中:
- [rsp+20h] - 第 5 个参数
- [rsp+28h] - 第 6 个参数
- [rsp+30h] - 第 7 个参数
实际上已经为前面 4 个参数保留了 stack 空间,分别是:
- [rsp] - 第 1 个参数(使用 rcx 代替)
- [rsp+08h] - 第 2 个参数(使用 rdx 代替)
- [rsp+10h] - 第 3 个参数(使用 r8 代替)
- [rsp+18h] - 第 4 个参数(使用 r9 代替)
虽然是使用了 registers 来传递参数,然而还是保留了 stack 空间。接下着就是 [rsp+20h], [rsp+28h] 以及 [rsp+30h] 对应的 4,5,6 个参数
2. 回写 caller stack
VC 使用了下面编译参数来实现回写 caller stack
/homeparams |
当使用了这个编译选项或者在 Debug 版下,它强制将 registers 里的值写回 stack 中
正如下面的代码:
上面所显示的是 CreateFile() 在 kernel32.dll 模块里的实现代码,上面对回写机制进行了标注,回写的 stack 正好是 caller 调用时未参数所保留的 stack 空间,上面的代码并不是那么直观。
下面我演示一下使用 /homeparams 选项来编译代码。
上面的 EditTextFile() 函数结果如下:
第 1 个参数回写 [rsp+8] 处,第 2 个参数回写 [rsp+10h] 处。
注意这里的 stack 就是对应 caller 调用时的 stack,经过调用后 [rsp] 是返回地址值,因此,在 callee 里设置:
- callee 写 [rsp+8] = caller 的 [rsp]
- callee 写 [rsp+10h] = caller 的 [rsp+8]
- callee 的 [rsp] = return address
上面很直观地显示了使用 /homeparams 选项时的效果,对比前一段没有使用 /homeparams 选项编译时的结果,很容易发现这个机制。
回写 caller stack 机制目的是为了 Debug 所需。
3. 由 callee 保存
在一个程序里应尽量使用 registers,在 x64 里有 16 个通用寄存器和 16 个 xmm 寄存器,可是一些 registers 在使用前必须保存原来值,以防丢失原来值。
因此,在 callee 使用它们时会将原值压入栈中保存,在 Win64 里,下面 registers 由 callee 负责保存:
- rbx, rbp, rsi, rdi
- r12 - r15
- xmm6 - xmm15
每进入一个 callee,在使用它们之前都保存起来,返回 caller 之前,恢复原来值。因此这些寄存器的值是保持恒定的。
4. stack frame 结构
进入每个 callee 时,都会生成属于自己的 stack frame 结构,返回时会注销自己的 stack frame
- rbp
- rsp
由这两个 registers 来构造 stack frame 结构,rbp 是 stack frame pointer,rsp 是 stack pointer
可是,在 Win64 里,似乎不使用 stack frame 结构,VC 不会为每个函数创建 stack frame 结构 |
在 Win64 里,始终在使用动态使用 rsp 来维护 stack
VC 不会生成 x86 下典型的 stack frame 结构,始终由 rsp 维护 stack,/Gd 编译选项在 Win64 下会被忽略,rbp 被保留起来
在 Win64 里,rdi 寄存器的角色变得很微妙,在某些场合下它充当了一部分 stack frame pointer 的角色。
5. r11 与 rcx 以及 r10
在 64 位模式下,在 sysret 指令返回时,将从 rcx 处得到返回地址,从 r11 处得到 rflags 值,因此在进入 system services routine(系统服务例程)前,或者在系统服务例程中的第1个任务是 rcx 与 r11 寄存器,以便 sysret 返回。
在 Win64 里,r10 寄存器充当保存 rcx 值的作用,如下:
在进入 system call 之前,保存 rcx 的值。