当堆栈指针未填充16时,libc的system()会导致分段错误

时间:2019-01-27 21:35:41

标签: x86 segmentation-fault libc sse

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

我注意到该行出现了分段错误:

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

根据manual,这是SIGSEGV的原因:

  

当源操作数或目标操作数是内存操作数时,该操作数必须在16字节边界上对齐,否则会生成一般保护异常(#GP)。

从更深层次看,我注意到确实我的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杀死的孩子结束。

这使我想知道一些事情:

  1. 为什么system会与xmm的寄存器混为一谈?
  2. 这是正常现象吗?还是我缺少关于如何正确使用system()函数的基本知识?

预先感谢

1 个答案:

答案 0 :(得分:3)

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

在进入函数时,在call按下返回地址后,RSP + -8对齐16字节,另外push将使您开始调用另一个函数。

通过使用奇数个push es或使用sub rsp, 16*n + 8来保留堆栈空间,GCC当然通常没有问题。只要您仅读取变量而不分配变量,就可以将{-{1}}与register-asm局部变量一起使用不会破坏这一点。

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

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

这是gcc7.3 -O3的代码源:请注意,除了在函数体中读取RSP以外,它对RSP均不做任何事情,因此如果call被有效的调用RSP(16字节对齐-8),main的所有函数调用也将使用16字节对齐的RSP进行。 (而且它将永远不会找到main,因此它永远不会首先调用sp & 8

system

如果您以某种非标准方式致电# 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 ,则违反了ABI 。而且您不在问题中解释它,所以这不是MCVE

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


gcc不能像movaps那样完全优化if()

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

允许

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

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

该手册确实指出,使用调用密集型寄存器(如x86上的"r"(var))会导致变量被函数调用所破坏,因此使用"rcx"的变量可能会受到编译器的影响生成的推送/弹出?

这是一个有趣的测试用例:在Godbolt链接上查看。

rsp

在没有// 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; } 的情况下,gcc会很好地进行编译,根本不会将asm("" : "+r"(dx));放入RDX。