下面是摘自 MSDN 的文章,在 Win64 下的 registers 用途
Register
Status
Use
RAXVolatileReturn value register
RCXVolatileFirst integer argument
RDXVolatileSecond integer argument
R8VolatileThird integer argument
R9VolatileFourth integer argument
R10:R11VolatileMust be preserved as needed by caller; used in syscall/sysret instructions
R12:R15NonvolatileMust be preserved by callee
RDINonvolatileMust be preserved by callee
RSINonvolatileMust be preserved by callee
RBXNonvolatileMust be preserved by callee
RBPNonvolatileMay be used as a frame pointer; must be preserved by callee
RSPNonvolatileStack pointer
XMM0VolatileFirst FP argument
XMM1VolatileSecond FP argument
XMM2VolatileThird FP argument
XMM3VolatileFourth FP argument
XMM4:XMM5VolatileMust be preserved as needed by caller
XMM6:XMM15 NonvolatileMust 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 个参数
下面的代码:
void EditTextFile(HWND hEdit, LPCTSTR szFileName)
{
HANDLE hFile;
DWORD dwFileSize;
DWORD dwFileSizeHigh;
LPTSTR lpFileText;
LPTSTR lpFileTextW;
WORD wSignature;
DWORD dwReadSize;
hFile = CreateFile(szFileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
... ...
}
CreateFile() 的参数有 7 个,那么看看 VC 是怎样安排参数传递:
void EditTextFile(HWND hEdit, LPCTSTR szFileName)
{
000000013F791570 40 56 push rsi
000000013F791572 41 54 push r12
000000013F791574 41 55 push r13
000000013F791576 48 83 EC 50 sub rsp,50h
000000013F79157A 48 8B C2 mov rax,rdx
HANDLE hFile;
DWORD dwFileSize;
DWORD dwFileSizeHigh;
LPTSTR lpFileText;
LPTSTR lpFileTextW;
WORD wSignature;
DWORD dwReadSize;
hFile = CreateFile(szFileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
000000013F79157D 45 33 ED xor r13d,r13d
000000013F791580 4C 8B E1 mov r12,rcx
000000013F791583 4C 89 6C 24 30 mov qword ptr [rsp+30h],r13 // 第 7 个参数
000000013F791588 45 8D 45 01 lea r8d,[r13+1] // 第 3 个参数
000000013F79158C 45 33 C9 xor r9d,r9d // 第 4 个参数
000000013F79158F BA 00 00 00 80 mov edx,80000000h // 第 2 个参数
000000013F791594 48 8B C8 mov rcx,rax // 第 1 个参数
000000013F791597 C7 44 24 28 80 00 00 00 mov dword ptr [rsp+28h],80h // 第 6 个参数
000000013F79159F C7 44 24 20 03 00 00 00 mov dword ptr [rsp+20h>],3 // 第 5 个参数
000000013F7915A7 FF 15 2B 0B 00 00 call qword ptr [__imp_CreateFileW (13F7920D8h)]
000000013F7915AD 48 8B F0 mov rsi,rax
... ...
上面已经对 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 中
正如下面的代码:
CreateFileWImplementation:
0000000076EC2A30 48 89 5C 24 08 mov qword ptr [rsp+8],rbx // 回写 caller stack
0000000076EC2A35 48 89 6C 24 10 mov qword ptr [rsp+10h],rbp // 回写 caller stack
0000000076EC2A3A 48 89 74 24 18 mov qword ptr [rsp+18h],rsi // 回写 caller stack
0000000076EC2A3F 57 push rdi
0000000076EC2A40 48 83 EC 50 sub rsp,50h
0000000076EC2A44 8B DA mov ebx,edx
0000000076EC2A46 48 8B F9 mov rdi,rcx
0000000076EC2A49 48 8B D1 mov rdx,rcx
0000000076EC2A4C 48 8D 4C 24 40 lea rcx,[rsp+40h]
0000000076EC2A51 49 8B F1 mov rsi,r9
0000000076EC2A54 41 8B E8 mov ebp,r8d
0000000076EC2A57 FF 15 33 A1 08 00 call qword ptr [__imp_RtlInitUnicodeStringEx (76F4CB90h)]
上面所显示的是 CreateFile() 在 kernel32.dll 模块里的实现代码,上面对回写机制进行了标注,回写的 stack 正好是 caller 调用时未参数所保留的 stack 空间,上面的代码并不是那么直观。
下面我演示一下使用 /homeparams 选项来编译代码。
上面的 EditTextFile() 函数结果如下:
void EditTextFile(HWND hEdit, LPCTSTR szFileName)
{
000000013F6A15C0 48 89 54 24 10 mov qword ptr [rsp+10h],rdx // 第 2 个参数回写
000000013F6A15C5 48 89 4C 24 08 mov qword ptr [rsp+8],rcx // 第 1 个参数回写
000000013F6A15CA 56 push rsi
000000013F6A15CB 41 54 push r12
000000013F6A15CD 48 83 EC 58 sub rsp,58h
HANDLE hFile;
DWORD dwFileSize;
DWORD dwFileSizeHigh;
LPTSTR lpFileText;
LPTSTR lpFileTextW;
WORD wSignature;
DWORD dwReadSize;
hFile = CreateFile(szFileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
000000013F6A15D1 48 8B 4C 24 78 mov rcx,qword ptr [szFileName]
000000013F6A15D6 45 33 E4 xor r12d,r12d
000000013F6A15D9 45 33 C9 xor r9d,r9d
000000013F6A15DC 4C 89 64 24 30 mov qword ptr [rsp+30h],r12
000000013F6A15E1 45 8D 44 24 01 lea r8d,[r12+1]
000000013F6A15E6 BA 00 00 00 80 mov edx,80000000h
000000013F6A15EB C7 44 24 28 80 00 00 00 mov dword ptr [rsp+28h],80h
000000013F6A15F3 C7 44 24 20 03 00 00 00 mov dword ptr [rsp+20h],3
000000013F6A15FB FF 15 D7 1A 00 00 call qword ptr [__imp_CreateFileW (13F6A30D8h)]
第 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 之前,恢复原来值。因此这些寄存器的值是保持恒定的。
void EditTextFile(HWND hEdit, LPCTSTR szFileName)
{
000000013F9115C0 48 89 54 24 10 mov qword ptr [rsp+10h],rdx
000000013F9115C5 48 89 4C 24 08 mov qword ptr [rsp+8],rcx
000000013F9115CA 56 push rsi // 保存
000000013F9115CB 41 54 push r12 // 保存
000000013F9115CD 48 83 EC 58 sub rsp,58h
HANDLE hFile;
DWORD dwFileSize;
... ...
000000013F911708 48 83 C4 58 add rsp,58h
000000013F91170C 41 5C pop r12 // 恢复
000000013F91170E 5E pop rsi // 恢复
000000013F91170F C3 ret
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
void EditTextFile(HWND hEdit, LPCTSTR szFileName)
{
000000013F9115C0 48 89 54 24 10 mov qword ptr [rsp+10h],rdx
000000013F9115C5 48 89 4C 24 08 mov qword ptr [rsp+8],rcx
000000013F9115CA 56 push rsi
000000013F9115CB 41 54 push r12
000000013F9115CD 48 83 EC 58 sub rsp,58h // 为 callee 分配 stack
HANDLE hFile;
DWORD dwFileSize;
... ...
000000013F911708 48 83 C4 58 add rsp,58h // 注销 callee stack 结构
000000013F91170C 41 5C pop r12
000000013F91170E 5E pop rsi
000000013F91170F C3
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 值的作用,如下:
NtCallbackReturn:
00000000770FFDA0 4C 8B D1 mov r10,rcx
00000000770FFDA3 B8 02 00 00 00 mov eax,2
00000000770FFDA8 0F 05 syscall
00000000770FFDAA C3 ret
在进入 system call 之前,保存 rcx 的值。