为什么一个只调用 printf 的函数,对应的汇编代码这么复杂?
#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 ?
实际上我看别人生成的汇编都是这样的( 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
这种问题,问大模型(比如 GPT-4 )是最合适的。搜索很难直接找到答案,但是也不算难。我问了下,回答的还不错。
printf 这个 f 可不简单,改成 puts 的汇编应该简单很多, 再改成系统调用估计会更简单,write(STDOUT_FILENO, "Hello, world!\n", 14);
#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反正给了几个解决方法,都不好用。
我也觉得这个汇编看着奇怪在 godbolt.org/ 中试试,对于这段代码, printf() 被编译为 puts() 调用是比较正常的 godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(filename:'1',fontScale:14,fontUsePx:'0',j:1,lang:c%2B%2B,selection:(endColumn:1,endLineNumber:14,positionColumn:1,positionLineNumber:14,selectionStartColumn:1,selectionStartLineNumber:14,startColumn:1,startLineNumber:14),source:'%23include+%3Cstdio.h%3E%0A%0Avoid+print_banner()%0A%7B%0A++++printf(%22Welcome+to+World+of+PLT+and+GOT%5Cn%22)%3B%0A%7D%0A%0Aint+main(void)%0A%7B%0A++++print_banner()%3B%0A%0A++++return+0%3B%0A%7D%0A'),l:'5',n:'1',o:'C%2B%2B+source+%231',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:g141,filters:(b:'0',binary:'1',binaryObject:'1',commentOnly:'0',debugCalls:'1',demangle:'0',directives:'0',execute:'1',intel:'0',libraryCode:'0',trim:'1',verboseDemangling:'0'),flagsViewOpen:'1',fontScale:14,fontUsePx:'0',j:1,lang:c%2B%2B,libs:!(),options:'',overrides:!(),selection:(endColumn:1,endLineNumber:1,positionColumn:1,positionLineNumber:1,selectionStartColumn:1,selectionStartLineNumber:1,startColumn:1,startLineNumber:1),source:1),l:'5',n:'0',o:'+x86-64+gcc+14.1+(Editor+%231)',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0')),l:'2',n:'0',o:'',t:'0')),version:4
可以用 ida 或者 ghidra 之类反汇编工具,看下 printf 实际调用的参数究竟是什么字符串。当然,从汇编也是可以看出来的,只要你找得着……
一针见血
没开优化吧,个人觉得奇怪的只有 endbr32 ,其他都是无优化情况下很正常的栈操作
猜测第一个 call 是找到符号,第二个 call 是调用对应的函数。直接用 objdump 的话,call 之后的符号是没有做链接的,对应的”call c“也没什么意义。记得那个 c 就是这个 call 指令的下一个指令
猜测错的, 搜了下__x86.get_pc_thunk.ax ,看起来是编译器的实现问题
你通过 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 压栈
你确定你不是拿 debug 和 release 做比较?
另外,release 的时候,优化技术之一就是内联
O2 please
呃,我想你这里其实不是两个函数调用吗?有两个正常吧。没优化掉的话。
gcc -Wall -g -o test.o -c test.c -m32
编译的那份,你用objdump -dx
看的话就会在相同的位置看到这个符号是什么重定位类型了
gcc 里面你这种调用应该是直接优化成 puts
你这个是没有做重定位的二进制,所以地址什么的都是 placeholder 而不是有意义的地址,你可以去掉-c 再看看。你这应该是编译成位置无关代码,我编译了一下,就是这个结果,i386 下位置无关代码需要通过特殊的函数获取当前的 eip ,就是第一个 call
要不编译成位置无关代码,加-fno-pie -no-pie
#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 参数),每一步都能看懂。
#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
一堆 sub 应该是在做栈对齐,i386 System V ABI 要求栈 esp+4 ( 4 是返回地址大小)对齐到 16 字节,按他这样算在 call 的时候刚好会对齐到 16 字节
#10 #17 关于这个 get_pc_thunk 附件的汇编,感觉有点神奇哦(请看上图)。明明“可重定位目标文件”里面还是 add $0x1,%eax 和 lea 0x0(%eax),%edx ,用 gdb 调试时,就变成了其他值,这是发生了 重定位吗
#2 printf 这个 f 可不简单,可以进一步说一下吗
是,去掉-c 进行链接之后就可以看到重定位的地址
指定 entry 试试
没链接的外部函数当然没地址了。编译加 -O2 会有新发现
#20R_386_PC32
、R_386_GOTPC
、R_386_GOTOFF
这几个都是重定位类型,指示链接器在重定位的时候要怎么计算这个偏移,也就是你在#22 提到的替换。具体的类型是什么意思,具体去查一下就知道了
printf 这个 f 可不简单,可以进一步说一下吗printf 用了 vararg 。一般 libc 实际实现是在 vprintf ,printf 是个 macro 。
#7 我试了,加-fcf-protection=none 参数,然后就没有 endbr32 了。但 print_banner 的其他汇编还是一样的。
#21 前两次 sub 确实是 为了汇编里面的 这两次 call 的对齐要求,来做的。我用 gdb 看了后,发现确实是这样的。
所以开了优化吗?
你好歹开个-O3
小米应用商店下的,版本号 7.5.0 。移动你想干嘛? PS ,据说电信曾经也这么干过,那是 17 年的帖子了。我刚看了看,联通和电信都没有反应,移动也没改什么特殊设置,一点…
昨天立马安装 24.04 ,怀着激动的心情,以为 x11 干掉了,gnome 又优化了这么久,46 版本咋着不美美哒?好吧,桌面还是一如既往的垃圾,卸载,继续 xfce ,为什…
Running Gradle task 'assembleRelease flutter run 能在 mac 上跑起来 ,但 build apk 的时候 一直 报这个错误,网…