汇编:为什么堆栈上有空内存?
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(我的猜测),我想了解更多底层逻辑。
所以我的问题是:
- 为什么灭菌不同?
- 为什么堆栈上有一个空的 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
的值为 2
,b
指向。在未优化的编译中,这些对象中的每一个都需要存储在堆栈上的某个唯一位置,并且编译器分配堆栈 space 天真,一个一个地处理变量并将每个 space 赋值在前一个下面.它显然选择按以下顺序处理它们:
变量a
,一个int
需要4个字节。它进入第一个可用的堆栈槽,位于 [rbp-4]
.
引用b
,存储为一个需要8字节的指针。您可能认为它会到达 [rbp-12]
,但 x86-64 ABI 要求指针在 8 字节边界上自然对齐。所以编译器再向下移动 4 个字节来实现这种对齐,将 b
放在 [rbp-16]
上。 [rbp-8]
处的 4 个字节目前尚未使用。
临时的int
,也需要4个字节。编译器将它放在先前放置的变量的正下方 [rbp-20]
。确实,[rbp-8]
处有 space 可以代替使用,这样效率会更高;但是由于您告诉编译器不要优化,它不会执行此 优化。如果您使用 -O
标志之一。
至于为什么 a
是通过立即存储到内存来初始化的,而临时是通过寄存器初始化的:要真正回答这个问题,您必须阅读 GCC 源代码的详细信息,坦率地说,我不认为你会发现它背后有什么非常有趣的东西。据推测,编译器中创建和初始化命名变量与临时变量的代码路径不同,而临时变量的代码可能恰好分为两步编写。
可能是为了方便,程序员选择在中间表示(GIMPLE或RTL)中创建一个额外的对象,也许是因为它在处理更一般的情况时简化了编译器代码。他们不会费心去避免这种情况,因为他们知道以后的优化过程会清理它。但是,如果您关闭了优化,则不会发生这种情况,您会收到针对这种不必要的传输发出的实际指令。
我用在线编译器写了一个简单的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(我的猜测),我想了解更多底层逻辑。
所以我的问题是:
- 为什么灭菌不同?
- 为什么堆栈上有一个空的 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
的值为 2
,b
指向。在未优化的编译中,这些对象中的每一个都需要存储在堆栈上的某个唯一位置,并且编译器分配堆栈 space 天真,一个一个地处理变量并将每个 space 赋值在前一个下面.它显然选择按以下顺序处理它们:
变量
a
,一个int
需要4个字节。它进入第一个可用的堆栈槽,位于[rbp-4]
.引用
b
,存储为一个需要8字节的指针。您可能认为它会到达[rbp-12]
,但 x86-64 ABI 要求指针在 8 字节边界上自然对齐。所以编译器再向下移动 4 个字节来实现这种对齐,将b
放在[rbp-16]
上。[rbp-8]
处的 4 个字节目前尚未使用。临时的
int
,也需要4个字节。编译器将它放在先前放置的变量的正下方[rbp-20]
。确实,[rbp-8]
处有 space 可以代替使用,这样效率会更高;但是由于您告诉编译器不要优化,它不会执行此 优化。如果您使用-O
标志之一。
至于为什么 a
是通过立即存储到内存来初始化的,而临时是通过寄存器初始化的:要真正回答这个问题,您必须阅读 GCC 源代码的详细信息,坦率地说,我不认为你会发现它背后有什么非常有趣的东西。据推测,编译器中创建和初始化命名变量与临时变量的代码路径不同,而临时变量的代码可能恰好分为两步编写。
可能是为了方便,程序员选择在中间表示(GIMPLE或RTL)中创建一个额外的对象,也许是因为它在处理更一般的情况时简化了编译器代码。他们不会费心去避免这种情况,因为他们知道以后的优化过程会清理它。但是,如果您关闭了优化,则不会发生这种情况,您会收到针对这种不必要的传输发出的实际指令。