我最近利用了一个危险的程序,发现了x86-64架构上gcc
版本之间差异的一些有趣内容。
注意:
错误地使用gets
不此问题。
如果我们将gets
替换为任何其他功能,则问题不会改变。
这是我使用的源代码:
#include <stdio.h>
int main()
{
char buf[16];
gets(buf);
return 0;
}
我使用gcc.godbolt.org以标记-m32 -fno-stack-protector -z execstack -g
反汇编程序。
在反汇编代码中,gcc
版本&gt; = 4.9.0 :
lea ecx, [esp+4] # begin of main
and esp, -16
push DWORD PTR [ecx-4] # push esp
push ebp
mov ebp, esp
/* between these comment is not related to the question
push ecx
sub esp, 20
sub esp, 12
lea eax, [ebp-24]
push eax
call gets
add esp, 16
mov eax, 0
*/
mov ebp, esp
mov ecx, DWORD PTR [ebp-4] # ecx = saved esp
leave
lea esp, [ecx-4]
ret # end of main
但gcc版本&lt; 4.9.0 只是:
push ebp # begin of main
mov ebp, esp
/* between these comment is not related to the question
and esp, -16
sub esp, 32
lea eax, [esp+16]
mov DWORD PTR [esp], eax
call gets
mov eax, 0
*/
leave
ret # end of main
我的问题是:反汇编代码及其好处的差异是什么原因造成的?它有这个技术的名称吗?
答案 0 :(得分:1)
如果没有实际值,我无法肯定地说:
and esp, 0xXX # XX is a number
但这看起来很像额外的代码,可以将堆栈对齐到比ABI要求的更大的值。
编辑:值为-16,即32位0xFFFFFFF0
或64位0xFFFFFFFFFFFFFFF0
,因此这确实是堆栈对齐到16字节,可能意味着使用SSE指令。正如评论中所提到的,&gt; = 4.9.0版本中有更多代码,因为它也会对齐帧指针而不是堆栈指针。
答案 1 :(得分:1)
用于32位程序的i386 ABI强制一个进程在加载后立即必须使堆栈在32位值上对齐:
%esp
执行通常的工作,堆栈指针保存地址 堆栈的底部,保证字对齐。
使用用于64位程序的x86_64 ABI 1 来对付它:
处 16字节对齐
%rsp
堆栈指针保存具有最低地址的字节的地址 是堆栈的一部分。保证在进程条目
新AMD的64位技术改写旧i386 ABI的机会允许进行大量由于向后兼容而缺乏的优化,其中包括更大(更严格的)堆栈对齐。
我不会详细讨论堆栈对齐的好处,但只要说4字节对齐好,那么16字节对齐就好了。
非常值得花费一些指令来对齐堆栈。
这就是GCC 4.9.0+的作用,它将堆栈对齐为16字节
这解释了and esp, -16
而不是其他说明。
当编译器只知道堆栈是4字节对齐时(因为and esp, -16
可以是0,4,8或12),将堆栈与esp MOD 16
对齐是最快的方法。
但是,它是破坏性方法,编译器会丢失原始esp
值。
但是现在它出现了鸡或蛋的问题:如果我们在对齐堆栈之前将原始esp
保存在堆栈上,我们会失去它,因为我们不知道有多远通过对齐降低堆栈指针。如果我们在对齐之后保存它,那么我们就不能。我们在路线上失去了它
因此,唯一可行的解决方案是将其保存在寄存器中,对齐堆栈,然后将所述寄存器保存在堆栈中。
;Save the stack pointer in ECX, actually is ESP+4 but still does
lea ecx, [esp+4] #ECX = ESP+4
;Align the stack
and esp, -16 #This lowers ESP by 0, 4, 8 or 12
;IGNORE THIS FOR NOW
push DWORD PTR [ecx-4]
;Usual prolog
push ebp
mov ebp, esp
;Save the original ESP (before alignment), actually is ESP+4 but OK
push ecx
GCC将esp+4
保存在ecx
中,我不知道为什么 2 但这个值仍然可以解决问题。
唯一的谜团是push DWORD PTR [ecx-4]
但事实证明这是一个简单的谜团:出于调试目的,GCC会在旧帧指针之前(push ebp
之前)推送返回地址,这是32位工具所期望的。
自ecx=esp_o+4
起,其中esp_o
是原始堆栈指针预对齐,[ecx-4] = [esp_o] = return address
。
注意,现在堆栈的模数为12个12字节,因此局部变量区域的大小必须为16 * k + 4,以使堆栈再次以16字节对齐。
在您的示例中, k 为1,区域大小为20字节。
后续sub esp, 12
用于对齐gets
函数的堆栈(要求是在函数调用时对齐堆栈)。
最后,代码
mov ebp,esp
mov ecx,DWORD PTR [ebp-4] #ecx = saved esp
离开
lea esp,[ecx-4]
转发
第一条指令是复制粘贴错误
人们可以check it out或者只是推理
如果它在那里,[ebp-4]
将在下面堆栈指针(并且i386 ABI没有red zone)。
其余的只是撤消prolog中的内容:
;Get the original stack pointer
mov ecx, DWORD PTR [ebp-4] ;ecx = esp_o+4
;Standard epilog
leave ;mov esp, ebp / pop ebp
;The stack pointer points to the copied return address
;Restore the original stack pointer
lea esp, [ecx-4] ;esp = esp_o
ret
GCC必须先获取保存在堆栈中的原始堆栈指针(+4),然后恢复旧的帧指针(ebp
),最后恢复原始堆栈指针。
执行lea esp, [ecx-4]
时,返回地址位于堆栈的顶部,因此理论上GCC可以返回,但它必须恢复原始esp
,因为main
不是< / em>要在C程序中执行的第一个函数,因此它不能使堆栈不平衡。
1 这是不是最新版本,但引用的文字在后续版本中没有变化。
2 这已在此处讨论,但我不记得是在某些评论或答案中。