繁体   English   中英

FreeBSD 系统调用破坏了比 Linux 更多的寄存器? 优化级别之间的内联 asm 不同行为

[英]FreeBSD syscall clobbering more registers than Linux? Inline asm different behaviour between optimization levels

最近我在玩 freebsd 系统调用,我对 i386 部分没有任何问题,因为它在这里有很好的记录。 但我找不到 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 次然后返回 main。 我使用 truss 来检查系统调用及其参数。 使用 -O0 一切正常,但是当我 go -O3 for 循环永远卡住时。 我相信系统调用将i变量或1000更改为奇怪的东西。

function main 的汇编代码转储:

0x0000000000201900 <+0>:     push   %rbp
0x0000000000201901 <+1>:     mov    %rsp,%rbp
0x0000000000201904 <+4>:     mov    $0x3e8,%r8d
0x000000000020190a <+10>:    lea    -0x1(%rbp),%rsi
0x000000000020190e <+14>:    mov    $0x1,%edx
0x0000000000201913 <+19>:    mov    $0xffffffffffffffff,%rdi
0x000000000020191a <+26>:    nopw   0x0(%rax,%rax,1)
0x0000000000201920 <+32>:    movb   $0x0,-0x1(%rbp)
0x0000000000201924 <+36>:    mov    $0x4,%eax
0x0000000000201929 <+41>:    syscall 
0x000000000020192b <+43>:    add    $0xffffffff,%r8d
0x000000000020192f <+47>:    jne    0x201920 <main+32>
0x0000000000201931 <+49>:    xor    %eax,%eax
0x0000000000201933 <+51>:    pop    %rbp
0x0000000000201934 <+52>:    ret

sys_write()有什么问题? 为什么 for 循环会卡住?

优化级别决定了 clang 决定保留其循环计数器的位置:在 memory(未优化)或寄存器中,在本例中r8d (优化)。 R8D 是编译器的一个合乎逻辑的选择:它是一个 call-clobbered reg,它可以在main的开始/结束时使用而无需保存,并且您已经告诉它它可以在没有 REX 前缀(如 ECX)的情况下使用的所有寄存器要么asm 语句的输入/输出或破坏。

