【问题标题】:GCC optimizer generating error in nostdlib codeGCC 优化器在 nostdlib 代码中生成错误
【发布时间】:2020-03-07 10:55:57
【问题描述】:

我有以下代码:

void cp(void *a, const void *b, int n) {
    for (int i = 0; i < n; ++i) {
        ((char *) a)[i] = ((const char *) b)[i];
    }
}

void _start(void) {
    char buf[20];

    const char m[] = "123456789012345";
    cp(buf, m, 15);

    register int rax __asm__ ("rax") = 60; // exit
    register int rdi __asm__ ("rdi") = 0; // status

    __asm__ volatile (
        "syscall" :: "r" (rax), "r" (rdi) : "cc", "rcx", "r11"
    );

    __builtin_unreachable();
}

如果我用gcc -nostdlib -O1 "./a.c" -o "./a" 编译它,我会得到一个正常运行的程序,但如果我用-O2 编译它,我会得到一个产生分段错误的程序。

这是带有-O1的生成代码:

0000000000001000 <cp>:
    1000:   b8 00 00 00 00          mov    $0x0,%eax
    1005:   0f b6 14 06             movzbl (%rsi,%rax,1),%edx
    1009:   88 14 07                mov    %dl,(%rdi,%rax,1)
    100c:   48 83 c0 01             add    $0x1,%rax
    1010:   48 83 f8 0f             cmp    $0xf,%rax
    1014:   75 ef                   jne    1005 <cp+0x5>
    1016:   c3                      retq   

0000000000001017 <_start>:
    1017:   48 83 ec 30             sub    $0x30,%rsp
    101b:   48 b8 31 32 33 34 35    movabs $0x3837363534333231,%rax
    1022:   36 37 38 
    1025:   48 ba 39 30 31 32 33    movabs $0x35343332313039,%rdx
    102c:   34 35 00 
    102f:   48 89 04 24             mov    %rax,(%rsp)
    1033:   48 89 54 24 08          mov    %rdx,0x8(%rsp)
    1038:   48 89 e6                mov    %rsp,%rsi
    103b:   48 8d 7c 24 10          lea    0x10(%rsp),%rdi
    1040:   ba 0f 00 00 00          mov    $0xf,%edx
    1045:   e8 b6 ff ff ff          callq  1000 <cp>
    104a:   b8 3c 00 00 00          mov    $0x3c,%eax
    104f:   bf 00 00 00 00          mov    $0x0,%edi
    1054:   0f 05                   syscall 

这是生成的带有-O2的代码:

0000000000001000 <cp>:
    1000:   31 c0                   xor    %eax,%eax
    1002:   66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)
    1008:   0f b6 14 06             movzbl (%rsi,%rax,1),%edx
    100c:   88 14 07                mov    %dl,(%rdi,%rax,1)
    100f:   48 83 c0 01             add    $0x1,%rax
    1013:   48 83 f8 0f             cmp    $0xf,%rax
    1017:   75 ef                   jne    1008 <cp+0x8>
    1019:   c3                      retq   
    101a:   66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)

0000000000001020 <_start>:
    1020:   48 8d 44 24 d8          lea    -0x28(%rsp),%rax
    1025:   48 8d 54 24 c9          lea    -0x37(%rsp),%rdx
    102a:   b9 31 00 00 00          mov    $0x31,%ecx
    102f:   66 0f 6f 05 c9 0f 00    movdqa 0xfc9(%rip),%xmm0        # 2000 <_start+0xfe0>
    1036:   00 
    1037:   48 8d 70 0f             lea    0xf(%rax),%rsi
    103b:   0f 29 44 24 c8          movaps %xmm0,-0x38(%rsp)
    1040:   eb 0d                   jmp    104f <_start+0x2f>
    1042:   66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)
    1048:   0f b6 0a                movzbl (%rdx),%ecx
    104b:   48 83 c2 01             add    $0x1,%rdx
    104f:   88 08                   mov    %cl,(%rax)
    1051:   48 83 c0 01             add    $0x1,%rax
    1055:   48 39 f0                cmp    %rsi,%rax
    1058:   75 ee                   jne    1048 <_start+0x28>
    105a:   b8 3c 00 00 00          mov    $0x3c,%eax
    105f:   31 ff                   xor    %edi,%edi
    1061:   0f 05                   syscall 

崩溃发生在103b,指令movaps %xmm0,-0x38(%rsp)

我注意到如果m 包含少于 15 个字符,则生成的代码不同,不会发生崩溃。

