【问题标题】:libc's system() when the stack pointer is not 16-padded causes segmentation fault当堆栈指针未填充 16 位时,libc 的 system() 会导致分段错误
【发布时间】:2019-01-27 21:35:40
【问题描述】:

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

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

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

根据manual,这是SIGSEGV的原因:

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

再往下看,我注意到我的 rsp 值确实不是 16 字节填充的(也就是说,它的十六进制表示不以 0 结尾)。在调用 system 之前手动修改 rsp 实际上可以使一切正常。

所以我写了以下程序:

#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 会乱用xmms 寄存器?
  2. 这是正常行为吗?或者我在如何正确使用 system() 函数方面遗漏了一些基本知识?

提前致谢

【问题讨论】:

  • 16(或 32 字节)对齐是 X86-64 System V ABI 的要求。它需要正确对齐 C 库出于性能原因经常使用 XMM 寄存器和相关指令。这是正常行为。使用register long long int sp asm ("rsp"); 并在没有扩展内联汇编的情况下将其作为变量访问是未定义的行为。幸运的是它有效。我想看看显然失败的原始代码。您能否向我们展示您调用系统的原始代码并且它失败了?这闻起来像 XY 问题
  • @MichaelPetch 实际上,“原始代码”是当我尝试一些面向返回的编程时,因为我试图将某些函数的返回地址覆盖为 system's
  • 那么问题出在那个代码上。编译器自己保留对齐。您的 ROB 代码有问题。它需要在根据 x86-64 ABI 调用 system 之前保持该对齐。正如您建议的那样,将 RSP 向下舍入到最接近的 16 字节边界有效。我假设你用 and rsp, -16 之类的东西来做? ABI 指出,在函数调用点,堆栈指针需要对齐 16 字节(某些情况下为 32 字节)。而且像system这样的函数使用对齐向量指令来提高性能也没有错(这是正常的)。
  • @MichaelPetch 所以我的代码 rsp 未对齐的原因是因为 asm ?我认为这是gcc 的已知指令,除了它编译我的程序之外,它不应该也不会“搞砸”我的rsp 吗?
  • 程序可以编译,但这并不意味着它们会正常运行。是的,您的装配是问题的原因。 GCC 维护自己代码的对齐,以确保满足对齐要求。它不知道任何正在运行的 ROB 代码。在调用 system之前,由 ROB 代码确保保持对齐。如果您在 ROB 代码中执行的任何操作都未对齐堆栈,则对齐它是您的工作。

标签: x86 segmentation-fault libc sse


【解决方案1】:

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

在进入函数时,call 推送返回地址后,RSP+-8 是 16 字节对齐的,另外一个 push 将设置您调用另一个函数。

GCC 这样做当然没有问题,通过使用奇数的pushes 或使用sub rsp, 16*n + 8 来保留堆栈空间。使用带有 asm("rsp") 的 register-asm 局部变量不会破坏这一点,只要您只读取变量,而不是分配给它。

你说你使用的是 GCC7.3。 I put your code on the Godbolt compiler explorer 并使用 -O3-O2-O1-O0 编译它。它在所有优化级别都遵循 ABI,生成以 sub rsp, 8 开头的 main,并且不会修改函数内部的 RSP(call 除外),直到函数结束。

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

这是 gcc7.3 -O3 的代码生成:请注意,除了在函数体中读取它之外,它对 RSP 做任何事情,所以如果 main 使用有效的 RSP ( 16 字节对齐 - 8),main 的所有函数调用也将使用 16 字节对齐的 RSP。 (它永远不会找到sp &amp; 8 true,所以它永远不会调用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

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

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


gcc 没有像 clang 那样完全优化掉 if(),这是一个错过的优化。

但是 clang 真的把它当作一个未初始化的变量;如果不在asm 语句中使用它,我认为注册本地asm("rsp") 对clang 没有任何影响。 Clang 在第一次调用 printf 之前未修改 RSI,因此 clang 的 main 实际上会打印 argv,根本不会读取 RSP。

Clang 允许这样做:支持 register-asm 本地变量的唯一用途是使"r"(var) extended-asm 约束选择您想要的寄存器。 (https://gcc.gnu.org/onlinedocs/gcc/Local-Register-Variables.html)。

手册并不暗示在其他时候简单地使用这样的变量可能会出现问题,所以我认为根据书面规则,这段代码通常应该是安全的,并且在实践中也能正常工作。

手册确实说使用调用破坏寄存器(如 x86 上的 "rcx")会导致变量被函数调用破坏,因此使用 rsp 的变量可能会受到编译器生成的 push/ 的影响流行?

这是一个有趣的测试用例:在 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;
}

没有asm("" : "+r"(dx));,gcc 编译得很好,根本不会将b 放入RDX。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-19
    • 2021-05-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-17
    相关资源
    最近更新 更多