在此编译器输出中,为什么 func(int) 使用其第一个 arg 作为指针,将指向内存的 24 个字节归零? arg 不是指针

In this compiler output, why does func(int) use its first arg as a pointer, zeroing 24 bytes of pointed-to memory? The arg isn't a pointer

我在理解这段汇编代码的作用时遇到了问题(这是更大的汇编代码的一小部分,这是英特尔语法):

vector<int> func(int i) { ...}  // C++ source

clang 输出 from the Godbolt compiler explorer:

func(int): # @func(int)
    push    rbp
    push    rbx
    push    rax
    mov     ebp, esi
    mov     rbx, rdi
    xorps   xmm0, xmm0
    movups  xmmword ptr [rbx], xmm0
    mov     qword ptr [rbx + 16], 0

这是在 Linux 上编译的,遵循官方的 System V AMD64 ABI。 根据 this link,rdi 寄存器用于将第一个参数传递给函数。 所以在这一行

mov rbx, rdi

我们将参数的值(在本例中为 int)移动到 rbx。不久之后,我们做:

movups xmmword ptr [rbx], xmm0

这就是我不明白的地方。 rbx 包含参数的值,它是一个int,这里我们将xmm0 的内容复制到rbx 指向的地址(但是rbx 不包含任何地址,只是函数的参数!)

有些地方我弄错了,但我不知道为什么。

在 Linux 和 Windows 之外的大多数其他 64 位 x86 操作系统使用的 SysV 64 位 ABI 中,structclass return 值在 raxrdx 寄存器中被 return 编辑,或者通过作为第一个参数传递的 隐藏指针

两个选项之间的决定主要取决于 returned 结构的大小:大于 16 字节的结构通常使用隐藏指针方法,但还有其他因素,我建议 以获得更全面的治疗。

当使用隐藏指针方法时,我们需要一种方法将此指针传递给函数。在这种情况下,指针的行为就好像它是第一个参数(传入 rdi),它将其他参数移到后面的位置 2

我们可以通过检查为函数生成的代码清楚地看到这一点 returning struct 对象的 1 到 5 int 值(因此在这个平台上是 4 到 20 个字节)。 C++代码:

struct one {
    int x;
};

struct two {
    int x1, x2;
};

struct three {
    int x1, x2, x3;
};

struct four {
    int x1, x2, x3, x4;
};

struct five {
    int x1, x2, x3, x4, x5;
};


one makeOne() {
    return {42};
}

two makeTwo() {
    return {42, 52};
}

three makeThree() {
    return {42, 52, 62};
}

four makeFour() {
    return {42, 52, 62, 72};
}

five makeFive() {
    return {42, 52, 62, 72, 82};
}

following assemblyclang 6.0 中的结果(但其他编译器的行为类似:

makeOne():                            # @makeOne()
        mov     eax, 42
        ret
makeTwo():                            # @makeTwo()
        movabs  rax, 223338299434
        ret
makeThree():                          # @makeThree()
        movabs  rax, 223338299434
        mov     edx, 62
        ret
makeFour():                           # @makeFour()
        movabs  rax, 223338299434
        movabs  rdx, 309237645374
        ret
.LCPI4_0:
        .long   42                      # 0x2a
        .long   52                      # 0x34
        .long   62                      # 0x3e
        .long   72                      # 0x48
makeFive():                           # @makeFive()
        movaps  xmm0, xmmword ptr [rip + .LCPI4_0] # xmm0 = [42,52,62,72]
        movups  xmmword ptr [rdi], xmm0
        mov     dword ptr [rdi + 16], 82
        mov     rax, rdi
        ret

基本模式是最多并包括 8 个字节,struct 完全 returned in rax(包括在 64 位寄存器中打包多个较小的值),并且对于最多 16 字节的对象,使用 raxrdx1.

之后策略完全变了,我们看到rdi指向的位置发生了内存写入——这就是上面提到的隐藏指针的做法。

最后,总结一下,我们注意到 sizeof(vector<int>) 在 Linux 上的主要 C++ 编译器上是 on 64-bit platforms, and is definitely 24 字节 - 因此隐藏指针方法适用于向量。


感谢 Jester,他已经以更简短的形式回答了这个问题,

1 存储到 64 位寄存器中的奇怪常量,如 223338299434 只是一种优化:编译器只是组合了两个 32 位存储转换为单个 64 位常量,如 52ul << 32 | 42ul 中的结果 223338299434.

2 这与用于为成员函数传递 this 的方法相同:在成员函数 also returns 一个通过隐藏指针方法传递的值,首先是隐藏指针(在 rdi 中),然后是 this 指针(在 rsi 中),最后是第一个用户提供的参数(通常在 rdx 中——但这取决于类型)。这里是 an example.