【问题标题】:Linking a program using printf with ld?使用 printf 和 ld 链接程序?
【发布时间】:2019-08-14 08:01:34
【问题描述】:

在 x86-64 Ubuntu 上使用 NASM 构建定义自己的 _start 而不是 main 的汇编程序时,我得到了 undefined reference to _printf

构建命令:

   nasm -f elf64 hello.asm
   ld -s -o hello hello.o
   hello.o: In function `_start':
   hello.asm:(.text+0x1a): undefined reference to `_printf'
   MakeFile:4: recipe for target 'compile' failed
   make: *** [compile] Error 1

消息来源:

extern _printf

section .text
    global _start
_start:
    mov rdi, format     ; argument #1
    mov rsi, message    ; argument #2
    mov rax, 0
  call _printf            ; call printf

    mov rax, 0
    ret                 ; return 0

section .data

    message:    db "Hello, world!", 0
    format:   db "%s", 0xa, 0

你好,世界!应该是输出

【问题讨论】:

    标签: linux assembly linker ld libc


    【解决方案1】:

    3 个问题:

    • 使用 ELF 目标文件的 GNU/Linux 不会使用前导下划线修饰/破坏 C 名称。 使用call printf,而不是_printf(与MacOS X 不同,它使用_ 装饰符号;如果您正在查看其他操作系统的教程,请记住这一点。Windows 也使用不同的调用约定,但只有 32 位 Windows 使用 _ 或其他编码调用约定选择的装饰来破坏名称。)

    • 你没有告诉ld链接libc,你也没有自己定义printf,所以你没有给链接器任何包含a的输入文件该符号的定义。 printf 是 libc.so 中定义的库函数,与 GCC 前端不同,ld 不会自动包含它。

    • _start 不是函数,您不能从中获取ret RSP 指向argc,而不是返回地址。如果您希望它成为普通函数,请改为定义 main

    如果您想要一个提供自己的_start 而不是main 但仍使用libc 的动态可执行文件,请与gcc -no-pie -nostartfiles hello.o -o hello 链接。


    这对于 GNU/Linux 上的 dynamic 可执行文件是安全的,因为 glibc 可以通过动态链接器挂钩运行它的 init 函数。在 Cygwin 上是不安全的,它的 libc 仅通过来自其 CRT 启动文件的调用来初始化(在调用 main 之前执行此操作)。

    使用call exit退出,而不是使用printf直接进行_exit系统调用;这让 libc 刷新任何缓冲的输出。 (如果将输出重定向到文件,stdout 将是全缓冲的,而不是在终端上缓冲的行。)

    -static 不安全;在静态可执行文件中,没有动态链接器代码在您的_start 之前运行,因此除非您手动调用这些函数,否则 libc 无法自行初始化。这是可能的,但通常不推荐。

    还有其他 libc 实现不需要在 printf / malloc / 其他函数工作之前调用任何 init 函数。在 glibc 中,像 stdio 缓冲区这样的东西是在运行时分配的。 (这个used to be the case for MUSL libc,但根据弗洛里安对此答案的评论,显然情况不再如此。)


    通常如果你想使用 libc 函数,最好定义一个 main 函数而不是你自己的 _start 入口点。 然后你可以正常链接到 gcc , 没有特殊选项。

    请参阅What parts of this HelloWorld assembly code are essential if I were to write the program in assembly? 以及直接使用 Linux 系统调用的版本,无需 libc。


    如果您希望您的代码在最近的发行版上默认使用 gcc 生成的 PIE 可执行文件(没有 --no-pie),您需要 call printf wrt ..plt

    无论哪种方式,您都应该使用lea rsi, [rel message],因为相对于 RIP 的 LEA 比使用 64 位绝对地址的mov r64, imm64 更有效。 (在位置相关代码中,将静态地址放入 64 位寄存器的最佳选择是 5 字节 mov esi, message,因为已知非 PIE 可执行文件中的静态地址位于低 2GiB 的虚拟地址空间中,因此可以作为 32 位符号或零扩展的可执行文件工作。 但与 RIP 相关的 LEA 并没有差多少,而且在任何地方都有效。)

    ;;; Defining your own _start but using libc
    ;;; works on Linux for non-PIE executables
    
    default rel                ; Use RIP-relative for [symbol] addressing modes
    extern printf
    extern exit                ; unlike _exit, exit flushes stdio buffers
    
    section .text
        global _start
    _start:
        ;; RSP is already aligned by 16 on entry at _start, unlike in functions
    
        lea    rdi, [format]        ; argument #1   or better  mov edi, format
        lea    rsi, [message]       ; argument #2
        xor    eax, eax             ; no FP args to the variadic function
        call   printf               ; for a PIE executable:  call printf wrt ..plt
    
        xor    edi, edi             ; arg #1 = 0
        call   exit                 ; exit(0)
        ; exit definitely does not return
    
    section .rodata        ;; read-only data can go in .rodata instead of read-write .data
    
        message:    db "Hello, world!", 0
        format:   db "%s", 0xa, 0
    

    正常组装,gcc -no-pie -nostartfiles hello.o 链接。 这省略了通常定义_start 的CRT 启动文件,该文件在调用main 之前执行一些操作。 Libc 初始化函数是从动态链接器挂钩调用的,因此 printf 可用。

    gcc -static -nostartfiles hello.o 不是这种情况。我提供了使用错误选项会发生什么情况的示例:

    peter@volta:/tmp$ nasm -felf64 nopie-start.asm 
    peter@volta:/tmp$ gcc -no-pie -nostartfiles nopie-start.o 
    peter@volta:/tmp$ ./a.out 
    Hello, world!
    peter@volta:/tmp$ file a.out 
    a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=0cd1cd111ba0c6926d5d69f9191bdf136e098e62, not stripped
    
    # link error without -no-pie because it doesn't automatically make PLT stubs
    peter@volta:/tmp$ gcc -nostartfiles nopie-start.o 
    /usr/bin/ld: nopie-start.o: relocation R_X86_64_PC32 against symbol `printf@@GLIBC_2.2.5' can not be used when making a PIE object; recompile with -fPIC
    /usr/bin/ld: final link failed: bad value
    collect2: error: ld returned 1 exit status
    
    
    # runtime error with -static
    peter@volta:/tmp$ gcc -static -no-pie -nostartfiles nopie-start.o -o static_start-hello
    peter@volta:/tmp$ ./static_start-hello 
    Segmentation fault (core dumped)
    

    替代版本,定义 main 而不是 _start

    (并通过使用puts 而不是printf 进行简化。)

    default rel                ; Use RIP-relative for [symbol] addressing modes
    extern puts
    
    section .text
        global main
    main:
        sub    rsp, 8    ;; RSP was 16-byte aligned *before* a call pushed a return address
                         ;; RSP is now 16-byte aligned, ready for another call
    
        mov    edi, message         ; argument #1, optimized to use non-PIE-only move imm32
        call   puts
    
        add    rsp, 8               ; restore the stack
        xor    eax, eax             ; return 0
        ret
    
    section .rodata
        message:    db "Hello, world!", 0     ; puts appends a newline
    

    puts 几乎完全实现了printf("%s\n", string); C 编译器会为你做这个优化,但在 asm 中你应该自己做。

    使用gcc -no-pie hello.o链接,甚至使用gcc -no-pie -static hello.o进行静态链接。 CRT 启动代码会调用 glibc 的 init 函数。

    peter@volta:/tmp$ nasm -felf64 nopie-main.asm 
    peter@volta:/tmp$ gcc -no-pie nopie-main.o 
    peter@volta:/tmp$ ./a.out 
    Hello, world!
    
    # link error if you leave out -no-pie  because of the imm32 absolute address
    peter@volta:/tmp$ gcc nopie-main.o 
    /usr/bin/ld: nopie-main.o: relocation R_X86_64_32 against `.rodata' can not be used when making a PIE object; recompile with -fPIC
    /usr/bin/ld: final link failed: nonrepresentable section on output
    collect2: error: ld returned 1 exit status
    

    main一个函数,所以你需要在调用另一个函数之前重新对齐堆栈。虚拟推送也是在函数入口对齐堆栈的有效方法,但add/sub rsp, 8 更清晰。

    另一种方法是jmp puts 对其进行尾调用,因此main 的返回值将是puts 返回的任何值。在这种情况下,您必须首先修改rsp:您只需跳转到puts,而您的返回地址仍在堆栈中,就像您的调用者调用了puts一样。


    定义main的PIE兼容代码

    (您可以创建一个定义自己的 _start 的 PIE。这留给读者作为练习。)

    default rel                ; Use RIP-relative for [symbol] addressing modes
    extern puts
    
    section .text
        global main
    main:
        sub    rsp, 8    ;; RSP was 16-byte aligned *before* a call pushed a return address
    
        lea    rdi, [message]         ; argument #1
        call   puts  wrt ..plt
    
        add    rsp, 8
        xor    eax, eax               ; return 0
        ret
    
    section .rodata
        message:    db "Hello, world!", 0     ; puts appends a newline
    
    peter@volta:/tmp$ nasm -felf64 pie.asm
    peter@volta:/tmp$ gcc pie.o
    peter@volta:/tmp$ ./a.out 
    Hello, world!
    peter@volta:/tmp$ file a.out
    a.out: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=b27e6032f955d628a542f6391b50805c68541fb9, not stripped
    

    【讨论】:

    • Not calling __libc_start_main is also not safe on Linux with musl. 这个特定示例不会因此而失败(缺少堆栈对齐可能会导致崩溃)。
    • @FlorianWeimer:OP 写的是_start,这不是函数。 RSP 16 字节对齐在进程入口。但是感谢您的更正,我的印象是 MUSL 没有需要调用的 init 函数。 (我正在对此答案进行重大更新,以提及 main 的堆栈对齐,并添加示例。看看 :)
    • 函数名称前的下划线 (_printf) 用于 16 位和 32 位 Windows,因为至少有 6 种不同的调用约定(Microsoft 使用 3 种,Borland、Watcom 等使用 3 种) ...)!不同的调用约定使用另一个“装饰”(_printfPRINTFprintf__printf@4 ...)作为函数名,以确保使用不同调用约定的代码不会混在一起! 64位调用约定使用“1:1修饰”,所以printf在Windows中也是printf,虽然调用约定与Linux不同!
    • 相关:Can't call C standard library function on 64-bit Linux from assembly (yasm) code 回复:gcc -no-pie vs. -fno-plt-style vs. PLT。
    猜你喜欢
    • 2021-10-12
    • 2015-05-17
    • 2018-08-12
    • 1970-01-01
    • 1970-01-01
    • 2023-03-30
    • 2011-10-03
    • 2011-10-20
    • 2014-11-26
    相关资源
    最近更新 更多