使用printf和ld链接程序?

时间:2019-03-23 14:31:52

标签: linux assembly linker ld libc

在x86-64 Ubuntu上使用NASM构建一个定义自己的undefined 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

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

你好,世界!应该是输出

1 个答案:

答案 0 :(得分:4)

3个问题:

  • 使用ELF对象文件的GNU / Linux不会用下划线修饰/修饰C名称。 使用call printf,而不是_printf (与MacOS X不同,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,但仍使用libc,请与main链接。


这对于GNU / Linux上的 dynamic 可执行文件是安全的,因为glibc可以通过动态链接器挂钩运行其初始化函数。在Cygwin上并不安全,因为它的libc只能通过其CRT起始文件中的调用来初始化(在调用main之前要进行初始化)。

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

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

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


通常,如果您想使用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默认的gpmakes之类的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链接。这会省略CRT启动文件,该文件通常会定义_start,该文件在调用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初始化函数。

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,而返回地址仍在堆栈中,就像调用者调用了{{1 }}。


定义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