阅读目录
目录
x64汇编第三讲,64位调用约定与函数传参.
一丶复习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个参数.
此时进入Call内部.看下栈.
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调用约定平栈. 也就是调用者平栈.