在汇编中如何将右值分配给左值?

Hal*_*ion 5 c++ assembly stack rvalue

第一个问题在这里。我将在几周/几个月内需要创建程序代码,其中将有函数将大(我的意思是非常大)的数据集直接分配给指针。这是我将要做的一些代码示例:

void MyFuntion(string* str)
{
     *str = "some data in a string";
}
Run Code Online (Sandbox Code Playgroud)

这确实很重要:我在 windows 10 上,在 Visual-studio 2019 中,使用 x86 版本上的默认 C++ 编译器进行编译。

想象一下类似的情况,但字符串可以包含数百万个字符,或者 int/float 数组也包含数百万个元素。因此,这是将右值分配给指针的单个操作,因此该指针位于堆上。当然,如果我创建一个包含数据的局部变量,它将超过 1MB,因此会导致堆栈溢出,对吗?

据我了解,由于数据在这里仅作为右值存在,因此它没有内存存在,但我想知道:右值如何分配给指针?比如,它是如何在装配中完成的?我必须说我从来没有做过任何组装,我有一些(很少)想法,但我想在有时间的时候参与其中。

它是在放入最终内存地址之前在堆栈或堆中临时创建的吗?我的猜测是,内存地址(我在其中分配数据的指针)直接填充了数据,就像一点一点一样,所以内存中不存在右值。

如果我是正确的,这里堆栈中存在的唯一内容是:函数调用,指针复制,然后是指令,应该类似于“将右值 X 分配给左值 Y”,并且指令的大小不取决于右值和左值的大小,因此这里的堆栈不应该有任何问题。

因此,如果我是正确的,那么无论右值有多大,这段代码都不会引起任何问题,但我仍然想知道它是如何在汇编方面准确完成的。请注意,我不仅仅是在寻找答案,而且更像是一些可以详细解释的参考文献、书籍或文档。我想我正在寻找的内容不会出现在一本 C++ 书中,而更像是一本汇编书,这可能是让自己进入其中的一个很好的起点!

Mon*_*nad 2

尽管提到了特定的操作系统和编译器,但此答案中的示例程序集可能与查询者的编译器输出的内容不同,因为我在撰写本文时没有可用的 Windows 10 计算机,并且使用了不同的环境,但我忘记了神箭。然而,在我看来,这个主题足够笼统,在这种特定情况下它并不重要。


赋值运算符右侧的值到底是什么?装配级别的分配是什么样的?这是一个简单的例子。

void assign_thing(int *p) {
    *p = 42;
}
Run Code Online (Sandbox Code Playgroud)
movl $42, (%rdi)
retq
Run Code Online (Sandbox Code Playgroud)

“将 32 位整数移至42所指向的内存位置rdi。” %rdi这里代表p、 和 的(%rdi)意思*p。对于像整数这样非常简单的东西,它几乎就是这么简单。简单的结构怎么样?

movl $42, (%rdi)
retq
Run Code Online (Sandbox Code Playgroud)
movabsq $4593671619917905962, %rax
movq    %rax, (%rdi)
movabsq $36762444129608, %rax
movq    %rax, 8(%rdi)
retq
Run Code Online (Sandbox Code Playgroud)

乍一看有点难读,但想法几乎相同。编译器很聪明,将整数和浮点值打包421.5单个 64 位值,并将其直接填充到(%rdi). 与字符串 类似"Hello!",它足够短以适合单个 64 位值并被填充到8(%rdi)(过去的 8 个字节p是 的偏移量text)。


到目前为止,当分配右值时,它们实际上并不存在于内存中。它们只是说明的一部分。如果它是更大的东西,比如一根绳子怎么办?

// Overflow checking omitted for brevity.
void assign_thing(char *p) {
    // Assignment with = doesn't actually do what you'd want here,
    // so this'll have to do.
    strcpy(p, "What if it's something a lot bigger, like a string?");
}
Run Code Online (Sandbox Code Playgroud)
vmovups -5484(%rip), %ymm0
vmovups %ymm0, 20(%rdi) ; I'm guessing the disassembler meant to say 0x20
vmovups -5517(%rip), %ymm0
vmovups %ymm0, (%rdi)
vzeroupper
retq
Run Code Online (Sandbox Code Playgroud)

现在,右值在被分配时确实驻留在内存中。请注意,这并不是因为strcpy使用了 was 来代替=,而是因为编译器认为最好将该“右值”字符串存储在只读区域中的某个位置.rodata,然后将其复制过来。如果我使用了更短的字符串,任何相当现代的编译器都可能会将其优化为几个mov或多个movabsq指令,如第二个示例中所示。除非p指向堆栈上的缓冲区并且strcpy最终导致其溢出,否则这里不会出现堆栈溢出。


现在你的例子怎么样?我猜你的string类型确实是std::string,而且这不是一个简单的类型。那么那里会发生什么呢?在C++中,赋值运算符=是可重载的,并且std::string确实有自己的重载,因此不是直接将值填充或复制到对象中,而是operator=调用一个特殊的成员函数。也就是说,你*str = "some data in a string"确实是一个str->operator=("some data in a string")。如何复制右值字符串取决于 的实现std::string::operator=,但它很可能会被优化为类似于我的上一个示例的内容。an 的实际字符串数据std::string驻留在堆上,因此堆栈溢出在这里仍然不是问题。


tl;dr(这个答案+评论,压缩成几句话)

如果您的字符串足够小,则在分配期间它可能不会存在于内存中。如果它足够大,它将位于某处的只读区域中,并在需要时进行复制。通常甚至不涉及堆栈,因此不必担心溢出。