对于X86 和 X64 的编码来说,主要有下面几个方面的不同:
1. X64 具有 64-bit 的寻址能力
2. 16 个64-bit 整数寄存器以及16 个 XMM/YMM 寄存器用于浮点运算
3. X64 默认使用的是__fastcall调用约定
4. 基于RISC 的异常处理结构
该调用约定使用寄存器来传递前4个参数,其余的参数使用栈来传递。尽管如此,主调函数依然为通过寄存器传递的参数保留了相应的栈空间。
Vs2010 关闭所有的优化选项,然后编译运行如下程序:
#include
void Func6(inta,intb,intc,int d,inte,intf)
{
return;
}
void Func5(inta,intb,intc,int d,inte)
{
Func6(0,1,2,3,4,5);
}
void Func4(inta,intb,intc,int d)
{
Func5(0,1,2,3,4);
}
void Func3(inta ,intb ,intc)
{
Func4(0,1,2,3);
}
void Func2(inta,intb)
{
Func3(0,1,2);
}
void Func1(inta)
{
Func2(0,1);
}
void Func0()
{
Func1(0);
}
int main()
{
Func0();
return 0;
}
; int __cdecl main(int argc, const char**argv, const char **envp)
main proc near
sub rsp, 28h
call sub_140001130
xor eax, eax
add rsp, 28h
retn
main endp
main 函数刚开始申请了 28 h 的空间,然后调用子函数
sub_140001130 proc near
sub rsp, 28h
xor ecx, ecx
call sub_140001110
add rsp, 28h
retn
sub_140001130 endp
xor ecx,ecx 即参数第一个参数为0,然后调用子层函数。
sub_140001110 proc near
arg_0= dword ptr 8
mov [rsp+arg_0], ecx
sub rsp, 28h
mov edx, 1
xor ecx, ecx
call sub_1400010E0
add rsp, 28h
retn
sub_140001110 endp
sub_1400010E0 proc near
arg_0= dword ptr 8
arg_8= dword ptr 10h
mov [rsp+arg_8], edx
mov [rsp+arg_0], ecx
sub rsp, 28h
mov r8d, 2
mov edx, 1
xor ecx, ecx
call sub_1400010B0
add rsp, 28h
retn
sub_1400010E0 endp
sub_1400010B0 proc near
arg_0= dword ptr 8
arg_8= dword ptr 10h
arg_10= dword ptr 18h
mov [rsp+arg_10], r8d
mov [rsp+arg_8], edx
mov [rsp+arg_0], ecx
sub rsp, 28h
mov r9d, 3
mov r8d, 2
mov edx, 1
xor ecx, ecx
call sub_140001070
add rsp, 28h
retn
sub_1400010B0 endp
上面三个函数调用分别传递了2,3,4个参数。我们知道x64 fast call 的参数传递规则,从右向左入栈且前四个参数由参数寄存器传递,即rcx,rdx,r8,r9。上面的汇编代码也证实了这点,那么我们就像知道,当参数个数超过4个时,参数如何传递,参数传递的位置又是哪儿呢?
sub_140001070 proc near
var_18= dword ptr -18h
arg_0= dword ptr 8
arg_8= dword ptr 10h
arg_10= dword ptr 18h
arg_18= dword ptr 20h
mov [rsp+arg_18], r9d
mov [rsp+arg_10], r8d
mov [rsp+arg_8], edx
mov [rsp+arg_0], ecx
sub rsp, 38h
mov [rsp+38h+var_18], 4
mov r9d, 3
mov r8d, 2
mov edx, 1
xor ecx, ecx
call sub_140001020
add rsp, 38h
retn
sub_140001070 endp
上面的 mov [rsp+20h],4 操作对应的显然就是第5个参数4。而我们看到其位置为:
[4]
[]
[]
[]
[]rsp的位置
中间的四个位置用于什么用途呢?我们知道,参数寄存器的个数刚好为四个。当我们在无参数内部增加一些代码,并观察函数行为:
void Func5(inta,intb,intc,int d,inte)
{
a += 1;
b += 2;
c += 3;
d += 4;
e += 5;
Func6(0,1,2,3,4,5);
}
sub_140001020 proc near
var_18= dword ptr -18h
var_10= dword ptr -10h
arg_0= dword ptr 8
arg_8= dword ptr 10h
arg_10= dword ptr 18h
arg_18= dword ptr 20h
arg_20= dword ptr 28h
mov [rsp+arg_18], r9d ;将 r9d 保留到 rsp+20h
mov [rsp+arg_10], r8d ; 将 r8d 保留到 rsp + 18h
mov [rsp+arg_8], edx ; 将 edx 保留到 rsp + 10h
mov [rsp+arg_0], ecx ; 将ecx 保留到 rsp + 8h
sub rsp, 38h
mov eax, [rsp+38h+arg_0]
inc eax
mov [rsp+38h+arg_0], eax ; 这里 [rsp + 8h+38h]++
mov eax, [rsp+38h+arg_8]
add eax, 2
mov [rsp+38h+arg_8], eax ; 这里 [rsp + 10h+38h]+=2
mov eax, [rsp+38h+arg_10]
add eax, 3
mov [rsp+38h+arg_10], eax ;这里 [rsp + 18h+38h]+=3
mov eax, [rsp+38h+arg_18]
add eax, 4
mov [rsp+38h+arg_18], eax ;这里 [rsp + 20h+38h]+=4
mov eax, [rsp+38h+arg_20]
add eax, 5
mov [rsp+38h+arg_20], eax ;这里 [rsp + 28h+38h]+=5 就是我们传入的第5个参数的位置
mov [rsp+38h+var_10], 5
mov [rsp+38h+var_18], 4
mov r9d, 3
mov r8d, 2
mov edx, 1
xor ecx, ecx
call sub_140001000
add rsp, 38h
retn
sub_140001020 endp
通过汇编我们看到,当我们对形参进行修改的时候,函数将参数寄存器的值放入了栈空间中,然后再对栈空间进行操作,跟我们之前所熟悉的对于函数参数的操作是相同的。无论子函数的参数是多少个,我们的函数都为其至少预留了4*8 的栈空间,当参数更多的时候,其增加栈的大小。
sub_140001020 proc near
var_18= dword ptr -18h
var_10= dword ptr -10h
arg_0= dword ptr 8
arg_8= dword ptr 10h
arg_10= dword ptr 18h
arg_18= dword ptr 20h
mov [rsp+arg_18], r9d
mov [rsp+arg_10], r8d
mov [rsp+arg_8], edx
mov [rsp+arg_0], ecx
sub rsp, 38h
mov [rsp+38h+var_10], 5
mov [rsp+38h+var_18], 4
mov r9d, 3
mov r8d, 2
mov edx, 1
xor ecx, ecx
call sub_140001000
add rsp, 38h
retn
sub_140001020 endp
通过查看上面的几个函数调用我们发现,函数刚开始都进行了
sub rsp [某个值] 的操作
由于函数内部均没有使用局部变量,可以总结出这个值有如下规律
参数个数 <= 4 时28h,参数个数为5,6个时38h
看了以上的程序我们应该想到的一个问题:
这里先给出一个结论:函数开始部分(即下面介绍的prolog)申请栈空间的时候(通过 push和 sub esp操作),大小需要符合16n+8,以满足在函数调用的时候rsp是16h对齐的要求。call指令的时候使用了8字节。而要理解这个过程如何实现,需要先了解易失性寄存器、Prolog 和 epilog的概念。
易失性寄存器 假设被调用者修改的寄存器,调用者负责其保存和恢复工作。
非易失性寄存器 被假设为“在函数调用之后依然不变”,因此被调用者负责其保存和恢复。
图片来源:https://msdn.microsoft.com/en-us/library/9z1stfyw.aspx
常用非易失性寄存器有:R12 R15 RDI RSI RBX RBP RSP
Prolog所有申请栈空间,调用子函数,保存非易失性寄存器或者使用SHE 的函数必须有prolog。而且其地址范围存储在与之相应函数表条目相关联的unwind(展开)数据中描述(PE 文件中)。
如果需要,prolog 将参数寄存器存储在其归属地址中,将堆栈上的非易失性寄存器分配给本地和临时的堆栈的固定部分,并且可选地建立帧指针。即
1.保存寄存器的值到栈空间中以便于函数返回的时候恢复
2.为函数执行操作所需要的局部变量以及函数调用申请栈空间。
相应的unwind 数据必须描述该prolog的动作,且提供足够的信息来撤销prolog所做的操作。Prolog 相当于函数执行前的准备工作。相应的Epilog 为函数执行完毕后的清理工作。
Epilog代码存储在与每个函数退出的时候。通常只有一个prolog,但是可以有很多epilog,Epilog代码将堆栈修改为固定分配大小(如果需要),释放固定堆栈分配,通过从堆栈中弹出其保存的值来恢复非易失性寄存器,并返回。
在prolog 中通常首先保存易失性寄存器,然后再分配固定的栈空间,否则在通过rsp 访问局部变量的时候需要一个额外的偏移量(push 操作的个数*8)。非易失性寄存器可以以任何顺序保存。但是,在prolog 中首次使用非易失性寄存器必须保存它。
Epilog 代码必须遵循一系列严格的规则,以使展开代码从中断和异常中可靠的展开。这减少了所需的展开数据结构的数量,因为不需要额外的数据来描述每个epilog。相反,展开代码可以通过向前扫描代码流来确定epilog正在执行,以识别一个epilog。
如果函数没有使用帧指针,那么 epilog 必须首先释放堆栈的固定部分,然后弹出非易失性寄存器,并将控制权返回给调用函数。
Epilog 的合法形式
Add rsp 或者 lea rsp,constant[FPReg]
后面加上一系列或者零个 pop 寄存器的操作。
最后为一个返回或者跳转操作。
在epilog 中,只有jmp 的一部分子集操作是允许的-----ModRM 跳转指令:且 ModRM mod 字段为00 -----即只允许直接内存跳转 [base]形式的memory寻址。
如果帧指针没有使用,epilog必须使用add rsp,constant回收栈空间,可能不使用lea RSP,constant [RSP],由于这些限制的存在,展开代码在寻找epilogs时有较少的识别模式。
遵循这些规则以允许展开代码确定epilog当前正在执行,并模拟epilog的其余部分的执行以允许重新创建调用函数的上下文。
参考链接:
http://www.mouseos.com/x64/doc6.html
其它的任何代码不能出现在epilog 中,
在了解上述概念之后我们开始解释上面的
通过上面的介绍,如果函数有prolog,其包含三部分的组成。Push 非易失性寄存器进栈,开辟固定大小的栈空间即 sub esp 【固定值】。
Push非易失性寄存器就是为了在函数中使用该寄存器之后的恢复。sub esp操作要负责一下几个方面:函数调用时参数的传递,函数内部的局部变量。局部变量是固定的,由于栈空间在函数刚开始的代码中确定直到后面函数返回都不会变化,对于不同的子函数调用,分配的用于函数调用的栈空间并不会动态改变大小,因此,我们需要考虑的是函数参数最多的子函数的函数参数个数,另外,发生函数调用时,函数内部总是会为rcx,rdx,r8,r9保留栈空间。综上,函数内部申请固定大小栈空间的时候,其大小=(局部变量+ max(参数最多的函数的参数个数,4)*8)。这个固定值+前面的push操作应该符合16n+8的要求。不同的编译器对于具体实现的方法不同,没有明确答案,但是其必须满足16 对齐。Vs2010 在实现的时候貌似其局部变量16对齐,然后函数参数个数16对齐,然后总的三个部分再合起来满足参数对齐的要求。比如下面这个函数:
void For()
{
unsigned long long j;
Func5(0,1,2,3,4);
}
微软sub rsp 48h,开48h栈空间=alignmen(8,16)+alignment(5*8,16) + 8(用于对齐)
而intel编译器是这样操作的:
push rbp
sub rsp,50h
void For()
{
unsigned long long j;
unsigned long long i;
Func6(0,1,2,3,4,5);
}
此时vs2010 同样开48h栈空间
Intel 编译器这样做:
push rbp
sub rsp,50h
相比微软,函数开始添加了push rbp的操作。我们这里仅仅介绍了解vs2010的做法,其实不必深究,知道了在call之前,rsp 16对齐且有足够空间用于参数传递即可。
void For()
{
char szTemp[] = "Hello";
for(inti = 0;i <sizeof(szTemp) ;i++)
{
putchar(szTemp[i]);
}
putchar('\n');
}
int main()
{
For();
return 0;
}
为了生成的汇编代码更简单易读,我们在上面的配置基础上进行如下配置:
sub_140001000 proc near
var_28= byte ptr -28h ; szTemp 其值指向的为一个栈空间的地址
var_20= dword ptr -20h ;i
push rsi
push rdi
sub rsp, 38h ;这里就是prolog 分别push rsi rdi,申请38h
;很容易发现,38h + rsi + rdi = 48h符合 16n+8
lea rax, [rsp+48h+var_28]
lea rcx, aHello ;"Hello"
mov rdi, rax
mov rsi, rcx
mov ecx, 6
rep movsb ;szTemp[] = “Hello”,拷贝操作需要循环,使用了rdi和dsi寄存器
mov [rsp+48h+var_20], 0
jmp short loc_140001034
loc_14000102A:
mov eax, [rsp+48h+var_20]
inc eax
mov [rsp+48h+var_20], eax ;i++
loc_140001034:
movsxd rax, [rsp+48h+var_20]
cmp rax, 6
jnb short loc_140001053
movsxd rax, [rsp+48h+var_20]
movsx eax, [rsp+rax+48h+var_28]
mov ecx, eax ; Ch
call cs:putchar
jmp short loc_14000102A
loc_140001053: ; Ch
mov ecx, 0Ah
call cs:putchar
add rsp, 38h
pop rdi
pop rsi
retn ;这里就是epilog,先add rsp后恢复寄存器rdi rsi,为prolog的逆操作,然后retn返回
sub_140001000 endp
函数内部 38h = szTemp 六个字节对齐后为8个字节 + i变量8字节 + putchar 4 *8 字节参数对16对齐依然是20h。然后为了符合 16n + 8的格式而添加的8 字节,总共为38h。如下一个程序所示,如果我们添加对于5参数函数的调用,此时最大参数个数为5,对应的5*8 并16对齐后为30h,函数申请内存应变为0x48h。而如果添加6参数函数调用,同理该值变为 48h-----经验证上述假设推理完全正确
void Func5(inta,intb,intc,int d,inte)
{
return;
}
void For()
{
char szTemp[] = "Hello";
for(inti = 0;i <sizeof(szTemp) ;i++)
{
putchar(szTemp[i]);
}
putchar('\n');
Func5(0,1,2,3,4);
}
int main()
{
For();
return 0;
}
sub_140001020 proc near
var_38= dword ptr -38h
var_28= byte ptr -28h
var_20= dword ptr -20h
push rsi
push rdi
sub rsp, 48h
lea rax, [rsp+58h+var_28]
lea rcx, aHello ;"Hello"
mov rdi, rax
mov rsi, rcx
mov ecx, 6
rep movsb
mov [rsp+58h+var_20], 0
jmp short loc_140001054
另外补充一下X64 中的两类函数类型
帧函数分配帧空间,调用其它函数,保存非易失性寄存器或使用异常处理函数,这样的函数需要一个function table entry 以及一个prolog 以及epilog,帧函数可以动态分配栈空间,使用栈帧。
叶子函数是不需要function table entry 的函数,不调用任何函数,不分配空间,或者保存非易失性寄存器,允许其在栈没有对齐的情况下执行。
关于函数参数传递的问题:
https://msdn.microsoft.com/en-us/library/zthk2dkh.aspx