大家好,你们可以叫我凌,是个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 标准库的函数,不是内核提供的服务。你需要做三件事:
声明外部函数:在汇编文件中写上 extern printf,告诉汇编器这个函数是外部的。
用 gcc 链接:printf 在 C 标准库中,ld 默认不会链接它,需要用 gcc 来自动链接标准库。
入口点使用 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
常见错误
- 忘记 extern printf
汇编器会报错:undefined symbol printf。
- 用 ld 链接而不是 gcc
ld 不知道去哪里找 printf,会报 undefined reference to printf。必须用 gcc。
- 格式符和参数数量不匹配
比如格式字符串是 "%d %d",但只传了 1 个整数,printf 会读取未定义的寄存器或栈数据,输出随机值。
- 浮点数参数忘了设 al
默认 al=0,printf 认为没有浮点数参数,可能会读取 xmm0 作为整数解释,导致输出乱码或崩溃。
- 入口用了 _start 而不是 main
如果用了 _start,C 运行时未初始化,调用 printf 会崩溃。要么改用 main,要么在 _start 中手动初始化(不推荐)。
- 字符串忘记 0 结尾
printf 会一直读取内存直到遇到 0,导致输出乱码。
速查表
|---------|-----|-------------|------------|----------|-------|
| 场景 | rdi | 第二个参数 | 第三个参数 | 浮点数寄存器 | al |
| 输出字符串 | 格式串 | rsi = 字符串地址 | - | - | 0 |
| 输出整数 | 格式串 | rsi = 整数 | - | - | 0 |
| 输出浮点数 | 格式串 | xmm0 = 浮点数 | - | - | 1 |
| 输出两个浮点数 | 格式串 | xmm0 = 第1个 | xmm1 = 第2个 | - | 2 |
| 混合输出 | 格式串 | 按顺序放通用寄存器 | 浮点数放 xmm | 浮点数放 xmm | 浮点数个数 |
总结
-
printf 是 C 标准库函数,比 sys_write 更灵活,支持格式化输出。
-
调用 printf 需要 extern printf、gcc 链接、入口用 main。
-
参数传递规则:
-
rdi = 格式字符串地址
-
整数/指针用 rsi、rdx、rcx、r8、r9
-
浮点数用 xmm0、xmm1......
-
al = 浮点数数量
-
常见错误:忘记 extern、用错链接器、格式符与参数不匹配、浮点数忘设 al。
-
掌握使用 printf 输出字符串、整数、浮点数,以及任意混合内容。