我做错了什么?

【问题讨论】:

  • 这个buf 是干什么用的?这个m 是干什么用的?两者都将得到优化,因为编译器认为不需要它。
  • 我试图从一段较大的代码中提取尽可能小的测试用例。代码已更新。
  • 我不知道我脑海中的系统调用,但它是否需要一个以空值结尾的字符串?因为buf 不是。
  • 这是否与crt0.o 相关联?如果不是,你不应该先设置堆栈吗?
  • 您的堆栈未对齐。

标签: c linux gcc segmentation-fault x86-64


【解决方案1】:

_start 不是函数。它没有被任何东西调用,并且在进入时堆栈是 16 字节对齐的不是 (根据 ABI 的要求)距离 16 字节对齐有 8 个字节。

(ABI 要求在 call 之前进行 16 字节对齐,call 推送 8 字节返回地址。因此,函数入口 RSP-8 和 RSP+8 是 16 字节对齐的。)


-O2处,GCC使用需要对齐的16字节指令来实现cp()完成的复制,将"123456789012345"从静态存储复制到堆栈中。

-O1,GCC 只使用两条mov r64, imm64 指令将字节转换为8 字节存储的整数寄存器。这些不需要对齐。


解决方法

如果您希望一切正常,只需像普通人一样用 C 语言编写 main

或者,如果您想在 asm 中对轻量级的东西进行微基准测试,您可以使用 gcc -nostdlib -O3 -mincoming-stack-boundary=3 (docs) 告诉 GCC 函数不能假定它们被调用超过 8 字节对齐。与-mpreferred-stack-boundary=3 不同,在进行进一步调用之前,它仍将对齐 16。因此,如果您有其他非叶函数,您可能只想在 hacky C _start() 上使用一个属性,而不是影响整个文件。


更糟糕、更老套的方法是尝试将
asm("push %rax"); 放在_start 的最顶部以将 RSP 修改为 8,GCC 希望在对堆栈执行任何其他操作之前运行它。 GNU C Basic asm 语句隐含为volatile,因此您不需要asm volatile,尽管这不会有什么坏处。

您 100% 靠自己负责使用适用于您正在使用的任何优化级别的内联汇编来正确欺骗编译器。


另一种更安全的方法是编写自己的轻量级 _start 调用 main:

// at global scope:
asm(
   ".globl _start \n"
   "_start:       \n"
   "    mov   (%rsp), %rdi  \n"     // argc
   "    lea   8(%rsp), %rsi  \n"    // argv
   "    lea   8(%rsi, %rdi, 8), %rdx \n"   // envp
   "    call  main \n"
          // NOT DONE: stdio cleanup or other atexit stuff
          // DO NOT USE WITH GLIBC; use libc's CRT code if you use libc
   "    mov   %eax, %edi \n"
   "    mov   $231, %eax \n"
   "    syscall"               // exit_group( main() )
);

int main(int argc, char**argv, char**envp) {
   ... your code here
   return 0;
}

如果你不想main返回,你可以pop %rdi; mov %rsp, %rsi ; jmp main 给它 argc 和 argv 没有返回地址。

然后main 可以通过内联asm 退出,或者如果您链接libc,则通过调用exit()_exit()。 (但如果你链接libc,你通常应该使用它的_start。)

另请参阅:How Get arguments value using inline assembly in C without Glibc? 其他手卷 _start 版本;这很像@zwol's there。

【讨论】:

  • 我真的很喜欢轻量级的_start 想法,但为什么它“稍微安全一些”而不是“生产安全”?它是否会导致在根本不使用任何库(包括 libc)且不关心 atexit 的 Linux x64 程序中发生任何意外情况,因为它实现了自己的 exit 来负责清理(关闭打开 fds,清理内存等)然后调用 exit_group 系统调用?这样的程序还必须做些什么吗?我觉得我错过了一些重要的事情。
  • @rid:我认为在这种情况下你没问题,只要所有清理工作都在 主返回之前完成。是的,在这种情况下,我认为它完全安全的。我没有说“生产代码”不安全,我说使用 glibc(任何完整的 ISO C 库)不安全。当我写“稍微安全一点”时,我的意思是一个仍然使用 stdio 函数或类似功能的程序。
  • -mincoming-stack-boundary=3?
  • @MarcGlisse:哦,是的,应该可以。显然我在最初写这个答案时忘记了这个选项。
猜你喜欢
  • 1970-01-01
  • 2011-08-30
  • 2011-12-16
  • 2013-10-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多