汇编语言从零开始完整教程(小白也能看懂)
前面我们学了 JavaScript、Go、C/C++。这一篇学汇编语言——它是离硬件最近、最底层的语言。
你可能觉得汇编是”上古大神”才碰的东西。其实只要方法对,新手也能看懂。而且学汇编能让你彻底明白:程序到底是怎么在 CPU 上跑起来的。这是其他任何语言都给不了的理解。
一、汇编是什么?为什么它”最底层”?
从”人话”到”机器话”
想一想,电脑只能识别 0 和 1 组成的机器码。比如某个指令的机器码是 1011 0000。人类直接写 0 和 1 根本没法写复杂程序。
所以发明了汇编语言:用人类能读懂的单词(比如 MOV、ADD)来代替那些 0 和 1。一个汇编指令对应一条机器指令。
机器码: 1011 0000 0110 0001汇编: MOV AL, 0x61 ← 意思是"把数值放入寄存器"一句话: 汇编就是把机器码”翻译”成人能读的简写。它离 CPU 最近,几乎是一对一。
语言金字塔
JavaScript / Python (离人最近,好懂) ↓ C / C++ (中间层) ↓ 汇编语言 (离硬件近) ↓ 机器码 0/1 (离人最远)越往上越好写、越抽象;越往下越贴近硬件、越难写但越快、越可控。
为什么还要学汇编?
- 搞懂计算机原理:学完你会真正明白”内存地址""寄存器""栈”是什么
- 调试高手:崩溃、性能问题时,能看懂底层在干嘛
- 逆向/安全:黑客攻防、破解软件的基础
- 追求极致性能:某些关键代码用手写汇编能榨干硬件
但要说实话: 日常开发几乎不会手写汇编,编译器(gcc)会自动帮你翻译 C 代码成汇编。学它的意义在于理解原理,而不是真的用它写业务。
二、CPU 怎么工作?寄存器是什么?
在学指令之前,先理解 CPU 里最重要的东西:寄存器(Register)。
寄存器:CPU 里的”极速小抽屉”
CPU 运算时,数据不能直接去内存里拿,而是先放进 CPU 内部的一批”小格子”里,这些格子就叫寄存器。它们速度极快,但数量很少、容量很小。
打个比方:
- 寄存器:你手边的几个小格子,随手能拿到(极快)
- 内存(RAM):仓库,东西多但拿得慢
- 硬盘:货柜仓库,更大更慢
程序运行时,数据要在”寄存器 ↔ 内存”之间不断搬动。
x86-64 的常用寄存器
我们用最常见的 x86-64 架构(你电脑 CPU 基本都是它)举例。寄存器是一组”命名”的存储单元:
| 寄存器 | 用途(常见约定) |
|---|---|
rax | 累加器,常用来放返回值 |
rbx | 通用 |
rcx | 计数(循环) |
rdx | 数据 |
rsi / rdi | 源/目标索引,函数参数 |
rsp | 栈指针(指向栈顶) |
rbp | 栈基址指针 |
rip | 指令指针(CPU 正在执行哪条指令) |
初学别全背,记住 rax(返回值)、rsp(栈)、rip(指令指针)这三个最关键就行。
寄存器大小: rax 是 64 位(8 字节)。它的”后半部分”还能单独用:eax(32 位)、ax(16 位)、al(8 位)。这是 x86 的兼容历史。
三、内存:带编号的”仓库”
内存是一大片连续的空间,每个字节都有一个地址(编号),就像仓库每个柜子有编号。
汇编里访问内存的典型写法是 [地址],方括号表示”去这个地址里取值”:
MOV rax, [0x400000] ; 把地址 0x400000 处存的值,读到 rax寄存器 vs 内存的区别(重点):
| 寄存器 | 内存 | |
|---|---|---|
| 速度 | 极快 | 慢 |
| 数量 | 几十个 | 非常多 |
| 访问 | 直接叫名字(rax) | 通过地址 [地址] |
| 作用 | CPU 运算临时存 | 存放大量数据 |
四、基本指令:汇编的”单词表”
汇编由一条条指令组成。每条指令格式基本是:指令 目标, 源。注意方向——很多指令是把”源”搬到”目标”。
MOV:搬家(最常用)
MOV rax, 5 ; 把数字 5 放进 rax(立即数→寄存器)MOV rbx, rax ; 把 rax 的值复制给 rbx(寄存器→寄存器)MOV [addr], rax ; 把 rax 的值存到内存 addr 处(寄存器→内存)MOV rax, [addr] ; 把内存 addr 处的值读进 rax(内存→寄存器)方向: MOV 目标, 源,从右往左搬。
ADD / SUB:加减
ADD rax, 3 ; rax = rax + 3SUB rax, 3 ; rax = rax - 3ADD rax, rbx ; rax = rax + rbxINC / DEC:自增自减
INC rax ; rax++(加 1)DEC rax ; rax--(减 1)MUL / DIV:乘除
MUL rbx ; rax = rax * rbx(注意 MUL 默认用 rax)DIV rbx ; rax = rax / rbxCMP / JMP:比较和跳转
CMP 比较两个值(不保存结果,只影响标志位),JMP 跳转:
CMP rax, 10 ; 比较 rax 和 10JE label ; 如果相等就跳转到 label(JE = Jump if Equal)JNE label ; 如果不相等就跳转JG label ; 如果大于就跳转JL label ; 如果小于就跳转这就是汇编里的”if”——它没有 if 关键字,而是靠”比较 + 条件跳转”实现。
五、第一个汇编程序:输出 “Hello”
写汇编之前,先做两件事:
- 装编译器:Linux 装
nasm(sudo apt install nasm)和ld(链接器) - 用 NASM 的 Intel 语法(最常用)
代码 hello.asm
; hello.asm — 第一个汇编程序; 在 Linux 上用 NASM + 系统调用输出 "Hello"
section .data ; 数据段:放常量 msg db 'Hello, World!', 0xA ; 定义字节数据,0xA 是换行符
section .text ; 代码段:放指令 global _start ; 告诉链接器入口是 _start
_start: ; 系统调用 write(1, msg, 13) mov rax, 1 ; 系统调用号 1 = write mov rdi, 1 ; 参数1:文件描述符 1 = 标准输出 mov rsi, msg ; 参数2:要输出的字符串地址 mov rdx, 13 ; 参数3:输出长度("Hello, World!" 是 13 个字符) syscall ; 触发系统调用
; 系统调用 exit(0) mov rax, 60 ; 系统调用号 60 = exit mov rdi, 0 ; 退出码 0 syscall编译并运行
nasm -f elf64 hello.asm -o hello.o # 编译成目标文件ld hello.o -o hello # 链接成可执行文件./hello# 输出:Hello, World!如果你成功了,你刚刚干了一件很了不起的事: 没有任何库、没有任何”魔法”,纯粹用 CPU 指令输出了文字。这就是汇编的”裸奔”。
逐行解释
section .data ; 数据段:存放数据 msg db 'Hello, World!', 0xAdb 意思是”define byte”,定义一串字节。0xA 是换行符的十六进制。
section .text ; 代码段 global _start_start 是程序入口(不用 main,因为汇编不用 C 库)。
最关键的 syscall(系统调用): 程序不能直接碰硬件和屏幕,得通过操作系统。syscall 就是”向操作系统请求服务”的指令。请求哪个服务看 rax 里的系统调用号:
rax = 1→ write(输出)rax = 60→ exit(退出)
参数依次放在 rdi、rsi、rdx(这就是前面说的寄存器用途)。
这就是”Hello World”最底层的真相! 你之前学的 cout、console.log,底层最终都是通过这种系统调用让操作系统打印东西的。
六、栈:函数调用的”临时记事本”
栈是什么?
栈(Stack)是一块后进先出(LIFO)的内存区域,专门给函数调用用的。就像一摞盘子:后放的先拿。
PUSH:往栈顶放一个值(放盘子)POP:从栈顶取出一个值(拿盘子)rsp:栈指针,永远指向栈顶
PUSH rax ; 把 rax 的值压入栈顶POP rbx ; 从栈顶弹出值到 rbx为什么函数调用需要栈?
当 main 调用一个函数时,需要记住”调用完之后回到哪”。这个”返回地址”就压入栈中。等函数执行完,RET 指令从栈里弹出返回地址,程序跳回去继续执行。
main 调用 func: 1. 把返回地址压栈 2. 跳转到 func 的代码 3. func 执行完,RET 弹出返回地址 4. 跳回 main 继续函数调用三步曲(经典):
CALL func:把返回地址压栈,跳到 func- func 里
RET:从栈弹出返回地址,跳回去 - 参数通过寄存器传递,局部变量放栈里
七、一个完整的计算例子
写个程序:计算 5 + 3,并把结果作为退出码返回(这样可以在 shell 里看到结果)。
; add.asm — 计算 5 + 3,结果作为退出码section .text global _start
_start: mov rax, 5 ; rax = 5 add rax, 3 ; rax = rax + 3 = 8
mov rdi, rax ; 把结果作为退出码 mov rax, 60 ; 系统调用:exit syscall编译运行:
nasm -f elf64 add.asm -o add.old add.o -o add./addecho $? # 查看上一条命令的退出码,应该显示 8看懂了吗? 程序执行:rax 先变成 5,add 之后变成 8,然后交给 exit 系统调用返回。echo $? 显示 8——你的程序确实算了 5+3=8!
八、分支和循环:让程序会”思考”
if 语句(用 CMP + 条件跳转)
section .data greater db '更大', 0xA, 0 smaller db '更小', 0xA, 0
section .text global _start
_start: mov rax, 10 mov rbx, 5
cmp rax, rbx ; 比较 rax 和 rbx jg do_greater ; 如果 rax > rbx 跳转 jmp do_smaller ; 否则跳转
do_greater: ; 这里执行"更大"的逻辑 mov rdi, 1 jmp exit
do_smaller: ; 这里执行"更小"的逻辑 mov rdi, 0
exit: mov rax, 60 syscall核心思路: CMP 比较 → JG/JL/JE 等条件跳转 → 跳到对应代码块。这就是汇编版的 if/else。
循环(用 JMP 往回跳)
section .data newline db 0xA, 0
section .text global _start
_start: mov rcx, 5 ; 计数从 5 开始
loop_start: ; ... 这里写循环体逻辑 ... dec rcx ; rcx-- jnz loop_start ; 如果 rcx 不为 0,跳回 loop_start
; 循环结束 mov rax, 60 mov rdi, 0 syscall循环的原理: 没有 for/while 关键字,就是”数数 + 跳回去”。DEC rcx 减一,JNZ(Jump if Not Zero)不为零就跳回开头,减到 0 就跳出。
学到这里你会发现: 汇编里的 if、循环,本质就是比较 + 跳转。C 语言里的 if、for,编译器其实就是翻译成这些跳转指令。
九、调用 C 函数:和高级语言”对接”
纯汇编写大程序太痛苦了。实际用法是:C 和汇编混合编程——C 写主要逻辑,关键热点用汇编。
从汇编调用 C 函数
先写 C 函数 helper.c:
int add(int a, int b) { return a + b;}再写汇编 main.asm 调用它:
extern add ; 声明外部函数 add
section .text global _start
_start: mov rdi, 3 ; 第一个参数 mov rsi, 4 ; 第二个参数 call add ; 调用 C 函数 add,返回值在 rax
; 此时 rax = 7 mov rdi, rax mov rax, 60 ; exit syscall编译链接:
gcc -c helper.c -o helper.onasm -f elf64 main.asm -o main.old main.o helper.o -o prog./progecho $? # 显示 7关键约定(System V ABI): 函数前 6 个参数依次放在 rdi、rsi、rdx、rcx、r8、r9 寄存器里,返回值放 rax。C 和汇编之间靠这个”约定”互相传递数据。
十、用 gcc 看 C 代码变成什么汇编
这是学汇编最实用的技巧:让编译器把 C 代码”翻译”成汇编给你看。
写个 C 文件 test.c:
int add(int a, int b) { return a + b;}然后:
gcc -S test.c -o test.scat test.s你会看到编译器生成的汇编代码——你会发现原来 C 的 return a + b 就是几条 mov 和 add 指令。这能帮你快速把 C 和汇编对应起来。
看汇编优化版本:
gcc -S -O2 test.c -o test.s # 开启优化对比一下,优化后的汇编通常更短。
十一、常见架构:x86 vs ARM
初学可能看到 x86 和 ARM 两种说法,简单分辨:
| x86 / x86-64 | ARM | |
|---|---|---|
| 谁在用 | 电脑(Intel/AMD) | 手机、嵌入式(苹果 M 芯片) |
| 指令风格 | 复杂(CISC) | 精简(RISC) |
| 语法 | Intel 语法 / AT&T 语法 | 自己的语法 |
初学建议: 用 x86-64 + NASM(Intel 语法),资料最多、最经典。理解原理后,转到 ARM 也只是”换一套指令名”的事,核心思想(寄存器、内存、跳转、栈)完全一样。
十二、总结
这一篇我们从零认识了汇编:
- 汇编是什么:机器码的人读版,离 CPU 最近
- 寄存器:CPU 里的”极速小抽屉”(rax、rsp、rip…)
- 内存:带地址的仓库,用
[地址]访问 - 基本指令:MOV 搬家、ADD/SUB 加减、CMP 比较、JMP 跳转
- 第一个程序:用 syscall 系统调用输出 Hello World
- 栈:函数调用的后进先出”记事本”,PUSH/POP、CALL/RET
- 分支循环:比较 + 条件跳转,就是汇编版的 if/for
- 调用 C 函数:靠寄存器传参的调用约定
- gcc -S:看 C 代码翻译成汇编
- 架构:x86 电脑 vs ARM 手机
最后给你一句实话: 汇编难在”繁琐”而非”神秘”。它没有复杂语法,只有最原始的操作。当你用汇编写出第一个程序时,你就真正站在了”程序如何运行”的最底层——这份理解,是任何高级语言都给不了你的。
下一步: 装好 nasm,把 Hello World 跑起来,再试试”计算 5+3”那个例子。跑通两个,你对计算机的理解就上了一个大台阶。