gcc可以省略在堆栈上保留数据吗?

sc4hvdpw  于 2023-06-23  发布在  其他
关注(0)|答案(1)|浏览(139)

我在x86_64上使用gcc 12.2.0,并在那里编译x64代码。我遇到了一个奇怪的问题,这给我带来了麻烦,并将其减少到最小的复制器:

#include <stdint.h>
#include <stdbool.h>

struct foobar_t {
    uint8_t data[512];
};

void my_memset(void *target) {
#if 1
    for (int i = 0; i < 256; i++) {
        ((uint16_t*)target)[i] = 0xabcd;
    }
#else
    for (int i = 0; i < 512; i++) {
        ((uint8_t*)target)[i] = 0xab;
    }
#endif
}

int main() {
    struct foobar_t foobar;
    my_memset(&foobar);
    if (foobar.data[123] == 0) {
        volatile int x = 0;
    }
    return 0;
}

当使用#if 1路径时,我会收到编译器警告:

$ gcc -O3 -fno-stack-protector -Wall -c -o x.o x.c
[...]
x.c:46:24: warning: ‘foobar’ is used uninitialized [-Wuninitialized]
   46 |         if (foobar.data[123] == 0) {

当我使用第二个代码路径(#if 0)时,这个错误完全消失了,唯一的区别是第一个代码路径设置了256个16位字,而第二个代码路径设置了512个字节。
在我得到警告的情况下,生成的程序集看起来也是错误的:

0000000000000000 <my_memset>:
   0:   f3 0f 1e fa             endbr64
   4:   66 0f 6f 05 00 00 00    movdqa 0x0(%rip),%xmm0        # c <my_memset+0xc>
   c:   48 8d 87 00 02 00 00    lea    0x200(%rdi),%rax
  13:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
  18:   0f 11 07                movups %xmm0,(%rdi)
  1b:   48 83 c7 10             add    $0x10,%rdi
  1f:   48 39 f8                cmp    %rdi,%rax
  22:   75 f4                   jne    18 <my_memset+0x18>
  24:   c3                      ret

0000000000000030 <main>:
  30:   f3 0f 1e fa             endbr64
  34:   48 81 ec a0 01 00 00    sub    $0x1a0,%rsp
  3b:   66 0f 6f 05 00 00 00    movdqa 0x0(%rip),%xmm0        # 43 <main+0x13>
  43:   48 8d 44 24 98          lea    -0x68(%rsp),%rax
  48:   48 8d 94 24 98 01 00    lea    0x198(%rsp),%rdx
  50:   0f 29 00                movaps %xmm0,(%rax)
  53:   48 83 c0 10             add    $0x10,%rax
  57:   48 39 c2                cmp    %rax,%rdx
  5a:   75 f4                   jne    50 <main+0x20>
  5c:   80 7c 24 13 00          cmpb   $0x0,0x13(%rsp)
  61:   75 08                   jne    6b <main+0x3b>
  63:   c7 44 24 94 00 00 00    movl   $0x0,-0x6c(%rsp)
  6b:   31 c0                   xor    %eax,%eax
  6d:   48 81 c4 a0 01 00 00    add    $0x1a0,%rsp
  74:   c3                      ret

这只保留堆栈上的0x 1a 0字节,即416字节。这不符合结构!这怎么可能?发生这种情况的原因是什么?
我已经尝试删除尽可能多的代码,同时仍然保留警告。如果我禁用优化,警告也会消失。

c9qzyr3d

c9qzyr3d1#

您的#if 1代码是非法的(未定义的行为),因为它违反了strict aliasing rule。非常粗略地说,除了某些狭义例外之外,您不能通过指向两个不同类型的指针访问相同的内存。
因此,编译器有权假设通过一个指针类型对内存的访问不会被通过另一个指针类型的访问所看到。因此,它会认为foobar未初始化并不奇怪,因为它没有考虑对uint16_t对象的访问可能会触及它。
在字符类型的标准中有一个例外,正是为了让您可以使用字符指针实现memsetmemcpy之类的东西。所以你的#else代码是法律的的,事实上编译器能够识别my_memset代码确实初始化了foobar,所以你不会收到警告。(严格地说,你的代码应该使用unsigned char而不是uint8_t--它们在大多数编译器上的类型化都是一样的,但是语言标准并不保证是这样。
关于“堆栈不足”的事情实际上是正常的,不是问题。对象foobar位于堆栈上的偏移量rsp-0x68rsp+0x198,正好是512字节,正如它应该的那样。它的一部分位于堆栈指针之下可能看起来很奇怪,但这没关系,因为它位于128字节的red zone中。
红色区域仅可用于叶函数(即那些不调用其他函数的函数),所以它只能在main中使用,如果对my_memset的调用是内联的。当优化关闭时,不会这样做,所以在这种情况下,您不会看到使用的红色区域。
在这个例子中,使用红色区域并没有真正实现很多。主要的好处是在函数中,通过使用红色区域,您完全不必调整堆栈指针。在这里,堆栈指针无论如何都必须进行调整,所以与从堆栈指针中减去完整的512字节的更自然的实现相比,我们没有获得任何好处。但是带有红色区域的代码在性能方面仍然是完全有效的和等效的,只是看起来很有趣。所以这只是编译器堆栈布局算法的一个稍微奇怪的怪癖。

相关问题