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



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

我注意到分段错误出现在以下行:

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

根据手册,这是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("%llxn", 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("%llxn", 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()函数的基本内容

提前感谢

x86-64 System V ABI保证在call之前进行16字节堆栈对齐,因此允许libcsystem利用这一点进行16字节对齐的加载/存储。如果你打破了ABI,如果事情崩溃了,那就是你的问题。

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

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

你说你在用GCC7.3。我将您的代码放在Godbolt编译器资源管理器上,并使用-O3-O2-O1-O0进行编译。它在所有优化级别上都遵循ABI,生成一个以sub rsp, 8开始的main,直到函数结束,它都不会修改函数内的RSP(call除外)。

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

这是gcc7.3-O3的代码生成:请注意,除了在函数体内部读取它之外,它不会对RSP做任何事情,所以如果使用有效的RSP(16字节对齐的-8)调用main,那么main的所有函数调用也将使用16字节调准的RSP。(而且它永远不会发现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

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

正如我在《C++标准允许未初始化的bool破坏程序吗?》中所解释的那样?,编译器被允许发出利用目标平台的ABI所做的任何保证的代码。这包括使用movaps进行16字节加载/存储,以利用传入对齐保证在堆栈上复制内容。


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

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

Clang可以做到这一点:对寄存器asm本地变量唯一支持的使用是使"r"(var)扩展的asm约束选择您想要的寄存器。(https://gcc.gnu.org/onlinedocs/gcc/Local-Register-Variables.html)。

该手册并不意味着在其他时候简单地使用这样的变量可能会有问题,所以我认为根据书面规则,这些代码通常应该是安全的,并且在实践中也会发生。

手册中确实指出,使用调用阻塞寄存器(如x86上的"rcx")会导致变量被函数调用阻塞,所以使用rsp的变量可能会受到编译器生成的推送/弹出的影响?

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

相关内容

  • 没有找到相关文章

最新更新