我有以下代码段:
#include <inttypes.h>
#include <stdio.h>
uint64_t
esp_func(void)
{
__asm__("movl %esp, %eax");
}
int
main()
{
uint32_t esp = 0;
__asm__("\t movl %%esp,%0" : "=r"(esp));
printf("esp: 0x%08x\n", esp);
printf("esp: 0x%08lx\n", esp_func());
return 0;
}
在多次执行时会打印以下内容:
❯ clang -g esp.c && ./a.out
esp: 0xbd3b7670
esp: 0x7f8c1c2c5140
❯ clang -g esp.c && ./a.out
esp: 0x403c9040
esp: 0x7f9ee8bd8140
❯ clang -g esp.c && ./a.out
esp: 0xb59b70f0
esp: 0x7fe301f8c140
❯ clang -g esp.c && ./a.out
esp: 0x6efa4110
esp: 0x7fd95941f140
❯ clang -g esp.c && ./a.out
esp: 0x144e72b0
esp: 0x7f246d4ef140
esp_func
表明ASLR具有28位熵,这在我的现代Linux内核上很有意义。
第一个值没有意义:为什么它大不相同?
我看了看组装,看起来很奇怪...
// From main
0x00001150 55 push rbp
0x00001151 4889e5 mov rbp, rsp
0x00001154 4883ec10 sub rsp, 0x10
0x00001158 c745fc000000. mov dword [rbp-0x4], 0
0x0000115f c745f8000000. mov dword [rbp-0x8], 0
0x00001166 89e0 mov eax, esp ; Move esp to eax
0x00001168 8945f8 mov dword [rbp-0x8], eax ; Assign eax to my variable `esp`
0x0000116b 8b75f8 mov esi, dword [rbp-0x8]
0x0000116e 488d3d8f0e00. lea rdi, [0x00002004]
0x00001175 b000 mov al, 0
0x00001177 e8b4feffff call sym.imp.printf ; For whatever reason, the value in [rbp-0x8]
; is assigned here. Why?
// From esp_func
0x00001140 55 push rbp
0x00001141 4889e5 mov rbp, rsp
0x00001144 89e0 mov eax, esp ; Move esp to eax (same instruction as above)
0x00001146 488b45f8 mov rax, qword [rbp-0x8] ; This changes everything. What is this?
0x0000114a 5d pop rbp
0x0000114b c3 ret
0x0000114c 0f1f4000 nop dword [rax]
所以我的问题是[rbp-0x8]
中有什么,它是如何到达那里的,为什么两个值不同?
答案 0 :(得分:3)
否,堆栈ASLR在程序启动时发生一次。函数之间对RSP的相对调整在编译时是固定的,并且只是为函数的局部var腾出空间的小常数。 (C99可变长度数组和alloca
对RSP执行运行时变量调整,但不是随机的。)
您的程序包含未定义的行为,并且实际上并没有打印RSP。而是由先前的printf
调用在寄存器中留下的某个堆栈地址(它似乎是堆栈地址,因此其高位确实随ASLR的不同而不同)。它没有告诉您有关函数之间的堆栈指针差异的任何信息,只是告诉您如何不使用GNU C内联汇编。
第一个值是正确打印当前的ESP,但这只是64位RSP的低32位。
将非void
函数的结尾降级是不安全的,并且使用返回值是Undefined Behaviour。使用返回值esp_func()
的任何调用者都必然会触发UB,因此编译器可以随意将所需的内容留在RAX中。
如果要编写mov %rsp, %rax
/ ret
,请以纯asm格式编写该函数,或者移动到"=r"(tmp)
局部变量中。使用GNU C内联asm修改RAX而不告诉编译器它不会改变任何东西。编译器仍然将此视为没有返回值的函数。
MSVC内联汇编程序有所不同:显然支持使用_asm{ mov eax, 123 }
之类的东西,然后退出非空函数的末尾,即使内联,MSVC也会将其视为函数返回值。 GNU C内联汇编不需要像这样的愚蠢的技巧:如果您希望您的汇编与C值交互,请像在main
中那样使用带有输出约束的扩展汇编。请记住,编译器不会解析GNU C内联asm,只是将模板字符串作为要汇编的编译器asm输出的一部分发出。
我不知道为什么clang从堆栈中重新加载返回值,但这只是clang内部的人工产物,以及禁用优化的代码生成方式。但是由于未定义的行为,允许执行此操作。这是一个非空函数,因此需要具有返回值。最简单的事情是只发出ret
,这是某些编译器在启用优化的情况下发生的事情,但即使如此,由于过程间优化也无法解决问题。
使用 未返回一个函数的返回值实际上是C中的未定义行为。这适用于C级别;使用内联asm修改寄存器而不告诉编译器有关寄存器,就编译器而言,不会改变任何内容。因此,您的程序整体上都包含UB,因为它将结果传递给printf
。这就是允许编译器以这种方式进行编译的原因:您的代码已经损坏。实际上,它只是从堆栈内存中返回一些垃圾。
TL:DR:这不是发出mov %rsp, %rax
/ ret
作为函数的asm定义的有效方法。
(C ++首先将其增强为UB,以UB结束,但是在C中,只要调用者不使用返回值就是合法的。如果您通过优化编译与C ++相同的源, g ++甚至没有在内联asm模板之后发出ret
指令。如果您声明不带返回类型的函数,这可能是为了支持C的default-int
返回类型。)
这个UB也是为什么您的注释(修改为固定的printf格式字符串)的修改版本在启用优化的情况下编译(https://godbolt.org/z/sE7e84)会打印出“令人惊讶”的不同“ RSP”值:第二个 isn完全不使用RSP。
#include <inttypes.h>
#include <stdio.h>
uint64_t __attribute__((noinline)) rsp_func(void)
{
__asm__("movq %rsp, %rax");
} // UB if return value used
int main()
{
uint64_t rsp = 0;
__asm__("\t movq %%rsp,%0" : "=r"(rsp));
printf("rsp: 0x%08lx\n", rsp);
printf("rsp: 0x%08lx\n", rsp_func()); // UB here
return 0;
}
输出示例:
Compiler stderr
<source>:7:1: warning: non-void function does not return a value [-Wreturn-type]
}
^
1 warning generated.
Program returned: 0
Program stdout
rsp: 0x7fff5c472f30
rsp: 0x7f4b811b7170
clang -O3 asm
的输出表明,编译器可见的UB是一个问题。即使您使用了noinline
,编译器仍然可以看到函数体并尝试进行过程间优化。在这种情况下,UB导致它放弃并且在mov %rsp, %rsi
和call rsp_func
之间不发出call printf
,因此它将打印以前的printf恰好在RSI中保留的任何值
# from the Godbolt link
rsp_func: # @rsp_func
mov rax, rsp
ret
main: # @main
push rax
mov rsi, rsp
mov edi, offset .L.str
xor eax, eax
call printf
call rsp_func # return value ignored because of UB.
mov edi, offset .L.str
xor eax, eax
call printf # printf("0x%08lx\n", garbage in RSI left from last printf)
xor eax, eax
pop rcx
ret
.L.str:
.asciz "rsp: 0x%08lx\n"
GNU C基本asm(无约束)对任何事物(__attribute__((naked))
函数的主体除外)都没有用。
在编译时看不到UB的情况下,不要以为编译器会按照您的期望进行操作。(当UB在编译时不可见时,编译器必须使代码适用于某些调用者或被调用者,您将获得预期的组合。但是在编译时可见的UB意味着所有选择均已关闭。)