本帖最后由 树上的鱼儿 于 2021-3-2 08:46 编辑
X64 汇编:浅谈关于16字节对齐的有趣表现
本编文章也作为个人学习过程中的心得记录,同时作为帖子发表个人的浅薄见解,如有错误,忘超神们嘴下留情,并客观指出问题,在此谢过。
本文可能不适合无x64汇编基础的朋友,因为这里不阐述汇编基础,也不详细阐述x64调用约定,只谈谈个人对x64汇编中16字节对齐的看法,有过学习汇编的朋友都不陌生对齐的说法,尤其是涉及x64汇编,基本都听过这个感念,那么到底什么是对齐,x64汇编为什么要对齐,并且为什么都说是16字节对齐,有的朋友可能光听说或看过一下网络文章,那么自己有没有佐证什么呢????
这里我先上一张图,朋友们仔细观察,也许你看过这张图,如柳暗花明般已扫清阴霾。
上图为一张调试器下断点后,堆栈空间显示的内容,我这大概给编辑了一下图片,方便观察。
我们知道 rsp表示当前rip位置的堆栈指针,从图中我们很明显的看到,所有call压入的返回地址所存储的rsp指针结尾数值都是8,对都是8,你没看错,十六进制的8,那么这个8跟16字节对齐有关系吗?
由于x64调用约定的关系,是由调用者平栈,我们在汇编函数体内部基本都能看到
调用函数前减堆栈 sub rsp,0x多少
调用函数后加堆栈 add rap,0x多少
也就说在call前由调用者申请栈空间,调用call以后由调用者平栈空间。
很多网络上的文章五花八门,对这个x64汇编16字节对齐的说话五花八门,有的说在call前的抬起的栈大小值要16字节对齐,每次0x10称为16字节对齐,甚至更有说者把call返回地址也算进去,4个参数的话sub rsp,0x28,可4个参数明明是0x20呀,等等众说纷纭,看的能把我这个小白搞蒙逼,搞得不知谁对谁错,一头雾水。
我们用实践观察来检验分析一下,不评论网络文章,我们只要搞清楚,到底怎么样才算对齐,我们需要注意什么?
我们可以观察到的情况是,所有call压入的返回地址所存储的rsp指针结尾数值都是8,我们知道call这个指令执行后会压入这个返回地址,然后rsp-8,进入函数体以后由被调用方尾部ret返回,然后rsp+8,那么倒推一下,就是在调用call指令前,rsp的值=返回地址所存储的rsp指针+8
那么也就是说在调用这条call指令前当时的 rsp栈指针其实是0x?????????????0
在调用这个call指令后进入了函数体后 rsp栈指针其实是0x?????????????8
函数体执行完整个代码尾部ret返回后 rsp栈指针其实是0x?????????????0
以上完成整个 栈指针16字节对齐过程,调用者在调用call前与调用call返回后栈指针无变化。
总结而言就是所谓堆栈16字节对齐就是在 代码走到call这条指令的时候 rsp栈指针其实是0x?????????????0
对,rsp的最后一位数值 ,最后一位数值是十六进制0,每条call指令的经过都是这个情况,那么所谓的call前
sub rsp,0x多少 这个抬起多少其实是根据函数体内部其他栈操作指令的影响来决定的,总是当运行到call这条指令的时候rsp最后一位值必须是0
所有的栈指针8进位,结尾不是0就是8,因为十进制8=十六进制8,而十进制8+8=16=十六进制10
那么在所有函数体的第一句代码未执行的时候,rsp最后一位值必须是8
以下用两个列子说明情况,先给上x64 参数的调用规律
' ----------调用call时参数的赋值规律
' mov [rsp+28],参数六
' mov [rsp+20],参数五
' mov r9,参数四
' mov r8,参数三
' mov rdx,参数二
' mov rcx,参数一
' CALL 函数入口
' ----------call内部参数入栈规律
' call以后函数内部rsp的指向返回地址,实际在函数里各参数的地址是
' mov 参数六,[rsp+30]
' mov 参数五,[rsp+28]
' mov [rsp+20],r9 参数四
' mov [rsp+18],r8 参数三
' mov [rsp+10],rdx 参数二
' mov [rsp+8],rcx 参数一 复制代码
比如进入一个函数体后会在调用一个1个参数的call
进入一个新函数体由于返回地址的压入,rsp不对齐,假如现在的rsp=0x00000000000028
注意观察每句指令结束后rsp的值
Push rbp // rsp=0x000000000000020
Push rdi // rsp=0x00000000000018
Push r14 // rsp=0x0000000000010
sub rsp,0x10 // rsp=0x00000000000000 //一个参数应该是0×8rsp-0x8,但是为了对齐必须抬0×10
mov rcx,0 // rsp=0x00000000000000 //参数一
call 0xfffffffffffffff // rsp=0x00000000000000 返回后依然不变
add rsp,0x10 // rsp=0x000000000000010 //平栈
pop r14 // rsp=0x00000000000018
pop rdi // rsp=0x000000000000020
pop rbp // rsp=0x000000000000028
复制代码
比如在调用一个2个参数的call
假如现在的rsp=0x00000000000030是对齐状态
注意观察每句指令结束后rsp的值
Push rbp // rsp=0x00000000000028
Push rdi // rsp=0x00000000000020
Push r14 // rsp=0x00000000000018
sub rsp,0x18 // rsp=0x00000000000000 //两个参数其实0x10就够了,这里0x18就是为了16字节对齐
mov rcx,0 // rsp=0x00000000000000//参数一
mov rdx,0 // rsp=0x00000000000000//参数二
call 0xfffffffffffffff // rsp=0x00000000000000 //返回后依然不变
add rsp,0x18 // rsp=0x00000000000018//平栈
pop r14 // rsp=0x00000000000020
pop rdi // rsp=0x00000000000028
pop rbp // rsp=0x00000000000030 复制代码
以上列子充分说明 sub rsp,0x值,这个值是由当时函数体情况而定的,目的只有一个在走到call这个指令前rsp最后一位数值必须为0
有的朋友取尝试用调试器下个断点,在观察堆栈可能会疑惑有的返回值地址栈指针最后一位是8,那么这个指针指向的仅仅是一个其他需要而存储的一个值而已,只是调试器把他解释为调用的返回地址而已,这个指针肯定不会被用来返回的,不信的话你可以单步调试,走到每句call指令时rsp最后一位数值必为0
这就是x64汇编中比较有意思的事情,今天的趣谈就到这里吧,欢迎指正。
评分
查看全部评分