and esp, 0xfffffff0

21

我不完全理解下面带有注释的那行代码。我在SO上读了几篇文章并查阅了gcc手册,得知它是用于堆栈地址对齐,但我不明白它是如何实现的。以下是代码:

(gdb) disas main
Dump of assembler code for function main:
   0x08048414 <+0>: push   ebp
   0x08048415 <+1>: mov    ebp,esp
   0x08048417 <+3>: and    esp,0xfffffff0 ; why??
   0x0804841a <+6>: sub    esp,0x10
   0x0804841d <+9>: mov    DWORD PTR [esp],0x8048510
   0x08048424 <+16>:    call   0x8048320 <puts@plt>
   0x08048429 <+21>:    mov    DWORD PTR [esp],0x8048520
   0x08048430 <+28>:    call   0x8048330 <system@plt>
   0x08048435 <+33>:    leave
   0x08048436 <+34>:    ret
End of assembler dump.

该代码是使用Linux上的gcc(版本4.6.3)生成的。谢谢。


9
这是通过蛮力对齐的,最低的4位被重置,因此根据定义,它现在至少对齐到16。 - harold
1
它使地址成为16的倍数,即针对128位处理器进行优化。 - Mr Lister
1
N字节对齐意味着起始地址是N字节的倍数。如果N是2的幂,则它还意味着存储整个值的所有字节地址完全相同,除了最低位的log2(N)位。这允许使用像您发布的代码中那样的简单掩码技术,而不是整数模数(除法余数)运算。 - Mike DeSimone
可能是为什么Mac ABI要求x86-32的16字节堆栈对齐?的重复问题。 - nneonneo
@MikeDeSimone,你的解释和其他人的确是合理的。谢谢大家。 - gumchew
显示剩余2条评论
3个回答

17

and esp, 0xfffffff0 对栈指针和一个常量进行按位与运算,并将结果存回到栈指针中。

该常量被选择为其低四位为零。因此,AND操作将在结果中将这些位设置为零,并保留esp的其他位。这将导致将栈指针向下舍入到最近的16的倍数。


1
这将导致将栈指针向下舍入到最接近16的倍数。 - gumchew
非常好。现在我完全明白了。感谢您的耐心。虽然花了一点时间,但我们终于做到了! :) - gumchew

7

看起来这是一些设置商店的代码,位于main开始部分。

函数开始:将基础帧指针保存在堆栈上(稍后leave指令需要用到):

   0x08048414 <+0>: push   ebp

现在,我们将栈指针对齐到16字节的边界,因为编译器(出于某种原因)需要这样。可能是因为它总是希望16字节对齐的帧,或者局部变量需要16字节对齐(也许有人使用了uint128_t,或者正在使用一个使用gcc向量扩展的类型)。基本上,由于结果始终小于或等于当前堆栈指针,并且栈向下增长,因此只需丢弃一些字节,直到它到达16字节对齐点即可。
   0x08048415 <+1>: mov    ebp,esp
   0x08048417 <+3>: and    esp,0xfffffff0

接下来,我们从堆栈指针中减去16,创建16字节的本地变量空间。
   0x0804841a <+6>: sub    esp,0x10

puts((const char*)0x8048510);

   0x0804841d <+9>: mov    DWORD PTR [esp],0x8048510
   0x08048424 <+16>:    call   0x8048320 <puts@plt>

system((const char*)0x8048520);

   0x08048429 <+21>:    mov    DWORD PTR [esp],0x8048520
   0x08048430 <+28>:    call   0x8048330 <system@plt>

退出函数(参见关于leave另一个答案):
   0x08048435 <+33>:    leave
   0x08048436 <+34>:    ret

“丢弃字节”的例子:假设在main函数开始时,esp = 0x123C。代码的第一行:

   0x08048414 <+0>: push   ebp
   0x08048415 <+1>: mov    ebp,esp

结果在这个内存映射中:

0x123C: (start of stack frame of calling function)
0x1238: (old ebp value) <-- esp, ebp

然后:

   0x08048417 <+3>: and    esp,0xfffffff0

将ESP的最后4位强制为0,这样做的目的是:

0x123C: (start of stack frame of calling function)
0x1238: (old ebp value) <-- ebp
0x1234: (undefined)
0x1230: (undefined) <-- esp

在这个阶段,程序员无法依赖于espebp之间有一定数量的内存可用;因此,该内存被废弃并未被使用。

最后,程序分配了16个字节的栈(局部)存储空间:

接下来,我们从堆栈指针中减去16,创建出16个字节的局部变量空间:

   0x0804841a <+6>: sub    esp,0x10

给我们提供了这张地图:
0x123C: (start of stack frame of calling function)
0x1238: (old ebp value) <-- ebp
0x1234: (undefined)
0x1230: (undefined)
0x123C: (undefined local space)
0x1238: (undefined local space)
0x1234: (undefined local space)
0x1230: (undefined local space) <-- esp

在这一点上,程序可以确定esp指向16个字节的内存,并且这些内存是按16字节对齐的。


哇,这很全面。你能再解释一下“它只是丢弃字节,直到达到16字节对齐点”的意思吗?我对其他部分都理解得很好。谢谢。 - gumchew
太好了。现在我完全明白了。感谢您的耐心。虽然花了一点时间,但我们终于做到了! :) - gumchew
更新了一个例子。 - Mike DeSimone
现在我们将栈指针对齐到16字节边界,因为编译器(出于某种原因)需要。这可能是因为它始终需要16字节对齐的帧,或者局部变量需要16字节对齐(也许有人使用了uint128_t,或者他们正在使用使用gcc向量扩展的类型)。<- 这可能是因为GCC生成支持SSE / SSE2的代码。此外,在main()中我没有使用uint128_t类型。 - gumchew

5

我知道这是很久以前发布的,但它可能对其他人有帮助。

1)在现代处理器中,我们知道GCC将堆栈默认对齐为16字节对齐。

2)16字节(128位)是由于SSE2指令具有MMXXMM寄存器,而XMM寄存器为128位。

3)所以当一个函数调用被执行时,它会自动对齐到16字节,在函数外部它保持8字节

4)使用0xfffffff0的逻辑是将低4位保持为0, 这是因为简单的布尔数学表明,在二进制中,16的倍数有低4位为零(为什么是四位? 2 ^ 4 = 16)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接