#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 2024-06-03 16:42:21 +08:00 这种问题,问大模型(比如 GPT-4 )是最合适的。搜索很难直接找到答案,但是也不算难。 我问了下,回答的还不错。 |
![]() | 2 AoEiuV020JP 2024-06-03 17:02:29 +08:00 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 2024-06-03 17:37:38 +08:00 可以用 ida 或者 ghidra 之类反汇编工具,看下 printf 实际调用的参数究竟是什么字符串。 当然,从汇编也是可以看出来的,只要你找得着…… |
![]() | 6 archxm 2024-06-03 17:39:47 +08:00 @AoEiuV020JP 一针见血 |
![]() | 7 ysc3839 2024-06-03 17:47:41 +08:00 via Android 没开优化吧,个人觉得奇怪的只有 endbr32 ,其他都是无优化情况下很正常的栈操作 |
![]() | 8 dhb233 2024-06-03 17:48:14 +08:00 猜测第一个 call 是找到符号,第二个 call 是调用对应的函数。 直接用 objdump 的话,call 之后的符号是没有做链接的,对应的”call c“也没什么意义。记得那个 c 就是这个 call 指令的下一个指令 |
![]() | 9 dhb233 2024-06-03 17:57:49 +08:00 @dhb233 猜测错的, 搜了下__x86.get_pc_thunk.ax ,看起来是编译器的实现问题 |
10 bfc0 2024-06-03 18:06:14 +08:00 你通过 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 2024-06-03 18:13:22 +08:00 你确定你不是拿 debug 和 release 做比较? |
12 augustheart 2024-06-03 18:16:26 +08:00 另外,release 的时候,优化技术之一就是内联 |
13 MrKrabs 2024-06-03 18:18:18 +08:00 O2 please |
![]() | 14 levelworm 2024-06-03 18:18:37 +08:00 via Android 呃,我想你这里其实不是两个函数调用吗?有两个正常吧。没优化掉的话。 |
15 chayuu 2024-06-03 18:24:29 +08:00 `gcc -Wall -g -o test.o -c test.c -m32`编译的那份,你用`objdump -dx`看的话就会在相同的位置看到这个符号是什么重定位类型了 |
![]() | 16 bombless 2024-06-03 18:32:57 +08:00 gcc 里面你这种调用应该是直接优化成 puts |
17 chitaotao 2024-06-03 23:26:37 +08:00 via Android 你这个是没有做重定位的二进制,所以地址什么的都是 placeholder 而不是有意义的地址,你可以去掉-c 再看看。你这应该是编译成位置无关代码,我编译了一下,就是这个结果,i386 下位置无关代码需要通过特殊的函数获取当前的 eip ,就是第一个 call |
18 chitaotao 2024-06-03 23:29:04 +08:00 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 2024-06-03 23:56:10 +08:00 via Android 一堆 sub 应该是在做栈对齐,i386 System V ABI 要求栈 esp+4 ( 4 是返回地址大小)对齐到 16 字节,按他这样算在 call 的时候刚好会对齐到 16 字节 |
![]() | 22 amiwrong123 OP @bfc0 #10 @chitaotao #17  关于这个 get_pc_thunk 附件的汇编,感觉有点神奇哦(请看上图)。 明明“可重定位目标文件”里面还是 add $0x1,%eax 和 lea 0x0(%eax),%edx ,用 gdb 调试时,就变成了其他值,这是发生了 重定位吗 |
![]() | 23 amiwrong123 OP @AoEiuV020JP #2 printf 这个 f 可不简单,可以进一步说一下吗 |
24 chitaotao 2024-06-04 00:03:32 +08:00 via Android @amiwrong123 是,去掉-c 进行链接之后就可以看到重定位的地址 |
![]() | 25 ashong 2024-06-04 00:11:43 +08:00 via iPhone 指定 entry 试试 |
26 iceheart 2024-06-04 05:48:44 +08:00 via Android 没链接的外部函数当然没地址了。 编译加 -O2 会有新发现 |
27 chayuu 2024-06-04 09:46:25 +08:00 @amiwrong123 #20 `R_386_PC32`、`R_386_GOTPC`、`R_386_GOTOFF`这几个都是重定位类型,指示链接器在重定位的时候要怎么计算这个偏移,也就是你在#22 提到的替换。具体的类型是什么意思,具体去查一下就知道了 |
![]() | 28 ZhiyuanLin 2024-06-04 12:05:34 +08:00 @amiwrong123 > printf 这个 f 可不简单,可以进一步说一下吗 printf 用了 vararg 。一般 libc 实际实现是在 vprintf ,printf 是个 macro 。 |
![]() | 29 amiwrong123 OP |
![]() | 30 amiwrong123 OP @chitaotao #21 前两次 sub 确实是 为了汇编里面的 这两次 call 的对齐要求,来做的。我用 gdb 看了后,发现确实是这样的。 |
![]() | 31 ysc3839 2024-06-04 16:12:40 +08:00 via Android @amiwrong123 所以开了优化吗? |
32 e3c78a97e0f8 2024-06-04 18:45:58 +08:00 你好歹开个-O3 |
33 nitro123 2024-06-04 22:49:24 +08:00 因为需要可变参数? |
![]() | 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 ,直接查看最后的可执行文件。  如上图,是执行的结果。是 objdump -dx test 的汇编。 看起来就是优化掉了,函数开头结尾的栈帧维护操作,比如开头的 push %ebp ; mov %esp,%ebp 。比如结束的 leave 。 PS:抱歉试得有点迟了 |