【问题标题】:Understanding Base Pointer and Stack Pointers: In Context with gcc Output了解基指针和堆栈指针:在 gcc 输出的上下文中
【发布时间】:2012-01-18 16:38:32
【问题描述】:

我有以下 C 程序:

int main()
{
    int c[10] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 2};
    return c[0];
}

当使用带有 gcc 的 -S 指令编译时,我得到以下程序集:

    .file   "array.c"
    .text
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $0, -48(%rbp)
    movl    $0, -44(%rbp)
    movl    $0, -40(%rbp)
    movl    $0, -36(%rbp)
    movl    $0, -32(%rbp)
    movl    $0, -28(%rbp)
    movl    $0, -24(%rbp)
    movl    $0, -20(%rbp)
    movl    $1, -16(%rbp)
    movl    $2, -12(%rbp)
    movl    -48(%rbp), %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (GNU) 4.4.5 20110214 (Red Hat 4.4.5-6)"
    .section        .note.GNU-stack,"",@progbits

我不明白的是为什么较早的数组元素离 bp 更远?看起来数组上的元素几乎是以相反的顺序放置的。

还有为什么 gcc 不使用 push 而不是 movl 来将数组元素压入堆栈?


不同的观点

将数组作为静态变量移动到全局命名空间到我得到的模块:

    .file   "array.c"
    .data
    .align 32
    .type   c, @object
    .size   c, 40
c:
    .long   0
    .long   0
    .long   0
    .long   0
    .long   0
    .long   0
    .long   0
    .long   0
    .long   1
    .long   2
    .text
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    c(%rip), %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (GNU) 4.4.5 20110214 (Red Hat 4.4.5-6)"
    .section    .note.GNU-stack,"",@progbits

使用以下 C 程序:

static int c[10] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 2};

int main() 
{
    return c[0];
}

这并没有提供对堆栈的更多了解。但是看到汇编的不同输出使用略有不同的语义是很有趣的。

【问题讨论】:

  • 那是使用 gcc 命令生成的代码,通常编译器会生成执行相同操作的不同代码。基本上,一个问题有多种解决方案。编译时是否使用 -o 进行了优化?可能是因为这个(或缺乏)。
  • 这实际上是编译器生成的代码,和编译器生成的代码不能有区别。优化标志是-O 而不是-o,是的,马修使用了-Os,正如他在问题中明确指出的那样......
  • 他说-S(将程序集生成为文本),而不是-Os(优化大小)。这个程序集似乎是在没有优化的情况下生成的;在-O 的任何级别,我看到数组存储完全消失了。

标签: c gcc assembly gnu-assembler


【解决方案1】:

还有为什么 gcc 不使用 push 而不是 movl 来将数组元素压入堆栈?

在堆栈帧的正确位置有一个大的初始化数组是非常罕见的,您可以使用一系列推送,所以 gcc 没有被教导这样做。 (更详细地说:数组初始化作为块内存副本处理,它作为移动指令序列或对memcpy的调用发出,具体取决于它有多大。决定发出什么的代码不会'不知道该块在内存中的位置,所以它不知道是否可以使用push。)

另外,movl 更快。具体来说,push%esp 进行了隐式的读-修改-写操作,因此必须按顺序执行一系列pushes。相比之下,movl 到独立地址可以并行执行。因此,通过使用 movls 序列而不是 pushes 序列,gcc 为 CPU 提供了更多指令级并行性以供利用。

请注意,如果我在激活任何优化级别的情况下编译您的代码,该数组就会完全消失!这里是-O1(这是在目标文件上运行objdump -dr的结果,而不是-S的输出,所以你可以看到实际的机器码)

0000000000000000 <main>:
   0:   b8 00 00 00 00          mov    $0x0,%eax
   5:   c3                      retq   

-Os:

0000000000000000 <main>:
   0:   31 c0                   xor    %eax,%eax
   2:   c3                      retq   

什么都不做总是比做某事快。使用xor 清除寄存器是两个字节而不是五个字节,但对寄存器的旧内容有正式的数据依赖性并修改条件代码,因此 可能 会更慢,因此仅在以下情况下选择优化大小。

【讨论】:

  • 嗯,我希望自己对寄存器进行异或运算是特殊情况,但是是的,它可能会更慢。
  • 我手头没有任何 x86 优化指南,但我记得有些模型会执行该操作的特殊情况,而有些则不会。如果你不告诉它,GCC 会尝试生成在各种 CPU 上经过合理调整的代码,尽管没有针对任何特定 CPU 进行完美调整。
  • 我认为它没有被教导这样做因为它会更慢。在这里有所作为很容易,但由于效率低下,因此没有完成。
  • push 不再慢了; Intel 自 Pentium-M(和 AMD 自类似时间以来)have a "stack engine" that breaks the dependency chain through RSP 并制作推送/弹出单指令。这将是对 mov-zero (What C/C++ compiler can use push pop instructions for creating local variables, instead of just increasing esp once?) 的胜利,对于代码大小也是如此(push imm8 与 REX movq imm32),但 pxor-zero / movdqa 16 字节存储在这里更好。
  • xor-zeroing 自之前(?)PPro(What is the best way to set a register to zero in x86 assembly: xor, mov or and?)以来一直是特殊情况,尽管它实际上在 PIII 之前仍然存在错误的依赖关系。您没有使用-O1 进行异或归零的原因是-fpeephole2 仅在-O2 和更高版本中启用。 -O1是部分优化; -Os 对大小(和速度)进行了全面优化; -O3 是对速度的全面优化。 (我敢肯定,自您发布以来的 10 年中,您已经弄清楚了大部分这些事情,但是这个答案有一些错误信息:/)
【解决方案2】:

首先,x86 堆栈向下增长。按照惯例,rbp 存储rsp 的原始值。因此,函数的参数位于相对于rbp 偏移处,其自动变量位于 偏移处。自动数组的第一个元素的地址低于所有其他元素,因此离rbp 最远。

这是一个方便的图表,出现在on this page

我看不出为什么编译器不能使用一系列push 指令来初始化您的数组。这是否是个好主意,我不确定。

【讨论】:

  • 这个图表通常会被颠倒过来,不是吗?既然高内存可以认为是在芯片的顶端?
  • @MatthewHoggan:也许吧。就清晰度而言,我个人对这两种方式都没有强烈的偏好。
  • @MatthewHoggan 内存区域的映射总是以较高的地址向页面顶部绘制。然而,数据结构、网络数据包等的图表通常在向页面底部偏移较大的情况下绘制。
  • @Zack:很久以前,我读到 little-endian 排序更喜欢上面的图形图,而 big-endian 更喜欢从左到右从上到下的表示。跨度>
  • @ninjalj IME 字节序有时会影响人们是否在左侧或右侧的单词中绘制高 ,但我从未见过它会改变高地址是否在顶部或在底部。
【解决方案3】:

请记住,在 x86 上,堆栈向下增长。压入堆栈将从堆栈指针中减去。

%rbp <-- Highest memory address
-12
-16
-20
-24
-28
-32
-36
-40
-44
-48  <-- Address of array

【讨论】:

  • 每次迷茫的时候都画一个这样的图表,很快就不会迷茫了!
猜你喜欢
  • 2021-09-17
  • 2012-05-03
  • 2021-12-26
  • 2012-09-05
  • 2011-02-13
  • 2013-01-20
  • 2015-08-05
  • 2015-11-16
  • 2011-12-09
相关资源
最近更新 更多