当我在x86-64 linux上使用libc的system()函数时,我注意到了一个非常奇怪的行为,有时对system()的调用因段错误而失败,这是在使用gdb对其进行调试后得到的结果。

我注意到在这一行中出现了段错误:

=> 0x7ffff7a332f6 <do_system+1094>: movaps XMMWORD PTR [rsp+0x40],xmm0

根据manual,这是SIGSEGV的原因:



从更深的角度看,我注意到确实我的rsp值不是16字节填充的(也就是说,它的十六进制表示没有以0结尾)。在对rsp进行调用之前,手动修改system实际上可以使所有工作正常。

因此,我编写了以下程序:
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    register long long int sp asm ("rsp");
    printf("%llx\n", sp);

    if (sp & 0x8) /* == 0x8*/
    {
        printf("running system...\n");
        system("touch hi");
    }

    return 0;
}

与gcc 7.3.0一起编译
当然,在观察输出时:
sha@sha-desktop:~/Desktop/tda$ ltrace -f ./o_sample2
[pid 26770] printf("%llx\n", 0x7ffe3eabe6c87ffe3eabe6c8
)                                           = 13
[pid 26770] puts("running system..."running system...
)                                                  = 18
[pid 26770] system("touch hi" <no return ...>
[pid 26771] --- SIGSEGV (Segmentation fault) ---
[pid 26771] +++ killed by SIGSEGV +++
[pid 26770] --- SIGCHLD (Child exited) ---
[pid 26770] <... system resumed> )           = 139
[pid 26770] +++ exited (status 0) +++

因此,使用此程序,我将无法执行system()

也是一件小事,我不能说出它是否与问题有关,几乎我所有的运行都以一个错误的rsp值结尾,并被SEGSEGV杀死了一个 child 。

这使我想知道一些事情:
  • 为什么system会与xmm的寄存器混为一谈?
  • 这是正常行为吗?还是我缺少关于如何正确使用system()函数的基本知识?

  • 提前致谢

    最佳答案

    x86-64 System V ABI确保在call之前进行16字节的堆栈对齐,因此,允许libc system对其进行16字节对齐的加载/存储加以利用。如果您破坏了ABI,那么如果事情崩溃了,那将是您的问题。

    在进入函数时,在call推送了返回地址之后,RSP + -8对齐16字节,再加上一个push将使您设置为调用另一个函数。

    当然,通过使用奇数个push es或使用sub rsp, 16*n + 8来保留堆栈空间,GCC通常不会出现任何问题。只要您仅读取变量,而不分配给asm("rsp"),将register-asm局部变量与-O3配合使用就不会破坏它。

    您说您正在使用GCC7.3。 I put your code on the Godbolt compiler explorer并使用-O2-O1-O0main进行编译。它在所有优化级别都遵循ABI,从而创建一个以sub rsp, 8开头且在函数内部(除非call除外)不会修改RSP的main,直到函数结束。

    我检查过的clang和gcc的其他所有版本和优化级别也是如此。

    这是gcc7.3 -O3的代码源:请注意,除了在函数体内读取RSP以外,它对RSP均不执行任何操作,因此,如果使用有效的RSP(16字节对齐-8)调用main,则所有sp & 8'的函数调用也将使用16字节对齐的RSP进行。 (,它将永远不会找到正确的system,因此它永远不会首先调用main)

    # gcc7.3 -O3
    main:
            sub     rsp, 8
            xor     eax, eax
            mov     edi, OFFSET FLAT:.LC0
            mov     rsi, rsp          # read RSP.
            call    printf
            test    spl, 8            # low 8 bits of RSP
            je      .L2
            mov     edi, OFFSET FLAT:.LC1
            call    puts
            mov     edi, OFFSET FLAT:.LC2
            call    system
    .L2:
            xor     eax, eax
            add     rsp, 8
            ret
    

    如果以某种非标准方式调用movaps,则违反了ABI 。而且您没有在问题中解释它,所以这不是MCVE

    正如我在Does the C++ standard allow for an uninitialized bool to crash a program?中解释的那样,允许编译器发出利用目标平台的ABI所作的任何保证的代码。这包括使用if()进行16字节加载/存储,以利用传入的对齐保证来在堆栈上复制内容。

    gcc不能像clang那样完全优化asm是一种错过的优化。

    但是clang确实将其视为未初始化的变量。我认为没有在asm("rsp")语句中使用它,所以本地寄存器printf对clang没有任何作用。 Clang在第一个main调用之前保留RSI不变,因此clang的argv实际上会打印"r"(var),根本不会读取RSP。

    允许Clang执行此操作:register-asm本地vars唯一受支持的用法是使"rcx" Extended-asm约束选择所需的寄存器。 (https://gcc.gnu.org/onlinedocs/gcc/Local-Register-Variables.html)。

    该手册并不意味着仅仅在其他时间使用这样的变量可能会有问题,因此我认为,根据书面规则,该代码通常应该是安全的,并且可以在实践中使用。

    手册确实说过使用调用优先寄存器(如x86上的rsp)会导致变量被函数调用所破坏,所以也许使用asm("" : "+r"(dx));的变量会受到编译器生成的push/pop的影响?

    这是一个有趣的测试用例:在Godbolt链接上查看。
    // gcc won't compile this: "error: unable to find a register to spill"
    // clang simply copies the value back out of RDX before idiv
    int sink;
    int divide(int a, int b) {
        register long long int dx asm ("rdx") = b;
        asm("" : "+r"(dx));  // actually make the compiler put the value in RDX
    
        sink = a/b;   // IDIV uses EDX as an input
    
        return dx;
    }
    

    没有b,gcc可以很好地进行编译,根本不要将ojit_code放入RDX中。

    关于x86 - libc的system()堆栈指针未填充16时导致段错误,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/54393105/

    10-11 16:41