NASM Overview

请注意:本文编写于 ,其中某些信息可能已经失去时效性。

前言

机器指令是用二进制代码表示的 CPU 能够直接识别和执行的一种指令,不同的 CPU 架构有不同的机器指令集。汇编指令是将机器指令对应到便于记忆和书写的字符串(注意并非一一对应,同一汇编器可能存在多个汇编指令对应一个机器指令的情况),汇编指令编写完成后通过汇编器将其翻译成机器指令供 CPU 执行。

不同汇编器针对同一机器指令可以有不同的汇编指令表达方式,只要汇编器最终能够正确无误地翻译就可以。 不同的汇编器对应不同的汇编指令格式,不同的汇编指令格式衍生出不同的汇编指令语法。没有一种汇编器可以将所有的汇编语法都正确地翻译成机器指令,因此,随着计算机的发展,不同厂家形成了自家的汇编语言体系并拥有自己的汇编器。

常见的汇编器有:GNU Assembler(GAS) | Microsoft Macro Assembler(MASM) | Netwide Assembler(NASM) | Flat Assembler(FASM) 等。GAS 使用 AT&T 汇编语法,MASM 使用 Intel 汇编语法,NASM 使用的汇编语法和 Intel 汇编语法类似但要更简单一些。

注:本文以 NASM 使用的汇编语法为例

句型句式

NASM 的基本句型可以由四部分组成:label: instruction operand(s) ; comment

理论上来说上面的四个部分都是可选的,但至少存在其中一个部分,一个语句可以没有指令而只存在一个标签。

而标签后的冒号也是可以省略的。

空格

NASM 语法对空格数量没有要求和限制,可以在任何两个部分的间隙添加任意数量的空格(至少一个用来区分两个部分)。

行连接

在 NASM 中使用反斜杠(\)作为行的延续符,如果一行以反斜杠结束,则当前行的下一行被认为是当前行的延续。

标签(label)

标签可以使用字母、数字、下划线(_)、美元符($)、井号(#)、艾特(@)、波浪线(~)、英文句号(.)、英文问号(?),其中字母、下划线(_)、英文句号(.)和英文问号(?)。其中以英文句号(.)开头有特殊的含义(详情见下文)。

局部标签(loacal label)

在 NASM 中所以英文句号(.)开头的的标签会被视为局部标签,所有局部标签会被认为与上一个非局部标签有关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
label1:
some code

.loop:
some code
jne .loop
ret

label2:
some code

.loop:
some code
jne .loop
ret

label3:
some code
jmp label1.loop

在上述代码片段中,所有的 jne 指令都会跳转到上面与之相邻的 .loop 标签,因为 .loop 标签的定义形式是一种局部标签定义形式,因此两个 .loop 标签分别会与该标签上面最近的全局标签产生关联。而在为特别指定的情况下局部标签只能在与其相关的全局标签下生效,但也可以通过「全局标签.局部标签」的形式进行调用。

特殊指令(符号)

``:转义操作符

NASM 使用 C 风格的转义字符,在反斜杠后跟转义码,转义码包括:字符转义码、八进制转义码、十六进制转义码,且转移字符需要使用反引号引用:

1
db `\x61` ; 等同于 db a

注:反引号也可以用来定义普通字符串

[]:索引操作符

有的地方称为「索引操作符」,表示一种间接取操作数的方式,即取括号内内存地址对应的操作数,类似 C 语言的指针概念。

括号中一般存放的是一个内存地址,可以是使用寄存器表示的内存地址,可以是使用标记表示的内存地址,也可以是直接用操作数表示的内存地址。

1
2
mov ax, [var]
mov byte [es:0x00], 'L'

$ 和 $$

$ 表示经过 NASM 编译后当前指令位置;
$$ 表示经过 NASM 编译后当前 section 起始位置;

ptr

ptr -> pointer 即指针的缩写,用来临时指定类型,可以类比为 C 语言中的强制类型转换。

1
2
mov ax, bx            ; 由于寄存器 ax 和 bx 都是 word 型,所以没有必要加 word
mov ax, word ptr [bx] ; 取出内存地址是 ds:bx 的值转换为 word 型存放到 ax 处

伪指令

伪指令不是真正的指令,而是为了方便 NASM 汇编器而存在,但是它们的地位与真正的指令相同。

表明指令操作数据大小

通常存在于操作指令和操作数之间,用来表明操作指令使用的操作数单位大小。

指令 代表数据大小(位)
byte 8
word 16
dword 32
qword 64
tword 80
oword 128
yword 256

db 家族:定义初始化数据

一系列用来声明并初始化数据的伪指令:

指令 功能 备注
db 定义字节数据
dw 定义字数据
dd 定义双字数据 可以定义单精度浮点数
dq 定义四字数据 可以定义双精度浮点数
dt 定义十字数据 可以定义扩展精度浮点数
do 定义 oword 可以定义四精度浮点数
dy 定义 yword 可以定义 ymm 数据

注:dt do dy 不接受整型数值。

resb 家族:定义非初始化数据

相比于 db 家族 resb 家族的指令只会在编译阶段声明一个未初始化的出处空间但并不会为其设置初始值。

resb: reserve byte

指令 功能
resb 以字节为单位声明一段未初始化数据
resw 以字为单位声明一段未初始化数据
resd 以双字节为单位声明一段未初始化数据
resq 以四字为单位声明一段未初始化数据
rest 以十字为单位声明一段未初始化数据
reso 以 oword 为单位声明一段未初始化数据
resy 以 yword 为单位声明一段未初始化数据

incbin:包含二进制文件

NASM 提供了一种包含二进制文件的方法,即使用 incbin 伪指令,此伪指令的作用是包含 graphics 以及 sound 这类数据文件。

equ:定义常量

equ 伪指令用来为某个标识符赋值一个整型常量,作用类似于 C 语言的 #define:

1
2
3
4
a equ 0             ; 正确
b equ 'abcd' ; 可执行,b = 0x64636261
c equ 'abcdefghi' ; 可执行会提示 warning,c = 0x64636261
d equ 1.2 ; 错误

在例子中,b 和 c 存储的是字符串对应的 ASCII 码,而因为整型常量最大是 quadword(8 bytes),因此 c 对应的字符串会被自动截断为 ‘abcd’。而 d 存储的是非整型值,因此会报错。

times:重复执行

用来重复指令(或伪指令),下面是一个比较经典的例子:

1
2
3
; 用于填充引导代码
times 510-($-$$) db 0
dw 0xaa55

Unicode 字符串

NASM 顶一个两个操作数符来定义 Unicode 字符串:

1
2
dw __utf16__('你好世界')
dd __utf32__('你好世界')

SECTION | SEGMENT:自定义段

在 NASM 中 SECTION 和 SEGMENT 指令是相同的同义词,可以改变所写代码被分配到哪一个 section 中。在一些 object file 中,section 的数量是固定的;在其他格式中,用户可以根据自己的需求来自定义 section。

本章节以 NASM 的 bin output formats 为例讲解多 section 用法

NASM 支持标准的 .data .text .bss,编译后程序文件中内存地址的顺序是 .text .data 用户 section,同名 section 编译后会放在同一块连续的内存上。

section 特性:

  • section 可以被指定为 progbits 或 nobits,默认为 probits(.bss 默认为 nobits)。
  • section 可以在定义时使用 align=start= 字句在指定对齐字节,区别是 align 只接受 2 的 N 次幂,而 start 可以接受任意整数值;
  • section 可以在定义时使用 vstart= 字句定义一个虚拟起始地址,它将被用于计算该 section 内的所有内存引用;
  • section 可以在定义时使用 follows=<section>vfollows=<section> 字句来进行排序;
  • 显示定义的 section 之前的所有代码默认会存储在 .text section 中;
  • 如果没有给出 ORG 语句,则 ORG 默认为零;
  • 除非显示指定了 start= vstart= follows 或 vfollow,否则 .bss section 将被放在最后一个 progbits section 之后;
  • 除非显示指定了对齐方式,否则所有 sectioin 都以 dword(4 bytes) 为边界对齐;
  • section 不能出现重叠;
  • NASM 为每个 section 提供了一个隐藏标签 section.<secname>.start 用来获取该 section 起始地址;

拓展:
progbits:程序内容,包含代码、数据、调试相关信息;
nobits:和PROGBITS类似,唯一不同的是在文件中不占空间,对应的进行内存空间是加载的时候申请的;

1
2
3
4
5
6
7
8
9
10
11
db '11'
section test1 align=16
db '12'
section test2 start=12
db '13'
section test3 vstart=0x7c00
db '14'
section test4 follows=test1
db '15'
section .text start=100
db '16'

编译结果

数据传送指令

通用数据传送指令

mov

movsx

movzx

push

pop

pusha

popa

pushad

popad

bswap

xchg

cmpxchg

xadd

xlat

输入输出端口传送指令

目的地之传送指令

标志传送指令

算数运算指令

算数运算指令主要包括二进制的定点、浮点的加减乘除运算指令;求反、求补、加一、减一、比较指令;十进制加减运算指令等;不同计算机对算数运算指令的支持有很大的差别。

add

add <目的操作数>, <源操作数>

影响标志位:OF,SF,ZF,AF,PF,CF。
将源操作数加到目的操作数上(结果存储在目的操作数上)。
源操作数和目的操作数类型必须一致,且两者不能同时使用存储器操作数。

adc

adc <目的操作数>, <源操作数>

带进位加法指令,与 ADD 基本相同,区别在于执行指令前会将标志位 CF 的值加到目的操作数上,多用于多字节加法运算。

1
2
3
4
5
6
7
8
9
mov DX, 0x2FFF
mov AX, 0xFF00
add AX, 0x5678
adc dx, 0x1234
; 以上指令实现了多字节数 0x2FFFFF00 与 0x12345678 相加
; 其中 DX 存放 0x2FFF + 0x1234 AX 存放 0xFF00 + 0x5678
; 执行第三步时,低 8 位进行相加,0xFF00 + 0x5678 = 0x5578,此时进位标志位 CF=1
; 执行第四步时,高 8 位进行相加,0x2FFF + 0x1234 = 0x4233,之后加上进位标志位的进位数, DX = 0x4234
; 最终得到 0x2FFFFF00 + 0x12345678 = 0x42345578

inc

inc <目的操作数>

加一指令,将目的操作数加一。

sub

sub <目的操作数>, <源操作数>

用目的操作数减去源操作数(结果存储在目的操作数上)。
源操作数和目的操作数类型必须一致,且两者不能同时使用存储器操作数。

sbb

dec

dec <目的操作数>

减一指令,将目的操作数减一。

neg

neg <目的操作数>

用于求目的操作数的补码(取反再加一)。
可以通过寄存器或内存单元向 neg 指令传送目的操作数。

cmp

用于比较源操作数和目的操作数。

cmp 指令类似 sub 指令,只是不保存计算结果但对标志寄存器产生影响,其他指令可以通过识别这些被影响的标志寄存器位来得知比较结果。

在指令中,目的操作数是被测量的对象,源操作数则作为测量的基准。指令使用目的操作数减去源操作数并在不保存结果的情况下对标志寄存器产生影响。

会被产生影响的标志位有:溢出 符号 零 进位 辅助进位 奇偶 | OF SF ZF CF AF PF

1
2
3
4
mov ax, 8
mov bx, 3
cmp ax, bx
; 执行结果 ax = 8 | ZF = 0 | PF = 1 | SF = 0 | CF = 0 | OF = 0

aas

ads

mul

mul <源操作数>

无符号乘法运算,结果为整数。
mul 指令可以通过寄存器或内存单元接受一个 8 位或 16 位的乘数:
如果乘数是 8 位的:那么源操作数与寄存器 AL 中的 8 位数相乘得到的结果存储在 AX 中;
如果乘数是 16 位的:那么源操作数与寄存器 AX 中的 16 位数相乘得到的结果存储在 DX:AX 中;

mul 执行后,如果结果的高位全是零则 OF 和 CF 清零,否则置一,对 SF ZF AF 和 PF 标志位影响未定义。

imul

aam

div

div <源操作数>

用于进行无符号除法运算,结果为整数。
除数作为源操作数传入,存储在寄存器或内存单元中。
被除数默认存放在 AX(16 位以内)或 AX 和 DX(32 位,DX 存放高位,AX 存放低位)中。

div 操作的结果分为商和余数两部分。
如果除数是 8 位的,那么结果中的商存储在 AL 中,余数存储在 AH 中。
如果除数是 16 位的,那么结果中的商存储在 AX 中,余数存储在 DX 中。

执行条件:

  1. 被除数的高位必须小于除数(否则商无法存储);
  2. 若除数为 16 位,则在运行 div 指令前需要清零 DX 寄存器;

idiv

aad

cbw

cbw

将寄存器 AL 中数据的最高位扩展到 AH 中,若 AL 中最高位为 0,则 AH 被设置为 00H,若 AL 中的最高位为 1,则 AH 被设置为 FFH。

cwd

cwd

将寄存器 AX 中数据的最高位拓展到 DX中,若 AX 中最高位为 0,则 DX 被设置为 0000H,若 AX 中的最高位为 1,则 DX 被设置为 FFFFH。

cwde

cdq

逻辑运算指令

and

and <目的操作数>, <源操作数>

将目的操作数和源操作数进行按位逻辑与运算,结果存储在目的操作数。

or

or <目的操作数>, <源操作数>

将目的操作数和源操作数进行按位逻辑或运算,结果存储在目的操作数。

xor

xor <目的操作数>, <源操作数>

将目的操作数和源操作数进行按位逻辑异或运算,结果存储在目的操作数。

not

not <目的操作数>

将目的操作数按位取反,结果存储在目的操作数。

test

test <目的操作数>, <源操作数>

将目的操作数和源操作数进行按位逻辑与运算,不存储结果。

shl & sal

1
2
shl <目的操作数> <源操作数(移位次数)>
sal <目的操作数> <源操作数(移位次数)>

shl(逻辑左移)和 sal(算数左移)的实际效果完全相同。
两者的作用是:将目的操作数向左移位源操作数个位数,最低位用 0 填充,最高位移入进位标志位(CF)。

shr & sar

1
2
shr <目的操作数> <源操作数(移位次数)>
sar <目的操作数> <源操作数(移位次数)>

shr(逻辑右移)和 sar(算数右移)有所不同:
shr:高位用 0 填充,低位移入进位标志位(CF)。
sar:高位用符号位填充,低位移入进位标志位(CF)。

rol

rol <目的操作数>, <源操作数(移位次数)>

rol(Rotate Left):循环左移指令,将目的操作数左移指定次数,最高位送入最低位和进位标志位(CF)。

ror

ror <目的操作数>, <源操作数(移位次数)>

ror(Rotate Right):循环右移指令,将目的操作数右移指定次数,最低位送入最高位和进位标志位(CF)。

rcr

rcl

串指令

movs(movsb, movsw, movsd)

1
2
movsw     ; 执行一次
rep movsw ; 利用 rep 命令重复执行

串传送,从源地址向目的地址批量传送数据。
16 位模式下源地址是 DS:SI,目的地址是 ES:DI
32 位模式下源地址是 DS:ESI,目的地址是 ES:EDI
根据传送数据大小又分为 movsb, movsw, movsd,分别对应传送一个字节,一个字,一个双字。

movs 命令可以使用重复执行,方向标志位 DF 决定了 SI 和 DI 在单次操作后是增加(0)还是减少(1)
每次变动的大小与具体执行命令有关:movsb -> 1B | movsw -> 2B | movsd -> 4B

cmps

scas

lods

stos

rep

1
rep movsw

重复前缀指令,不能单独使用,可以用来重复执行跟在后面的指令,重复次数由 CX 控制(每次重复 CX 减一,知道 CX 值为零停止)。

repe & repz

repne & repnz

repc

repnc

程序转移指令

可以修改 IP 或同时修改 CS 和 IP 寄存器内容的指令统称为转移指令。可以通俗理解为:转移指令就是可以控制 CPU 下一步执行内存中哪一处指令的指令。

在 8086 中按照转移行为可分为:

  1. 段内转移:只修改 IP
    1. 短转移:IP 修改范围为 -128~127(2^7-1)
    2. 近转移:IP 修改范围为 -32768~32767(2^15-1)
  2. 段间转移(远转移):同时修改 CS 和 IP

按照功能不同,转移指令又可细分为一下几种:

无条件转移指令

描述:无条件转移指令可以控制 CPU 下一步执行代码段(CS)中任意内存地址对应的指令

offset

1
2
start: mov ax,offset start ; 相当于 mov ax,0
next: mov ax,offset next ; 相当于 mov ax,3 | 第一条指令长度为三个字节,因此 next 的偏移地址为 3

操作符,由编译器处理,功能是获取标签的偏移地址

jmp

描述

转移地址可以在指令、内存或寄存器中指出。可以只修改 IP,也可以同时修改 CS 和 IP

使用 jmp 指令时需要提供两种信息:

  1. 转移的目的地址
  2. 转移类型(段间转移(远转移)、段内短转移、段内近转移)
语法
  1. 转移目的地址在指令中

语法:jmp short <标签>
作用:转移到标签处执行指令
描述:这种格式的 jmp 指令实现的是段内短转移,short 为短转移标志
原理:ip = ip + 8 位位移 | 8 位位移 = 标签地址 - jmp 指令后第一个字节的地址
此指令形式是针对当前指令所在位置(即当前 IP)进行跳转的,且 8 位位移范围是 -128~127,由编译程序在编译时计算
示例:

1
2
3
4
5
6
7
start:
mov ax,0
jmp short next
add ax,1
next:
inc ax
; 最终结果 ax 内的值为 1

语法:jmp near ptr <标签>
作用:转移到标签处执行指令
描述:这种格式的 jmp 指令实现的是段内近转移,near 为近转移标志
原理:ip = ip + 16 位位移 | 16 位位移 = 标签地址 - jmp 指令后的第一个字节地址
此指令也是针对当前指令所在位置(即当前 IP)进行跳转的,且 16 位位移范围是 -32768~32767,由编译程序在编译时计算

语法:jmp far ptr <标签标签>
作用:转移到标签处执行命令
描述:这种格式的 jmp 指令实现的是段间转移(即远转移),far ptr 为远转移标志
原理:cs = 标签所在段的段地址 | ip = 标签所在段中的偏移 | 高位存储段地址,低位存储偏移地址

  1. 转移目的地址在内存中

语法:jmp word ptr <[内存单元地址]>
作用:转移到目标内存地址所存储的地址处执行指令
描述:这种格式的 jmp 指令实现的是段内转移,word ptr 是转移标志
原理:ip = 内存地址所存储的内容

语法:jmp dword ptr <[内存单元地址]>
作用:在内存单元地址处存放两个字,高地址存放转移的目的段地址,低地址存放转移的目的偏移地址
描述:这种格式的 jmp 指令实现的是段间转移(即远转移),dword ptr 为远转移标志
原理:cs = 内存单元地址 + 2 所存储的内容 | ip = 内存单元地址存储的内容
示例:

1
2
3
4
5
mov ax,0123H
mov ds:[0],ax
mov word ptr ds:[2],0
jmp dword ptr ds:[0]
; 执行后 cs = 0 | ip = 0123H
  1. 转移目的地址在寄存器中

语法:jmp <16位寄存器>
作用:转移到目标寄存器所存储的地址处执行指令
描述:这种格式的 jmp 指令实现的是段内转移
原理:ip = 16位寄存器内容

语法:jmp <[段地址:偏移地址]>
作用:转移到目标地址处执行命令
描述:这种格式的 jmp 执行实现的是段间转移
原理:cs = 段地址 | ip = 偏移地址

条件转移指令

jz(je) & jnz(jne) | 零判断(相等判断)

jz 和 je 意义相同,只是写法不同。
jnz 和 jne 意义相同,只是写法不同。

jz:如果标志位 ZF = 1,则跳转到指定地址。
jnz:如果标志位 ZF = 0,则跳转到指定地址。

ZF:零标志位,相关指令执行后结果是否为零 | 0 -> 否 | 1 -> 是。

jc & jnc | 进位判断

jc:如果标志位 CF = 1,则跳转到指定地址。
jnc:如果标志位 CF = 0,则跳转到指定地址。

CF:进位标志位,相关指令执行后是否产生了进位或借位 | 0 -> 没产生 | 1 -> 产生了。

jp(jpe) & jnp(jpo) | 奇偶判断

jp 和 jpe 意义相同,只是写法不同。
jnp 和 jpo 意义相同,只是写法不同。

jp:如果标志位 PF = 1,则跳转到指定地址。
jnp:如果标志位 PF = 0,则跳转到指定地址。

PF:奇偶标志位,相关指令执行后结果中为 1 的比特的个数是否为偶数 | 0 -> 奇 | 1 -> 偶。

js & jns | 正负判断

js:如果标志位 SF = 1,则跳转到指定地址。
jns:如果标志位 SF = 0,则跳转到指定地址。

SF:符号标志位,相关指令执行后结果是否为负数 | 0 -> 非负数 | 1 -> 负数。

jo & jno

jo:如果标志位 OF = 1,则跳转到指定地址。
jnp:如果标志位 OF = 0,则跳转到指定地址。

OF:溢出标志位,有符号运算结果是否产生溢出 | 0 -> 否 | 1 -> 是。

循环控制指令

终端指令

处理器控制指令

处理器控制指令包括标志操作指令和 CPU 控制指令

标志操作指令

stc & clc & cmc

stc:将 CF 设置为 1
clc:将 CF 设置为 0
cmc:将 CF 取反
CF:进位标志,计算中是否产生了进位或借位

std & cld

std:将 DF 设置为 1
cld:将 DF 设置为 0
DF:串处理指令中,每次操作后 SI 或 DI 自增(0)还是自减(1)

sti & cli

sti:将 IF 设置为 1
cli:将 IF 设置为 0
IF:中断允许标志,CPU 是否能响应外部课评比中断请求

参考

  1. Inno’s Blog:汇编语言学习笔记(九):转移指令的原理
  2. jasonM:一步步学习汇编(10)之jmp指令原理分析(破解软件的必修课)
  3. FullSky:8086运算、位移、处理器指令
  4. 知乎-愛是等待是细水长流:汇编语言–x86汇编指令集大全
  5. blubiu:汇编语言笔记(七)–DIV指令(除法指令)
  6. CSDN-YiShiWenYan:汇编中的PTR含义
  7. 退思园:关于汇编中的PTR操作符
  8. 511遇见: 汇编语言标志寄存器cmp 指令
  9. Demon’s Blog:汇编语言中SAR和SHR指令的区别
  10. C 语言中文网:汇编语言
  11. 享乐主:两类风格汇编语法对比
  12. red_rock:nasm指令详解