为什么一个只调用 printf 的函数,对应的汇编代码这么复杂?

277 天前
 amiwrong123
#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
3258 次点击
所在节点    程序员
34 条回复
swulling
277 天前
这种问题,问大模型(比如 GPT-4 )是最合适的。搜索很难直接找到答案,但是也不算难。

我问了下,回答的还不错。
AoEiuV020JP
277 天前
printf 这个 f 可不简单,
改成 puts 的汇编应该简单很多,
再改成系统调用估计会更简单,
write(STDOUT_FILENO, "Hello, world!\n", 14);
amiwrong123
277 天前
@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

反正给了几个解决方法,都不好用。
wudicgi
277 天前
InkStone
277 天前
可以用 ida 或者 ghidra 之类反汇编工具,看下 printf 实际调用的参数究竟是什么字符串。

当然,从汇编也是可以看出来的,只要你找得着……
archxm
277 天前
@AoEiuV020JP 一针见血
ysc3839
277 天前
没开优化吧,个人觉得奇怪的只有 endbr32 ,其他都是无优化情况下很正常的栈操作
dhb233
277 天前
猜测第一个 call 是找到符号,第二个 call 是调用对应的函数。
直接用 objdump 的话,call 之后的符号是没有做链接的,对应的”call c“也没什么意义。记得那个 c 就是这个 call 指令的下一个指令
dhb233
277 天前
@dhb233 猜测错的, 搜了下__x86.get_pc_thunk.ax ,看起来是编译器的实现问题
bfc0
277 天前
你通过 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 压栈
augustheart
277 天前
你确定你不是拿 debug 和 release 做比较?
augustheart
277 天前
另外,release 的时候,优化技术之一就是内联
MrKrabs
277 天前
O2 please
levelworm
277 天前
呃,我想你这里其实不是两个函数调用吗?有两个正常吧。没优化掉的话。
chayuu
277 天前
`gcc -Wall -g -o test.o -c test.c -m32`编译的那份,你用`objdump -dx`看的话就会在相同的位置看到这个符号是什么重定位类型了
bombless
277 天前
gcc 里面你这种调用应该是直接优化成 puts
chitaotao
276 天前
你这个是没有做重定位的二进制,所以地址什么的都是 placeholder 而不是有意义的地址,你可以去掉-c 再看看。你这应该是编译成位置无关代码,我编译了一下,就是这个结果,i386 下位置无关代码需要通过特殊的函数获取当前的 eip ,就是第一个 call
chitaotao
276 天前
要不编译成位置无关代码,加-fno-pie -no-pie
amiwrong123
276 天前
@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 参数),每一步都能看懂。
amiwrong123
276 天前
@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

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1046414

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX