FreeBSD 系统调用破坏的寄存器比 Linux 多?优化级别之间的内联 asm 不同行为
FreeBSD syscall clobbering more registers than Linux? Inline asm different behaviour between optimization levels
最近我在玩 freebsd 系统调用我对 i386 部分没有问题,因为它在 here. 有很好的记录但是我找不到 x86_64 的相同文档。
我看到人们使用与 linux 相同的方式,但他们只使用汇编而不是 c。我想在我的案例中,系统调用实际上改变了一些由高优化级别使用的寄存器,因此它给出了不同的行为。
/* for SYS_* constants */
#include <sys/syscall.h>
/* for types like size_t */
#include <unistd.h>
ssize_t sys_write(int fd, const void *data, size_t size){
register long res __asm__("rax");
register long arg0 __asm__("rdi") = fd;
register long arg1 __asm__("rsi") = (long)data;
register long arg2 __asm__("rdx") = size;
__asm__ __volatile__(
"syscall"
: "=r" (res)
: "0" (SYS_write), "r" (arg0), "r" (arg1), "r" (arg2)
: "rcx", "r11", "memory"
);
return res;
}
int main(){
for(int i = 0; i < 1000; i++){
char a = 0;
int some_invalid_fd = -1;
sys_write(some_invalid_fd, &a, 1);
}
return 0;
}
在上面的代码中,我只是希望它先调用 sys_write 1000 次,然后再调用 return main。我使用 truss 检查系统调用及其参数。使用 -O0 一切正常,但是当我使用 -O3 for 循环时,它会永远卡住。我相信系统调用将 i
变量或 1000
更改为奇怪的东西。
函数 main 的汇编代码转储:
0x0000000000201900 <+0>: push %rbp
0x0000000000201901 <+1>: mov %rsp,%rbp
0x0000000000201904 <+4>: mov [=12=]x3e8,%r8d
0x000000000020190a <+10>: lea -0x1(%rbp),%rsi
0x000000000020190e <+14>: mov [=12=]x1,%edx
0x0000000000201913 <+19>: mov [=12=]xffffffffffffffff,%rdi
0x000000000020191a <+26>: nopw 0x0(%rax,%rax,1)
0x0000000000201920 <+32>: movb [=12=]x0,-0x1(%rbp)
0x0000000000201924 <+36>: mov [=12=]x4,%eax
0x0000000000201929 <+41>: syscall
0x000000000020192b <+43>: add [=12=]xffffffff,%r8d
0x000000000020192f <+47>: jne 0x201920 <main+32>
0x0000000000201931 <+49>: xor %eax,%eax
0x0000000000201933 <+51>: pop %rbp
0x0000000000201934 <+52>: ret
sys_write()
有什么问题吗?为什么 for 循环会卡住?
这是一个可以使用的测试框架。它 [松散地] 模仿了 H/W 逻辑分析仪 and/or 诸如 dtrace
.
之类的东西
它将syscall
指令前后的寄存器保存在一个大的全局缓冲区中。
循环终止后,它将转储所有存储的寄存器值的踪迹。
它是多个文件。提取:
- 将下面的代码保存到文件中(例如
/tmp/archive
)。
- 创建一个目录:(例如)
/tmp/extract
- cd 到
/tmp/extract
.
- 然后做:
perl /tmp/archive -go
.
- 它将创建一些子目录:
/tmp/extract/syscall
和 /tmp/extract/snaplib
并在其中存储一些文件。
- cd 到程序目标目录(例)
cd /tmp/extract/syscall
- 构建方式:
make
- 然后,运行 与:
./syscall
这是文件:
编辑: 我在 snapnow
函数中添加了 snaplist
缓冲区溢出检查。如果缓冲区已满,将自动调用 dumpall
。这在一般情况下是好的,但如果 main
中的循环永远不会终止(即 没有 检查 post循环转储永远不会发生)
编辑: 而且,我添加了可选的“x86_64 红区”支持
#!/usr/bin/perl
# FILE: ovcbin/ovcext.pm 755
# ovcbin/ovcext.pm -- ovrcat archive extractor
#
# this is a self extracting archive
# after the __DATA__ line, files are separated by:
# % filename
ovcext_cmd(@ARGV);
exit(0);
sub ovcext_cmd
{
my(@argv) = @_;
local($xfdata);
local($xfdiv,$divcur,%ovcdiv_lookup);
$pgmtail = "ovcext";
ovcinit();
ovcopt(\@argv,qw(opt_go opt_f opt_t));
$xfdata = "ovrcat::DATA";
$xfdata = \*$xfdata;
ovceval($xfdata);
ovcfifo($zipflg_all);
ovcline($xfdata);
$code = ovcwait();
ovcclose($xfdata);
ovcdiv();
ovczipd_spl()
if ($zipflg_spl);
}
sub ovceval
{
my($xfdata) = @_;
my($buf,$err);
{
$buf = <$xfdata>;
chomp($buf);
last unless ($buf =~ s/^%\s+([\@$;])//);
eval($buf);
$err = $@;
unless ($err) {
undef($buf);
last;
}
chomp($err);
$err = " (" . $err . ")"
}
sysfault("ovceval: bad options line -- '%s'%s\n",$buf,$err)
if (defined($buf));
}
sub ovcline
{
my($xfdata) = @_;
my($buf);
my($tail);
while ($buf = <$xfdata>) {
chomp($buf);
if ($buf =~ /^%\s+(.+)$/) {
$tail = ;
ovcdiv($tail);
next;
}
print($xfdiv $buf,"\n")
if (ref($xfdiv));
}
}
sub ovcdiv
{
my($ofile) = @_;
my($mode);
my($xfcur);
my($err,$prt);
($ofile,$mode) = split(" ",$ofile);
$mode = oct($mode);
$mode &= 0777;
{
unless (defined($ofile)) {
while ((undef,$divcur) = each(%ovcdiv_lookup)) {
close($divcur->{div_xfdst});
}
last;
}
$ofile = ovctail($ofile);
$divcur = $ovcdiv_lookup{$ofile};
if (ref($divcur)) {
$xfdiv = $divcur->{div_xfdst};
last;
}
undef($xfdiv);
if (-e $ofile) {
msg("ovcdiv: file '%s' already exists -- ",$ofile);
unless ($opt_f) {
msg("rerun with -f to force\n");
last;
}
msg("overwriting!\n");
}
unless (defined($err)) {
ovcmkdir()
if ($ofile =~ m,^(.+)/[^/]+$,);
}
msg("$pgmtail: %s %s",ovcnogo("extracting"),$ofile);
msg(" chmod %3.3o",$mode)
if ($mode);
msg("\n");
last unless ($opt_go);
last if (defined($err));
$xfcur = ovcopen(">$ofile");
$divcur = {};
$ovcdiv_lookup{$ofile} = $divcur;
if ($mode) {
chmod($mode,$xfcur);
$divcur->{div_mode} = $mode;
}
$divcur->{div_xfdst} = $xfcur;
$xfdiv = $xfcur;
}
}
sub ovcinit
{
{
last if (defined($ztmp));
$ztmp = "/tmp/ovrcat_zip";
$PWD = $ENV{PWD};
$quo_2 = '"';
$ztmp_inp = $ztmp . "_0";
$ztmp_out = $ztmp . "_1";
$ztmp_perl = $ztmp . "_perl";
ovcunlink();
$ovcdbg = ($ENV{"ZPXHOWOVC"} != 0);
}
}
sub ovcunlink
{
_ovcunlink($ztmp_inp,1);
_ovcunlink($ztmp_out,1);
_ovcunlink($ztmp_perl,($pgmtail ne "ovcext") || $opt_go);
}
sub _ovcunlink
{
my($file,$rmflg) = @_;
my($found,$tag);
{
last unless (defined($file));
$found = (-e $file);
$tag //= "notfound"
unless ($found);
$tag //= $rmflg ? "cleaning" : "keeping";
msg("ovcunlink: %s %s ...\n",$tag,$file)
if (($found or $ovcdbg) and (! $ovcunlink_quiet));
unlink($file)
if ($rmflg and $found);
}
}
sub ovcopt
{
my($argv) = @_;
my($opt);
while (1) {
$opt = $argv->[0];
last unless ($opt =~ s/^-/opt_/);
shift(@$argv);
$$opt = 1;
}
}
sub ovctail
{
my($file,$sub) = @_;
my(@file);
$file =~ s,^/,,;
@file = split("/",$file);
$sub //= 2;
@file = splice(@file,-$sub)
if (@file >= $sub);
$file = join("/",@file);
$file;
}
sub ovcmkdir
{
my($odir) = @_;
my(@lhs,@rhs);
@rhs = split("/",$odir);
foreach $rhs (@rhs) {
push(@lhs,$rhs);
$odir = join("/",@lhs);
if ($opt_go) {
next if (-d $odir);
}
else {
next if ($ovcmkdir{$odir});
$ovcmkdir{$odir} = 1;
}
msg("$pgmtail: %s %s ...\n",ovcnogo("mkdir"),$odir);
next unless ($opt_go);
mkdir($odir) or
sysfault("$pgmtail: unable to mkdir '%s' -- $!\n",$odir);
}
}
sub ovcopen
{
my($file,$who) = @_;
my($xf);
$who //= $pgmtail;
$who //= "ovcopen";
open($xf,$file) or
sysfault("$who: unable to open '%s' -- $!\n",$file);
$xf;
}
sub ovcclose
{
my($xfp) = @_;
my($ref);
my($xf);
{
$ref = ref($xfp);
last unless ($ref);
if ($ref eq "GLOB") {
close($xfp);
last;
}
if ($ref eq "REF") {
$xf = $$xfp;
if (ref($xf) eq "GLOB") {
close($xf);
undef($$xfp);
}
}
}
undef($xf);
$xf;
}
sub ovcnogo
{
my($str) = @_;
unless ($opt_go) {
$str = "NOGO-$str";
$nogo_msg = 1;
}
$str;
}
sub ovcdbg
{
if ($ovcdbg) {
printf(STDERR @_);
}
}
sub msg
{
printf(STDERR @_);
}
sub msgv
{
$_ = join(" ",@_);
print(STDERR $_,"\n");
}
sub sysfault
{
printf(STDERR @_);
exit(1);
}
sub ovcfifo
{
}
sub ovcwait
{
my($code);
if ($pid_fifo) {
waitpid($pid_fifo,0);
$code = $? >> 8;
}
$code;
}
sub prtstr
{
my($val,$fmtpos,$fmtneg) = @_;
{
unless (defined($val)) {
$val = "undef";
last;
}
if (ref($val)) {
$val = sprintf("(%s)",$val);
last;
}
$fmtpos //= "'%s'";
if (defined($fmtneg) && ($val <= 0)) {
$val = sprintf($fmtneg,$val);
last;
}
$val = sprintf($fmtpos,$val);
}
$val;
}
sub prtnum
{
my($val) = @_;
$val = prtstr($val,"%d");
$val;
}
END {
msg("$pgmtail: rerun with -go to actually do it\n")
if ($nogo_msg);
ovcunlink();
}
1;
package ovrcat;
__DATA__
% ;
% syscall/syscall.c
/* for SYS_* constants */
#include <sys/syscall.h>
/* for types like size_t */
#include <unistd.h>
#include <snaplib/snaplib.h>
ssize_t
my_write(int fd, const void *data, size_t size)
{
register long res __asm__("rax");
register long arg0 __asm__("rdi") = fd;
register long arg1 __asm__("rsi") = (long)data;
register long arg2 __asm__("rdx") = size;
__asm__ __volatile__(
SNAPNOW
"\tsyscall\n"
SNAPNOW
: "=r" (res)
: "0" (SYS_write), "r" (arg0), "r" (arg1), "r" (arg2)
: "rcx", "r11", "memory"
);
return res;
}
int
main(void)
{
for (int i = 0; i < 8000; i++) {
char a = 0;
int some_invalid_fd = -1;
my_write(some_invalid_fd, &a, 1);
}
snapreg_dumpall();
return 0;
}
% snaplib/snaplib.h
// snaplib/snaplib.h -- register save/dump
#ifndef _snaplib_snaplib_h_
#define _snaplib_snaplib_h_
#ifdef _SNAPLIB_GLO_
#define EXTRN_SNAPLIB /**/
#else
#define EXTRN_SNAPLIB extern
#endif
#ifdef RED_ZONE
#define SNAPNOW \
"\tsubq\t8,%%rsp\n" \
"\tcall\tsnapreg\n" \
"\taddq\t8,%%rsp\n"
#else
#define SNAPNOW "\tcall\tsnapreg\n"
#endif
typedef unsigned long reg_t;
#ifndef SNAPREG
#define SNAPREG (1500 * 2)
#endif
typedef struct {
reg_t snap_regs[16];
} __attribute__((packed)) snapreg_t;
typedef snapreg_t *snapreg_p;
EXTRN_SNAPLIB snapreg_t snaplist[SNAPREG];
#ifdef _SNAPLIB_GLO_
snapreg_p snapcur = &snaplist[0];
snapreg_p snapend = &snaplist[SNAPREG];
#else
extern snapreg_p snapcur;
extern snapreg_p snapend;
#endif
#include <snaplib/snaplib.proto>
#include <snaplib/snapgen.h>
#endif
% snaplib/snapall.c
// snaplib/snapall.c -- dump routines
#define _SNAPLIB_GLO_
#include <snaplib/snaplib.h>
#include <stdio.h>
#include <stdlib.h>
void
snapreg_dumpall(void)
{
snapreg_p cur = snaplist;
snapreg_p endp = (snapreg_p) snapcur;
int idx = 0;
for (; cur < endp; ++cur, ++idx) {
printf("\n");
printf("%d:\n",idx);
snapreg_dumpgen(cur);
}
snapcur = snaplist;
}
// snapreg_crash -- invoke dump and abort
void
snapreg_crash(void)
{
snapreg_dumpall();
exit(9);
}
// snapreg_dumpone -- dump single element
void
snapreg_dumpone(snapreg_p cur,int regidx,const char *regname)
{
reg_t regval = cur->snap_regs[regidx];
printf(" %3s %16.16lX %ld\n",regname,regval,regval);
}
% snaplib/snapreg.s
.text
.globl snapreg
snapreg:
push %r14
push %r15
movq snapcur(%rip),%r15
movq %rax,0(%r15)
movq %rbx,8(%r15)
movq %rcx,16(%r15)
movq %rdx,24(%r15)
movq %rsi,32(%r15)
movq %rsi,40(%r15)
movq %rbp,48(%r15)
movq %rsp,56(%r15)
movq %r8,64(%r15)
movq %r9,72(%r15)
movq %r10,80(%r15)
movq %r11,88(%r15)
movq %r12,96(%r15)
movq %r13,104(%r15)
movq %r14,112(%r15)
movq 0(%rsp),%r14
movq %r14,120(%r15)
addq 8,%r15
movq %r15,snapcur(%rip)
cmpq snapend(%rip),%r15
jae snapreg_crash
pop %r15
pop %r14
ret
% snaplib/snapgen.h
#ifndef _snapreg_snapgen_h_
#define _snapreg_snapgen_h_
static inline void
snapreg_dumpgen(snapreg_p cur)
{
snapreg_dumpone(cur,0,"rax");
snapreg_dumpone(cur,1,"rbx");
snapreg_dumpone(cur,2,"rcx");
snapreg_dumpone(cur,3,"rdx");
snapreg_dumpone(cur,5,"rsi");
snapreg_dumpone(cur,5,"rsi");
snapreg_dumpone(cur,6,"rbp");
snapreg_dumpone(cur,7,"rsp");
snapreg_dumpone(cur,8,"r8");
snapreg_dumpone(cur,9,"r9");
snapreg_dumpone(cur,10,"r10");
snapreg_dumpone(cur,11,"r11");
snapreg_dumpone(cur,12,"r12");
snapreg_dumpone(cur,13,"r13");
snapreg_dumpone(cur,14,"r14");
snapreg_dumpone(cur,15,"r15");
}
#endif
% snaplib/snaplib.proto
// /home/cae/OBJ/ovrgen/snaplib/snaplib.proto -- prototypes
// FILE: /home/cae/preserve/ovrbnc/snaplib/snapall.c
// snaplib/snapall.c -- dump routines
void
snapreg_dumpall(void);
// snapreg_crash -- invoke dump and abort
void
snapreg_crash(void);
// snapreg_dumpone -- dump single element
void
snapreg_dumpone(snapreg_p cur,int regidx,const char *regname);
% syscall/Makefile
# /home/cae/preserve/ovrbnc/syscall -- makefile
PGMTGT += syscall
LIBSRC += ../snaplib/snapreg.s
LIBSRC += ../snaplib/snapall.c
ifndef COPTS
COPTS += -O2
endif
CFLAGS += $(COPTS)
CFLAGS += -mno-red-zone
CFLAGS += -g
CFLAGS += -Wall
CFLAGS += -Werror
CFLAGS += -I..
all: $(PGMTGT)
syscall: syscall.c $(CURSRC) $(LIBSRC)
cc -o syscall $(CFLAGS) syscall.c $(CURSRC) $(LIBSRC)
clean:
rm -f $(PGMTGT)
优化级别决定了 clang 决定将其循环计数器保存在何处:在内存中(未优化)还是在寄存器中,在本例中为 r8d
(优化)。 R8D 是编译器的一个合乎逻辑的选择:它是一个调用破坏的 reg,它可以在不保存在 main
的 start/end 处使用,并且你已经告诉它它可以在没有 REX 前缀的情况下使用的所有寄存器(如 ECX)是 asm 语句的输入/输出或破坏者。
注意:如果 FreeBSD 类似于 MacOS,系统调用错误/无错误状态是 return 在 CF(进位标志)中编辑,而不是通过 RAX在 -4095..-1 范围内。在这种情况下,您需要 GCC6 标志输出操作数,例如 "=@ccc" (err)
for int err
(#ifdef __GCC_ASM_FLAG_OUTPUTS__
- ) 或模板中的 setc %cl
手动实现一个布尔值。 (CL 是一个不错的选择,因为您可以将它用作输出而不是破坏。)
FreeBSD 的 syscall
处理垃圾 R8、R9 和 R10,除了 Linux 所做的最低限度破坏之外:RAX(retval)和RCX / R11(syscall
instruction itself uses them to 这样内核可以找到返回用户的方式-space,因此内核甚至看不到原始值。)
可能还有黑索金, ;评论称其为“return 值 2”(即作为 RDX:RAX return 值的一部分?)。我们也不知道 FreeBSD 打算在未来的内核中维护哪些面向未来的 ABI 保证。
您不能假设 R8-R10 在 syscall
之后为零,因为在跟踪/单步执行时它们实际上被保留而不是归零。 (因为然后内核选择不通过 sysret
return,原因与 Linux 相同:如果寄存器可能在系统内部被 ptrace 修改,则硬件/设计错误会导致不安全调用。例如,尝试使用非规范 RIP sysret
将在 Intel CPU 上在 ring 0(内核模式)中#GP!这是一场灾难,因为 RSP = user stack at the point.)
相关的内核代码是 the sysret
path(@NateEldredge 发现得很好;我通过搜索 swapgs 找到了系统调用入口点,但还没有查看 return 路径)。
函数调用保留寄存器不需要通过该代码恢复,因为调用 C 函数一开始并没有破坏它们。并且代码 确实 恢复了函数调用破坏的“遗留”寄存器 RDI、RSI 和 RDX。
R8-R11 是在函数调用约定中被调用破坏的寄存器,并且在原始 8 x86 寄存器之外。这就是使它们“特别”的原因。 (R11 不会归零;syscall/sysret 将其用于 RFLAGS,因此这就是您在 syscall
之后可以找到的值)
归零比加载它们 稍微 快,并且在正常情况下(libc 包装函数中的 syscall
指令)你即将 return 给一个只假定函数调用约定的调用者,因此将假定 R8-R11 被丢弃(对于 RDI、RSI、RDX 和 RCX 相同,尽管 FreeBSD 确实 打扰到出于某种原因恢复它们。)
此归零仅在非单步执行或跟踪时发生(例如truss
或 GDB si
)。 syscall
entry point into an amd64 kernel (Github) 确实保存了所有传入的寄存器,因此可以通过内核之外的其他方式恢复它们。
已更新 asm()
包装器
// Should be fixed for FreeBSD, plus other improvements
ssize_t sys_write(int fd, const void *data, size_t size){
register ssize_t res __asm__("rax");
register int arg0 __asm__("edi") = fd;
register const void *arg1 __asm__("rsi") = data; // you can use real types
register size_t arg2 __asm__("rdx") = size;
__asm__ __volatile__(
"syscall"
// RDX *maybe* clobbered
: "=a" (res), "+r" (arg2)
// RDI, RSI preserved
: "a" (SYS_write), "r" (arg0), "r" (arg1)
// An arg in R10, R8, or R9 definitely would be
: "rcx", "r11", "memory", "r8", "r9", "r10" ////// The fix: r8-r10
// see below for a version that avoids the "memory" clobber with a dummy input operand
);
return res;
}
使用 "+r"
output/input 操作数和任何需要 register long arg3 asm("r10")
或类似的 r8 或 r9 的参数。
这是在包装函数中,因此 C 变量的修改值被丢弃,每次都强制重复调用以设置 args。这将是“防御性”方法,直到另一个答案确定更多绝对非垃圾寄存器。
I did break *0x000000000020192b then info registers when break happened. r8 is zero. Program still gets stuck in this case
我假设 r8
在你跨 syscall
指令执行 GDB continue
之前 不是零。是的,该测试证实 FreeBSD 内核在非单步执行时正在破坏 r8
。 (并且以与我们在源代码中看到的相匹配的方式运行。)
请注意,您可以使用虚拟 "m"
输入操作数而不是"memory"
破坏。这将使它把 c
的存储提升到循环之外。 ()
即"m"(*(const char (*)[size]) data)
作为输入而不是 "memory"
破坏。
如果你要为你使用的每个系统调用编写特定的包装器,而不是为每个 3 操作数系统调用使用的通用包装器,它只是将所有操作数转换为 unsigned long
,这就是你的优势可以从中得到。
说到这里,让你的系统调用参数全部为 long
完全没有意义;使 user-space 符号扩展 int fd
成为 64 位寄存器只是浪费指令。内核 ABI 将(几乎可以肯定)忽略窄参数的寄存器的高字节,就像 Linux 那样。 (同样,除非你正在制作一个通用的 syscall3
包装器,你只是使用不同的 SYS_
数字来定义写入、读取和其他 3 操作数系统调用;那么你将把所有东西都转换为 register-宽度,只需使用 "memory"
clobber)。
我对下面的修改版本进行了这些更改。
另请注意,对于 RDI、RSI 和 RDX,有特定的寄存器字母约束,您可以使用这些约束来代替寄存器 asm 局部变量,就像您在 RAX 中对 return 值所做的那样("=a"
)。顺便说一句,您真的不需要电话号码的匹配约束,只需使用 "a"
输入即可;它更容易阅读,因为您不需要查看另一个操作数来检查您是否匹配正确的输出。
// assuming RDX *is* clobbered.
// could remove the + if it isn't.
ssize_t sys_write(int fd, const void *data, size_t size)
{
// register long arg3 __asm__("r10") = ??;
// register-asm is useful for R8 and up
ssize_t res;
__asm__ __volatile__("syscall"
// RDX
: "=a" (res), "+d" (size)
// EAX/RAX RDI RSI
: "a" (SYS_write), "D" (fd), "S" (data),
"m" (*(const char (*)[size]) data) // tells compiler this mem is an input
: "rcx", "r11" //, "memory"
#ifndef __linux__
, "r8", "r9", "r10" // Linux always restores these
#endif
);
return res;
}
有些人更喜欢 register ... asm("")
作为所有操作数,因为您可以使用完整的寄存器名称,并且不必记住 RDI/EDI/DI/DIL 的完全不明显的“D” vs . "d" 表示 RDX/EDX/DX/DL
最近我在玩 freebsd 系统调用我对 i386 部分没有问题,因为它在 here. 有很好的记录但是我找不到 x86_64 的相同文档。
我看到人们使用与 linux 相同的方式,但他们只使用汇编而不是 c。我想在我的案例中,系统调用实际上改变了一些由高优化级别使用的寄存器,因此它给出了不同的行为。
/* for SYS_* constants */
#include <sys/syscall.h>
/* for types like size_t */
#include <unistd.h>
ssize_t sys_write(int fd, const void *data, size_t size){
register long res __asm__("rax");
register long arg0 __asm__("rdi") = fd;
register long arg1 __asm__("rsi") = (long)data;
register long arg2 __asm__("rdx") = size;
__asm__ __volatile__(
"syscall"
: "=r" (res)
: "0" (SYS_write), "r" (arg0), "r" (arg1), "r" (arg2)
: "rcx", "r11", "memory"
);
return res;
}
int main(){
for(int i = 0; i < 1000; i++){
char a = 0;
int some_invalid_fd = -1;
sys_write(some_invalid_fd, &a, 1);
}
return 0;
}
在上面的代码中,我只是希望它先调用 sys_write 1000 次,然后再调用 return main。我使用 truss 检查系统调用及其参数。使用 -O0 一切正常,但是当我使用 -O3 for 循环时,它会永远卡住。我相信系统调用将 i
变量或 1000
更改为奇怪的东西。
函数 main 的汇编代码转储:
0x0000000000201900 <+0>: push %rbp
0x0000000000201901 <+1>: mov %rsp,%rbp
0x0000000000201904 <+4>: mov [=12=]x3e8,%r8d
0x000000000020190a <+10>: lea -0x1(%rbp),%rsi
0x000000000020190e <+14>: mov [=12=]x1,%edx
0x0000000000201913 <+19>: mov [=12=]xffffffffffffffff,%rdi
0x000000000020191a <+26>: nopw 0x0(%rax,%rax,1)
0x0000000000201920 <+32>: movb [=12=]x0,-0x1(%rbp)
0x0000000000201924 <+36>: mov [=12=]x4,%eax
0x0000000000201929 <+41>: syscall
0x000000000020192b <+43>: add [=12=]xffffffff,%r8d
0x000000000020192f <+47>: jne 0x201920 <main+32>
0x0000000000201931 <+49>: xor %eax,%eax
0x0000000000201933 <+51>: pop %rbp
0x0000000000201934 <+52>: ret
sys_write()
有什么问题吗?为什么 for 循环会卡住?
这是一个可以使用的测试框架。它 [松散地] 模仿了 H/W 逻辑分析仪 and/or 诸如 dtrace
.
它将syscall
指令前后的寄存器保存在一个大的全局缓冲区中。
循环终止后,它将转储所有存储的寄存器值的踪迹。
它是多个文件。提取:
- 将下面的代码保存到文件中(例如
/tmp/archive
)。 - 创建一个目录:(例如)
/tmp/extract
- cd 到
/tmp/extract
. - 然后做:
perl /tmp/archive -go
. - 它将创建一些子目录:
/tmp/extract/syscall
和/tmp/extract/snaplib
并在其中存储一些文件。 - cd 到程序目标目录(例)
cd /tmp/extract/syscall
- 构建方式:
make
- 然后,运行 与:
./syscall
这是文件:
编辑: 我在 snapnow
函数中添加了 snaplist
缓冲区溢出检查。如果缓冲区已满,将自动调用 dumpall
。这在一般情况下是好的,但如果 main
中的循环永远不会终止(即 没有 检查 post循环转储永远不会发生)
编辑: 而且,我添加了可选的“x86_64 红区”支持
#!/usr/bin/perl
# FILE: ovcbin/ovcext.pm 755
# ovcbin/ovcext.pm -- ovrcat archive extractor
#
# this is a self extracting archive
# after the __DATA__ line, files are separated by:
# % filename
ovcext_cmd(@ARGV);
exit(0);
sub ovcext_cmd
{
my(@argv) = @_;
local($xfdata);
local($xfdiv,$divcur,%ovcdiv_lookup);
$pgmtail = "ovcext";
ovcinit();
ovcopt(\@argv,qw(opt_go opt_f opt_t));
$xfdata = "ovrcat::DATA";
$xfdata = \*$xfdata;
ovceval($xfdata);
ovcfifo($zipflg_all);
ovcline($xfdata);
$code = ovcwait();
ovcclose($xfdata);
ovcdiv();
ovczipd_spl()
if ($zipflg_spl);
}
sub ovceval
{
my($xfdata) = @_;
my($buf,$err);
{
$buf = <$xfdata>;
chomp($buf);
last unless ($buf =~ s/^%\s+([\@$;])//);
eval($buf);
$err = $@;
unless ($err) {
undef($buf);
last;
}
chomp($err);
$err = " (" . $err . ")"
}
sysfault("ovceval: bad options line -- '%s'%s\n",$buf,$err)
if (defined($buf));
}
sub ovcline
{
my($xfdata) = @_;
my($buf);
my($tail);
while ($buf = <$xfdata>) {
chomp($buf);
if ($buf =~ /^%\s+(.+)$/) {
$tail = ;
ovcdiv($tail);
next;
}
print($xfdiv $buf,"\n")
if (ref($xfdiv));
}
}
sub ovcdiv
{
my($ofile) = @_;
my($mode);
my($xfcur);
my($err,$prt);
($ofile,$mode) = split(" ",$ofile);
$mode = oct($mode);
$mode &= 0777;
{
unless (defined($ofile)) {
while ((undef,$divcur) = each(%ovcdiv_lookup)) {
close($divcur->{div_xfdst});
}
last;
}
$ofile = ovctail($ofile);
$divcur = $ovcdiv_lookup{$ofile};
if (ref($divcur)) {
$xfdiv = $divcur->{div_xfdst};
last;
}
undef($xfdiv);
if (-e $ofile) {
msg("ovcdiv: file '%s' already exists -- ",$ofile);
unless ($opt_f) {
msg("rerun with -f to force\n");
last;
}
msg("overwriting!\n");
}
unless (defined($err)) {
ovcmkdir()
if ($ofile =~ m,^(.+)/[^/]+$,);
}
msg("$pgmtail: %s %s",ovcnogo("extracting"),$ofile);
msg(" chmod %3.3o",$mode)
if ($mode);
msg("\n");
last unless ($opt_go);
last if (defined($err));
$xfcur = ovcopen(">$ofile");
$divcur = {};
$ovcdiv_lookup{$ofile} = $divcur;
if ($mode) {
chmod($mode,$xfcur);
$divcur->{div_mode} = $mode;
}
$divcur->{div_xfdst} = $xfcur;
$xfdiv = $xfcur;
}
}
sub ovcinit
{
{
last if (defined($ztmp));
$ztmp = "/tmp/ovrcat_zip";
$PWD = $ENV{PWD};
$quo_2 = '"';
$ztmp_inp = $ztmp . "_0";
$ztmp_out = $ztmp . "_1";
$ztmp_perl = $ztmp . "_perl";
ovcunlink();
$ovcdbg = ($ENV{"ZPXHOWOVC"} != 0);
}
}
sub ovcunlink
{
_ovcunlink($ztmp_inp,1);
_ovcunlink($ztmp_out,1);
_ovcunlink($ztmp_perl,($pgmtail ne "ovcext") || $opt_go);
}
sub _ovcunlink
{
my($file,$rmflg) = @_;
my($found,$tag);
{
last unless (defined($file));
$found = (-e $file);
$tag //= "notfound"
unless ($found);
$tag //= $rmflg ? "cleaning" : "keeping";
msg("ovcunlink: %s %s ...\n",$tag,$file)
if (($found or $ovcdbg) and (! $ovcunlink_quiet));
unlink($file)
if ($rmflg and $found);
}
}
sub ovcopt
{
my($argv) = @_;
my($opt);
while (1) {
$opt = $argv->[0];
last unless ($opt =~ s/^-/opt_/);
shift(@$argv);
$$opt = 1;
}
}
sub ovctail
{
my($file,$sub) = @_;
my(@file);
$file =~ s,^/,,;
@file = split("/",$file);
$sub //= 2;
@file = splice(@file,-$sub)
if (@file >= $sub);
$file = join("/",@file);
$file;
}
sub ovcmkdir
{
my($odir) = @_;
my(@lhs,@rhs);
@rhs = split("/",$odir);
foreach $rhs (@rhs) {
push(@lhs,$rhs);
$odir = join("/",@lhs);
if ($opt_go) {
next if (-d $odir);
}
else {
next if ($ovcmkdir{$odir});
$ovcmkdir{$odir} = 1;
}
msg("$pgmtail: %s %s ...\n",ovcnogo("mkdir"),$odir);
next unless ($opt_go);
mkdir($odir) or
sysfault("$pgmtail: unable to mkdir '%s' -- $!\n",$odir);
}
}
sub ovcopen
{
my($file,$who) = @_;
my($xf);
$who //= $pgmtail;
$who //= "ovcopen";
open($xf,$file) or
sysfault("$who: unable to open '%s' -- $!\n",$file);
$xf;
}
sub ovcclose
{
my($xfp) = @_;
my($ref);
my($xf);
{
$ref = ref($xfp);
last unless ($ref);
if ($ref eq "GLOB") {
close($xfp);
last;
}
if ($ref eq "REF") {
$xf = $$xfp;
if (ref($xf) eq "GLOB") {
close($xf);
undef($$xfp);
}
}
}
undef($xf);
$xf;
}
sub ovcnogo
{
my($str) = @_;
unless ($opt_go) {
$str = "NOGO-$str";
$nogo_msg = 1;
}
$str;
}
sub ovcdbg
{
if ($ovcdbg) {
printf(STDERR @_);
}
}
sub msg
{
printf(STDERR @_);
}
sub msgv
{
$_ = join(" ",@_);
print(STDERR $_,"\n");
}
sub sysfault
{
printf(STDERR @_);
exit(1);
}
sub ovcfifo
{
}
sub ovcwait
{
my($code);
if ($pid_fifo) {
waitpid($pid_fifo,0);
$code = $? >> 8;
}
$code;
}
sub prtstr
{
my($val,$fmtpos,$fmtneg) = @_;
{
unless (defined($val)) {
$val = "undef";
last;
}
if (ref($val)) {
$val = sprintf("(%s)",$val);
last;
}
$fmtpos //= "'%s'";
if (defined($fmtneg) && ($val <= 0)) {
$val = sprintf($fmtneg,$val);
last;
}
$val = sprintf($fmtpos,$val);
}
$val;
}
sub prtnum
{
my($val) = @_;
$val = prtstr($val,"%d");
$val;
}
END {
msg("$pgmtail: rerun with -go to actually do it\n")
if ($nogo_msg);
ovcunlink();
}
1;
package ovrcat;
__DATA__
% ;
% syscall/syscall.c
/* for SYS_* constants */
#include <sys/syscall.h>
/* for types like size_t */
#include <unistd.h>
#include <snaplib/snaplib.h>
ssize_t
my_write(int fd, const void *data, size_t size)
{
register long res __asm__("rax");
register long arg0 __asm__("rdi") = fd;
register long arg1 __asm__("rsi") = (long)data;
register long arg2 __asm__("rdx") = size;
__asm__ __volatile__(
SNAPNOW
"\tsyscall\n"
SNAPNOW
: "=r" (res)
: "0" (SYS_write), "r" (arg0), "r" (arg1), "r" (arg2)
: "rcx", "r11", "memory"
);
return res;
}
int
main(void)
{
for (int i = 0; i < 8000; i++) {
char a = 0;
int some_invalid_fd = -1;
my_write(some_invalid_fd, &a, 1);
}
snapreg_dumpall();
return 0;
}
% snaplib/snaplib.h
// snaplib/snaplib.h -- register save/dump
#ifndef _snaplib_snaplib_h_
#define _snaplib_snaplib_h_
#ifdef _SNAPLIB_GLO_
#define EXTRN_SNAPLIB /**/
#else
#define EXTRN_SNAPLIB extern
#endif
#ifdef RED_ZONE
#define SNAPNOW \
"\tsubq\t8,%%rsp\n" \
"\tcall\tsnapreg\n" \
"\taddq\t8,%%rsp\n"
#else
#define SNAPNOW "\tcall\tsnapreg\n"
#endif
typedef unsigned long reg_t;
#ifndef SNAPREG
#define SNAPREG (1500 * 2)
#endif
typedef struct {
reg_t snap_regs[16];
} __attribute__((packed)) snapreg_t;
typedef snapreg_t *snapreg_p;
EXTRN_SNAPLIB snapreg_t snaplist[SNAPREG];
#ifdef _SNAPLIB_GLO_
snapreg_p snapcur = &snaplist[0];
snapreg_p snapend = &snaplist[SNAPREG];
#else
extern snapreg_p snapcur;
extern snapreg_p snapend;
#endif
#include <snaplib/snaplib.proto>
#include <snaplib/snapgen.h>
#endif
% snaplib/snapall.c
// snaplib/snapall.c -- dump routines
#define _SNAPLIB_GLO_
#include <snaplib/snaplib.h>
#include <stdio.h>
#include <stdlib.h>
void
snapreg_dumpall(void)
{
snapreg_p cur = snaplist;
snapreg_p endp = (snapreg_p) snapcur;
int idx = 0;
for (; cur < endp; ++cur, ++idx) {
printf("\n");
printf("%d:\n",idx);
snapreg_dumpgen(cur);
}
snapcur = snaplist;
}
// snapreg_crash -- invoke dump and abort
void
snapreg_crash(void)
{
snapreg_dumpall();
exit(9);
}
// snapreg_dumpone -- dump single element
void
snapreg_dumpone(snapreg_p cur,int regidx,const char *regname)
{
reg_t regval = cur->snap_regs[regidx];
printf(" %3s %16.16lX %ld\n",regname,regval,regval);
}
% snaplib/snapreg.s
.text
.globl snapreg
snapreg:
push %r14
push %r15
movq snapcur(%rip),%r15
movq %rax,0(%r15)
movq %rbx,8(%r15)
movq %rcx,16(%r15)
movq %rdx,24(%r15)
movq %rsi,32(%r15)
movq %rsi,40(%r15)
movq %rbp,48(%r15)
movq %rsp,56(%r15)
movq %r8,64(%r15)
movq %r9,72(%r15)
movq %r10,80(%r15)
movq %r11,88(%r15)
movq %r12,96(%r15)
movq %r13,104(%r15)
movq %r14,112(%r15)
movq 0(%rsp),%r14
movq %r14,120(%r15)
addq 8,%r15
movq %r15,snapcur(%rip)
cmpq snapend(%rip),%r15
jae snapreg_crash
pop %r15
pop %r14
ret
% snaplib/snapgen.h
#ifndef _snapreg_snapgen_h_
#define _snapreg_snapgen_h_
static inline void
snapreg_dumpgen(snapreg_p cur)
{
snapreg_dumpone(cur,0,"rax");
snapreg_dumpone(cur,1,"rbx");
snapreg_dumpone(cur,2,"rcx");
snapreg_dumpone(cur,3,"rdx");
snapreg_dumpone(cur,5,"rsi");
snapreg_dumpone(cur,5,"rsi");
snapreg_dumpone(cur,6,"rbp");
snapreg_dumpone(cur,7,"rsp");
snapreg_dumpone(cur,8,"r8");
snapreg_dumpone(cur,9,"r9");
snapreg_dumpone(cur,10,"r10");
snapreg_dumpone(cur,11,"r11");
snapreg_dumpone(cur,12,"r12");
snapreg_dumpone(cur,13,"r13");
snapreg_dumpone(cur,14,"r14");
snapreg_dumpone(cur,15,"r15");
}
#endif
% snaplib/snaplib.proto
// /home/cae/OBJ/ovrgen/snaplib/snaplib.proto -- prototypes
// FILE: /home/cae/preserve/ovrbnc/snaplib/snapall.c
// snaplib/snapall.c -- dump routines
void
snapreg_dumpall(void);
// snapreg_crash -- invoke dump and abort
void
snapreg_crash(void);
// snapreg_dumpone -- dump single element
void
snapreg_dumpone(snapreg_p cur,int regidx,const char *regname);
% syscall/Makefile
# /home/cae/preserve/ovrbnc/syscall -- makefile
PGMTGT += syscall
LIBSRC += ../snaplib/snapreg.s
LIBSRC += ../snaplib/snapall.c
ifndef COPTS
COPTS += -O2
endif
CFLAGS += $(COPTS)
CFLAGS += -mno-red-zone
CFLAGS += -g
CFLAGS += -Wall
CFLAGS += -Werror
CFLAGS += -I..
all: $(PGMTGT)
syscall: syscall.c $(CURSRC) $(LIBSRC)
cc -o syscall $(CFLAGS) syscall.c $(CURSRC) $(LIBSRC)
clean:
rm -f $(PGMTGT)
优化级别决定了 clang 决定将其循环计数器保存在何处:在内存中(未优化)还是在寄存器中,在本例中为 r8d
(优化)。 R8D 是编译器的一个合乎逻辑的选择:它是一个调用破坏的 reg,它可以在不保存在 main
的 start/end 处使用,并且你已经告诉它它可以在没有 REX 前缀的情况下使用的所有寄存器(如 ECX)是 asm 语句的输入/输出或破坏者。
注意:如果 FreeBSD 类似于 MacOS,系统调用错误/无错误状态是 return 在 CF(进位标志)中编辑,而不是通过 RAX在 -4095..-1 范围内。在这种情况下,您需要 GCC6 标志输出操作数,例如 "=@ccc" (err)
for int err
(#ifdef __GCC_ASM_FLAG_OUTPUTS__
- setc %cl
手动实现一个布尔值。 (CL 是一个不错的选择,因为您可以将它用作输出而不是破坏。)
FreeBSD 的 syscall
处理垃圾 R8、R9 和 R10,除了 Linux 所做的最低限度破坏之外:RAX(retval)和RCX / R11(syscall
instruction itself uses them to
可能还有黑索金,
您不能假设 R8-R10 在 syscall
之后为零,因为在跟踪/单步执行时它们实际上被保留而不是归零。 (因为然后内核选择不通过 sysret
return,原因与 Linux 相同:如果寄存器可能在系统内部被 ptrace 修改,则硬件/设计错误会导致不安全调用。例如,尝试使用非规范 RIP sysret
将在 Intel CPU 上在 ring 0(内核模式)中#GP!这是一场灾难,因为 RSP = user stack at the point.)
相关的内核代码是 the sysret
path(@NateEldredge 发现得很好;我通过搜索 swapgs 找到了系统调用入口点,但还没有查看 return 路径)。
函数调用保留寄存器不需要通过该代码恢复,因为调用 C 函数一开始并没有破坏它们。并且代码 确实 恢复了函数调用破坏的“遗留”寄存器 RDI、RSI 和 RDX。
R8-R11 是在函数调用约定中被调用破坏的寄存器,并且在原始 8 x86 寄存器之外。这就是使它们“特别”的原因。 (R11 不会归零;syscall/sysret 将其用于 RFLAGS,因此这就是您在 syscall
之后可以找到的值)
归零比加载它们 稍微 快,并且在正常情况下(libc 包装函数中的 syscall
指令)你即将 return 给一个只假定函数调用约定的调用者,因此将假定 R8-R11 被丢弃(对于 RDI、RSI、RDX 和 RCX 相同,尽管 FreeBSD 确实 打扰到出于某种原因恢复它们。)
此归零仅在非单步执行或跟踪时发生(例如truss
或 GDB si
)。 syscall
entry point into an amd64 kernel (Github) 确实保存了所有传入的寄存器,因此可以通过内核之外的其他方式恢复它们。
已更新 asm()
包装器
// Should be fixed for FreeBSD, plus other improvements
ssize_t sys_write(int fd, const void *data, size_t size){
register ssize_t res __asm__("rax");
register int arg0 __asm__("edi") = fd;
register const void *arg1 __asm__("rsi") = data; // you can use real types
register size_t arg2 __asm__("rdx") = size;
__asm__ __volatile__(
"syscall"
// RDX *maybe* clobbered
: "=a" (res), "+r" (arg2)
// RDI, RSI preserved
: "a" (SYS_write), "r" (arg0), "r" (arg1)
// An arg in R10, R8, or R9 definitely would be
: "rcx", "r11", "memory", "r8", "r9", "r10" ////// The fix: r8-r10
// see below for a version that avoids the "memory" clobber with a dummy input operand
);
return res;
}
使用 "+r"
output/input 操作数和任何需要 register long arg3 asm("r10")
或类似的 r8 或 r9 的参数。
这是在包装函数中,因此 C 变量的修改值被丢弃,每次都强制重复调用以设置 args。这将是“防御性”方法,直到另一个答案确定更多绝对非垃圾寄存器。
I did break *0x000000000020192b then info registers when break happened. r8 is zero. Program still gets stuck in this case
我假设 r8
在你跨 syscall
指令执行 GDB continue
之前 不是零。是的,该测试证实 FreeBSD 内核在非单步执行时正在破坏 r8
。 (并且以与我们在源代码中看到的相匹配的方式运行。)
请注意,您可以使用虚拟 "m"
输入操作数而不是"memory"
破坏。这将使它把 c
的存储提升到循环之外。 (
即"m"(*(const char (*)[size]) data)
作为输入而不是 "memory"
破坏。
如果你要为你使用的每个系统调用编写特定的包装器,而不是为每个 3 操作数系统调用使用的通用包装器,它只是将所有操作数转换为 unsigned long
,这就是你的优势可以从中得到。
说到这里,让你的系统调用参数全部为 long
完全没有意义;使 user-space 符号扩展 int fd
成为 64 位寄存器只是浪费指令。内核 ABI 将(几乎可以肯定)忽略窄参数的寄存器的高字节,就像 Linux 那样。 (同样,除非你正在制作一个通用的 syscall3
包装器,你只是使用不同的 SYS_
数字来定义写入、读取和其他 3 操作数系统调用;那么你将把所有东西都转换为 register-宽度,只需使用 "memory"
clobber)。
我对下面的修改版本进行了这些更改。
另请注意,对于 RDI、RSI 和 RDX,有特定的寄存器字母约束,您可以使用这些约束来代替寄存器 asm 局部变量,就像您在 RAX 中对 return 值所做的那样("=a"
)。顺便说一句,您真的不需要电话号码的匹配约束,只需使用 "a"
输入即可;它更容易阅读,因为您不需要查看另一个操作数来检查您是否匹配正确的输出。
// assuming RDX *is* clobbered.
// could remove the + if it isn't.
ssize_t sys_write(int fd, const void *data, size_t size)
{
// register long arg3 __asm__("r10") = ??;
// register-asm is useful for R8 and up
ssize_t res;
__asm__ __volatile__("syscall"
// RDX
: "=a" (res), "+d" (size)
// EAX/RAX RDI RSI
: "a" (SYS_write), "D" (fd), "S" (data),
"m" (*(const char (*)[size]) data) // tells compiler this mem is an input
: "rcx", "r11" //, "memory"
#ifndef __linux__
, "r8", "r9", "r10" // Linux always restores these
#endif
);
return res;
}
有些人更喜欢 register ... asm("")
作为所有操作数,因为您可以使用完整的寄存器名称,并且不必记住 RDI/EDI/DI/DIL 的完全不明显的“D” vs . "d" 表示 RDX/EDX/DX/DL