V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
amiwrong123
V2EX  ›  程序员

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

  •  
  •   amiwrong123 · 176 天前 · 3120 次点击
    这是一个创建于 176 天前的主题,其中的信息可能已经有所发展或是发生改变。
    #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  
       176 天前
    这种问题,问大模型(比如 GPT-4 )是最合适的。搜索很难直接找到答案,但是也不算难。

    我问了下,回答的还不错。
    AoEiuV020JP
        2
    AoEiuV020JP  
       176 天前
    printf 这个 f 可不简单,
    改成 puts 的汇编应该简单很多,
    再改成系统调用估计会更简单,
    write(STDOUT_FILENO, "Hello, world!\n", 14);
    amiwrong123
        3
    amiwrong123  
    OP
       176 天前
    @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  
       176 天前
    可以用 ida 或者 ghidra 之类反汇编工具,看下 printf 实际调用的参数究竟是什么字符串。

    当然,从汇编也是可以看出来的,只要你找得着……
    archxm
        6
    archxm  
       176 天前
    @AoEiuV020JP 一针见血
    ysc3839
        7
    ysc3839  
       176 天前 via Android
    没开优化吧,个人觉得奇怪的只有 endbr32 ,其他都是无优化情况下很正常的栈操作
    dhb233
        8
    dhb233  
       176 天前
    猜测第一个 call 是找到符号,第二个 call 是调用对应的函数。
    直接用 objdump 的话,call 之后的符号是没有做链接的,对应的”call c“也没什么意义。记得那个 c 就是这个 call 指令的下一个指令
    dhb233
        9
    dhb233  
       176 天前
    @dhb233 猜测错的, 搜了下__x86.get_pc_thunk.ax ,看起来是编译器的实现问题
    bfc0
        10
    bfc0  
       176 天前
    你通过 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  
       176 天前
    你确定你不是拿 debug 和 release 做比较?
    augustheart
        12
    augustheart  
       176 天前
    另外,release 的时候,优化技术之一就是内联
    MrKrabs
        13
    MrKrabs  
       176 天前
    O2 please
    levelworm
        14
    levelworm  
       176 天前 via Android
    呃,我想你这里其实不是两个函数调用吗?有两个正常吧。没优化掉的话。
    chayuu
        15
    chayuu  
       176 天前
    `gcc -Wall -g -o test.o -c test.c -m32`编译的那份,你用`objdump -dx`看的话就会在相同的位置看到这个符号是什么重定位类型了
    bombless
        16
    bombless  
       176 天前
    gcc 里面你这种调用应该是直接优化成 puts
    chitaotao
        17
    chitaotao  
       176 天前 via Android
    你这个是没有做重定位的二进制,所以地址什么的都是 placeholder 而不是有意义的地址,你可以去掉-c 再看看。你这应该是编译成位置无关代码,我编译了一下,就是这个结果,i386 下位置无关代码需要通过特殊的函数获取当前的 eip ,就是第一个 call
    chitaotao
        18
    chitaotao  
       176 天前 via Android
    要不编译成位置无关代码,加-fno-pie -no-pie
    amiwrong123
        19
    amiwrong123  
    OP
       176 天前
    @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
       176 天前
    @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  
       176 天前 via Android
    一堆 sub 应该是在做栈对齐,i386 System V ABI 要求栈 esp+4 ( 4 是返回地址大小)对齐到 16 字节,按他这样算在 call 的时候刚好会对齐到 16 字节
    amiwrong123
        22
    amiwrong123  
    OP
       176 天前
    @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
       176 天前
    @AoEiuV020JP #2
    printf 这个 f 可不简单,可以进一步说一下吗
    chitaotao
        24
    chitaotao  
       176 天前 via Android
    @amiwrong123 是,去掉-c 进行链接之后就可以看到重定位的地址
    ashong
        25
    ashong  
       176 天前 via iPhone
    指定 entry 试试
    iceheart
        26
    iceheart  
       176 天前 via Android
    没链接的外部函数当然没地址了。
    编译加 -O2 会有新发现
    chayuu
        27
    chayuu  
       176 天前
    @amiwrong123 #20
    `R_386_PC32`、`R_386_GOTPC`、`R_386_GOTOFF`这几个都是重定位类型,指示链接器在重定位的时候要怎么计算这个偏移,也就是你在#22 提到的替换。具体的类型是什么意思,具体去查一下就知道了
    ZhiyuanLin
        28
    ZhiyuanLin  
       176 天前
    @amiwrong123
    > printf 这个 f 可不简单,可以进一步说一下吗
    printf 用了 vararg 。一般 libc 实际实现是在 vprintf ,printf 是个 macro 。
    amiwrong123
        29
    amiwrong123  
    OP
       175 天前
    @ysc3839 #7
    我试了,加-fcf-protection=none 参数,然后就没有 endbr32 了。
    但 print_banner 的其他汇编还是一样的。
    amiwrong123
        30
    amiwrong123  
    OP
       175 天前
    @chitaotao #21
    前两次 sub 确实是 为了汇编里面的 这两次 call 的对齐要求,来做的。我用 gdb 看了后,发现确实是这样的。
    ysc3839
        31
    ysc3839  
       175 天前 via Android
    @amiwrong123 所以开了优化吗?
    e3c78a97e0f8
        32
    e3c78a97e0f8  
       175 天前
    你好歹开个-O3
    nitro123
        33
    nitro123  
       175 天前
    因为需要可变参数?
    amiwrong123
        34
    amiwrong123  
    OP
       175 天前
    @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   ·   实用小工具   ·   4133 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 33ms · UTC 05:20 · PVG 13:20 · LAX 21:20 · JFK 00:20
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.