【问题标题】:Assembly of variable-size stack frame: these stack-alignment instructions seem useless in allocating a VLA?可变大小堆栈帧的组装:这些堆栈对齐指令在分配 VLA 时似乎没用?
【发布时间】:2020-04-20 20:17:14
【问题描述】:

我正在阅读计算机系统:程序员视角第 3 版,3.10.5 Supporting Variable-Size Stack Frames, Figure 3.43 中的程序集让我感到困惑。

本书的部分试图解释可变大小的堆栈帧是如何生成的,并给出了一个 C 代码及其汇编版本作为示例。

这里是C和汇编的代码(书中图3.43):

不知道汇编中line 8-10有什么用。为什么不在第 7 行之后使用movq %rsp, %r8

(a) C 代码

long vframe(long n, long idx, long *q) {
    long i;
    long *p[n];
    p[0] = &i;
    for (i = 1; i < n; i++)
        p[i] = q;
    return *p[idx];
}

(b) 生成的汇编代码部分

vframe:
2:    pushq    %rbp
3:    movq     %rsp, %rbp  
4:    subq     $16, %rsp
5:    leaq     22(, %rdi, 8), %rax      
6:    andq     $-16, %rax       
7:    subq     %rax, %rsp
8:    leaq     7(%rsp), %rax
9:    shrq     $3, %rax
10:   leaq     0(, %rax, 8), %r8
11:   movq     %r8, %rcx
................................
12: L3:
13:   movq     %rdx, (%rcx, %rax, 8)
14:   addq     $1, %rax
15:   movq     %rax, -8(%rbp)
16: L2:
17:   movq     -8(%rbp), %rax
18:   cmpq     %rdi, %rax
19:   jl       L3
20:   leave
21:   ret

这是我的想法:

在第 7 行之后,%rsp 应该是 16 的倍数(%rsp 在调用 vframe 之前应该是 16 的倍数,因为堆栈帧对齐。当调用 vframe 时,%rsp 是减去 8 以保存调用者的返回地址,然后第 2 行中的 pushq 指令将 %rsp 再减去 8,并在第 4 行中减去 16。所以在第 7 行的开头,%rsp 是16的倍数。在第7行中,%rsp减去%rax。由于第6行使%rax成为16的倍数,所以第7行的结果是设置%rsp是16的倍数)这意味着%rsp 的低 4 位全为零。 然后在第 8 行,%rsp+7 存储在 %rax 中,在第 9 行,%rax 逻辑右移 3 位,在第 10 行,%rax*8 存储在 %r8 中。

在第 7 行之后,%rsp 的低 4 位全为零。在第 8 行 %rsp+7 只是使低 3 位全为 1,第 9 行截断这 3 个,在第 10 行 %rax*8 使结果左移 3 位。所以最终的结果应该就是原来的%rsp(第7行的结果)。

所以我想知道第 8-10 行是否没用。

为什么不在第 7 行之后使用 movq %rsp, %r8 并删除原来的第 8-10 行?

【问题讨论】:

  • 嘿@NameNull。让一个人阅读/拥有您提到的书的可能性很小。人们不会走极端,比如找书只回答你的问题。因此,最好在此处提供所有相关信息,这些信息会在书中看到。希望您已经阅读了How To Ask 链接。
  • @RC0993,您认为缺少哪些信息?这个问题对我来说似乎很完整。
  • NameNull,你的分析是正确的。这不是很好的代码。我唯一能想到的是,编译器有一些通用的代码序列,无论是否需要,它都可以用于对齐,并且没有动力优化可变大小的数组,因为它们不常用。
  • @prl 我发现,在我提到的书中的一个实践问题中,它在第 4 行的末尾给了我一些特定的 %rsp 值,然后要求我计算以下修改的值寄存器。而那些给定的 %rsp 值并不都是 16 的倍数。所以我猜这本书并没有假设第 7 行末尾的 %rsp 是 16 的倍数。解决方案说第 8-10 行的目的是对齐%r8 到 8 的倍数,以便指针数组对齐(%r8 是数组的地址),如果 %rsp 不是 16 的倍数,这很有意义。
  • 是的,这没用而且很奇怪。我能够使用 gcc4.8.5 -Og 重现本书的 asm 输出。 (-O1 优化了循环中i 的存储/重新加载,因为p[i] 实际上不能别名为i,即使它们都是longgodbolt.org/z/qwQxBu。因此,与其他一些 CS:APP 示例不同,这不是作者在伪造的 GCC 输出中编辑并弄错的情况。这种疯狂是真实的。您所询问的指令甚至在-O2 甚至在当前的 GCC9.2 中都存在!是的,由于 sizeof(long)

标签: c assembly x86-64 callstack stack-frame


【解决方案1】:

我认为一个有用的探索性程序是将您生成的代码减少到:

.globl _vframe
_vframe:
    pushq    %rbp
    movq     %rsp, %rbp  
    subq     $16, %rsp
    leaq     22(, %rdi, 8), %rax      
    andq     $-16, %rax       
    subq     %rax, %rsp
    leaq     7(%rsp), %rax
    shrq     $3, %rax
    leaq     0(, %rax, 8), %r8
    mov %r8, %rax
    sub %rsp, %rax
    leave
    ret

请注意,我只是删除了有用的代码,并返回了 %r8 和 %rsp 之间的差异。 然后写了一个驱动:

extern void *vframe(unsigned long n);


#include <stdio.h>

int main(void) {
    int i;
    for (i = 0; i < (1<<18); i++) {
        void *p = vframe(i);
        if (p) {
            printf("%d %p\n", i, p);
        }
    }
    return 0;
}

检查一下。他们总是一样的。所以为什么?当面对给定的构造(var len 数组)时,它可能是标准代码发射。编译器必须维护某些标准,例如可跟踪的调用框架和对齐,操作系统可能只是将此代码作为已知的解决方案发出。可变长度数组通常被认为是语言中的错误;向 c++ 致敬,为 C 添加了一个半工作半深思熟虑的机制;因此编译器实现者可能不会过多关注代表他们生成的代码。

【讨论】:

  • 您的测试调用者在调用之前将始终对齐 RSP 16 字节。看起来 VLA 分配代码正在将数组地址与alignof(long*) 对齐,以防 RSP 甚至没有对齐 8。这太疯狂了,当前的 GCC 仍然这样做。也许它是 32 位模式遗留下来的样板(其中 GCC 希望在例如 Windows 上的 4 字节对齐堆栈上对 int64_t 进行 8 字节对齐),或者来自 alignof() > 堆栈对齐的数组元素的可能性?但是是的,您的测试证明,鉴于 ABI 保证 16 字节 RSP 对齐,对于 long* 的数组来说,这些东西是多余的。
  • 我对 VLA 成为 codegen 死水的看法有点像在 rdtsc 之前“aam”是猜测 cpu 频率的最稳定来源。是的,它就在那里,但没有人真正关心它的工作情况。
  • VLA 最初旨在更好地处理多维数组。他们在那里工作得很好。可变长度堆栈分配的能力只是一个附带功能,它以某种方式变得如此流行和臭名昭著,以至于它使人们蒙蔽了 VLA 的真正用途。 std::vector 不能替代 VLA。这些是正交特征。
猜你喜欢
  • 2016-05-04
  • 2019-04-13
  • 2011-03-22
  • 2018-06-29
  • 1970-01-01
  • 1970-01-01
  • 2014-02-25
  • 2021-03-15
  • 2021-04-07
相关资源
最近更新 更多