参考菜鸟教程

1.基础语法

汇编语言与机器码的关系

汇编指令和机器指令之间存在 一一对应 的关系。

每一条汇编指令都精确映射到一条 CPU 机器指令,不存在中间抽象层。

汇编程序的基本结构

.data 数据段 存放已初始化的全局变量和常量

.bss BSS段 存放未初始化的数据

.text 代码段 存放程序的可执行指令(已初始化)

至少需要.text段才能构成有效汇编

汇编语句格式

[标签:]   指令助记符   [操作数1 [, 操作数2 [, 操作数3]]]   [; 注释]

标签 可选 代表一个内存地址的符号名,以冒号结尾

指令助记符 必须 mov,add,sub等告诉cpu要做什么

操作数 可选(部分指令无操作数) 指令操作的数据对象,可为寄存器、内存地址、立即数

注释 可选 分号开头,延续到结尾

汇编代码中注释极其重要。没有注释的汇编代码过几周连作者自己都可能看不懂。养成每条指令都写注释的习惯。

标识符命名规则

标识符(标签、变量名、常量名等)须遵循以下规则:

规则

说明

组成字符

字母、数字、下划线 _、点 .、问号 ?、@、$、# 等

起始字符

必须以字母、下划线、点或问号开头,不能以数字开头

大小写

默认区分大小写(可通过编译选项修改)

保留字

不能和指令助记符、寄存器名或 NASM 关键字重名

伪指令(Directives)

伪指令 是给汇编器的命令,不是给 CPU 的指令,用于控制汇编过程和定义数据结构。

伪指令

用途

示例

意思

db

定义字节(1 字节)

byte_val db 0x55

定义byte_val为0x55的字节

dw

定义字(2 字节)

word_val dw 0x1234

定义word_val为0x1234的字

dd

定义双字(4 字节)

dword_val dd 0x12345678

定义dword_val为0x12345678的双字

equ

定义常量

MAX_SIZE equ 256

定义MAX_SIZE为256的常量

resb

预留字节空间

buffer resb 128

预留128字节的空间

resw

预留字空间

wbuf resw 64

预留64字的空间(128字节)

resd

预留双字空间

dbuf resd 32

预留32双字的空间(128字节)

%define

宏定义常量

%define COUNT 10

这里的 equ 或 %define 相当于一个“文本替换”工具。 汇编器在编译代码时,只要看到 MAX_SIZE,就会像文字替换一样,直接把它换成数字 256。

equ和%define区别

