c-GCC优化器在nostdlib代码中生成错误



我有以下代码:

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个,那么生成的代码是不同的,并且不会发生崩溃。

我做错了什么?

_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。因此,如果你有其他非叶函数,你可能只想在你的黑客C_start()上使用一个属性,而不是影响整个文件。


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

你100%靠自己,并负责通过使用适用于任何优化级别的内联asm来正确地欺骗编译器。


另一种更安全的方法是编写自己的轻量级_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 %rdimov %rsp, %rsijmp main为其提供argc和argv而不提供返回地址。

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

另请参阅:如何在没有Glibc的C中使用内联程序集获取参数值?对于其他手工轧制的CCD_ 33版本;这很像@zwol在那里。

相关内容

  • 没有找到相关文章

最新更新