x64汇编之printf输出内容

大家好,你们可以叫我凌,是个16岁的网络安全学习者。

我大概看了下学习路径,发现关于引用C函数printf的内容之后将基本不会再单独讲了,并且也十分地重要。因此,我决定单独写篇文章讲讲这个东西。当然,我保证迄今为止其他该学的东西都没有落下,所以请你们放心好了。

那么,我们就直接开始吧!


为什么要用 printf

在前面,我们一直使用 sys_write 来输出内容。sys_write 是系统调用,它只能输出原始字节,也就是说,你给它什么字节,它就输出什么字节。它不会帮你做任何格式化。

比如你想输出一个整数 42,你不能直接 sys_write 写 42,因为 42 在内存中是一个二进制数(0x2A),不是字符 '4' 和 '2'。你需要自己把 42 转换成字符 '4' 和 '2',然后才能输出。

printf 不一样。它是一个 C 标准库函数,支持格式化输出。你可以写 printf("%d", 42),它会自动把整数转成字符串并输出。

调试的时候,printf 非常方便:

  • 想看看某个寄存器的值 → printf("rax = %d", rax)

  • 想输出字符串 → printf("%s", msg)

  • 想输出浮点数 → printf("%f", pi)

本章就是要讲清楚:在汇编中怎么调用 printf。

调用 printf 需要什么

在汇编中调用 printf,和直接用 syscall 不一样。printf 是 C 标准库的函数,不是内核提供的服务。你需要做三件事:

  1. 声明外部函数:在汇编文件中写上 extern printf,告诉汇编器这个函数是外部的。

  2. 用 gcc 链接:printf 在 C 标准库中,ld 默认不会链接它,需要用 gcc 来自动链接标准库。

  3. 入口点使用 main:C 标准库需要初始化环境(比如设置堆、栈、标准输入输出等),这些初始化工作在 _start 中不会自动执行,但在 main 中会由 C 运行时(CRT)自动完成。

如果坚持用 _start 也可以,但需要手动处理栈对齐和调用 exit 而不是 ret。但建议初学阶段直接使用 main,省心省力。

参数传递规则

在 x64 中,调用一个函数(包括 printf)时,参数通过以下寄存器传递:

|------|-----|-----------------|
| 参数顺序 | 寄存器 | 说明 |
| 第1个 | rdi | 格式字符串地址(永远放在这里) |
| 第2个 | rsi | 第一个值 |
| 第3个 | rdx | 第二个值 |
| 第4个 | rcx | 第三个值 |
| 第5个 | r8 | 第四个值 |
| 第6个 | r9 | 第五个值 |
| 第7个起 | 栈 | 超出6个时,通过栈传递 |

这个顺序是固定的。你只需要记住:rdi 永远是格式字符串地址,后面的参数依次往后排。

输出字符串

这是最简单的用法。我们调用 printf("Hello\n"),实际上相当于:

cpp 复制代码
printf("Hello\n");

完整代码

cpp 复制代码
; printf_str.asm
; 输出一个字符串

extern printf          ; 声明外部函数

section .data
    fmt db "Hello", 10, 0   ; 10 是换行符,0 是字符串结束符

section .text
    global main

main:
    push rbp
    mov rbp, rsp

    mov rdi, fmt       ; 第1个参数:格式字符串地址
    call printf        ; 调用 printf

    mov rsp, rbp
    pop rbp
    ret

逐行解释

  • extern printf:告诉汇编器,printf 是一个外部函数,链接时会去标准库中找它的实现。

  • fmt db "Hello", 10, 0:字符串定义。10 是换行符,0 是字符串结束符(C 标准要求)。

  • global main:入口点用 main,因为 gcc 默认从 main 开始执行。

  • mov rdi, fmt:把格式字符串的地址放入 rdi,作为第一个参数。

  • call printf:调用 printf。

  • ret:返回 C 运行时,由它负责退出程序(自动调用 exit)。

注意

字符串必须以 0 结尾,因为 printf 会从起始地址一直读到 0 才停止。如果忘记加 0,printf 会继续读取内存,直到遇到一个随机的 0 为止,导致输出乱码或崩溃。

编译运行

nasm -f elf64 printf_str.asm -o printf_str.o

gcc -no-pie printf_str.o -o printf_str

./printf_str

输出:

Hello

输出整数

输出整数时,格式字符串中用 %d 占位符,整数放在第二个参数位置。

比如以下C代码(其他编程语言例如Python同理)

cpp 复制代码
printf("answer = %d\n", 42);

完整代码

cpp 复制代码
; printf_int.asm
; 输出一个整数

extern printf

section .data
    fmt db "answer = %d", 10, 0
    val dd 42

section .text
    global main

main:
    push rbp
    mov rbp, rsp

    mov rdi, fmt       ; 第1个参数:格式字符串
    mov rsi, [val]     ; 第2个参数:整数(%d 对应 rsi)
    call printf

    mov rsp, rbp
    pop rbp
    ret

解释

  • %d 表示此处输出一个十进制整数。

  • val dd 42 定义了一个 4 字节的整数。

  • mov rsi, val 将整数值放入 rsi,作为第二个参数。

  • 注意:mov rsi, val 这里 rsi 是 64 位寄存器,而 val 只有 4 字节。但因为是小端序,高 32 位自动清零(因为 mov rsi, val 会读取 4 字节,并扩展到 64 位)。

输出

answer = 42

输出浮点数

浮点数的传递方式和整数不同。浮点数通过 xmm0、xmm1 等向量寄存器传递,而不是 rsi、rdx。

另外,printf 是一个变参函数,它需要知道有多少个浮点数参数被传递了。这个信息放在 al 寄存器中:

  • al 0:没有浮点数参数(前面输出字符串和整数时就是这个值)

  • al = 1:传递了 1 个浮点数

  • al = 2:传递了 2 个浮点数

  • 依此类推

