汇编语言 - 基础语法
本章介绍 NASM 汇编程序的基本结构、语法规则和书写规范,帮助你理解汇编代码的骨架。
汇编程序的基本结构
一个完整的 NASM 汇编程序通常由以下几个部分组成:
实例
; 文件路径:structure.asm
; NASM 程序基本结构示例
section .data ; 数据段:存放已初始化的数据
; 这里定义变量和常量
msg db 'Hello, RUNOOB!', 0xA
len equ $ - msg
section .bss ; BSS 段:存放未初始化的数据
; 这里预留内存空间
buffer resb 64 ; 预留 64 字节的缓冲区
section .text ; 代码段:存放可执行指令
global _start
_start:
; 这里写程序逻辑
mov eax, 4
mov ebx, 1
mov ecx, msg
mov edx, len
int 0x80
mov eax, 1
mov ebx, 0
int 0x80
; NASM 程序基本结构示例
section .data ; 数据段:存放已初始化的数据
; 这里定义变量和常量
msg db 'Hello, RUNOOB!', 0xA
len equ $ - msg
section .bss ; BSS 段:存放未初始化的数据
; 这里预留内存空间
buffer resb 64 ; 预留 64 字节的缓冲区
section .text ; 代码段:存放可执行指令
global _start
_start:
; 这里写程序逻辑
mov eax, 4
mov ebx, 1
mov ecx, msg
mov edx, len
int 0x80
mov eax, 1
mov ebx, 0
int 0x80
| 段(Section) | 用途 | 特点 |
|---|---|---|
.data | 存放已初始化的全局变量和常量 | 编译时确定大小和内容,存入可执行文件 |
.bss | 存放未初始化的全局变量 | 只在运行时分配空间,不占用可执行文件大小 |
.text | 存放可执行的机器指令 | 只读,包含程序的全部逻辑代码 |
至少需要
.text段才能构成一个有效的汇编程序。如果没有数据,可以省略.data和.bss段。
汇编语句格式
每条汇编语句的通用格式为:
[标签:] 指令助记符 [操作数1 [, 操作数2 [, 操作数3]]] [; 注释]
各部分说明:
| 部分 | 是否必须 | 说明 |
|---|---|---|
| 标签(Label) | 可选 | 代表一个内存地址的符号名,以冒号结尾 |
| 指令助记符 | 必须 | 如 mov、add、sub 等,告诉 CPU 要做什么 |
| 操作数 | 可选(部分指令无操作数) | 指令操作的数据对象,可为寄存器、内存地址、立即数 |
| 注释 | 可选 | 以分号开头,一直延续到行尾 |
实例
; 各种语句格式示例
; 只有指令,无操作数
ret ; 从子程序返回
; 指令 + 单个操作数
push eax ; 将 eax 的值压入栈中
inc ecx ; ecx 加 1
; 指令 + 两个操作数(最常见)
mov eax, 42 ; 将 42 复制到 eax 寄存器
add ebx, ecx ; ebx = ebx + ecx
; 带有标签
loop_start: ; 标签:标记循环开始位置
dec ecx ; ecx 减 1
jnz loop_start ; 如果 ecx 不为 0,跳回 loop_start
; 只有指令,无操作数
ret ; 从子程序返回
; 指令 + 单个操作数
push eax ; 将 eax 的值压入栈中
inc ecx ; ecx 加 1
; 指令 + 两个操作数(最常见)
mov eax, 42 ; 将 42 复制到 eax 寄存器
add ebx, ecx ; ebx = ebx + ecx
; 带有标签
loop_start: ; 标签:标记循环开始位置
dec ecx ; ecx 减 1
jnz loop_start ; 如果 ecx 不为 0,跳回 loop_start
注释规范
NASM 使用 分号(;) 表示注释,从分号到行尾的内容都会被汇编器忽略。
实例
; 整行注释:说明下面代码块的用途
; 计算两个数的和并输出结果
mov eax, 10 ; 行内注释:将 10 放入 eax
add eax, 20 ; 行内注释:将 20 加到 eax,现在 eax = 30
; 计算两个数的和并输出结果
mov eax, 10 ; 行内注释:将 10 放入 eax
add eax, 20 ; 行内注释:将 20 加到 eax,现在 eax = 30
汇编代码中注释极其重要。没有注释的汇编代码过几周连作者自己都可能看不懂。养成每条指令都写注释的习惯。
标识符命名规则
标识符(标签、变量名、常量名等)须遵循以下规则:
| 规则 | 说明 |
|---|---|
| 组成字符 | 字母、数字、下划线 _、点 .、问号 ?、@、$、# 等 |
| 起始字符 | 必须以字母、下划线、点或问号开头,不能以数字开头 |
| 大小写 | 默认区分大小写(可通过编译选项修改) |
| 保留字 | 不能和指令助记符、寄存器名或 NASM 关键字重名 |
实例
; 合法的标识符
my_variable: ; 字母开头 + 下划线
.loop_start: ; 点开头(局部标签)
?error_handler: ; 问号开头
counter2: ; 字母 + 数字
; 不合法的标识符(仅供参考,不要使用)
; 1st_value: ; 错误:不能以数字开头
; mov: ; 错误:mov 是保留字
; my-variable: ; 错误:减号不是合法字符
my_variable: ; 字母开头 + 下划线
.loop_start: ; 点开头(局部标签)
?error_handler: ; 问号开头
counter2: ; 字母 + 数字
; 不合法的标识符(仅供参考,不要使用)
; 1st_value: ; 错误:不能以数字开头
; mov: ; 错误:mov 是保留字
; my-variable: ; 错误:减号不是合法字符
伪指令(Directives)
伪指令 是给汇编器的命令,不是给 CPU 的指令,用于控制汇编过程和定义数据结构。
| 伪指令 | 用途 | 示例 |
|---|---|---|
db | 定义字节(1 字节) | byte_val db 0x55 |
dw | 定义字(2 字节) | word_val dw 0x1234 |
dd | 定义双字(4 字节) | dword_val dd 0x12345678 |
equ | 定义常量 | MAX_SIZE equ 256 |
resb | 预留字节空间 | buffer resb 128 |
resw | 预留字空间 | wbuf resw 64 |
resd | 预留双字空间 | dbuf resd 32 |
%define | 宏定义常量 | %define COUNT 10 |
大小写规范
NASM 默认对标签和标识符 区分大小写:
实例
; 大小写敏感的示例
section .data
msg db 'RUNOOB', 0 ; 定义变量 msg
section .text
global _start
_start:
mov eax, MSG ; 错误:MSG 和 msg 不同(除非开启忽略大小写)
mov eax, msg ; 正确:msg 与定义完全一致
MOV EAX, 42 ; 语法正确:指令助记符不区分大小写
mov eax, 42 ; 推荐写法:用小写,可读性更好
section .data
msg db 'RUNOOB', 0 ; 定义变量 msg
section .text
global _start
_start:
mov eax, MSG ; 错误:MSG 和 msg 不同(除非开启忽略大小写)
mov eax, msg ; 正确:msg 与定义完全一致
MOV EAX, 42 ; 语法正确:指令助记符不区分大小写
mov eax, 42 ; 推荐写法:用小写,可读性更好
指令助记符和寄存器名不区分大小写(
MOV、Mov、mov效果相同),但推荐的风格是统一小写。
数值表示方式
NASM 支持多种进制的数值表示:
实例
; 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 前缀
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 前缀
推荐使用
0x前缀表示十六进制(如0x2A),这样不容易和标签混淆。
一个完整的语法示例
下面程序综合运用以上语法元素,计算 1 到 10 的和并输出:
实例
; 文件路径:sum.asm
; 计算 1+2+...+10 并输出结果字符
section .data
result db 0 ; 存放计算结果(1字节)
newline db 0xA ; 换行符
section .text
global _start
_start:
; 初始化寄存器和变量
mov ecx, 10 ; 循环计数器:从 10 开始倒数
mov eax, 0 ; eax 存放累加和,初始为 0
sum_loop: ; 循环开始标签
add eax, ecx ; eax = eax + ecx
dec ecx ; ecx 减 1
jnz sum_loop ; 如果 ecx != 0,继续循环
; 此时 eax = 55(10+9+...+1)
add eax, '0' ; 将数字转为 ASCII 字符('0'=48,55+48=103='g',不对)
; 实际演示需要更复杂的转换,见后续章节
; 这里只输出 result(简化演示)
mov [result], al ; 将累加结果存入 result
; 退出程序
mov eax, 1
mov ebx, 0
int 0x80
; 计算 1+2+...+10 并输出结果字符
section .data
result db 0 ; 存放计算结果(1字节)
newline db 0xA ; 换行符
section .text
global _start
_start:
; 初始化寄存器和变量
mov ecx, 10 ; 循环计数器:从 10 开始倒数
mov eax, 0 ; eax 存放累加和,初始为 0
sum_loop: ; 循环开始标签
add eax, ecx ; eax = eax + ecx
dec ecx ; ecx 减 1
jnz sum_loop ; 如果 ecx != 0,继续循环
; 此时 eax = 55(10+9+...+1)
add eax, '0' ; 将数字转为 ASCII 字符('0'=48,55+48=103='g',不对)
; 实际演示需要更复杂的转换,见后续章节
; 这里只输出 result(简化演示)
mov [result], al ; 将累加结果存入 result
; 退出程序
mov eax, 1
mov ebx, 0
int 0x80
注意:上面的示例中,直接加 '0' 只在数字是 0-9 范围内正确。处理两位数及以上的数字转换,将在后续章节详细讲解。
