【问题标题】:and esp, 0xfffffff0尤其是 0xffffffff0
【发布时间】:2014-07-05 17:23:58
【问题描述】:

我不完全理解下面带有注释的那一行。我阅读了一些关于 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)生成的。谢谢。

【问题讨论】:

  • 它是通过蛮力对齐的,最低 4 位被重置,因此根据定义它现在至少对齐到 16。
  • 它使地址成为 16 的倍数,即针对 128 位处理器进行了优化。
  • @harold 好的。是的。 ESP 的最后 4 位与 0000b 进行“与”运算(因此复位)。但这如何使它 2^4 字节对齐?也许我应该先了解对齐的含义。
  • N字节对齐意味着起始地址是N字节的倍数。如果 N 是 2 的幂,这也意味着存储整个值的所有字节地址都完全相同 除了 最低阶 log2(N) 位。这允许像您发布的代码中那样简单的屏蔽技术,而不是整数模(除法的余数)操作。

标签: assembly x86


【解决方案1】:

and esp, 0xfffffff0 在堆栈指针和常量之间进行按位与运算,并将结果存储回堆栈指针中。

选择常数,使其低四位为零。因此,AND 操作会将结果中的这些位设置为零,而将esp 的其他位保持不变。这具有将堆栈指针向下舍入到最接近的 16 倍数的效果。

【讨论】:

  • "这具有将堆栈指针向下舍入到最接近的 16 倍数的效果。"
  • 优秀。现在,我完全明白了。谢谢你的耐心。它确实花了一些时间,但到了那里! :)
【解决方案2】:

它看起来像是在main开头的一些代码的一部分。

函数开始:将基帧指针保存在堆栈上(后面的leave指令需要):

   0x08048414 <+0>: push   ebp

现在我们将堆栈指针对齐到一个 16 字节的边界,因为编译器(无论出于何种原因)需要它。这可能是它总是需要 16 字节对齐的帧,或者局部变量需要 16 字节对齐(可能有人使用了 uint128_t 或者他们正在使用使用 gcc vector extensions 的类型)。基本上,由于结果总是小于或等于当前堆栈指针,并且堆栈向下增长,它只是丢弃字节,直到它到达 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>

退出函数(参见another answer 了解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 字节对齐点”。方法?我非常了解他们的其余部分。谢谢
  • 优秀。现在,我完全明白了。谢谢你的耐心。它确实花了一些时间,但到了那里! :)
  • 更新了一个例子。
  • 现在我们将堆栈指针对齐到一个 16 字节的边界,因为编译器(无论出于何种原因)需要它。这可能是它总是需要 16 字节对齐的帧,或者局部变量需要 16 字节对齐(可能有人使用了 uint128_t 或者他们正在使用使用 gcc 向量扩展的类型)。 GCC 生成了 SSE/SSE2 支持代码。另外,我不在 main() 中使用 uint128_t 类型。
【解决方案3】:

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

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

2) 16 byte(128 位)是因为 SSE2 指令具有 MMXXMM 寄存器XMM 寄存器为 128 位。

3) 因此,当进行函数调用时,它会自动对齐到 16 字节,在函数之外它仍然保持在 8 字节

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

【讨论】:

    猜你喜欢
    • 2015-04-12
    • 2014-09-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-23
    相关资源
    最近更新 更多