【问题标题】:Clang 11 and GCC 8 O2 Breaks Inline AssemblyClang 11 和 GCC 8 O2 破坏了内联装配
【发布时间】:2020-06-21 22:11:44
【问题描述】:

我有一个简短的 sn-p 代码,带有一些内联程序集,可以在 O0 中正确打印 argv[0],但在 O2 中不打印任何内容(另一方面,使用 Clang 时,GCC 会打印存储的字符串打印 argv[0] 时在 envp[0] 中)。这个问题也仅限于 argv(其他两个函数参数可以按预期使用,无论是否启用优化)。我用 GCC 和 Clang 测试了这个,两个编译器都有这个问题。

代码如下:

void exit(unsigned long long status) {
    asm volatile("movq $60, %%rax;" //system call 60 is exit
        "movq %0, %%rdi;" //return code 0
        "syscall"
        : //no outputs
        :"r"(status)
        :"rax", "rdi");
}

int open(const char *pathname, unsigned long long flags) {
    asm volatile("movq $2, %%rax;" //system call 2 is open
        "movq %0, %%rdi;"
        "movq %1, %%rsi;"
        "syscall"
        : //no outputs
        :"r"(pathname), "r"(flags)
        :"rax", "rdi", "rsi");
        return 1;
}

int write(unsigned long long fd, const void *buf, size_t count) {
    asm volatile("movq $1, %%rax;" //system call 1 is write
        "movq %0, %%rdi;"
        "movq %1, %%rsi;"
        "movq %2, %%rdx;"
        "syscall"
        : //no outputs
        :"r"(fd), "r"(buf), "r"(count)
        :"rax", "rdi", "rsi", "rdx");
        return 1;
}

static void entry(unsigned long long argc, char** argv, char** envp);

/*https://www.systutorials.com/x86-64-calling-convention-by-gcc/: "The calling convention of the System V AMD64 ABI is followed on GNU/Linux. The registers RDI, RSI, RDX, RCX, R8, and R9 are used for integer and memory address arguments
and XMM0, XMM1, XMM2, XMM3, XMM4, XMM5, XMM6 and XMM7 are used for floating point arguments.
For system calls, R10 is used instead of RCX. Additional arguments are passed on the stack and the return value is stored in RAX."*/

//__attribute__((naked)) defines a pure-assembly function
__attribute__((naked)) void _start() {
    asm volatile("xor %%rbp,%%rbp;" //http://dbp-consulting.com/tutorials/debugging/linuxProgramStartup.html: "%ebp,%ebp sets %ebp to zero. This is suggested by the ABI (Application Binary Interface specification), to mark the outermost frame."
    "pop %%rdi;" //rdi: arg1: argc -- can be popped off the stack because it is copied onto register
    "mov %%rsp, %%rsi;" //rsi: arg2: argv
    "mov %%rdi, %%rdx;"
    "shl $3, %%rdx;" //each argv pointer takes up 8 bytes (so multiply argc by 8)
    "add $8, %%rdx;" //add size of null word at end of argv-pointer array (8 bytes)
    "add %%rsp, %%rdx;" //rdx: arg3: envp
    "andq $-16, %%rsp;" //align stack to 16-bits (which is required on x86-64)
    "jmp %P0" //https://stackoverflow.com/questions/3467180/direct-c-function-call-using-gccs-inline-assembly: "After looking at the GCC source code, it's not exactly clear what the code P in front of a constraint means. But, among other things, it prevents GCC from putting a $ in front of constant values. Which is exactly what I need in this case."
    :
    :"i"(entry)
    :"rdi", "rsp", "rsi", "rdx", "rbp", "memory");
}

//Function cannot be optimized-away, since it is passed-in as an argument to asm-block above
//Compiler Options: -fno-asynchronous-unwind-tables;-O2;-Wall;-nostdlibinc;-nobuiltininc;-fno-builtin;-nostdlib; -nodefaultlibs;--no-standard-libraries;-nostartfiles;-nostdinc++
//Linker Options: -nostdlib; -nodefaultlibs
static void entry(unsigned long long argc, char** argv, char** envp) {
    int ttyfd = open("/dev/tty", O_WRONLY);

    write(ttyfd, argv[0], 9);
    write(ttyfd, "\n", 1);

    exit(0);
}

编辑:添加系统调用定义。

编辑:将 rcx 和 r11 添加到系统调用的 clobber 列表修复了 clang 的问题,但 gcc 出现了错误。

编辑:GCC 实际上没有错误,但是我的构建系统 (CodeLite) 中出现了某种奇怪的错误,因此程序运行了某种部分构建的程序,即使 GCC 报告了关于它无法识别的错误传入的两个编译器标志。 对于 GCC,请改用以下标志:-fomit-frame-pointer;-fno-asynchronous-unwind-tables;-O2;-Wall;-nostdinc;-fno-builtin;-nostdlib; -nodefaultlibs;--no-standard-libraries;-nostartfiles;-nostdinc++。由于 Clang 支持上述 GCC 选项,您也可以将这些标志用于 Clang。

【问题讨论】:

  • 参数可能没有通过堆栈传递 - 你调试了吗
  • 另请注意,系统调用允许销毁rcxr11,因此您应该将它们添加到您的clobber 列表中。
  • gcc manual 表示您不应该将扩展 asm 用于“裸”功能,只能使用基本 asm,因此您需要相应地修改 _start。我很惊讶您的代码被编译器接受。使用i 约束传递entry 的地址应该是不必要的;就做jmp entry。破坏者也是不必要的。
  • 您是否尝试过单步执行_start 函数并使用调试器检查它的堆栈,以查看实际存在的内容是否符合您的期望?这可能会帮助您了解正在发生的事情,并找出您的处理不正确的原因。
  • 您似乎遗漏了定义size_tO_WRONLY 的必要#include 文件。你的真实代码到底是什么? (以及您使用哪些确切的 gcc 和 clang 命令行选项构建?)