注意:如果 FreeBSD 类似于 MacOS,系统调用错误/无错误状态在 CF(进位标志)中返回,而不是通过 RAX 在 -4095..-1 范围内返回。 在这种情况下,您需要一个 GCC6 标志输出操作数,例如int err ( #ifdef __GCC_ASM_FLAG_OUTPUTS__ - example ) 的"=@ccc" (err) ) 或模板中的setc %cl来手动实现 boolean。 (CL 是一个不错的选择,因为您可以将它用作 output 而不是clobber。)


FreeBSD 的syscall处理垃圾 R8、R9 和 R10 ,除了对 Linux 的最低限度的破坏之外:RAX (retval) 和 RCX / R11 ( syscall指令本身使用它们来保存 RIP / RFLAGS ,因此 Z50484C19F1AFDAF3841A0D821ED3 可以找到它的方式返回到用户空间,因此 kernel 甚至从未看到原始值。)

可能还有 RDX, 我们不确定 评论称它为“返回值 2”(即作为 RDX:RAX 返回值的一部分?)。 我们也不知道 FreeBSD 打算在未来的内核中维护什么面向未来的 ABI。

您不能假设 R8-R10 在syscall之后为零,因为在跟踪/单步执行时它们实际上被保留而不是归零。 (因为 kernel 选择不通过sysret返回,原因与 Linux 相同:如果寄存器可能在系统调用中被 ptrace 修改,则硬件/设计错误会导致不安全。例如,尝试使用非规范 RIP 进行sysret将 #GP 在 Intel CPU 上的 ring 0(内核模式)中。这是一场灾难,因为此时 RSP = 用户堆栈。)


相关的 kernel 代码是sysret路径(@NateEldredge 很好地发现了;我通过搜索 swapgs 找到了 syscall 入口点,但还没有查看返回路径)。

保留函数调用的寄存器不需要由该代码恢复,因为调用 C function 并没有首先破坏它们。 并且代码确实恢复了函数调用破坏的“遗留”寄存器 RDI、RSI 和 RDX。

R8-R11 是在函数调用约定中被调用破坏的寄存器,并且在原始 8 个 x86 寄存器之外。 所以这就是让他们“特别”的原因。 (R11 不会归零; syscall/sysret 将它用于 RFLAGS,所以这就是您在syscall之后可以找到的值)

归零比加载它们快,并且在正常情况下(libc 包装函数中的syscall指令)您将返回到仅假设函数调用约定的调用者,因此将假设 R8-R11 被丢弃(对于 RDI、RSI、RDX 和 RCX 也是如此,尽管 FreeBSD 出于某种原因确实会费心恢复它们。)


这种归零仅在非单步或跟踪时发生(例如truss或 GDB si )。 amd64 kernel(Github)的syscall入口点确实保存了所有传入的寄存器,因此它们可以通过其他方式从 kernel 中恢复。


更新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"输出/输入操作数与需要register long arg3 asm("r10")或类似 r8 或 r9 的任何 args 一起使用。

这是在包装器 function 内,因此 C 变量的修改值被丢弃,迫使重复调用每次都设置参数。 这将是“防御性”方法,直到另一个答案确定更明确的非垃圾寄存器。


我确实打破了 *0x000000000020192b 然后在发生中断时信息寄存器。 r8 为零。 在这种情况下程序仍然卡住

我假设r8在您执行 GDB continue执行syscall指令之前为零。 是的,该测试证实了 FreeBSD kernel 在不单步执行时会破坏r8 (并且行为方式与我们在源代码中看到的相匹配。)


请注意,您可以告诉编译器write系统调用仅使用虚拟"m"输入操作数而不是"memory" clobber读取memory(不写入)。 这将让它从循环中提升c的存储。 如何指示可以使用内联 ASM 参数指向的 memory *pointed*?

"m"(*(const char (*)[size]) data)作为输入而不是"memory" clobber。

如果您要为您使用的每个系统调用编写特定的包装器,而不是为每个 3 操作数系统调用使用的通用包装器,它只是将所有操作数转换为unsigned long ,这是您可以从中获得的优势。

说到这一点,让你的系统调用 args 都是long绝对没有意义; 使用户空间符号扩展int fd到 64 位寄存器只是浪费指令。 kernel ABI 将(几乎可以肯定)忽略窄参数的寄存器的高字节,就像 Linux 一样。 (同样,除非您正在制作一个通用的syscall3包装器,您只需使用不同的SYS_编号来定义写入、读取和其他 3 操作数系统调用;然后您会将所有内容转换为寄存器宽度并仅使用"memory"破坏者)。

我对下面的修改版本进行了这些更改。

另请注意,对于 RDI、RSI 和 RDX,您可以使用特定的寄存器字母约束来代替 register-asm 本地变量,就像您在 RAX ( "=a" ) 中为返回值所做的那样。 顺便说一句,您实际上并不需要电话号码的匹配约束,只需使用"a"输入即可; 它更容易阅读,因为您不需要查看另一个操作数来检查您是否匹配正确的 output。

// 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”与 RDX/EDX/DX/DL 的“d”相比

这是一个可以使用的测试框架。 它[松散地]以 H/W 逻辑分析仪和/或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 function 中添加了对snaplist缓冲区溢出的检查。 如果缓冲区已满,则自动调用dumpall 这通常很好,但如果main中的循环永远不会终止(即没有检查后循环转储将永远不会发生),这也是必要

编辑:而且,我添加了可选的“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+([\@\$;])/$1/);

        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 = $1;
            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($1)
                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\t$128,%%rsp\n" \
    "\tcall\tsnapreg\n" \
    "\taddq\t$128,%%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    $128,%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)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM