你需要-fno-pie
;默认(在大多数现代发行版中)是-fpie
:为与位置无关的可执行文件生成代码。这是一个与链接器选项分开的代码生成-pie
选项(默认情况下 gcc 也通过),并且独立于-ffreestanding
. -fpie -ffreestanding
意味着您想要一个使用 GOT 的独立 PIE,这就是 GCC 的目标。
-fpie
仅在 64 位代码中花费一点速度(其中 RIP 相对寻址是可能的),但对于 32 位代码来说非常糟糕;编译器在其中一个整数寄存器中获得指向 GOT 的指针(占用 8 个寄存器中的另一个),并使用[reg + disp32]
寻址模式访问相对于该地址的静态数据,例如[eax + foo@GOTOFF]
禁用优化gcc -fpie -m32
后,即使函数不访问任何静态数据,也会在寄存器中生成 GOT 的地址。如果您查看编译器输出(gcc -S
而不是-c
在您正在编译的机器上),您会看到这一点。
在 Godbolt 上,我们可以使用-m32 -fpie
与配置 GCC 相同的效果--enable-default-pie
:
# gcc9.2 -O0 -m32 -fpie
function():
push ebp
mov ebp, esp # frame pointer
call __x86.get_pc_thunk.ax
add eax, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_ # EAX points to the GOT
mov eax, 305441742 # overwrite with the return value
pop ebp
ret
__x86.get_pc_thunk.ax: # this is the helper function gcc calls
mov eax, DWORD PTR [esp]
ret
“thunk”返回它的返回地址。即 . 之后的指令地址call
。该.ax
名称的意思是在 EAX 中返回。现代 GCC 可以选择任何寄存器;传统上,32 位 PIC 基址寄存器始终是 EBX,但现代 GCC 在避免额外保存/恢复 EBX 时选择了一个 call-clobbered 寄存器。
有趣的事实:call +0; pop eax
效率更高,每个呼叫站点只增加 1 个字节。您可能会认为这会使返回地址预测器堆栈失衡,但实际上call +0
大多数 CPU 都不会这样做。 http://blog.stuffedcow.net/2018/04/ras-microbenchmarks/#call0。(call +0
表示 rel32 = 0,因此它调用下一条指令。不过,NASM 不会这样解释该语法。)
除非需要,否则 clang 不会生成 GOT 指针,即使在-O0
. 但它是这样做的call +0
;pop %eax
:https ://godbolt.org/z/GFY9Ht