X64位调用约定与函数传参.
一丶复习X86传参
在x86下我们汇编的传参如下:
push eax
call xxx
xxx fun proc
push ebp 保存栈底
mov ebp,esp 设置ebp
sub esp,0C0h 开辟局部变量空间
push ebx 保存寄存器环境
push esi
push edi
pop edi 恢复寄存器环境
pop esi
pop ebx
mov esp,ebp 释放局部变量空间
pop ebp 恢复栈底
ret 返回,平展, 如果是 C在外平展 add esp,xxx stdcall 则内部平展 ret 4
看到上面这段代码.我们就应该在脑海中有一个示意图.
我们可以根据上图可以看到.在调用函数的时候做了那些事情.
1.往栈中存放参数
2.将返回地址入栈
3.保存栈底
4.栈内部进行自己的 申请空间 保存环境 以及释放.
二丶x64汇编
2.1汇编详解
在x64下,万变不离其宗.大部分跟x86一样.
如汇编代码为:
sub rsp,0x28
mov r9,1
mov r8,2
mov rdx,3
mov rcx,4
call xxx
add rsp,0x28
1.传参方式
首先说明一下,在X64下,是寄存器传参. 前4个参数分别是 rcx rdx r8 r9进行传参.多余的通过栈传参.从右向左入栈.
2.申请参数预留空间
在x64下,在调用一个函数的时候,会申请一个参数预留空间.用来保存我们的参数.比如以前我们通过push压栈
参数的值.相应的栈就会抬高.其实x64下,一样会申请.只不过这个地方在进函数的时候并没有值.进入函数之后才会将寄存器的值在拷贝到这个栈中.其实就相当于你还是push了.只不过我是外边申请空间,内部进行赋值.
如下:
sub rsp,0x28 //申请的栈空间为0x28,就相当于我们push rcx rdx r8 r9.只不过只是申请.
call xxxx
add rsp,0x28
xxx //函数内部
mov [rsp - 8],rcx
mov [rsp - 0x10],rdx
mov [rsp - 0x18],r8
mov [rsp - 0x20],r9
xxx
如下图:
我们编写一个简单的x64程序.对其反汇编进行查看.
首先开辟我们的参数空间,以及返回地址空间.我们单步一下查看
可以看大开辟了 5*8个字节大小的空间.
然后下方的汇编对其寄存器赋值.进行传参.说明我们只有4个参数.
3.栈按照16字节对齐
现在我们应该明白了.在调用一个函数的时候. 使用 *sub rsp,xxx**进行抬栈,函数内部则进行参数赋值.
其实也是相当于push了参数.只不过它不像x86一样.在里面进行平栈了.而是外面进行平栈了.
那么有个疑问.比如说我们就4个参数. 通过上面来说.我们应该申请 sub rsp,0x20个字节才对.在CALL的时候
x86 x64都是一样的会将返回地址入栈. 那为什么要rsp,0x28.这样的话会多申请一个参数的值哪.
原因是这样的.栈要按照16字节对齐进行申请.
那么还有人会说.按照16字节对齐,那么我们的参数已经是16字节对齐了.比如为我们4个寄存器申请预留空间. rsp,0x20. (4 * 8 = 32 = 16j进制的 0x20)
那为什么还是会申请 rsp,0x28个字节,并且不对齐.
其实是这样的.当我们在 Call函数的时候.返回地址会入栈.如果按照我们之前申请的rsp,0x20个字节的话.那么当
返回地址入栈之后,现在总共抬栈大小是 0x28个字节.并不是16进制对齐. 但是当我们一开始就申请0x28个字节.
当返回地址入栈.那么就是0x28+8 = 0x30个字节. 0x30个字节不是正好跟16字节对齐吗.
所以我们的疑问也就没有了.
所以申请了0x28个字节,其实多出了的8字节是要跟返回地址一样.进行栈对齐使用.
那么申请的这个8字节空间,是没有用的.只是为了对齐使用.
所以x64汇编其实也就搞明白了.
1.在调用函数之前,会申请参数预留空间.(rcx,rdx,r8,r9)
2.函数内部,会将寄存器传参的值(rcx,rdx,r8,r9)保存到我们申请的预留空间中.
上面这两步其实就相当于x86下的 push r9 push r8 push rdx,push rcx
3.调用约定是__fastcall.传参有rcx rdx,平栈是按照c调用约定平栈. 也就是调用者平栈.