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