1. %define MAX_SIZE 256 —— 纯粹的“文本替换”

  • 处理阶段:预处理阶段(类似于 C 语言的 #define)。

  • 原理:汇编器在正式编译代码前,会把代码里所有的 MAX_SIZE 像 word 里的“查找替换”一样,直接硬生生替换成 256。替换完之后,MAX_SIZE 这个名字就彻底消失了。

  • 特点:它可以被重新定义,比如你后面再写一句 %define MAX_SIZE 512,它就会变成 512。

2. MAX_SIZE equ 256 —— 真正的“符号常量”

  • 处理阶段:汇编阶段。

  • 原理:汇编器会在内部的“符号表”里登记一条记录:“MAX_SIZE 这个符号,对应数值 256”。它不会粗暴地做文字替换。

  • 特点:一旦用 equ 定义,绝对不能重新定义(比如再写一次 MAX_SIZE equ 512 就会报错)。它有自己的“身份”。

NASM 默认对标签和标识符 区分大小写

指令助记符和寄存器名不区分大小写(MOV、Mov、mov 效果相同),但推荐的风格是统一小写。

数值表示方式

; NASM 中不同进制的表示方式

mov eax, 42             ; 十进制:直接写数字
mov eax, 0x2A           ; 十六进制:0x 前缀(推荐写法)
mov eax, 2Ah            ; 十六进制:h 后缀
mov eax, 0o52           ; 八进制:0o 前缀
mov eax, 52o            ; 八进制:o 后缀
mov eax, 101010b        ; 二进制:b 后缀
mov eax, 0b101010       ; 二进制:0b 前缀

2.汇编语言 - 内存分段

在 x86 实模式下,CPU 使用 16 位寄存器,但需要访问 1MB 的内存空间(20 位地址)。 16 位寄存器只能表示 64KB 范围(2^16 = 65536),远远不够。 Intel 的解决方案是将内存划分为 段(Segment),每个段有基地址和偏移量,通过 段地址 + 偏移量 的方式组合出完整的物理地址。

物理地址计算公式

物理地址 = 段地址 × 16 + 偏移地址

三种基本段

一个典型的汇编程序使用三个主要段:

段名称

英文名

用途

对应的 Section

代码段

Code Segment

存放可执行的机器指令

section .text

数据段

Data Segment

存放已初始化的全局变量

section .data

栈段

Stack Segment

存放函数调用信息、局部变量

操作系统自动管理

代码段(.text)

为了防止程序意外修改指令

代码段 只读,可执行

数据段(.data)

存放已初始化的全局变量

可读可写,程序可以修改其中内容

BSS 段(.bss)

存放未初始化的全局变量

不占用实际空间,只在程序加载时由系统分配内存

将未初始化的数据放在 BSS 段而不是 .data 段中,可以减小可执行文件的体积。例如,一个 10KB 的未初始化缓冲区在 BSS 段中不占用文件大小,但如果放在 .data 段中则会让文件增大 10KB。

内存分段示意图

高地址
+-------------------+
|   栈 (Stack)      |  <-- SS:ESP 指向栈顶
|   向下增长         |
+-------------------+
|                   |
|   空闲内存         |
|                   |
+-------------------+
|   BSS 段 (.bss)   |  未初始化的全局变量
+-------------------+
|   数据段 (.data)   |  已初始化的全局变量
+-------------------+
|   代码段 (.text)   |  程序指令(只读)
+-------------------+
|   保留区域         |  操作系统使用
+-------------------+
低地址

栈顶其实是低地址,也就是压栈的时候向下移动

3.汇编语言 - 寄存器

x86 32 位寄存器分类

通用寄存器

用于存放运算数据和临时结果

x86 提供了 8 个 32 位通用寄存器:

32位

16位

低8位

高8位(低16位)

主要用途

EAX

AX

AL

AH

累加器,存放函数返回值、算术运算结果

EBX

BX

BL

BH

基址寄存器,常用于存放内存基地址

ECX

CX

CL

CH

计数器,常用于循环计数和移位

EDX

DX

DL

DH

数据寄存器,存放乘除法的高位结果

ESI

SI

SIL

-

源变址寄存器,字符串操作的源地址

EDI

DI

DIL

-

目的变址寄存器,字符串操作的目标地址

EBP

BP

BPL

-

基址指针,指向当前栈帧的底部

ESP

SP

SPL

-

栈指针,始终指向栈顶

修改 32 位寄存器的低 16 位(如 AX)时,高 16 位保持不变。但将 32 位寄存器作为目标操作数时,会覆盖整个 32 位。这是初学者容易出错的地方。

段寄存器

寄存器

名称

用途

CS

代码段寄存器

指向当前指令所在的段

DS

数据段寄存器

指向数据所在的段

SS

栈段寄存器

指向栈所在的段

ES

附加段寄存器

额外的数据段

FS

附加段寄存器

通用,常用于线程局部存储

GS

附加段寄存器

通用,常用于线程局部存储

在 32 位保护模式下编程时,操作系统已经设置好了段寄存器,你通常不需要手动修改它们。

指针和变址寄存器

寄存器

全称

用途

EIP

指令指针(Instruction Pointer)

指向 CPU 下一条要执行的指令地址(不可直接访问)

ESP

栈指针(Stack Pointer)

指向栈顶,PUSH/POP 指令自动调整它

EBP

基址指针(Base Pointer)

指向当前函数栈帧的底部,用于访问函数参数和局部变量

ESI

源变址(Source Index)

字符串/内存操作的源地址

EDI

目的变址(Destination Index)

字符串/内存操作的目标地址

ESP 和 EBP 不能当作普通通用寄存器随意使用。ESP 指向栈顶,push/pop/call/ret 都会改变它;EBP 是访问函数参数的关键。随意修改它们会导致程序崩溃。

标志寄存器(EFLAGS)

标志位

名称

含义

CF

进位标志(Carry Flag)

无符号运算产生进位/借位时置 1

PF

奇偶标志(Parity Flag)

结果低 8 位中 1 的个数为偶数时置 1

AF

辅助进位标志

低 4 位向高 4 位进位/借位时置 1

ZF

零标志(Zero Flag)

运算结果为 0 时置 1

SF

符号标志(Sign Flag)

运算结果为负数时置 1(等于结果的最高位)

OF

溢出标志(Overflow Flag)

有符号运算溢出时置 1

CPU 会根据运算结果自动更新这些标志位,条件跳转指令则根据标志位决定是否跳转。

寄存器使用约定

在实际编程中,一些寄存器有约定俗成的用法——称为 调用约定(Calling Convention):

寄存器

调用约定中的用途

EAX

存放函数返回值

ECX

计数器(循环计数)

EDX

存放除法的高位结果、扩展 EAX

EBX、ESI、EDI、EBP

被调用函数必须保存和恢复(callee-saved)

EAX、ECX、EDX

调用者负责保存(caller-saved)

在编写自己的汇编程序时,不一定严格遵循调用约定。但如果你要和 C 语言混合编程,就必须遵守 cdecl 等调用约定。

4.汇编语言 - 系统调用

用户程序运行在受限的用户态,需要进行io操作,内存分配等内核级操作时就需要用到系统调用

流程:

用户发起系统调用-CPU切换到内核态-执行对应服务-返回用户态

Linux系统调用机制

1.将 系统调用号放入 EAX寄存器

2.将参数放入EBX,ECX,EDX,ESI,EDI等寄存器

3.执行int 0x80指令触发软件中断

4.CPU切换至内核态,根据EAX的调用号执行对应指令

5.将返回值给EAX,切回用户态继续执行

常用系统调用指令

系统调用 调用号(EAX) 功能 参数

sys_exit 1 退出程序 EBX = 返回值(exit code)

sys_fork 2 创建子进程 无参数

sys_read 3 读取文件/输入 EBX=fd, ECX=buf, EDX=count

sys_write 4 写入文件/输出 EBX=fd, ECX=buf, EDX=count

sys_open 5 打开文件 EBX=filename, ECX=flags, EDX=mode

sys_close 6 关闭文件 EBX=fd

sys_creat 8 创建文件 EBX=filename, ECX=mode

sys_lseek 19 移动文件指针 EBX=fd, ECX=offset, EDX=whence

sys_brk 45 调整数据段大小(内存分配) EBX=新地址

sys_write_输出字符串

用于向屏幕打印内容

; 文件路径:write_syscall.asm

; 使用 sys_write 输出字符串

section .data

msg db 'Hello, runoob! Welcome to assembly.', 0xA ;注意0xA是换行符

len equ $ - msg ;equ是等于的意思,常用来定义常量,$代表当前行地址,msg是字符串的起始地址,$ -msg就是指字符串的长度,用equ赋给len

section .text

global 的作用是声明全局符号,让链接器可见;但它不是“让 _start 变成程序起点”的机制。程序起点是由链接器脚本或 -e 选项指定的,global 只是把符号暴露出去。

_start:

; 调用 sys_write (4)

mov eax, 4 ; 系统调用号 4 = sys_write

mov ebx, 1 ; 文件描述符 1 = stdout(标准输出)

mov ecx, msg ; 要输出的数据地址

mov edx, len ; 数据长度(字节数)

int 0x80 ; 触发系统调用

; 成功时 EAX 返回实际写入的字节数

; 退出程序

mov eax, 1 ;系统调用号1

mov ebx, 0 ;返回值0

int 0x80 ;执行系统调用

sys_read - 读取用户输入

; 文件路径:read_syscall.asm
; 使用 sys_read 读取用户输入并回显

section .bss
    buffer resb 64              ; 预留 64 字节输入缓冲区

section .data
    prompt db 'Please enter your name: '
    prompt_len equ $ - prompt
    output_msg db 'Hello, '
    output_msg_len equ $ - output_msg

section .text
    global start

start:
    ; 输出提示信息
    mov eax, 4
    mov ebx, 1
    mov ecx, prompt
    mov edx, prompt_len
    int 0x80

    ; 读取用户输入
    mov eax, 3                  ; 系统调用号 3 = sys_read
    mov ebx, 0                  ; 文件描述符 0 = stdin(标准输入)
    mov ecx, buffer             ; 输入缓冲区地址
    mov edx, 64                 ; 最多读取 64 字节
    int 0x80

    ; EAX 返回实际读取的字节数(包含末尾的换行符)
    mov esi, eax                ; 保存实际输入长度到 esi,esi是自带的寄存器,不声明,直接用

    ; 输出 "Hello, "
    mov eax, 4
    mov ebx, 1
    mov ecx, output_msg
    mov edx, output_msg_len
    int 0x80

    ; 输出用户输入的内容
    mov eax, 4
    mov ebx, 1
    mov ecx, buffer
    mov edx, esi                ; 使用实际输入的字节数
    int 0x80

    ; 退出程序
    mov eax, 1
    mov ebx, 0
    int 0x80

sys_exit - 退出程序

; 使用不同退出码退出

mov eax, 1 ; 系统调用号 1 = sys_exit

mov ebx, 0 ; 退出码 0 = 正常退出(也可以是非零值),负值表示非正常退出

int 0x80

系统调用通用模板

; 系统调用通用模板

; 适用于 Linux 32 位系统

; 步骤1:将系统调用号放入 EAX

mov eax, 系统调用号

; 步骤2:按顺序放入参数

mov ebx, 第1个参数

mov ecx, 第2个参数

mov edx, 第3个参数

mov esi, 第4个参数

mov edi, 第5个参数

; 步骤3:触发系统调用

int 0x80

; 步骤4:检查返回值(在 EAX 中)

; 通常负值表示错误

cmp eax, 0

jl error_handler ; 如果 EAX < 0,跳转到错误处理

5.汇编语言-寻址方式

决定CPU去哪拿数据

立即寻址(Immediate Addressing)

就是立即数
mov eax, 42                 ; 42 是立即数,直接编码在指令中
add ebx, 100                ; 100 是立即数
mov ecx, 0x2A               ; 十六进制立即数
mov edx, 'A'                ; 字符 'A' = 0x41,也是立即数

寄存器寻址(Register Addressing)

mov eax, ebx ; 将 ebx 的值复制到 eax(两个操作数都是寄存器寻址)

add ecx, edx ; ecx = ecx + edx

push eax ; 将 eax 的值压入栈

inc ebx ; ebx = ebx + 1

直接寻址(Direct Addressing)

操作数是内存地址

; 文件路径:direct_addr.asm

; 直接寻址示例



section .data

    value dd 12345678          ; 在内存中定义一个双字变量

    name db 'runoob', 0



section .text

    global _start



_start:

    ; 直接寻址读取内存

    mov eax, [value]           ; 从内存地址 value 读取 4 字节(因为eax是32位,所以读取四字节)到 eax

    ; eax 现在是 12345678



    ; 直接寻址写入内存

    mov dword [value], 98765   ; 将 98765 写入 value 所在的内存地址



    ; 直接寻址读取字节

    mov al, [name]             ; 读取 name 的第一个字节(因为al,bl都是8位,所以读取一字节) 'r' = 0x72

    mov bl, [name + 1]         ; 读取 name 的第二个字节 'u' = 0x75


    mov eax, 1
    mov ebx, 0
    int 0x80

寄存器间接寻址(Register Indirect Addressing)

寄存器中存放的是一个内存地址,CPU 用该地址去访问内存。

方括号内的寄存器被当作指针使用。

; 文件路径:indirect_addr.asm
; 寄存器间接寻址示例

section .data
    msg db 'Hello, RUNOOB!', 0xA
    len equ $ - msg

section .text
    global _start

_start:
    mov eax, msg                ; 将 msg 的地址(指针)放入 eax
    mov al, [eax]               ; 间接寻址:读取 eax 指向的内存字节
    ; 现在 al = 'H' = 0x48

    ; 遍历字符串,将小写字母转为大写
    mov esi, msg                ; esi 指向字符串起始位置
    mov ecx, len                ; ecx 存放字符串长度

convert_loop:
    mov al, [esi]               ; 间接寻址:读取 esi 指向的字符
    cmp al, 'a'                 ; 是否大于等于 'a'
    jb next_char                ; 否,跳过
    cmp al, 'z'                 ; 是否小于等于 'z'
    ja next_char                ; 否,跳过
    sub al, 32                  ; 转为大写(ASCII 表中小写-大写=32)
    mov [esi], al               ; 间接寻址:写回 esi 指向的位置

next_char:
    inc esi                     ; 指针移动到下一个字符
    loop convert_loop           ; 继续循环直到全部处理完,loop 指令会无条件地自动把 ECX(或 CX/RCX,取决于位数)减 1,然后判断是否等于0,等于零跳出循环,代码向下执行
;loop等效于
;dec ecx       ; ECX 减 1
;jnz convert_loop  ; 结果不为 0,则跳转

    ; 输出转换后的字符串
    mov eax, 4
    mov ebx, 1
    mov ecx, msg
    mov edx, len
    int 0x80

    mov eax, 1
    mov ebx, 0
    int 0x80

间接寻址是数组遍历、字符串操作和数据结构访问的基础。ESI 和 EDI 是专门设计用来配合间接寻址的寄存器,配合 inc esi 可以轻松遍历连续内存。

基址寻址(Base Addressing)

有效地址 = 基址寄存器的值 + 偏移量(位移量)。

基址寄存器可以是 EBX、EBP、ESI、EDI 等。

; 基址寻址示例:访问结构体成员

section .data
    ; 模拟一个简单的结构体:{id, age, score}
    ; id   = 2 字节
    ; age  = 2 字节
    ; score = 4 字节
    student db 0x01, 0x00      ; id = 1  因为是小端,所以1在2字节内存中是 0x01,0x00
            db 0x14, 0x00      ; age = 20
            dd 95              ; score = 95

section .text
    global _start

_start:
    mov ebx, student            ; ebx 存放结构体基址

    ; 基址 + 偏移量访问各成员
    mov ax, [ebx]               ; 读取 id(偏移 0)  ax访问2字节
    mov ax, [ebx + 2]           ; 读取 age(偏移 2)ax访问2字节
    mov eax, [ebx + 4]          ; 读取 score(偏移 4)eax访问4字节

    ; 修改 age
    mov word [ebx + 2], 21      ; age = 21  word告诉编译器数字是2字节

    ; 修改 score
    mov dword [ebx + 4], 98     ; score = 98  dword告诉编译器数字是4字节

    mov eax, 1
    mov ebx, 0
    int 0x80

变址寻址(Indexed Addressing)

使用变址寄存器(ESI 或 EDI)加上偏移量来访问数组元素。

; 变址寻址示例:遍历数组

section .data
    array dd 10, 20, 30, 40, 50     ; 5 个双字元素的数组
    array_len equ ($ - array) / 4    ; 元素个数 = 总字节数 / 4

section .text
    global _start

_start:
    mov ecx, array_len              ; 循环计数器
    mov esi, 0                      ; 变址(下标从 0 开始)
    mov ebx, 0                      ; 累加和

sum_loop:
    mov eax, [array + esi * 4]      ; 变址寻址:array + 下标 * 元素大小
    ; esi * 4 因为每个元素是 4 字节
    add ebx, eax                    ; 累加到 ebx
    inc esi                         ; 下标加 1
    loop sum_loop
    ; ebx = 10+20+30+40+50 = 150

    ; 变址 + 偏移:访问第二个元素
    ; array + 2*4 = array + 8,即 30
    mov eax, [array + 2*4]          ; eax = 30

    mov eax, 1
    mov ebx, 0
    int 0x80

基址变址寻址(Base-Indexed Addressing)

有效地址 = 基址寄存器 + 变址寄存器 × 比例因子 + 偏移量。

注意:x86 仅支持一个变址寄存器 × 比例因子,不支持多个带比例因子的寄存器。严格按照以上公式写就行

; 基址变址寻址示例:二维数组访问

section .data
    ; 3 行 4 列的二维数组
    matrix dd 1, 2, 3, 4
           dd 5, 6, 7, 8
           dd 9, 10, 11, 12

section .text
    global _start

_start:
    ; 访问 matrix[1][2](第 2 行第 3 列,下标从 0 开始)
    ; 地址 = matrix + 行*每行字节数 + 列*每元素字节数
    ;       = matrix + 1*16 + 2*4
    ;       = matrix + 24

    mov ebx, matrix             ; 基址寄存器
    mov esi, 24                 ; 计算好的总偏移量

    ; 正确格式:基址 + 偏移量
    mov eax, [ebx + esi]         ; eax = 7

    ; 标准基址变址寻址格式:[基址 + 变址*比例因子 + 偏移量]
    ; 直接访问第 6 个元素(matrix[6] = 7)
    mov edi, 6
    mov eax, [matrix + edi*4]    ; eax = 7

    ; 程序退出
    mov eax, 1
    mov ebx, 0
    int 0x80

寻址方式总览

addressing-modes.svg

寻址方式

语法格式

有效地址/值

典型用途

立即寻址

mov eax, 42

42(常数值)

初始化、常量运算

寄存器寻址

mov eax, ebx

寄存器 ebx 的值

寄存器间数据传递

直接寻址

mov eax, [var]

内存地址 var 处的值

访问全局变量

间接寻址

mov eax, [ebx]

地址 = ebx 的值

指针操作、遍历内存

基址寻址

mov eax, [ebx+8]

地址 = ebx + 8

结构体成员访问

变址寻址

mov eax, [arr+esi*4]

地址 = arr + esi × 4

一维数组访问

基址变址

mov eax, [ebx+esi*4+8]

地址 = ebx + esi × 4 + 8

二维数组、复杂结构体

6.汇编语言 - 常量

常量不占用数据段内存,它们在 编译时 就被替换,相当于 C 语言中的 #define

EQU - 等值常量

定义后不可重新定义

; EQU 常量定义示例

; 数值常量
MAX_SIZE equ 256                 ; 缓冲区最大大小
DEFAULT_PORT equ 8080            ; 默认端口号
TRUE equ 1                       ; 真值
FALSE equ 0                      ; 假值

; 字符常量
LF equ 0xA                       ; 换行符
CR equ 0xD                       ; 回车符
NULL equ 0                       ; 空字符

; 表达式常量
ARRAY_SIZE equ 100 * 4           ; 100 个双字的字节数
BUFFER_SIZE equ MAX_SIZE * 2     ; 引用另一个常量
TOTAL equ 10 + 20 + 30           ; 计算结果

section .data
    ; 使用常量
    buffer db MAX_SIZE dup(0)    ; 定义 MAX_SIZE 字节的缓冲区

section .text
    global _start

_start:
    mov eax, MAX_SIZE            ; eax = 256
    mov ebx, BUFFER_SIZE         ; ebx = 512
    mov ecx, ARRAY_SIZE          ; ecx = 400

    mov eax, 1
    mov ebx, 0
    int 0x80

EQU 定义的常量不能在程序中修改,也不能重新定义。如果需要可以重新定义的常量(类似宏变量),使用 %define。

%define - 宏定义常量

%define 比 EQU 更灵活,支持重新定义和作用域控制

; %define 常量定义与重定义示例

%define VERSION 1                ; 定义版本号
%define APP_NAME 'runoob'        ; 定义应用名称

; 使用 %define 定义的常量
    mov eax, VERSION             ; eax = 1

; 可以重新定义(EQU 不允许这样做)
%define VERSION 2                ; 版本升级
    mov eax, VERSION             ; 现在 eax = 2

; %undef 取消定义
%undef VERSION                   ; VERSION 不再有效
; mov eax, VERSION               ; 错误:VERSION 未定义

; %define 可以定义多行(带括号的宏)
%define sum(a, b) (a + b)
    mov eax, sum(10, 20)         ; mov eax, (10+20) -> eax = 30

%assign - 可赋值的常量

%assign 用于定义可以在编译时多次修改的数值常量:

; %assign 编译时可变常量

%assign counter 0                ; 初始为 0

section .data
    ; 配合 %rep 重复块使用
    ; 生成 table_0, table_1, ..., table_9
    %rep 10
        db counter               ; 使用当前 counter 值
        %assign counter counter + 1
    %endrep

常量在程序中的实际应用

; 文件路径:constants_practice.asm
; 综合使用常量提高代码可读性

; 系统调用号常量
SYS_EXIT equ 1
SYS_FORK equ 2
SYS_READ equ 3
SYS_WRITE equ 4
SYS_OPEN equ 5
SYS_CLOSE equ 6

; 文件描述符常量
STDIN equ 0
STDOUT equ 1
STDERR equ 2

; 程序常量
MAX_INPUT equ 256
EXIT_SUCCESS equ 0
EXIT_FAILURE equ 1

section .data
    msg db 'Hello, RUNOOB!', 0xA
    msg_len equ $ - msg

section .bss
    buffer resb MAX_INPUT        ; 使用常量定义缓冲区大小

section .text
    global _start

_start:
    ; 使用常量名代替数字,代码意图一目了然
    mov eax, SYS_WRITE           ; 清晰:这是写操作
    mov ebx, STDOUT              ; 清晰:输出到屏幕
    mov ecx, msg
    mov edx, msg_len
    int 0x80

    mov eax, SYS_EXIT            ; 清晰:退出程序
    mov ebx, EXIT_SUCCESS        ; 清晰:正常退出
    int 0x80

常使用常量来替换表示指令的数字能方便理解记忆

NASM 的反引号字符串支持 \n(换行)、\t(制表符)、\\(反斜杠)、\'(单引号)等标准 C 转义序列。

7.汇编语言 - 算术指令

ADD - 加法指令

; 文件路径:add_demo.asm
; ADD 指令示例:计算 100 + 200 + 300

section .data
    a dd 100
    b dd 200
    c dd 300
    sum dd 0

section .text
    global _start

_start:
    ; 方式1:寄存器 += 立即数
    mov eax, [a]                ; eax = 100
    add eax, 200                ; eax = 100 + 200 = 300

    ; 方式2:寄存器 += 寄存器
    mov ebx, [b]                ; ebx = 200
    add eax, ebx                ; eax = 300 + 200 = 500

    ; 方式3:寄存器 += 内存
    add eax, [c]                ; eax = 500 + 300 = 800

    ; 存储结果
    mov [sum], eax              ; sum = 800

    ; 加法对标志位的影响
    mov eax, 0xFFFFFFFF         ; eax = 最大的 32 位无符号数
    add eax, 1                  ; eax = 0(溢出回绕)
    ; CF = 1(产生进位)
    ; ZF = 1(结果为 0)
    ; OF = 0(有符号视角无溢出)

    mov eax, 1
    mov ebx, 0
    int 0x80

ZF CF OF

1. ZF (Zero Flag) —— 零标志位

  • 含义:只要运算结果是 0,ZF 就变成 1。否则为 0。

  • 赛场作用:jz(Jump if Zero)、je(Jump if Equal)都靠它。

  • 代码分析:0xFFFFFFFF + 1 = 0x100000000,但是寄存器只有 32 位,所以把溢出的高位砍掉,剩下 0x00000000。结果确实是 0,所以 ZF=1。

2. CF (Carry Flag) —— 进位标志位(无符号视角)

  • 含义:把数字当成无符号数看时,运算结果超出了最大值(32位无符号最大是 0xFFFFFFFF),产生了进位或借位,CF 就变 1。

  • 赛场作用:jb(Jump if Below)、ja(Jump if Above)等无符号比较指令靠它。

  • 代码分析:无符号数 0xFFFFFFFF 是 42 亿多,再加 1 就超出了 32 位。CPU 最高位产生了进位,所以 CF=1。

3. OF (Overflow Flag) —— 溢出标志位(有符号视角)

  • 含义:把数字当成有符号数看时(最高位是符号位,0正1负),运算结果超出了有符号数的表示范围(32位有符号范围是 -2^31 到 2^31-1),OF 就变 1。

  • 赛场作用:jl(Jump if Less)、jg(Jump if Greater)等有符号比较指令靠它。

  • 代码分析:在有符号视角下,0xFFFFFFFF 代表 -1。那么 -1 + 1 = 0。-1 加 1 得到 0,这是一个完全合理的数学运算,并没有超出有符号数的范围,所以 OF=0。

SUB - 减法指令

SUB 从目标操作数中减去源操作数,结果存入目标操作数。

; 文件路径:sub_demo.asm
; SUB 指令示例

section .data
    x dd 1000
    y dd 300

section .text
    global _start

_start:
    ; 基本减法
    mov eax, [x]                ; eax = 1000
    sub eax, [y]                ; eax = 1000 - 300 = 700  eax为目标操作数   [y]为源操作数

    ; 减法对标志位的影响
    mov eax, 10
    sub eax, 20                 ; eax = -10(即 0xFFFFFFF6)
    ; CF = 1(产生借位:10 < 20)
    ; SF = 1(结果为负)
    ; ZF = 0(结果非零)
    ; OF = 0(无符号溢出)

    ; 减自身:常用于清零
    mov eax, 12345
    sub eax, eax                ; eax = 0
    ; ZF = 1, CF = 0
    ; 这是一条将寄存器清零的经典方式(比 mov eax, 0 效率高)

    mov eax, 1
    mov ebx, 0
    int 0x80

标志位

全称

核心逻辑

对应跳转指令

对应 C 语言场景

ZF

Zero Flag

结果是不是 0?

jz / je

if (a == b)

SF

Sign Flag

结果是正还是负?

js / jns

if (a < 0)

CF

Carry Flag

无符号视角,越界没?

jb / ja

if (a < b),a、b 是 unsigned

OF

Overflow Flag

有符号视角,算错没?

jl / jg

if (a < b),a、b 是 int

sub eax, eax 是将寄存器清零的经典技巧,它只占用 2 字节,而 mov eax, 0 占用 5 字节。在需要极致优化体积的场景(如 shellcode)中常用。

INC / DEC - 自增/自减指令

; INC 和 DEC 示例

section .data
    counter dd 0

section .text
    global _start

_start:
    mov dword [counter], 0      ; counter = 0
    inc dword [counter]         ; counter = 1(内存操作数)
    inc dword [counter]         ; counter = 2

    mov ecx, 10
    dec ecx                     ; ecx = 9
    dec ecx                     ; ecx = 8

    ; INC/DEC 不影响 CF 标志位(这是与 ADD/SUB 的重要区别)
    ; 但会影响 ZF、SF、OF、PF

    ; 循环中使用 INC
    mov ecx, 5
    mov eax, 0
loop_inc:
    inc eax                     ; eax 每次加 1
    loop loop_inc               ; 重复 5 次,eax 最终 = 5

    mov ebx, eax                ; 返回值 = 5
    mov eax, 1
    int 0x80

MUL - 无符号乘法

(全都当正数算)

在无符号乘法中
CPU 规定死了:“做 32 位乘法时,被乘数永远默认从 EAX 拿,结果永远默认放到 EDX:EAX 里。”

EDX 保存高32位结果,EAX 保存低32位结果

操作数大小

乘数

被乘数(隐式)

结果存放

1 字节

任何 8 位寄存器或内存

AL

AX = AL × 操作数

2 字节

任何 16 位寄存器或内存

AX

DX:AX = AX × 操作数

4 字节

任何 32 位寄存器或内存

EAX

EDX:EAX = EAX × 操作数

; 文件路径:mul_demo.asm
; MUL 无符号乘法示例

section .data
    val1 dd 1000
    val2 dd 2000
    result_low dd 0
    result_high dd 0

section .text
    global _start

_start:
    ; 32 位乘法:EDX:EAX = EAX × 操作数
    mov eax, [val1]             ; eax = 1000
    mul dword [val2]            ; edx:eax = 1000 × 2000 = 2,000,000
    ; 结果 = 2,000,000 = 0x001E8480
    ; EAX = 0x001E8480(低 32 位)
    ; EDX = 0x00000000(高 32 位,因为结果没超过 32 位)

    ; 大数乘法演示(结果超过 32 位)
    mov eax, 0xFFFFFFFF         ; eax = 4,294,967,295
    mov ebx, 2                  ; ebx = 2
    mul ebx                     ; edx:eax = 0xFFFFFFFF × 2
    ; EAX = 0xFFFFFFFE
    ; EDX = 0x00000001(高 32 位)
    ; CF = 1(结果超出 32 位)

    ; 保存结果
    mov [result_low], eax
    mov [result_high], edx

    mov eax, 1
    mov ebx, 0
    int 0x80

IMUL - 有符号乘法

; 文件路径:imul_demo.asm
; IMUL 有符号乘法示例

section .data
    a dd -100
    b dd 3

section .text
    global _start

_start:
    ; 单操作数形式:同 MUL
    mov eax, [a]                ; eax = -100
    imul dword [b]              ; edx:eax = -100 × 3 = -300
    ; EAX = -300(0xFFFFFED4)
    ; EDX = 0xFFFFFFFF(符号扩展)

    ; 双操作数形式:reg = reg × 操作数
    mov ebx, [a]                ; ebx = -100
    imul ebx, [b]               ; ebx = -100 × 3 = -300
    ; 结果必须是 32 位能容纳的

    ; 三操作数形式:reg = 操作数1 × 立即数
    imul ecx, [a], 5            ; ecx = -100 × 5 = -500

    mov eax, 1
    mov ebx, 0
    int 0x80

注意:在 x86 汇编里,有一个硬性规定:不能将两个内存地址直接进行运算。

MUL 和 IMUL 的主要区别是对符号的处理:MUL 解释为无符号数,IMUL 解释为有符号数。比如 0xFFFFFFFF 在 MUL 中是 4294967295,在 IMUL 中是 -1。

DIV - 无符号除法

做除法前务必用 mov edx, 0 或 xor edx, edx 清零 EDX!如果 EDX 中有旧数据,被除数的值就不对了。这是初学者最常见的除法 bug。

DIV 执行无符号除法,规则与 MUL 对称:

除数大小

被除数(隐式)

商

余数

1 字节

AX

AL

AH

2 字节

DX:AX

AX

DX

4 字节

EDX:EAX

EAX

EDX

32 位除法:edx:eax / 除数

被除数要先扩展到 EDX:EAX

EAX 保存商,EDX保存余数

; 文件路径:div_demo.asm
; DIV 无符号除法示例

section .data
    dividend dd 1000
    divisor dd 7
    quotient dd 0
    remainder dd 0

section .text
    global _start

_start:
    ; 32 位除法:edx:eax / 除数
    ; 被除数要先扩展到 EDX:EAX
    mov eax, [dividend]         ; eax = 1000
    mov edx, 0                  ; edx = 0(高 32 位清零)
    div dword [divisor]         ; edx:eax / 7
    ; EAX = 142(商)
    ; EDX = 6(余数:1000 = 142×7 + 6)

    mov [quotient], eax         ; quotient = 142
    mov [remainder], edx        ; remainder = 6

    ; 字节除法示例:55 / 4
    mov ax, 55                  ; 被除数
    mov bl, 4                   ; 除数
    div bl                      ; al = 13(商), ah = 3(余数)

    mov eax, 1
    mov ebx, 0
    int 0x80

IDIV - 有符号除法

; IDIV 有符号除法示例

    ; 计算 -100 / 3
    mov eax, -100               ; eax = -100
    cdq                         ; 符号扩展:edx:eax = -100
                                ; cdq 将 eax 的符号位复制到 edx 的所有位
    mov ebx, 3                  ; 除数
    idiv ebx                    ; eax = -33(商), edx = -1(余数)
    ; 验证:-100 = -33 × 3 + (-1)

ADC / SBB - 带进位/借位运算

; 文件路径:adc_sbb_demo.asm
; 64 位加法:两个 64 位数相加

section .data
    ; 64 位数 a = 0x00000001 FFFFFFFF
    a_low dd 0xFFFFFFFF
    a_high dd 0x00000001

    ; 64 位数 b = 0x00000000 00000005
    b_low dd 0x00000005
    b_high dd 0x00000000

    ; 结果
    result_low dd 0
    result_high dd 0

section .text
    global start

start:
    ; 低 32 位相加
    mov eax, [a_low]
    add eax, [b_low]            ; eax = 0xFFFFFFFF + 5 = 0x00000004
    ; CF = 1(产生进位!)
    mov [result_low], eax       ; result_low = 0x00000004

    ; 高 32 位加进位
    mov eax, [a_high]
    adc eax, [b_high]           ; adc = add + CF
    ; eax = 1 + 0 + 1(进位) = 2
    mov [result_high], eax      ; result_high = 2

    ; 最终 64 位结果:0x00000002 00000004
    ; 验证:0x1FFFFFFF + 5 = 0x200000004

    ; SBB 减法同理(带借位)
    mov eax, [a_low]
    sub eax, [b_low]            ; 低 32 位相减
    mov [result_low], eax

    mov eax, [a_high]
    sbb eax, [b_high]           ; sbb = sub - CF

    mov eax, 1
    mov ebx, 0
    int 0x80

算术指令速查表

指令

格式

功能

影响的标志位

ADD

add dest, src

dest = dest + src

CF, ZF, SF, OF, PF

SUB

sub dest, src

dest = dest - src

CF, ZF, SF, OF, PF

INC

inc dest

dest = dest + 1

ZF, SF, OF, PF(不影响 CF)

DEC

dec dest

dest = dest - 1

ZF, SF, OF, PF(不影响 CF)

MUL

mul src

无符号乘法

CF, OF

IMUL

imul src

有符号乘法

CF, OF

DIV

div src

无符号除法

无定义(不定)

IDIV

idiv src

有符号除法

无定义(不定)

ADC

adc dest, src

dest = dest + src + CF

CF, ZF, SF, OF, PF

SBB

sbb dest, src

dest = dest - src - CF

CF, ZF, SF, OF, PF

NEG

neg dest

dest = -dest(取负)

CF, ZF, SF, OF, PF

8.汇编语言 - 逻辑指令

跟算数运算一样

  • ❌ 非法:and [a], [b](CPU 不能直接对两个内存地址做运算)

AND - 按位与

111 100 010 000

    ; 基本按位与
    mov eax, 0x0F0F             ; 0000 1111 0000 1111
    and eax, 0x00FF             ; 0000 0000 1111 1111
    ; 结果:eax = 0x000F         ; 0000 0000 0000 1111

常用技巧1:屏蔽低 4 位(保留低 4 位)

    mov eax, 0xAB               ; 1010 1011
    and eax, 0x0F               ; 0000 1111
    ; eax = 0x0B                ; 0000 1011

常用技巧2:判断奇偶性(和 1 做 AND)

    mov eax, 42                 ; 偶数
    and eax, 1                  ; eax = 0(偶数)
    ; 42 的二进制末尾是 0,42 & 1 = 0

    mov eax, 43                 ; 奇数
    and eax, 1                  ; eax = 1(奇数)
    ; 43 的二进制末尾是 1,43 & 1 = 1

OR - 按位或

111 101 011 000

; OR 指令示例

    ; 合并标志位
    mov eax, 0x0F00             ; 0000 1111 0000 0000
    or  eax, 0x00FF             ; 0000 0000 1111 1111
    ; eax = 0x0FFF              ; 0000 1111 1111 1111

    ; 设置某个位(把第 3 位设为 1)
    mov eax, 0                  ; 0000 0000
    or  eax, 0x08               ; 0000 1000
    ; eax = 0x8                 ; 0000 1000

大小写转换:大写转小写

    mov al, 'A'                 ; al = 0x41 (0100 0001)
    or  al, 0x20                ; 0x20 = 0010 0000
    ; al = 0x61 = 'a'          ; 0110 0001

字符

十六进制

二进制

'A'

0x41

0100 0001

'a'

0x61

0110 0001

'B'

0x42

0100 0010

'b'

0x62

0110 0010

...

...

...

'Z'

0x5A

0101 0010

'z'

0x7A

0111 0010

发现大小写差在第五位(从第0位算)是0还是1

or 0010 0000就可以做到大写转小写,小写还是小写

NOT - 按位取反

10 01

; NOT 指令示例

    mov eax, 0x0F0F0F0F        ; 0000 1111 0000 1111 ...
    not eax                     ; 1111 0000 1111 0000 ...
    ; eax = 0xF0F0F0F0

    ; NOT 不影响任何标志位(与 AND/OR/XOR 不同)

XOR - 按位异或

110 101 011 000

经典技巧1:寄存器清零(比 mov reg, 0 高效)

    xor eax, eax                ; eax = 0,只占用 2 字节

经典技巧2:交换两个寄存器(不需要第三个临时寄存器)

    mov eax, 100                ; eax = 100
    mov ebx, 200                ; ebx = 200
    xor eax, ebx                ; eax = 100 xor 200
    xor ebx, eax                ; ebx = 200 xor (100 xor 200) = 100
    xor eax, ebx                ; eax = (100 xor 200) xor 100 = 200
    ; 现在 eax = 200, ebx = 100(交换完成!)

XOR 交换技巧虽然很酷,但在现代 CPU 上效率不如使用 XCHG 指令或临时寄存器。了解即可,不必执着使用。

经典技巧3:简单的加密/解密

    mov al, 'A'                 ; 原始字符 'A' = 0x41
    xor al, 0x55                ; 加密:al = 'A' xor 0x55
    ; al 现在是某个乱码值
    xor al, 0x55                ; 解密:再 xor 0x55 恢复
    ; al = 'A' 回来了!

移位指令

移位指令将二进制位向左或向右移动,常用于高效乘除运算(乘2、除2等)。

指令

功能

示例

SHL

逻辑左移(左边出去,右边补0)

shl eax, 1(乘以 2)

SHR

逻辑右移(右边出去,左边补0)

shr eax, 1(无符号除以 2)

SAL

算术左移(同 SHL)

sal eax, 1

SAR

算术右移(右边出去,左边保持符号位)

sar eax, 1(有符号除以 2)

; 文件路径:shift_demo.asm
; 移位指令示例

section .text
    global _start

_start:
    ; SHL:逻辑左移 = 乘以 2 的幂
    mov eax, 10                 ; eax = 10 (1010)
    shl eax, 1                  ; eax = 20 (10100),即 10×2
    shl eax, 2                  ; eax = 80 (1010000),即 20×4

    ; SHR:逻辑右移 = 无符号除以 2 的幂
    mov eax, 80                 ; eax = 80
    shr eax, 3                  ; eax = 10,即 80÷8

    ; SAR:算术右移 = 有符号除以 2 的幂(保留符号位)
    mov eax, -16                ; eax = -16 (0xFFFFFFF0)
    sar eax, 2                  ; eax = -4 (0xFFFFFFFC),即 -16÷4
    ; SAR 对比 SHR:
    ; 如果 eax = 0xFFFFFFF0 (-16),SHR 会得到 0x3FFFFFFC (很大的正数)
    ; 而 SAR 会得到 0xFFFFFFFC (-4),保留了符号位

    ; 移出的最后一位会进入 CF 标志位
    mov eax, 5                  ; 0101
    shr eax, 1                  ; eax = 2, CF = 1(末尾的1被移出)
    jc carry_was_set            ; 如果 CF=1,说明原数是奇数

carry_was_set:
    mov eax, 1
    mov ebx, 0
    int 0x80

循环移位指令

循环移位将移出的位重新填入另一端,形成循环:

指令

功能

ROL

循环左移(绕过 CF)

ROR

循环右移(绕过 CF)

RCL

带进位的循环左移(通过 CF)

RCR

带进位的循环右移(通过 CF)

; 循环移位示例

    ; ROL:循环左移
    mov al, 0x85                ; 1000 0101
    rol al, 1                   ; 左移 1 位:0000 1011
    ; CF = 1(最高位移出到 CF)
    ; 同时 CF 原本的 1 被移入最低位,形成循环

    ; ROL 实际效果:向左移 1 位,最高位同时进入 CF 和最低位
    ; 1000 0101 -> ROL 1 -> 0000 1011

    ; RCL:带进位循环左移(CF 参与循环)
    clc                         ; 清除 CF = 0
    mov al, 0x85                ; 1000 0101
    rcl al, 1                   ; CF 参与:CF bit7...bit0 -> CF
    ; 结果:0000 1010, CF = 1

    ; 循环移位在加密算法和位操作中常用

逻辑运算的实际用途

一个综合示例——使用逻辑运算实现简单的位标志系统:

; 文件路径:bit_flags.asm
; 使用逻辑运算实现位标志系统

section .data
    flags db 0                  ; 8 个标志位,初始全为 0
    ; bit0: 是否激活
    ; bit1: 是否可见
    ; bit2: 是否需要保存
    ; bit3: 是否已修改

    FLAG_ACTIVE equ 1           ; 0000 0001
    FLAG_VISIBLE equ 2          ; 0000 0010
    FLAG_NEED_SAVE equ 4        ; 0000 0100
    FLAG_MODIFIED equ 8         ; 0000 1000

section .text
    global _start

_start:
    ; 设置标志位:OR
    mov al, [flags]
    or  al, FLAG_ACTIVE         ; 设置 bit0
    or  al, FLAG_VISIBLE        ; 设置 bit1
    ; al = 0000 0011 = 3
    mov [flags], al

    ; 检查标志位:AND + TEST
    test byte [flags], FLAG_ACTIVE    ; 测试 bit0 是否为 1
    jnz is_active                     ; ZF=0 表示该位是 1

is_active:
    ; 清除标志位:AND + NOT
    mov al, [flags]
    and al, ~FLAG_VISIBLE       ; 清除 bit1(保留其他位不变)
    ; al = 0000 0001 = 1
    mov [flags], al

    ; 切换标志位:XOR
    mov al, [flags]
    xor al, FLAG_MODIFIED       ; 切换 bit3
    ; 如果原来是 0 变成 1,原来是 1 变成 0

    mov eax, 1
    mov ebx, 0
    int 0x80

TEST 和 AND 的区别(极其重要)

  • and al, 0x01:会修改 al,把结果存回 al。

  • test al, 0x01:不修改 al,只是偷偷算一下,然后根据结果设置标志位。相当于“影子运算”。

~ 是按位取反运算符(Bitwise NOT),在汇编和 C 语言里都表示把二进制的每一位 0 变 1,1 变 0

9.汇编语言 - 条件判断

CMP - 比较指令