Clang 11 和 GCC 8 O2 破坏了内联汇编
Clang 11 and GCC 8 O2 Breaks Inline Assembly
我有一小段代码,有一些内联汇编在 O0 中正确打印 argv[0],但在 O2 中不打印任何内容(使用 Clang 时。另一方面,GCC 打印存储的字符串在打印 argv[0] 时在 envp[0] 中)。这个问题也仅限于 argv(其他两个函数参数可以在启用或不启用优化的情况下按预期使用)。我用 GCC 和 Clang 测试了这个,两个编译器都有这个问题。
代码如下:
void exit(unsigned long long status) {
asm volatile("movq , %%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 , %%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 , %%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 , %%rdx;" //each argv pointer takes up 8 bytes (so multiply argc by 8)
"add , %%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" // "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) 中的某种 st运行ge 错误导致程序 运行 某种部分-构建的程序,即使 GCC 报告了有关它的错误,但无法识别传入的两个编译器标志。
对于 GCC,请改用这些标志:-fomit-frame-pointer;-fno-asynchronous-unwind-tables;-O2;-Wall;-nostdinc;-fno-builtin;-nostdlib; -nodefaultlibs;--no-standard-libraries;-nostartfiles;-nostdinc++。由于 Clang 支持上述 GCC 选项,您也可以将这些标志用于 Clang。
根据 the gcc manual,您不能在 naked
函数中使用扩展汇编,只能使用基本汇编。您不需要通知编译器已损坏的寄存器(因为它不会对它们做任何事情;在 naked
函数中,您负责所有寄存器管理)。并且不需要在扩展操作数中传递 entry
的地址;就做 jmp entry
.
(在我的测试中,您的代码根本无法编译,所以我假设您没有向我们展示您的确切代码 - 下次请这样做,以免浪费人们的时间。)
Linux x86-64 syscall
系统调用允许破坏 rcx
和 r11
寄存器,因此您需要将它们添加到系统调用的破坏列表。
在跳转到 entry
之前,您将堆栈对齐到 16 字节边界。但是,16 字节对齐规则基于以下假设:您将使用 call
调用函数,这会将额外的 8 个字节压入堆栈。因此,被调用的函数实际上期望堆栈最初不是 16 的倍数,而是比 16 的倍数多或少 8。所以你实际上错误地对齐了堆栈,这可能是各种原因神秘的麻烦。
所以要么用 call
替换你的 jmp
,要么从 rsp
中再减去 8 个字节(或者只是 push
你选择的一些 64 位寄存器).
风格注释:unsigned long
在 Linux x86-64 上已经是 64 位了,所以用它代替 unsigned long long
更符合习惯.
一般提示:了解扩展 asm 中的寄存器约束。您可以让编译器为您加载所需的寄存器,而不是在您的 asm 中编写指令来自己完成。因此,您的 exit
函数可能看起来像:
void exit(unsigned long status) {
asm volatile("syscall"
: //no outputs
:"a"(60), "D" (status)
:"rcx", "r11");
}
这特别为您节省了一些指令,因为 status
已经在函数入口的 %rdi
寄存器中。对于您的原始代码,编译器必须将其移动到其他地方,以便您可以自己将其加载到 %rdi
。
你的open
函数总是returns 1,这通常不是实际打开的fd。因此,如果您的程序 运行 具有重定向的标准输出,您的程序将写入重定向的标准输出,而不是它似乎想要执行的 tty。事实上,这使得 open
系统调用完全没有意义,因为你永远不会使用你打开的文件。
您应该将 open
设置为 return 系统调用实际 return 的值,当 syscall
return秒。您可以使用输出操作数将其存储在临时变量中(编译器可能会对其进行优化),然后 return 将其存储在临时变量中。您需要使用数字约束,因为它与输入操作数位于同一寄存器中。我把这个留给你作为练习。如果您的 write
函数实际上 return 编辑了写入的字节数,那也很好。
我有一小段代码,有一些内联汇编在 O0 中正确打印 argv[0],但在 O2 中不打印任何内容(使用 Clang 时。另一方面,GCC 打印存储的字符串在打印 argv[0] 时在 envp[0] 中)。这个问题也仅限于 argv(其他两个函数参数可以在启用或不启用优化的情况下按预期使用)。我用 GCC 和 Clang 测试了这个,两个编译器都有这个问题。
代码如下:
void exit(unsigned long long status) {
asm volatile("movq , %%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 , %%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 , %%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 , %%rdx;" //each argv pointer takes up 8 bytes (so multiply argc by 8)
"add , %%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" // "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) 中的某种 st运行ge 错误导致程序 运行 某种部分-构建的程序,即使 GCC 报告了有关它的错误,但无法识别传入的两个编译器标志。 对于 GCC,请改用这些标志:-fomit-frame-pointer;-fno-asynchronous-unwind-tables;-O2;-Wall;-nostdinc;-fno-builtin;-nostdlib; -nodefaultlibs;--no-standard-libraries;-nostartfiles;-nostdinc++。由于 Clang 支持上述 GCC 选项,您也可以将这些标志用于 Clang。
根据 the gcc manual,您不能在
naked
函数中使用扩展汇编,只能使用基本汇编。您不需要通知编译器已损坏的寄存器(因为它不会对它们做任何事情;在naked
函数中,您负责所有寄存器管理)。并且不需要在扩展操作数中传递entry
的地址;就做jmp entry
.(在我的测试中,您的代码根本无法编译,所以我假设您没有向我们展示您的确切代码 - 下次请这样做,以免浪费人们的时间。)
Linux x86-64
syscall
系统调用允许破坏rcx
和r11
寄存器,因此您需要将它们添加到系统调用的破坏列表。在跳转到
entry
之前,您将堆栈对齐到 16 字节边界。但是,16 字节对齐规则基于以下假设:您将使用call
调用函数,这会将额外的 8 个字节压入堆栈。因此,被调用的函数实际上期望堆栈最初不是 16 的倍数,而是比 16 的倍数多或少 8。所以你实际上错误地对齐了堆栈,这可能是各种原因神秘的麻烦。所以要么用
call
替换你的jmp
,要么从rsp
中再减去 8 个字节(或者只是push
你选择的一些 64 位寄存器).风格注释:
unsigned long
在 Linux x86-64 上已经是 64 位了,所以用它代替unsigned long long
更符合习惯.一般提示:了解扩展 asm 中的寄存器约束。您可以让编译器为您加载所需的寄存器,而不是在您的 asm 中编写指令来自己完成。因此,您的
exit
函数可能看起来像:
void exit(unsigned long status) {
asm volatile("syscall"
: //no outputs
:"a"(60), "D" (status)
:"rcx", "r11");
}
这特别为您节省了一些指令,因为 status
已经在函数入口的 %rdi
寄存器中。对于您的原始代码,编译器必须将其移动到其他地方,以便您可以自己将其加载到 %rdi
。
你的
open
函数总是returns 1,这通常不是实际打开的fd。因此,如果您的程序 运行 具有重定向的标准输出,您的程序将写入重定向的标准输出,而不是它似乎想要执行的 tty。事实上,这使得open
系统调用完全没有意义,因为你永远不会使用你打开的文件。您应该将
open
设置为 return 系统调用实际 return 的值,当syscall
return秒。您可以使用输出操作数将其存储在临时变量中(编译器可能会对其进行优化),然后 return 将其存储在临时变量中。您需要使用数字约束,因为它与输入操作数位于同一寄存器中。我把这个留给你作为练习。如果您的write
函数实际上 return 编辑了写入的字节数,那也很好。