3372 字
17 分钟
汇编语言从零开始完整教程(小白也能看懂)

汇编语言从零开始完整教程(小白也能看懂)#

前面我们学了 JavaScript、Go、C/C++。这一篇学汇编语言——它是离硬件最近、最底层的语言。

你可能觉得汇编是”上古大神”才碰的东西。其实只要方法对,新手也能看懂。而且学汇编能让你彻底明白:程序到底是怎么在 CPU 上跑起来的。这是其他任何语言都给不了的理解。


一、汇编是什么?为什么它”最底层”?#

从”人话”到”机器话”#

想一想,电脑只能识别 0 和 1 组成的机器码。比如某个指令的机器码是 1011 0000。人类直接写 0 和 1 根本没法写复杂程序。

所以发明了汇编语言:用人类能读懂的单词(比如 MOVADD)来代替那些 0 和 1。一个汇编指令对应一条机器指令。

机器码: 1011 0000 0110 0001
汇编: MOV AL, 0x61 ← 意思是"把数值放入寄存器"

一句话: 汇编就是把机器码”翻译”成人能读的简写。它离 CPU 最近,几乎是一对一。

语言金字塔#

JavaScript / Python (离人最近,好懂)
C / C++ (中间层)
汇编语言 (离硬件近)
机器码 0/1 (离人最远)

越往上越好写、越抽象;越往下越贴近硬件、越难写但越快、越可控。

为什么还要学汇编?#

  1. 搞懂计算机原理:学完你会真正明白”内存地址""寄存器""栈”是什么
  2. 调试高手:崩溃、性能问题时,能看懂底层在干嘛
  3. 逆向/安全:黑客攻防、破解软件的基础
  4. 追求极致性能:某些关键代码用手写汇编能榨干硬件

但要说实话: 日常开发几乎不会手写汇编,编译器(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 + 3
SUB rax, 3 ; rax = rax - 3
ADD rax, rbx ; rax = rax + rbx

INC / DEC:自增自减#

INC rax ; rax++(加 1)
DEC rax ; rax--(减 1)

MUL / DIV:乘除#

MUL rbx ; rax = rax * rbx(注意 MUL 默认用 rax)
DIV rbx ; rax = rax / rbx

CMP / JMP:比较和跳转#

CMP 比较两个值(不保存结果,只影响标志位),JMP 跳转:

CMP rax, 10 ; 比较 rax 和 10
JE label ; 如果相等就跳转到 label(JE = Jump if Equal)
JNE label ; 如果不相等就跳转
JG label ; 如果大于就跳转
JL label ; 如果小于就跳转

这就是汇编里的”if”——它没有 if 关键字,而是靠”比较 + 条件跳转”实现。


五、第一个汇编程序:输出 “Hello”#

写汇编之前,先做两件事:

  1. 装编译器:Linux 装 nasmsudo apt install nasm)和 ld(链接器)
  2. 用 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

编译并运行#

Terminal window
nasm -f elf64 hello.asm -o hello.o # 编译成目标文件
ld hello.o -o hello # 链接成可执行文件
./hello
# 输出:Hello, World!

如果你成功了,你刚刚干了一件很了不起的事: 没有任何库、没有任何”魔法”,纯粹用 CPU 指令输出了文字。这就是汇编的”裸奔”。

逐行解释#

section .data ; 数据段:存放数据
msg db 'Hello, World!', 0xA

db 意思是”define byte”,定义一串字节。0xA 是换行符的十六进制。

section .text ; 代码段
global _start

_start 是程序入口(不用 main,因为汇编不用 C 库)。

最关键的 syscall(系统调用): 程序不能直接碰硬件和屏幕,得通过操作系统。syscall 就是”向操作系统请求服务”的指令。请求哪个服务看 rax 里的系统调用号

  • rax = 1 → write(输出)
  • rax = 60 → exit(退出)

参数依次放在 rdirsirdx(这就是前面说的寄存器用途)。

这就是”Hello World”最底层的真相! 你之前学的 coutconsole.log,底层最终都是通过这种系统调用让操作系统打印东西的。


六、栈:函数调用的”临时记事本”#

栈是什么?#

栈(Stack)是一块后进先出(LIFO)的内存区域,专门给函数调用用的。就像一摞盘子:后放的先拿

  • PUSH:往栈顶放一个值(放盘子)
  • POP:从栈顶取出一个值(拿盘子)
  • rsp:栈指针,永远指向栈顶
PUSH rax ; 把 rax 的值压入栈顶
POP rbx ; 从栈顶弹出值到 rbx

为什么函数调用需要栈?#

当 main 调用一个函数时,需要记住”调用完之后回到哪”。这个”返回地址”就压入栈中。等函数执行完,RET 指令从栈里弹出返回地址,程序跳回去继续执行。

main 调用 func:
1. 把返回地址压栈
2. 跳转到 func 的代码
3. func 执行完,RET 弹出返回地址
4. 跳回 main 继续

函数调用三步曲(经典):

  1. CALL func:把返回地址压栈,跳到 func
  2. func 里 RET:从栈弹出返回地址,跳回去
  3. 参数通过寄存器传递,局部变量放栈里

七、一个完整的计算例子#

写个程序:计算 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

编译运行:

Terminal window
nasm -f elf64 add.asm -o add.o
ld add.o -o add
./add
echo $? # 查看上一条命令的退出码,应该显示 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 语言里的 iffor,编译器其实就是翻译成这些跳转指令。


九、调用 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

编译链接:

Terminal window
gcc -c helper.c -o helper.o
nasm -f elf64 main.asm -o main.o
ld main.o helper.o -o prog
./prog
echo $? # 显示 7

关键约定(System V ABI): 函数前 6 个参数依次放在 rdirsirdxrcxr8r9 寄存器里,返回值放 rax。C 和汇编之间靠这个”约定”互相传递数据。


十、用 gcc 看 C 代码变成什么汇编#

这是学汇编最实用的技巧:让编译器把 C 代码”翻译”成汇编给你看

写个 C 文件 test.c

int add(int a, int b) {
return a + b;
}

然后:

Terminal window
gcc -S test.c -o test.s
cat test.s

你会看到编译器生成的汇编代码——你会发现原来 C 的 return a + b 就是几条 movadd 指令。这能帮你快速把 C 和汇编对应起来。

看汇编优化版本:

Terminal window
gcc -S -O2 test.c -o test.s # 开启优化

对比一下,优化后的汇编通常更短。


十一、常见架构:x86 vs ARM#

初学可能看到 x86 和 ARM 两种说法,简单分辨:

x86 / x86-64ARM
谁在用电脑(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”那个例子。跑通两个,你对计算机的理解就上了一个大台阶。

汇编语言从零开始完整教程(小白也能看懂)
https://021028.xyz/posts/default/78/
作者
021028
发布于
2026-08-11
许可协议
CC BY-NC-SA 4.0