汇编:为什么堆栈上有空内存?

Assembly: Why there is an empty memory on stack?

我用在线编译器写了一个简单的c++代码:

int main()
{
    int a = 4;
    int&& b = 2;
}

gcc 11.20编译的汇编代码主要功能部分如下图

main:

push    rbp
mov     rbp, rsp
mov     DWORD PTR [rbp-4], 4
mov     eax, 2
mov     DWORD PTR [rbp-20], eax
lea     rax, [rbp-20]
mov     QWORD PTR [rbp-16], rax
mov     eax, 0
pop     rbp
ret

我注意到在初始化'a'时,指令只是简单地将一个立即操作数直接移动到内存,而对于右值引用'b',它首先将立即值存储到寄存器eax中,然后将它移动到内存中,并且在 [rbp-8] ~ [rbp-4] 之间还有一个未使用的内存,我认为无论立即值是什么,它们都存在,所以它必须在某个地方或者它只是简单地使用信号来initialize(我的猜测),我想了解更多底层逻辑。

所以我的问题是:

  1. 为什么灭菌不同?
  2. 为什么堆栈上有一个空的 4 字节未使用内存?

 int a = 4;

你声明了一个(通常)4 字节的变量,并要求编译器用 4 的位表示来填充它。 在

int&& b = 2;

你声明了一个引用(r-value 引用),嗯,对什么?字面上的?可能吗?在 C++ 中,引用通常在汇编级别上转换为指针。因此可以预期 b 将是“伪装的指针”,也就是说,没有 *-> 语义。但它可能会在 64 位机器上占用 64 位。现在,指针必须指向存储在 RAM 中的内存,而不是寄存器、缓存等。因此编译器很可能会创建一个临时(未命名)整数,用 2 初始化它,然后将其地址绑定到 b.我写“最有可能”是因为我怀疑标准是否如此详细地对此进行了标准化。我们可以确定的是,int&& b = 2;.

中的b的初始化涉及到一个额外的未命名变量

关于汇编,我的知识太少,不敢跟你解释。但是,我想 && 引用后面的临时变量和指针的概念可以解决您在这里的所有问题。

先说第二个问题

注意这个函数中实际上定义了三个对象:int变量a、引用b(实现为指针)、未命名的临时int 的值为 2b 指向。在未优化的编译中,这些对象中的每一个都需要存储在堆栈上的某个唯一位置,并且编译器分配堆栈 space 天真,一个一个地处理变量并将每个 space 赋值在前一个下面.它显然选择按以下顺序处理它们:

  1. 变量a,一个int需要4个字节。它进入第一个可用的堆栈槽,位于 [rbp-4].

  2. 引用b,存储为一个需要8字节的指针。您可能认为它会到达 [rbp-12],但 x86-64 ABI 要求指针在 8 字节边界上自然对齐。所以编译器再向下移动 4 个字节来实现这种对齐,将 b 放在 [rbp-16] 上。 [rbp-8] 处的 4 个字节目前尚未使用。

  3. 临时的int,也需要4个字节。编译器将它放在先前放置的变量的正下方 [rbp-20]。确实,[rbp-8] 处有 space 可以代替使用,这样效率会更高;但是由于您告诉编译器不要优化,它不会执行此 优化。如果您使用 -O 标志之一。

至于为什么 a 是通过立即存储到内存来初始化的,而临时是通过寄存器初始化的:要真正回答这个问题,您必须阅读 GCC 源代码的详细信息,坦率地说,我不认为你会发现它背后有什么非常有趣的东西。据推测,编译器中创建和初始化命名变量与临时变量的代码路径不同,而临时变量的代码可能恰好分为两步编写。

可能是为了方便,程序员选择在中间表示(GIMPLE或RTL)中创建一个额外的对象,也许是因为它在处理更一般的情况时简化了编译器代码。他们不会费心去避免这种情况,因为他们知道以后的优化过程会清理它。但是,如果您关闭了优化,则不会发生这种情况,您会收到针对这种不必要的传输发出的实际指令。