文章目录
- 一、程序编写
-
- [1. 编写汇编代码](#1. 编写汇编代码)
- [2. 汇编与编译过程](#2. 汇编与编译过程)
- [💡 补充说明:如果你使用的是 Windows](#💡 补充说明:如果你使用的是 Windows)
- 二、解释hello.asm的每一行代码
-
- [1. 数据段定义 (`.section .data`)](#1. 数据段定义 (
.section .data)) - [2. 代码段与入口点 (`.section .text`)](#2. 代码段与入口点 (
.section .text)) - [3. 输出字符串 (sys_write)](#3. 输出字符串 (sys_write))
- [4. 退出程序 (sys_exit)](#4. 退出程序 (sys_exit))
- [1. 数据段定义 (`.section .data`)](#1. 数据段定义 (
- 三、介绍用到的寄存器
-
- [1. `rax` (Accumulator Register / 累加器)](#1.
rax(Accumulator Register / 累加器)) - [2. `rdi` (Destination Index Register / 目的变址寄存器)](#2.
rdi(Destination Index Register / 目的变址寄存器)) - [3. `rsi` (Source Index Register / 源变址寄存器)](#3.
rsi(Source Index Register / 源变址寄存器)) - [4. `rdx` (Data Register / 数据寄存器)](#4.
rdx(Data Register / 数据寄存器)) - [💡 扩展知识:Linux x86-64 系统调用的完整传参顺序](#💡 扩展知识:Linux x86-64 系统调用的完整传参顺序)
- [⚠️ 一个容易混淆的细节:寄存器的大小后缀](#⚠️ 一个容易混淆的细节:寄存器的大小后缀)
- [1. `rax` (Accumulator Register / 累加器)](#1.
一、程序编写
这里我以最经典的 x86-64 Linux 汇编语言 (NASM) 为例,为你编写一个 "Hello, World!" 程序,并给出完整的编译和运行步骤。
1. 编写汇编代码
创建一个名为 hello.asm 的文件,写入以下代码:
nasm
; hello.asm - x86-64 Linux Hello World (NASM)
section .data
msg db "Hello, World!", 10 ; 字符串内容,10 是换行符 '\n'
msg_len equ $ - msg ; 计算字符串长度
section .text
global _start ; 链接器入口点
_start:
; 系统调用: sys_write (向标准输出写数据)
mov rax, 1 ; 系统调用号 1 = sys_write
mov rdi, 1 ; 文件描述符 1 = stdout (标准输出)
mov rsi, msg ; 指向要输出的字符串地址
mov rdx, msg_len ; 要输出的字节数
syscall ; 触发系统调用
; 系统调用: sys_exit (退出程序)
mov rax, 60 ; 系统调用号 60 = sys_exit
xor rdi, rdi ; 退出码 0 (表示成功)
syscall ; 触发系统调用
2. 汇编与编译过程
在 Linux 环境下(确保已安装 nasm 和 ld),依次执行以下命令:
第一步:汇编 (Assemble)
将汇编源代码转换为机器码目标文件:
bash
nasm -f elf64 hello.asm -o hello.o
说明:-f elf64 指定生成 64位 ELF 格式的目标文件。
第二步:链接 (Link)
将目标文件链接为可执行文件:
bash
ld hello.o -o hello
说明:ld 是 GNU 链接器,它会将你的代码与操作系统的启动规范结合,生成最终的可执行文件。
第三步:运行 (Run)
bash
./hello
预期输出:
text
Hello, World!
💡 补充说明:如果你使用的是 Windows
Windows 下直接调用系统调用非常复杂(涉及 PE 格式和 Win32 API)。如果你在 Windows 上学习,通常有两种选择:
-
使用 WSL (Windows Subsystem for Linux):直接在 WSL 中按照上面的 Linux 步骤操作即可。
-
使用 C 运行时库 (CRT) :通过调用 C 语言的
printf函数来实现。nasm; win_hello.asm (Windows x64, 需配合 gcc 链接) extern printf section .data msg db "Hello, World!", 10, 0 ; C字符串需要以 0 结尾 section .text global main main: sub rsp, 40 ; 遵守 Windows x64 调用约定 (Shadow Space) lea rcx, [rel msg] ; 第一个参数:字符串指针 call printf wrt ..plt ; 调用 printf add rsp, 40 xor eax, eax ; return 0 retWindows 编译命令:
bashnasm -f win64 win_hello.asm -o win_hello.o gcc win_hello.o -o win_hello.exe ./win_hello.exe
二、解释hello.asm的每一行代码
这份 hello.asm 代码是专为 x86-64 Linux 系统编写的,它通过直接调用操作系统的底层接口(系统调用)来实现功能。下面我为你逐行进行详细拆解:
1. 数据段定义 (.section .data)
这一部分用于存放程序运行时需要用到的静态数据。
section .data:声明这是一个数据段。在这个段里定义的变量在程序运行期间不会被修改。msg db "Hello, World!", 10:定义一个名为msg的字节数组(字符串)。db代表 define byte。字符串内容是Hello, World!,最后的10是换行符\n的 ASCII 码值。msg_len equ $ - msg:计算字符串的长度并赋值给msg_len。$代表当前行的内存地址,$ - msg就是用当前地址减去字符串起始地址,从而得出字符串占用的总字节数(即长度)。
2. 代码段与入口点 (.section .text)
这一部分包含了程序的执行逻辑和机器指令。
section .text:声明这是代码段,专门用来存放汇编指令。global _start:告诉链接器_start是一个全局符号。在没有 C 语言运行时库(CRT)的情况下,_start就是整个程序的绝对入口点,相当于 C 语言中的main函数。_start::定义了一个标号(Label),标记了程序开始执行的第一条指令的位置。
3. 输出字符串 (sys_write)
这段代码的作用是向屏幕打印文字。在 x86-64 Linux 中,我们通过 syscall 指令与内核交互,参数需要按照特定的寄存器规则传递。
mov rax, 1:将数字1存入rax寄存器。在 Linux 64位系统中,1是sys_write(写文件/输出)的系统调用号。这告诉操作系统:"我要执行写操作"。mov rdi, 1:将数字1存入rdi寄存器。这是sys_write的第一个参数,表示文件描述符。1代表标准输出(stdout,也就是终端屏幕)。mov rsi, msg:将字符串msg所在的内存地址存入rsi寄存器。这是第二个参数,告诉操作系统去哪里找要输出的内容。mov rdx, msg_len:将之前计算好的字符串长度存入rdx寄存器。这是第三个参数,告诉操作系统要输出多少个字节。syscall:核心指令 。触发系统调用。此时 CPU 会从用户态切换到内核态,Linux 内核会读取刚才设置好的寄存器,把msg的内容输出到屏幕上。
4. 退出程序 (sys_exit)
程序执行完毕后,必须主动通知操作系统释放资源并安全退出,否则会导致进程变成僵尸进程或发生段错误。
mov rax, 60:将数字60存入rax寄存器。60是sys_exit(退出程序)的系统调用号。xor rdi, rdi:对rdi寄存器进行异或运算。任何数与自己异或结果都是0。这是一种非常高效的汇编技巧,用来将rdi清零。这里作为sys_exit的第一个参数,表示程序的退出状态码为0(在 Linux 中,返回0通常代表程序正常、成功结束)。syscall:再次触发系统调用。内核接收到退出信号后,会清理该程序的内存空间,并将控制权交还给 Shell 终端。
三、介绍用到的寄存器
在 x86-64 架构中,寄存器是 CPU 内部极其高速的存储单元。在我们上面的 hello.asm 代码中,主要用到了 通用寄存器(General-Purpose Registers)。
x86-64 架构将传统的 32位寄存器扩展为了 64位,通常以字母 r 开头(例如 eax 变成了 rax)。在 Linux 系统调用(System Call)时,内核有一套严格的传参约定,规定了我们必须把数据放在特定的寄存器里。
以下是我们代码中用到的寄存器的详细拆解:
1. rax (Accumulator Register / 累加器)
- 角色 :在我们的代码中,它扮演了 "任务调度员" 的角色。
- 作用 :在执行
syscall指令之前,rax必须存放系统调用号 。操作系统就是靠读取rax里的数字来决定你要干什么(比如1代表写文件,60代表退出程序)。 - 补充说明 :当系统调用执行完毕后,
rax还会被用来存放返回值(比如函数返回的结果或错误码)。
2. rdi (Destination Index Register / 目的变址寄存器)
- 角色 :第一个参数传递者。
- 作用 :在 Linux x86-64 的系统调用规范中,
rdi永远用来传递函数的第 1 个参数。- 在
sys_write中,它传递的是"文件描述符"(1代表屏幕)。 - 在
sys_exit中,它传递的是"退出状态码"(0代表成功)。
- 在
3. rsi (Source Index Register / 源变址寄存器)
- 角色 :第二个参数传递者。
- 作用 :专门用来传递第 2 个参数。在
sys_write中,它存放的是我们要输出的字符串所在的内存地址 (即msg的地址)。
4. rdx (Data Register / 数据寄存器)
- 角色 :第三个参数传递者。
- 作用 :专门用来传递第 3 个参数。在
sys_write中,它存放的是要输出的数据的字节长度 (即msg_len)。
💡 扩展知识:Linux x86-64 系统调用的完整传参顺序
虽然我们的 Hello World 只用到了前三个参数,但了解完整的规则有助于你以后编写更复杂的汇编程序。如果系统调用需要更多参数,它们会按照以下顺序依次放入寄存器:
| 参数序号 | 对应寄存器 | 记忆提示 |
|---|---|---|
| 系统调用号 | rax |
决定"做什么操作" |
| 第 1 个参数 | rdi |
Destination Index |
| 第 2 个参数 | rsi |
Source Index |
| 第 3 个参数 | rdx |
Data Register |
| 第 4 个参数 | r10 |
(注意:不是 rcx,因为 syscall 会覆盖 rcx) |
| 第 5 个参数 | r8 |
- |
| 第 6 个参数 | r9 |
- |
⚠️ 一个容易混淆的细节:寄存器的大小后缀
你在查阅资料时可能会看到 eax, ax, al 等名称。它们是同一个物理寄存器的不同部分:
rax:整个 64位寄存器。eax:rax的低 32位。ax:rax的低 16位。al:rax的低 8位。
(注:在 x86-64 架构下,向低 32位寄存器如 eax 写入数据时,CPU 会自动将整个 64位寄存器的高 32位清零。)