为什么一个只调用 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
在Unix操作中有太多太多的命令,这些命令的强大之处就是一个命令只干一件事,并把这件事干好。Do one thing, do it well。这是unix的哲学。而且Unix首…
Bret Victor(简历) – 苹果公司的UI交互设计师(大神级的人),在 CUSEC(Canadian University Software Enginee…
单元测试有必要吗? 我在现在的公司工作近半年了,公司的项目都需要写单元测试。但是写了这么久,感觉写单元测试费力不讨好,有时候写单元测试的时间大于写业务逻辑的时间,需要 mock…