#include <stdio.h>
void print_banner()
{
printf("Welcome to World of PLT and GOT\n");
}
int main(void)
{
print_banner();
return 0;
}
如上,有一个 test.c ,使用 gcc -Wall -g -o test.o -c test.c -m32 编译后(最开始报错了,然后通过 sudo apt-get install libc6-dev:i386 解决),得到了 test.o 文件。
然后通过 objdump -d test.o 查看汇编,却发现print_banner
函数的汇编很奇怪,是这样的:
test.o: file format elf32-i386
Disassembly of section .text:
00000000 <print_banner>:
0: f3 0f 1e fb endbr32
4: 55 push %ebp
5: 89 e5 mov %esp,%ebp
7: 53 push %ebx
8: 83 ec 04 sub $0x4,%esp
b: e8 fc ff ff ff call c <print_banner+0xc>
10: 05 01 00 00 00 add $0x1,%eax
15: 83 ec 0c sub $0xc,%esp
18: 8d 90 00 00 00 00 lea 0x0(%eax),%edx
1e: 52 push %edx
1f: 89 c3 mov %eax,%ebx
21: e8 fc ff ff ff call 22 <print_banner+0x22>
26: 83 c4 10 add $0x10,%esp
29: 90 nop
2a: 8b 5d fc mov -0x4(%ebp),%ebx
2d: c9 leave
2e: c3 ret
感觉 call 22 之前做的很多事情都不理解。比如为什么上面还有一次 call c ?
实际上我看别人生成的汇编都是这样的( https://blog.csdn.net/linyt/article/details/51635768 ):
00000000 <print_banner>:
0: 55 push %ebp
1: 89 e5 mov %esp, %ebp
3: 83 ec 08 sub $0x8, %esp
6: c7 04 24 00 00 00 00 movl $0x0, (%esp)
d: e8 fc ff ff ff call e <print_banner+0xe>
12: c9 leave
13: c3 ret
问一下各位大佬,为什么我的print_banner
函数的汇编这么奇怪啊?
另外,用gcc -S -o test.s test.c -m32
生成了 test.s 这种方式来看汇编,发现是这样的,第一次的 call 是调用的__x86.get_pc_thunk.ax
:
print_banner:
.LFB0:
.cfi_startproc
endbr32
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
pushl %ebx
subl $4, %esp
.cfi_offset 3, -12
call __x86.get_pc_thunk.ax
addl $_GLOBAL_OFFSET_TABLE_, %eax
subl $12, %esp
leal .LC0@GOTOFF(%eax), %edx
pushl %edx
movl %eax, %ebx
call puts@PLT
addl $16, %esp
nop
movl -4(%ebp), %ebx
leave
.cfi_restore 5
.cfi_restore 3
.cfi_def_cfa 4, 4
ret
.cfi_endproc
![]() |
1
swulling 96 天前
这种问题,问大模型(比如 GPT-4 )是最合适的。搜索很难直接找到答案,但是也不算难。
我问了下,回答的还不错。 |
![]() |
2
AoEiuV020JP 96 天前
printf 这个 f 可不简单,
改成 puts 的汇编应该简单很多, 再改成系统调用估计会更简单, write(STDOUT_FILENO, "Hello, world!\n", 14); |
![]() |
3
amiwrong123 OP @swulling #1
问了大模型,它也觉得很奇怪😓 检查编译器版本:确保你和别人使用相同的编译器版本。 sh 复制代码 gcc --version 使用相同的编译选项:确保你们使用相同的编译选项和优化级别。 sh 复制代码 gcc -Wall -g -O0 -o test.o -c test.c -m32 禁用安全特性:如果不需要 Intel CET ,可以通过编译选项禁用它: sh 复制代码 gcc -Wall -g -o test.o -c test.c -m32 -fcf-protection=none 反正给了几个解决方法,都不好用。 |
![]() |
5
InkStone 96 天前
可以用 ida 或者 ghidra 之类反汇编工具,看下 printf 实际调用的参数究竟是什么字符串。
当然,从汇编也是可以看出来的,只要你找得着…… |
![]() |
6
archxm 96 天前
@AoEiuV020JP 一针见血
|
![]() |
7
ysc3839 96 天前 via Android
没开优化吧,个人觉得奇怪的只有 endbr32 ,其他都是无优化情况下很正常的栈操作
|
![]() |
8
dhb233 96 天前
猜测第一个 call 是找到符号,第二个 call 是调用对应的函数。
直接用 objdump 的话,call 之后的符号是没有做链接的,对应的”call c“也没什么意义。记得那个 c 就是这个 call 指令的下一个指令 |
![]() |
9
dhb233 96 天前
@dhb233 猜测错的, 搜了下__x86.get_pc_thunk.ax ,看起来是编译器的实现问题
|
10
bfc0 96 天前
你通过 gcc -Wall -g -o test.o -c test.c -m32 生成的是“可重定位目标文件”,其经过链接后得到“可执行目标文件”
在链接前,符号的具体地址是不知道的,所以会生成占位的指令,就是那两个指向 `print_banner+xxx` 的 call 指令 链接后两个 call 应该是 `__x86.get_pc_thunk.ax` 和 `puts@plt` 至于为什么要有 `get_pc_thunk` 调用是因为 x86 没有 PC 相对寻址,所以需要通过 call 让处理器将 PC 压栈 |
11
augustheart 96 天前
你确定你不是拿 debug 和 release 做比较?
|
12
augustheart 96 天前
另外,release 的时候,优化技术之一就是内联
|
13
MrKrabs 96 天前
O2 please
|
![]() |
14
levelworm 96 天前 via Android
呃,我想你这里其实不是两个函数调用吗?有两个正常吧。没优化掉的话。
|
15
chayuu 96 天前
`gcc -Wall -g -o test.o -c test.c -m32`编译的那份,你用`objdump -dx`看的话就会在相同的位置看到这个符号是什么重定位类型了
|
![]() |
16
bombless 96 天前
gcc 里面你这种调用应该是直接优化成 puts
|
17
chitaotao 96 天前 via Android
你这个是没有做重定位的二进制,所以地址什么的都是 placeholder 而不是有意义的地址,你可以去掉-c 再看看。你这应该是编译成位置无关代码,我编译了一下,就是这个结果,i386 下位置无关代码需要通过特殊的函数获取当前的 eip ,就是第一个 call
|
18
chitaotao 96 天前 via Android
要不编译成位置无关代码,加-fno-pie -no-pie
|
![]() |
19
amiwrong123 OP @chitaotao #18
老哥,你应该解决了我的这个疑问:为什么汇编里面会有两个 call 。我尝试加了-fno-pie -no-pie ,print_banner 的汇编就只有一个 call 了。 就是这个“位置无关代码”的知识点没有掌握,明天我去研究一下。 新的汇编如下: Disassembly of section .text: 00000000 <print_banner>: 0: f3 0f 1e fb endbr32 4: 55 push %ebp 5: 89 e5 mov %esp,%ebp 7: 83 ec 08 sub $0x8,%esp a: 83 ec 0c sub $0xc,%esp d: 68 00 00 00 00 push $0x0 e: R_386_32 .rodata 12: e8 fc ff ff ff call 13 <print_banner+0x13> 13: R_386_PC32 puts 17: 83 c4 10 add $0x10,%esp 1a: 90 nop 1b: c9 leave 1c: c3 ret 不过这里面的栈操作还是有点奇怪,先减 8 ,再减 c ,最后加 0x10 。感觉减和加的操作 不对等(而且 sp 都减完了,也不用,还是要用 push 再隐式得减 sp ,奇怪)。 不像那篇博客里 print_banner 的汇编( sp 减 8 ,是为了放入 0 参数),每一步都能看懂。 |
![]() |
20
amiwrong123 OP @chayuu #15
objdump -dx 这个命令能看到的信息 更多了: Disassembly of section .text: 00000000 <print_banner>: 0: f3 0f 1e fb endbr32 4: 55 push %ebp 5: 89 e5 mov %esp,%ebp 7: 53 push %ebx 8: 83 ec 04 sub $0x4,%esp b: e8 fc ff ff ff call c <print_banner+0xc> c: R_386_PC32 __x86.get_pc_thunk.ax 10: 05 01 00 00 00 add $0x1,%eax 11: R_386_GOTPC _GLOBAL_OFFSET_TABLE_ 15: 83 ec 0c sub $0xc,%esp 18: 8d 90 00 00 00 00 lea 0x0(%eax),%edx 1a: R_386_GOTOFF .rodata 1e: 52 push %edx 1f: 89 c3 mov %eax,%ebx 21: e8 fc ff ff ff call 22 <print_banner+0x22> 22: R_386_PLT32 puts 26: 83 c4 10 add $0x10,%esp 29: 90 nop 2a: 8b 5d fc mov -0x4(%ebp),%ebx 2d: c9 leave 2e: c3 ret 比如 call 22 ,它解释了是 PLT 表的内容。 不过上面的这几个解释还没太看懂:R_386_PC32 R_386_GOTPC R_386_GOTOFF |
21
chitaotao 96 天前 via Android
一堆 sub 应该是在做栈对齐,i386 System V ABI 要求栈 esp+4 ( 4 是返回地址大小)对齐到 16 字节,按他这样算在 call 的时候刚好会对齐到 16 字节
|
![]() |
22
amiwrong123 OP @bfc0 #10
@chitaotao #17 ![]( https://s3.bmp.ovh/imgs/2024/06/03/03f454618e14e907.png) 关于这个 get_pc_thunk 附件的汇编,感觉有点神奇哦(请看上图)。 明明“可重定位目标文件”里面还是 add $0x1,%eax 和 lea 0x0(%eax),%edx ,用 gdb 调试时,就变成了其他值,这是发生了 重定位吗 |
![]() |
23
amiwrong123 OP @AoEiuV020JP #2
printf 这个 f 可不简单,可以进一步说一下吗 |
24
chitaotao 96 天前 via Android
@amiwrong123 是,去掉-c 进行链接之后就可以看到重定位的地址
|
![]() |
25
ashong 96 天前 via iPhone
指定 entry 试试
|
26
iceheart 96 天前 via Android
没链接的外部函数当然没地址了。
编译加 -O2 会有新发现 |
27
chayuu 95 天前
@amiwrong123 #20
`R_386_PC32`、`R_386_GOTPC`、`R_386_GOTOFF`这几个都是重定位类型,指示链接器在重定位的时候要怎么计算这个偏移,也就是你在#22 提到的替换。具体的类型是什么意思,具体去查一下就知道了 |
![]() |
28
ZhiyuanLin 95 天前
@amiwrong123
> printf 这个 f 可不简单,可以进一步说一下吗 printf 用了 vararg 。一般 libc 实际实现是在 vprintf ,printf 是个 macro 。 |
![]() |
29
amiwrong123 OP |
![]() |
30
amiwrong123 OP @chitaotao #21
前两次 sub 确实是 为了汇编里面的 这两次 call 的对齐要求,来做的。我用 gdb 看了后,发现确实是这样的。 |
![]() |
31
ysc3839 95 天前 via Android
@amiwrong123 所以开了优化吗?
|
32
e3c78a97e0f8 95 天前
你好歹开个-O3
|
33
nitro123 95 天前
因为需要可变参数?
|
![]() |
34
amiwrong123 OP @ysc3839 #31
@e3c78a97e0f8 #32 @MrKrabs #13 @iceheart #26 我这里试了 gcc -Wall -g -O3 -o test.o -c test.c -m32 && gcc -o test test.o -m32 然后用 objdump -dx test ,直接查看最后的可执行文件。 ![]( https://s3.bmp.ovh/imgs/2024/06/05/3b0683fd31bd5d68.png) 如上图,是执行的结果。是 objdump -dx test 的汇编。 看起来就是优化掉了,函数开头结尾的栈帧维护操作,比如开头的 push %ebp ; mov %esp,%ebp 。比如结束的 leave 。 PS:抱歉试得有点迟了 |