cpp 复制代码
printf("pi = %f\n", 3.14159);

完整代码

cpp 复制代码
; printf_float.asm
; 输出一个浮点数

extern printf

section .data
    fmt db "pi = %f", 10, 0
    pi dq 3.14159          ; dq = 64位双精度浮点数

section .text
    global main

main:
    push rbp
    mov rbp, rsp

    mov rdi, fmt           ; 第1个参数:格式字符串
    movsd xmm0, [pi]       ; 第2个参数:浮点数(放入 xmm0,不是 rsi)
    mov al, 1              ; 告诉 printf 有 1 个浮点数参数
    call printf

    mov rsp, rbp
    pop rbp
    ret

解释

  • %f 表示输出一个浮点数(双精度)。

  • pi dq 3.14159:双精度浮点数用 dq 定义(8 字节)。

  • movsd xmm0, pi:movsd 指令将内存中的双精度浮点数装入 xmm0 寄存器。

  • mov al, 1:告诉 printf` 有 1 个浮点数参数(xmm0 算一个)。

  • 注意:浮点数不是放在 rsi,而是放在 xmm0,这是关键区别。

输出

pi = 3.141590

单精度浮点数

如果数据是单精度(float),用 dd 定义,装入指令用 movss(不是 movsd):

cpp 复制代码
pi dd 3.14159
movss xmm0, [pi]

其他部分完全一样。%f 会自动处理单精度和双精度。

输出多个浮点数

如果需要输出两个浮点数,依次放入 xmm0、xmm1,al 设为 2。

cpp 复制代码
extern printf

section .data
    fmt db "pi = %f, e = %f", 10, 0
    pi dq 3.14159
    e  dq 2.71828

section .text
    global main

main:
    push rbp
    mov rbp, rsp

    mov rdi, fmt
    movsd xmm0, [pi]       ; 第一个浮点数
    movsd xmm1, [e]        ; 第二个浮点数
    mov al, 2              ; 2 个浮点数参数
    call printf

    mov rsp, rbp
    pop rbp
    ret

输出:

pi = 3.141590, e = 2.718280

注意

  • xmm0 是第一个浮点数,xmm1 是第二个。

  • al 的值必须等于实际传入的浮点数数量。如果设少了,printf 会读取错误的寄存器;设多了,printf 可能会读取未初始化的寄存器。

混合输出字符串、整数、浮点数

你可以在一行中同时输出多种类型的数据,只要格式字符串中的占位符与参数顺序一致。

cpp 复制代码
printf("name = %s, age = %d, pi = %f\n", "凌", 16, 3.14159);

完整代码

cpp 复制代码
; printf_mixed.asm
; 混合输出字符串、整数、浮点数

extern printf

section .data
    fmt db "name = %s, age = %d, pi = %f", 10, 0
    name db "凌", 0          ; 字符串必须 0 结尾
    age dd 16
    pi dq 3.14159

section .text
    global main

main:
    push rbp
    mov rbp, rsp

    mov rdi, fmt            ; 格式字符串
    mov rsi, name           ; 第2个参数:字符串指针(%s)
    mov rdx, [age]          ; 第3个参数:整数(%d)
    movsd xmm0, [pi]        ; 第4个参数:浮点数(%f)
    mov al, 1               ; 有 1 个浮点数
    call printf

    mov rsp, rbp
    pop rbp
    ret

输出

name = 凌, age = 16, pi = 3.141590

常见错误

  1. 忘记 extern printf

汇编器会报错:undefined symbol printf。

  1. 用 ld 链接而不是 gcc

ld 不知道去哪里找 printf,会报 undefined reference to printf。必须用 gcc。

  1. 格式符和参数数量不匹配

比如格式字符串是 "%d %d",但只传了 1 个整数,printf 会读取未定义的寄存器或栈数据,输出随机值。

  1. 浮点数参数忘了设 al

默认 al=0,printf 认为没有浮点数参数,可能会读取 xmm0 作为整数解释,导致输出乱码或崩溃。

  1. 入口用了 _start 而不是 main

如果用了 _start,C 运行时未初始化,调用 printf 会崩溃。要么改用 main,要么在 _start 中手动初始化(不推荐)。

  1. 字符串忘记 0 结尾

printf 会一直读取内存直到遇到 0,导致输出乱码。

速查表

|---------|-----|-------------|------------|----------|-------|
| 场景 | rdi | 第二个参数 | 第三个参数 | 浮点数寄存器 | al |
| 输出字符串 | 格式串 | rsi = 字符串地址 | - | - | 0 |
| 输出整数 | 格式串 | rsi = 整数 | - | - | 0 |
| 输出浮点数 | 格式串 | xmm0 = 浮点数 | - | - | 1 |
| 输出两个浮点数 | 格式串 | xmm0 = 第1个 | xmm1 = 第2个 | - | 2 |
| 混合输出 | 格式串 | 按顺序放通用寄存器 | 浮点数放 xmm | 浮点数放 xmm | 浮点数个数 |

总结

  1. printf 是 C 标准库函数,比 sys_write 更灵活,支持格式化输出。

  2. 调用 printf 需要 extern printf、gcc 链接、入口用 main。

  3. 参数传递规则:

  • rdi = 格式字符串地址

  • 整数/指针用 rsi、rdx、rcx、r8、r9

  • 浮点数用 xmm0、xmm1......

  • al = 浮点数数量

  1. 常见错误:忘记 extern、用错链接器、格式符与参数不匹配、浮点数忘设 al。

  2. 掌握使用 printf 输出字符串、整数、浮点数,以及任意混合内容。