为什么一个只调用 printf 的函数,对应的汇编代码这么复杂? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
amiwrong123
V2EX    程序员

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

 
  •   amiwrong123 2024-06-03 16:36:46 +08:00 3627 次点击
    这是一个创建于 498 天前的主题,其中的信息可能已经有所发展或是发生改变。
    #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 
    34 条回复    2024-06-05 10:24:34 +08:00
    swulling
        1
    swulling  
       2024-06-03 16:42:21 +08:00
    这种问题,问大模型(比如 GPT-4 )是最合适的。搜索很难直接找到答案,但是也不算难。

    我问了下,回答的还不错。
    AoEiuV020JP
        2
    AoEiuV020JP  
       2024-06-03 17:02:29 +08:00
    printf 这个 f 可不简单,
    改成 puts 的汇编应该简单很多,
    再改成系统调用估计会更简单,
    write(STDOUT_FILENO, "Hello, world!\n", 14);
    amiwrong123
        3
    amiwrong123  
    OP
       2024-06-03 17:10:44 +08:00
    @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

    反正给了几个解决方法,都不好用。
    InkStone
        5
    InkStone  
       2024-06-03 17:37:38 +08:00
    可以用 ida 或者 ghidra 之类反汇编工具,看下 printf 实际调用的参数究竟是什么字符串。

    当然,从汇编也是可以看出来的,只要你找得着……
    archxm
        6
    archxm  
       2024-06-03 17:39:47 +08:00
    @AoEiuV020JP 一针见血
    ysc3839
        7
    ysc3839  
       2024-06-03 17:47:41 +08:00 via Android
    没开优化吧,个人觉得奇怪的只有 endbr32 ,其他都是无优化情况下很正常的栈操作
    dhb233
        8
    dhb233  
       2024-06-03 17:48:14 +08:00
    猜测第一个 call 是找到符号,第二个 call 是调用对应的函数。
    直接用 objdump 的话,call 之后的符号是没有做链接的,对应的”call c“也没什么意义。记得那个 c 就是这个 call 指令的下一个指令
    dhb233
        9
    dhb233  
       2024-06-03 17:57:49 +08:00
    @dhb233 猜测错的, 搜了下__x86.get_pc_thunk.ax ,看起来是编译器的实现问题
    bfc0
        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 压栈
    augustheart
        11
    augustheart  
       2024-06-03 18:13:22 +08:00
    你确定你不是拿 debug 和 release 做比较?
    augustheart
        12
    augustheart  
       2024-06-03 18:16:26 +08:00
    另外,release 的时候,优化技术之一就是内联
    MrKrabs
        13
    MrKrabs  
       2024-06-03 18:18:18 +08:00
    O2 please
    levelworm
        14
    levelworm  
       2024-06-03 18:18:37 +08:00 via Android
    呃,我想你这里其实不是两个函数调用吗?有两个正常吧。没优化掉的话。
    chayuu
        15
    chayuu  
       2024-06-03 18:24:29 +08:00
    `gcc -Wall -g -o test.o -c test.c -m32`编译的那份,你用`objdump -dx`看的话就会在相同的位置看到这个符号是什么重定位类型了
    bombless
        16
    bombless  
       2024-06-03 18:32:57 +08:00
    gcc 里面你这种调用应该是直接优化成 puts
    chitaotao
        17
    chitaotao  
       2024-06-03 23:26:37 +08:00 via Android
    你这个是没有做重定位的二进制,所以地址什么的都是 placeholder 而不是有意义的地址,你可以去掉-c 再看看。你这应该是编译成位置无关代码,我编译了一下,就是这个结果,i386 下位置无关代码需要通过特殊的函数获取当前的 eip ,就是第一个 call
    chitaotao
        18
    chitaotao  
       2024-06-03 23:29:04 +08:00 via Android
    要不编译成位置无关代码,加-fno-pie -no-pie
    amiwrong123
        19
    amiwrong123  
    OP
       2024-06-03 23:44:09 +08:00
    @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
        20
    amiwrong123  
    OP
       2024-06-03 23:46:31 +08:00
    @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
    chitaotao
        21
    chitaotao  
       2024-06-03 23:56:10 +08:00 via Android
    一堆 sub 应该是在做栈对齐,i386 System V ABI 要求栈 esp+4 ( 4 是返回地址大小)对齐到 16 字节,按他这样算在 call 的时候刚好会对齐到 16 字节
    amiwrong123
        22
    amiwrong123  
    OP
       2024-06-04 00:01:54 +08:00
    @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 调试时,就变成了其他值,这是发生了 重定位吗
    amiwrong123
        23
    amiwrong123  
    OP
       2024-06-04 00:03:01 +08:00
    @AoEiuV020JP #2
    printf 这个 f 可不简单,可以进一步说一下吗
    chitaotao
        24
    chitaotao  
       2024-06-04 00:03:32 +08:00 via Android
    @amiwrong123 是,去掉-c 进行链接之后就可以看到重定位的地址
    ashong
        25
    ashong  
       2024-06-04 00:11:43 +08:00 via iPhone
    指定 entry 试试
    iceheart
        26
    iceheart  
       2024-06-04 05:48:44 +08:00 via Android
    没链接的外部函数当然没地址了。
    编译加 -O2 会有新发现
    chayuu
        27
    chayuu  
       2024-06-04 09:46:25 +08:00
    @amiwrong123 #20
    `R_386_PC32`、`R_386_GOTPC`、`R_386_GOTOFF`这几个都是重定位类型,指示链接器在重定位的时候要怎么计算这个偏移,也就是你在#22 提到的替换。具体的类型是什么意思,具体去查一下就知道了
    ZhiyuanLin
        28
    ZhiyuanLin  
       2024-06-04 12:05:34 +08:00
    @amiwrong123
    > printf 这个 f 可不简单,可以进一步说一下吗
    printf 用了 vararg 。一般 libc 实际实现是在 vprintf ,printf 是个 macro 。
    amiwrong123
        29
    amiwrong123  
    OP
       2024-06-04 14:47:24 +08:00
    @ysc3839 #7
    我试了,加-fcf-protection=none 参数,然后就没有 endbr32 了。
    但 print_banner 的其他汇编还是一样的。
    amiwrong123
        30
    amiwrong123  
    OP
       2024-06-04 14:52:52 +08:00
    @chitaotao #21
    前两次 sub 确实是 为了汇编里面的 这两次 call 的对齐要求,来做的。我用 gdb 看了后,发现确实是这样的。
    ysc3839
        31
    ysc3839  
       2024-06-04 16:12:40 +08:00 via Android
    @amiwrong123 所以开了优化吗?
    e3c78a97e0f8
        32
    e3c78a97e0f8  
       2024-06-04 18:45:58 +08:00
    你好歹开个-O3
    nitro123
        33
    nitro123  
       2024-06-04 22:49:24 +08:00
    因为需要可变参数?
    amiwrong123
        34
    amiwrong123  
    OP
       2024-06-05 10:24:34 +08:00
    @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:抱歉试得有点迟了
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2816 人在线   高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 14:41 PVG 22:41 LAX 07:41 JFK 10:41
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86