将使用 printf 和 ld 的程序链接起来?

Fre*_*ers 4 linux assembly linker libc ld

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

构建命令:

   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
Run Code Online (Sandbox Code Playgroud)

nasm 来源:

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
Run Code Online (Sandbox Code Playgroud)

你好,世界!应该是输出

Pet*_*des 8

3个问题:

  • GNU / Linux的使用ELF目标文件并没有装饰与一家领先的下划线/过滤C的名字。 使用call printf,而不是_printf (与MacOS X系统,这确实装饰符号用_;记住这一点,如果你正在寻找在教程其他操作系统Windows还采用了不同的调用约定,但只有32位Windows轧液与名称_或其他装饰对调用约定的选择进行编码。)

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

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

与链接gcc -no-pie -nostartfiles hello.o -o hello,如果你想有一个动态可执行文件,它提供了自己的_start替代main,但仍然使用的libc。


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

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

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

有没有需要之前调用任何初始化函数其他的libc实现printf/ malloc/其他职能的工作。在 glibc 中,像 stdio 缓冲区这样的东西是在运行时分配的。(根据弗洛里安对此答案的评论,这曾经是 MUSL libc 的情况,但显然不再如此。)


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

如果我要用汇编编写程序,请参阅此 HelloWorld 汇编代码的哪些部分是必不可少的?对于那个和一个直接使用 Linux 系统调用的版本,没有 libc。


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

无论哪种方式,您都应该使用,lea rsi, [rel message]因为相对于 RIP 的 LEA 比mov r64, imm64使用 64 位绝对地址更有效。(在位置相关代码中,将静态地址放入 64 位寄存器的最佳选择是 5-byte 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
Run Code Online (Sandbox Code Playgroud)

正常组装,链接gcc -no-pie -nostartfiles hello.o 这省略了 CRT 启动文件,这些文件通常会定义_start在调用main. Libc init 函数是从动态链接器挂钩调用的,因此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)
Run Code Online (Sandbox Code Playgroud)

替代版本,定义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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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

另一种方法是尾jmp puts调用它,因此main的返回值将是puts返回的任何值。在这种情况下,你不能rsp先修改:你只是跳转到puts你的返回地址仍然在堆栈上,就像你的调用者调用了 一样puts


PIE 兼容代码定义了一个 main

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

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
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)

  • [不调用`__libc_start_main` 在带有musl 的Linux 上也不安全。](https://github.com/golang/go/issues/28909) 这个特定的例子不会因为这个原因而失败(缺少堆栈对齐可能会导致崩溃)。 (2认同)