我如何摆脱通话 __x86.get_pc_thunk.ax
How do i get rid of call __x86.get_pc_thunk.ax
我尝试编译一个非常简单的 C 程序并将其转换为汇编语言。
我正在使用 Ubuntu 并且 OS 类型是 64 位。
这是 C 程序。
void add();
int main() {
add();
return 0;
}
如果我使用 gcc -S -m32 -fno-asynchronous-unwind-tables -o simple.S simple.c 这就是我的汇编源代码文件的样子:
.file "main1.c"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
call add
movl [=12=], %eax
movl %ebp, %esp
popl %ebp
ret
.size main, .-main
.ident "GCC: (Debian 4.4.5-8) 4.4.5" // this part should say Ubuntu instead of Debian
.section .note.GNU-stack,"",@progbits
但它看起来像这样:
.file "main0.c"
.text
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ebx
pushl %ecx
call __x86.get_pc_thunk.ax
addl $_GLOBAL_OFFSET_TABLE_, %eax
movl %eax, %ebx
call add@PLT
movl [=13=], %eax
popl %ecx
popl %ebx
popl %ebp
leal -4(%ecx), %esp
ret
.size main, .-main
.section
.text.__x86.get_pc_thunk.ax,"axG",@progbits,__x86.get_pc_thunk.ax,comdat
.globl __x86.get_pc_thunk.ax
.hidden __x86.get_pc_thunk.ax
.type __x86.get_pc_thunk.ax, @function
__x86.get_pc_thunk.ax:
movl (%esp), %eax
ret
.ident "GCC: (Ubuntu 6.3.0-12ubuntu2) 6.3.0 20170406"
.section .note.GNU-stack,"",@progbits
在我的大学里,如果我使用 64 位 Linux 版本,他们告诉我使用标志 -m32。有人可以告诉我我做错了什么吗?
我是否使用了正确的标志?
在 -fno-pie 之后编辑
.file "main0.c"
.text
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl , %esp
call add
movl [=14=], %eax
addl , %esp
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
.size main, .-main
.ident "GCC: (Ubuntu 6.3.0-12ubuntu2) 6.3.0 20170406"
.section .note.GNU-stack,"",@progbits
看起来好多了,但不完全一样。
例如,leal 是什么意思?
您看到的额外垃圾是由于您的 GCC 特殊外壳版本 main
补偿以未对齐的堆栈开始的可能损坏的入口点代码。我不确定如何禁用它,或者它是否可能,但是为了您的阅读,将函数重命名为 main 以外的其他名称会抑制它。
重命名为 xmain
后,我得到:
xmain:
pushl %ebp
movl %esp, %ebp
subl , %esp
call add
movl [=10=], %eax
leave
ret
作为一般规则,您不能指望两个不同的编译器为相同的输入生成相同的汇编代码,即使它们具有相同的版本号;他们的代码生成可以有任意数量的额外 "patches"。只要可观察到的行为是相同的,任何事情都会发生。
您还应该知道,GCC 在其默认 -O0
模式下会生成 故意错误的 代码。它的调整是为了便于调试和编译速度,而不是为了生成代码的清晰度或效率。通常 gcc -O1
生成的代码比 gcc -O0
.
生成的代码更容易理解
您还应该知道 main
函数通常需要做其他函数不需要做的额外设置和拆卸。指令 leal 4(%esp),%ecx
是该额外设置的一部分。如果您只想了解与 您 编写的代码相对应的机器代码,而不是 ABI 的详细信息,请将您的测试函数命名为 [=22= 以外的名称].
(正如评论中所指出的,设置代码没有尽可能紧密地调整,但这通常无关紧要,因为它在程序的生命周期中只执行一次。)
现在,回答字面上的问题,
出现的原因
call __x86.get_pc_thunk.ax
是因为你的编译器默认生成"position-independent" executables。位置无关意味着操作系统可以在(虚拟)内存中的任何地址加载程序的机器代码,并且它仍然可以工作。这允许像 address space layout randomization 这样的事情,但要让它工作,你必须采取特殊步骤在每个访问全局变量或调用另一个函数的函数的开头设置一个 "global pointer" (有一些例外) .如果打开优化,实际上更容易解释生成的代码:
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ebx
pushl %ecx
这只是设置main
的堆栈框架和保存需要保存的寄存器。你可以忽略它。
call __x86.get_pc_thunk.bx
addl $_GLOBAL_OFFSET_TABLE_, %ebx
特殊函数__x86.get_pc_thunk.bx
将它的return地址——即紧随其后的addl
指令的地址——加载到EBX寄存器中。然后我们将魔法常量 _GLOBAL_OFFSET_TABLE_
的值添加到该地址,在与位置无关的代码中,它是使用 [=28] 的指令地址之间的 差异 =] 和 global offset table 的地址。因此,EBX 现在指向全局偏移量 table.
call add@PLT
现在我们调用add@PLT
,也就是调用add
,而是跳过"procedure linkage table"来做。 PLT 处理了 add
在共享库而不是主执行程序 table 中定义的可能性。 PLT 中的代码使用全局偏移量 table 并假定您在调用 @PLT 符号之前已经将 EBX 设置为指向它。这就是为什么 main
必须设置 EBX,即使似乎没有人使用它。如果你写的是
extern int number;
int main(void) { return number; }
然后你会看到直接使用 GOT,类似
call __x86.get_pc_thunk.bx
addl $_GLOBAL_OFFSET_TABLE_, %ebx
movl number@GOT(%ebx), %eax
movl (%eax), %eax
我们用GOT的地址加载EBX,然后我们可以从GOT加载全局变量number
的地址,然后我们实际上解引用获取 number
.
值的地址
如果您改为编译 64 位代码,您会看到一些不同且简单得多的东西:
movl number(%rip), %eax
我们可以从程序计数器的固定偏移量加载 number
而不是所有这些与 GOT 打交道的事情。 PC 相对寻址与 64 位扩展一起添加到 x86 体系结构中。同样,在 64 位位置无关模式下,您的原始程序只会说
call add@PLT
无需先设置 EBX。调用仍然需要通过 PLT,但是 PLT 本身使用 PC 相对寻址,不需要调用者的任何帮助。
__x86.get_pc_thunk.bx
和 __x86.get_pc_thunk.ax
之间的唯一区别是它们将 return 地址存储在哪个寄存器中:.bx
为 EBX,.ax
为 EAX。我还看到 GCC 生成 .cx
和 .dx
变体。这只是它想为全局指针使用哪个寄存器的问题——如果要通过 PLT 进行调用,它必须是 EBX,但如果没有,那么它可以使用任何寄存器,所以它会尝试选择一个不需要的。
为什么要调用函数获取return地址?旧的编译器会这样做:
call 1f
1: pop %ebx
但这搞砸了 return-address prediction,所以现在编译器要花点额外的精力来确保每个 call
都与 ret
.
配对
我尝试编译一个非常简单的 C 程序并将其转换为汇编语言。
我正在使用 Ubuntu 并且 OS 类型是 64 位。
这是 C 程序。
void add();
int main() {
add();
return 0;
}
如果我使用 gcc -S -m32 -fno-asynchronous-unwind-tables -o simple.S simple.c 这就是我的汇编源代码文件的样子:
.file "main1.c"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
call add
movl [=12=], %eax
movl %ebp, %esp
popl %ebp
ret
.size main, .-main
.ident "GCC: (Debian 4.4.5-8) 4.4.5" // this part should say Ubuntu instead of Debian
.section .note.GNU-stack,"",@progbits
但它看起来像这样:
.file "main0.c"
.text
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ebx
pushl %ecx
call __x86.get_pc_thunk.ax
addl $_GLOBAL_OFFSET_TABLE_, %eax
movl %eax, %ebx
call add@PLT
movl [=13=], %eax
popl %ecx
popl %ebx
popl %ebp
leal -4(%ecx), %esp
ret
.size main, .-main
.section
.text.__x86.get_pc_thunk.ax,"axG",@progbits,__x86.get_pc_thunk.ax,comdat
.globl __x86.get_pc_thunk.ax
.hidden __x86.get_pc_thunk.ax
.type __x86.get_pc_thunk.ax, @function
__x86.get_pc_thunk.ax:
movl (%esp), %eax
ret
.ident "GCC: (Ubuntu 6.3.0-12ubuntu2) 6.3.0 20170406"
.section .note.GNU-stack,"",@progbits
在我的大学里,如果我使用 64 位 Linux 版本,他们告诉我使用标志 -m32。有人可以告诉我我做错了什么吗? 我是否使用了正确的标志?
在 -fno-pie 之后编辑
.file "main0.c"
.text
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl , %esp
call add
movl [=14=], %eax
addl , %esp
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
.size main, .-main
.ident "GCC: (Ubuntu 6.3.0-12ubuntu2) 6.3.0 20170406"
.section .note.GNU-stack,"",@progbits
看起来好多了,但不完全一样。 例如,leal 是什么意思?
您看到的额外垃圾是由于您的 GCC 特殊外壳版本 main
补偿以未对齐的堆栈开始的可能损坏的入口点代码。我不确定如何禁用它,或者它是否可能,但是为了您的阅读,将函数重命名为 main 以外的其他名称会抑制它。
重命名为 xmain
后,我得到:
xmain:
pushl %ebp
movl %esp, %ebp
subl , %esp
call add
movl [=10=], %eax
leave
ret
作为一般规则,您不能指望两个不同的编译器为相同的输入生成相同的汇编代码,即使它们具有相同的版本号;他们的代码生成可以有任意数量的额外 "patches"。只要可观察到的行为是相同的,任何事情都会发生。
您还应该知道,GCC 在其默认 -O0
模式下会生成 故意错误的 代码。它的调整是为了便于调试和编译速度,而不是为了生成代码的清晰度或效率。通常 gcc -O1
生成的代码比 gcc -O0
.
您还应该知道 main
函数通常需要做其他函数不需要做的额外设置和拆卸。指令 leal 4(%esp),%ecx
是该额外设置的一部分。如果您只想了解与 您 编写的代码相对应的机器代码,而不是 ABI 的详细信息,请将您的测试函数命名为 [=22= 以外的名称].
(正如评论中所指出的,设置代码没有尽可能紧密地调整,但这通常无关紧要,因为它在程序的生命周期中只执行一次。)
现在,回答字面上的问题,
出现的原因call __x86.get_pc_thunk.ax
是因为你的编译器默认生成"position-independent" executables。位置无关意味着操作系统可以在(虚拟)内存中的任何地址加载程序的机器代码,并且它仍然可以工作。这允许像 address space layout randomization 这样的事情,但要让它工作,你必须采取特殊步骤在每个访问全局变量或调用另一个函数的函数的开头设置一个 "global pointer" (有一些例外) .如果打开优化,实际上更容易解释生成的代码:
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ebx
pushl %ecx
这只是设置main
的堆栈框架和保存需要保存的寄存器。你可以忽略它。
call __x86.get_pc_thunk.bx
addl $_GLOBAL_OFFSET_TABLE_, %ebx
特殊函数__x86.get_pc_thunk.bx
将它的return地址——即紧随其后的addl
指令的地址——加载到EBX寄存器中。然后我们将魔法常量 _GLOBAL_OFFSET_TABLE_
的值添加到该地址,在与位置无关的代码中,它是使用 [=28] 的指令地址之间的 差异 =] 和 global offset table 的地址。因此,EBX 现在指向全局偏移量 table.
call add@PLT
现在我们调用add@PLT
,也就是调用add
,而是跳过"procedure linkage table"来做。 PLT 处理了 add
在共享库而不是主执行程序 table 中定义的可能性。 PLT 中的代码使用全局偏移量 table 并假定您在调用 @PLT 符号之前已经将 EBX 设置为指向它。这就是为什么 main
必须设置 EBX,即使似乎没有人使用它。如果你写的是
extern int number;
int main(void) { return number; }
然后你会看到直接使用 GOT,类似
call __x86.get_pc_thunk.bx
addl $_GLOBAL_OFFSET_TABLE_, %ebx
movl number@GOT(%ebx), %eax
movl (%eax), %eax
我们用GOT的地址加载EBX,然后我们可以从GOT加载全局变量number
的地址,然后我们实际上解引用获取 number
.
如果您改为编译 64 位代码,您会看到一些不同且简单得多的东西:
movl number(%rip), %eax
我们可以从程序计数器的固定偏移量加载 number
而不是所有这些与 GOT 打交道的事情。 PC 相对寻址与 64 位扩展一起添加到 x86 体系结构中。同样,在 64 位位置无关模式下,您的原始程序只会说
call add@PLT
无需先设置 EBX。调用仍然需要通过 PLT,但是 PLT 本身使用 PC 相对寻址,不需要调用者的任何帮助。
__x86.get_pc_thunk.bx
和 __x86.get_pc_thunk.ax
之间的唯一区别是它们将 return 地址存储在哪个寄存器中:.bx
为 EBX,.ax
为 EAX。我还看到 GCC 生成 .cx
和 .dx
变体。这只是它想为全局指针使用哪个寄存器的问题——如果要通过 PLT 进行调用,它必须是 EBX,但如果没有,那么它可以使用任何寄存器,所以它会尝试选择一个不需要的。
为什么要调用函数获取return地址?旧的编译器会这样做:
call 1f
1: pop %ebx
但这搞砸了 return-address prediction,所以现在编译器要花点额外的精力来确保每个 call
都与 ret
.