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指令前后的寄存器保存在一个大的全局缓冲区中。

循环终止后,它将转储所有存储的寄存器值的踪迹。


它是多个文件。提取:

  1. 将下面的代码保存到文件中(例如 /tmp/archive)。
  2. 创建一个目录:(例如)/tmp/extract
  3. cd 到 /tmp/extract.
  4. 然后做:perl /tmp/archive -go.
  5. 它将创建一些子目录:/tmp/extract/syscall/tmp/extract/snaplib 并在其中存储一些文件。
  6. cd 到程序目标目录(例)cd /tmp/extract/syscall
  7. 构建方式:make
  8. 然后,运行 与:./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