libc 的 system() 当堆栈指针不是 16 位填充时导致分段错误
libc's system() when the stack pointer is not 16-padded causes segmentation fault
当我在 x86-64 linux 上使用 libc 的 system() 函数时,我注意到一个非常奇怪的行为,有时对 system()
的调用会因分段错误而失败,这是什么我用 gdb
.
调试后得到
我注意到分段错误出现在这一行中:
=> 0x7ffff7a332f6 <do_system+1094>: movaps XMMWORD PTR [rsp+0x40],xmm0
根据 manual,这是 SIGSEGV 的原因:
When the source or destination operand is a memory operand, the operand must be aligned on a 16-byte boundary or a general-protection exception (#GP) is generated.
更深入地看,我注意到我的 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 杀死的 child 结束。
这让我想知道一些事情:
- 为什么
system
乱用 xmm
s 寄存器?
- 这是正常行为吗?或者我可能遗漏了一些关于如何正确使用
system()
函数的基本知识?
提前致谢
x86-64 System V ABI 保证 call
之前的 16 字节堆栈对齐,因此允许 libc system
利用它进行 16 字节对齐loads/stores。如果你破坏了 ABI,如果事情崩溃,那是你的问题。
在一个函数的入口处,在 call
推送了一个 return 地址后,RSP+-8 是 16 字节对齐的,再多一个 push
会让你调用另一个函数。
GCC 这样做当然通常没有问题,通过使用奇数个 push
es 或使用 sub rsp, 16*n + 8
来保留堆栈 space。使用带有 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 & 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.
正如我在 中解释的那样,允许编译器发出利用目标平台 ABI 提供的任何保证的代码。这包括对 16 字节 movaps
使用 loads/stores 来复制堆栈上的内容,利用传入的对齐保证。
gcc 没有像 clang
那样完全优化掉 if()
,这是一个遗漏的优化。
但是clang真的把它当作一个未初始化的变量;没有在 asm
语句中使用它,所以我认为 register-local 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
的变量可能会受到影响由编译器生成 push/pop?
这是一个有趣的测试用例:在 Godbolt 上查看它 link。
// 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。
当我在 x86-64 linux 上使用 libc 的 system() 函数时,我注意到一个非常奇怪的行为,有时对 system()
的调用会因分段错误而失败,这是什么我用 gdb
.
我注意到分段错误出现在这一行中:
=> 0x7ffff7a332f6 <do_system+1094>: movaps XMMWORD PTR [rsp+0x40],xmm0
根据 manual,这是 SIGSEGV 的原因:
When the source or destination operand is a memory operand, the operand must be aligned on a 16-byte boundary or a general-protection exception (#GP) is generated.
更深入地看,我注意到我的 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 杀死的 child 结束。
这让我想知道一些事情:
- 为什么
system
乱用xmm
s 寄存器? - 这是正常行为吗?或者我可能遗漏了一些关于如何正确使用
system()
函数的基本知识?
提前致谢
x86-64 System V ABI 保证 call
之前的 16 字节堆栈对齐,因此允许 libc system
利用它进行 16 字节对齐loads/stores。如果你破坏了 ABI,如果事情崩溃,那是你的问题。
在一个函数的入口处,在 call
推送了一个 return 地址后,RSP+-8 是 16 字节对齐的,再多一个 push
会让你调用另一个函数。
GCC 这样做当然通常没有问题,通过使用奇数个 push
es 或使用 sub rsp, 16*n + 8
来保留堆栈 space。使用带有 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 & 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.
正如我在 movaps
使用 loads/stores 来复制堆栈上的内容,利用传入的对齐保证。
gcc 没有像 clang
那样完全优化掉 if()
,这是一个遗漏的优化。
但是clang真的把它当作一个未初始化的变量;没有在 asm
语句中使用它,所以我认为 register-local 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
的变量可能会受到影响由编译器生成 push/pop?
这是一个有趣的测试用例:在 Godbolt 上查看它 link。
// 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。