标签: c linux assembly x86-64 inline-assembly


【解决方案1】:
  1. 根据the gcc manual,您不能在naked 函数中使用扩展asm,只能使用基本asm。您无需通知编译器已损坏的寄存器(因为无论如何它不会对它们做任何事情;在naked 函数中,您负责所有寄存器管理)。并且不需要在扩展操作数中传递entry 的地址;就做jmp entry

    (在我的测试中,您的代码根本无法编译,所以我假设您没有向我们展示您的确切代码 - 下次请这样做,以免浪费人们的时间。)

  2. Linux x86-64 syscall 系统调用允许破坏rcxr11 寄存器,因此您需要将它们添加到系统调用的破坏列表中。

  3. 在跳转到 entry 之前,您将堆栈对齐到 16 字节边界。但是,16 字节对齐规则是基于您将使用 call 调用函数的假设,这会将额外的 8 个字节压入堆栈。因此,被调用的函数实际上期望堆栈最初不是 16 的倍数,而是比 16 的倍数多或少 8。因此,您实际上错误地对齐了堆栈,这可能是各种原因神秘的麻烦。

    所以要么用call 替换你的jmp,要么从rsp 中再减去8 个字节(或者只是push 一些你选择的64 位寄存器)。

  4. 样式说明:unsigned long 在 Linux x86-64 上已经是 64 位,因此在任何地方使用它来代替 unsigned long long 会更符合习惯。

  5. 一般提示:了解扩展 asm 中的寄存器约束。您可以让编译器为您加载所需的寄存器,而不是在您的 asm 中编写指令来自己完成。所以你的 exit 函数可能看起来像:

    void exit(unsigned long status) {
        asm volatile("syscall"
            : //no outputs
            :"a"(60), "D" (status)
            :"rcx", "r11");
    }

这特别为您节省了一些指令,因为status 已经在函数入口的%rdi 寄存器中。使用您的原始代码,编译器必须将其移动到其他位置,以便您可以自己将其加载到%rdi

  1. 您的open 函数总是返回 1,这通常不是实际打开的 fd。因此,如果您的程序在标准输出重定向的情况下运行,您的程序将写入重定向的标准输出,而不是像它似乎想要的那样写入 tty。事实上,这使得open 系统调用完全没有意义,因为您从不使用您打开的文件。

    您应该安排open 返回系统调用实际返回的值,当syscall 返回时,该值将留在%rax 寄存器中。您可以使用输出操作数将其存储在临时变量中(编译器可能会对其进行优化),然后将其返回。您需要使用数字约束,因为它与输入操作数在同一个寄存器中。我把这个作为练习留给你。如果您的 write 函数实际上返回了写入的字节数,那同样会很好。

【讨论】:

  • 3.或更简单地说,如果您的entry“函数”也不是真正的函数并且无法返回,请删除and 并保留jmp。在进入_start 时,ABI 保证堆栈指针是 16 字节对齐的,因此一个 pop 为您设置函数进入 noreturn 函数。或者只是 mov load 而不是 pop 然后你可以正常 callHow Get arguments value using inline assembly in C without Glibc? 展示了一些手写的基本框架 _start 使用 argc、argv 调用 C 函数的实现。
  • 5. #include <sys/syscalls.h> 获取呼叫号码的宏,例如SYS_exit 或来自asm/unistd.h 的Linux __NR_exit 宏名称。这些头文件甚至可以安全地包含在纯 asm 中。
  • 6.您可以使用unsigned long rax = __NR_write 并使用"+a"(rax) 约束来更简单地表达包装函数中该寄存器的双重输入/输出使用,而不是使用匹配约束来为不同的输入和输出C vars使用相同的寄存器.或者只使用 "a"(__NR_write)"=a"(retval) - 特定寄存器约束使匹配约束变得不必要,因为您知道它们将选择哪个寄存器,因此您可以手动对匹配进行硬编码。
  • OP 代码的附加代码审查:它们的 C 包装器类型与底层系统调用不匹配。例如fd args 是 32 位 int,而不是 64 位 long。如果您包含为这些函数提供原型的普通 C 头文件 (godbolt.org/z/XuyMQK),则会产生不匹配。您应该能够做到这一点并且仍然定义您自己的内联版本,但 OP 不能,因为它们把类型弄得一团糟。这会导致额外的movslqint ttyfdopen 符号扩展到unsigned long long fd 以在编译器生成的asm (godbolt.org/z/isdSYv) 中为write (godbolt.org/z/isdSYv)
  • (编译为 C++ 允许重载函数名称,因此拥有两个具有不同签名的 open 版本并不是错误,让这个混乱编译。因为 asm 使用 ; 作为指令分隔符而不是\n\t 的每个 asm 块都变成一条长长的线,这是一团糟。)无论如何,我认为这个代码审查不值得单独回答,所以把它留在这里。
猜你喜欢
  • 2015-12-25
  • 1970-01-01
  • 2018-10-20
  • 1970-01-01
  • 2016-09-29
  • 1970-01-01
  • 2014-06-30
  • 1970-01-01
  • 2016-12-12
相关资源
最近更新 更多