1. 数据的表示
1.1. 二进制数据的组织
名字 | 宽度 | 英文 | 定义方式 | C 语言中等价类型 |
---|---|---|---|---|
字节 | 8 bit | byte | db | (unsigned) char; |
字 | 16 bit | word | dw | (unsigned) short int; |
双字 | 32 bit | double word | dd | (unsigned) long int; float; |
四字 | 64 bit | quadruple word (qword) | dq | (unsigned) long long; double; |
十字节 | 80 bit | ten byte (tbyte) | dt | long double; |
在 data 段中可以定义变量和数组,对应语法如下:
data segment
s db "Hello, World" ; 字符串 char[13]
a dd 1, 2, 3, 4, 5 ; 数组 int[5]
b dw 5 dup(0) ; 数组 short int[5],初始值全为0
c db 0FFh; ; 值为0FFh的字符 char
f dd 3.14 ; 值为3.14的浮点数 float
data ends
- 用
dup(数)
来表示数组,用?
表示无初始值或初始值任意。 - 在定义字符串时,可以通过
0Dh
、0Ah
来表示换行。
1.2. 二进制浮点数的表示
略。
2. 数据的运算
2.1. 零扩充和符号扩充
- 当把低位无符号数赋给高位无符号数时,必须采用零扩充规则,即在左边补 0。
- 当把低位有符号数赋给高位有符号数时,必须采用符号扩充规则,即在左边补上原数的最高位(符号位)。
2.1.1. cbw
、cwd
符号扩充
cbw
,即 convert byte to word,可将 AL 中的值符号扩充到 AX 中cwd
,即 convert word to double word,可将 AX 中的值符号扩充到 DX:AX 中
2.1.2. movzx
零扩充
movzx dest, src
把 src 零扩充到寄存器 dest 中。指令格式:
movzx reg16, reg8|reg8
movzx reg32, reg8|mem8|reg16|mem16
2.1.3. movsx
符号扩充
movsx dest, src
功能与格式与 movzx
类似,区别在于 movsx
是符号扩充。
2.2. 整数运算
2.2.1. 加法 add
/ inc
/ adc
add dest, src
:dest += src
adc dest, src
(带进位加):dest += src + CF
inc op
:op++
add
/ adc
指令若发生进位会将 CF 寄存器置 1,否则将 CF 寄存器置 0。inc
指令不会影响 CF 寄存器
2.2.2. 减法 sub
/ sbb
/ dec
/ neg
sub dest, src
:dest -= src
sbb dest, src
(带错位减):dest -= src + CF
(dest = dest - src - CF
)dec op
:op--
neg op
(取相反数):op = -op
2.2.3. 乘法 mul
/ imul
用 mul
进行无符号整数乘法。
- 当 mul 后跟一个 8 位寄存器或 8 位变量作为乘数时,被乘数一定是 AL,乘积一定是 AX。
- 当 mul 后跟一个 16 位寄存器或 16 位变量作为乘数时,被乘数一定是 AX,乘积一定是 DX:AX。
- 当 mul 后跟一个 32 位寄存器或 32 位变量作为乘数时,被乘数一定是 EAX,乘积一定是 EDX:EAX。
用 imul
进行有符号整数乘法,其第一类用法和 mul
的上述用法相同。第二类用法可以包含两个或三个操作数,其中前两个操作数可以是寄存器或变量,第三个操作数只能是常数:
imul eax, ebx
:EAX = EAX × EBXimul eax, ebx, 3
:EAX = EAX × EBX × 3
2.2.4. 除法 div
/ idiv
用 div
进行无符号整数除法。
- 当 div 后跟一个 8 位寄存器或 8 位变量作为除数时,被除数一定是 AX,商是 AL,余数是 AH
- 当 div 后跟一个 16 位寄存器或 16 位变量作为除数时,被除数一定是 DX:AX,商是 AX,余数是 DX
- 当 div 后跟一个 32 位寄存器或 32 位变量作为除数时,被除数一定是 EDX:EAX,商是 EAX,余数是 EDX
除法溢出有来两种情形:
- 除数是 0。
- 被除数除以除数的商无法保存到相应寄存器中。
当发生除法溢出时,会在 div
指令上方插入并调用一条 int 00h
中断指令(默认行为:显示 divide overflow 并强行结束程序运行)。如果修改 0:0 处的中断向量,则可以在发生中断时转到我们自己的中断函数,对除法溢出的问题进行处理。iret
后会重新执行除法指令。
用 idiv
进行有符号整数除法,用法参考 div
。
2.3. 浮点数运算
fld
压入小数类型的变量。fild
把整数类型的变量转化为小数类型并压入。fst
把小数寄存器 st(0) 保存到变量中。fstp
把小数寄存器 st(0) 保存到变量中并弹出 st(0)。fadd
/fsub
/fmul
/fdiv
进行浮点数的四则运算;如用fmul st, st(1)
进行乘法计算。
CPU 内部一共有 8 个小数寄存器,分别叫做 st(0)、st(1)、…、st(7)。其中 st(0)简称 st 这 8 个寄存器的宽度均达到 80 位,相当于 C 语言中的 long double
类型。
2.4. 位运算
2.4.1. 逻辑运算
指令 | 用法 | 含义 | C 语言等价运算符言 |
---|---|---|---|
and |
add ax, bx |
与 | & |
or |
or ax, bx |
或 | | |
xor |
xor ax, ax |
异或 | ^ |
not |
not ax |
取反 | ~ |
2.4.2. 逻辑移位 shl
、shr
适用于无符号数的移位运算,空位补 0,最后一个被移动的位会被放到 CF 标志中。
2.4.3. 算术移位 sal
、sar
适用于有符号数的移位运算。sal
与 shl
完全相同,sar
会在被操作数是负数时补 1,否则行为和 shr
相同。同样的,最后一个被移动的位会被放到 CF 标志中。
2.4.4. 循环移位 rol
、ror
对应变量进行循环移位操作,即被移出的值会从另一侧填补到数中。注意最后一个被移动的位会在被放到另一端的同时放到 CF 标志中。
2.4.5. 带进位循环移位 rcl
、rcr
同样是循环移位操作,但会把 CF 标志看作其中一位参与运算(如被操作数宽度为 16 位,可看作一个长度为 17 的环)。
2.5. 字符串操作与运算
strcpy(target, source); 永远按正方向复制 memcpy(target, source); 永远按正方向复制 memmove(target, source); 会自行选择方向保证部分重叠时正确 Fun Fact:C 语言中的三个关于字符串的库函数
字符串指令的方向会被 DF 标志控制,当 DF=0 时会按照正方向(低地址到高地址)运行,当 DF=1 时会按照反方向(高地址到低地址)运行。可通过执行 cld
和 std
来控制。
2.5.1. 字符串复制指令 rep movsb/w/d
rep movsb
,即 move string in byte,会以字节(byte)为单位复制字符串。如果将 movsb
换成 movsw
和 movsd
,则分别会以 word、double word 为单位复制字符串。
在执行 repo movsb
执行前需要做以下准备工作:
- DS:SI 指向源字符串(SI 就是 source index)
- ES:DI 指向目标字符串(DI 就是 destination index)
- CX 寄存器存储移动次数
- DF 标志表示方向(如果)
rep movsb
的行为的伪代码如下:
again:
if (cx == 0) goto done
asm mov byte ptr es:[di], byte ptr es:[do] // movsw/d的话这里就换为word/dword
df == 0 ? (si++, di++) : (si--, di--) // movsw/d的话这里就需要增减2/4
cx--
goto again
done:
2.5.2. 字符串比较指令 repe/ne cmpsb/w/d
单条 cmpsb
指令的行为如下:
asm cmp byte ptr ds:[si], byte ptr es:[di]
df == 0 ? (si++, di++) : (si--, di--)
repe
和 repne
则控制了 cmp
比较结果的行为,如 repe
的行为如下(若本次比较相等则继续比较下一个):
again:
if (cx == 0) goto done
asm cmp byte ptr ds:[si], byte ptr es:[di]
df == 0 ? (si++, di++) : (si--, di--)
cx--
if (zf == 1) goto again // repne的话这里就要求ZF=0
done:
2.5.3. 字符串搜索指令 repe/ne sacsb/w/d
这条字符串指令就只和 ES:DI (注意不是 DS:SI 哦)所指向的字符串有关,CPU 将在其中搜索 AL/AX/EAX 的值(取决于是 sacsb
还是 sacsw
或 sacsd
)。
repe sacsb
的行为如下:
again:
if (cx == 0) goto done
asm cmp al, byte ptr es:[di] // sacsw/d的话这里还要换成AX/EAX
df == 0 ? (di) : (di--)
cx--
if (zf == 1) goto again // repne的话这里就要求ZF=0
done:
2.5.4. 写入字符串指令 rep stosb/w/d
把 AL/AX/EAX 的值写入 ES:DI 指向的目标字符串中。
2.5.5. 读取字符串指令 lodsb/w/d
从 DS:SI 所指向的源字符串中读取一个 byte/word/double word 保存到 AL/AX/EAX。
例:用
lodsb
、stosb
指令过滤掉字符串中的空格并存储到另一字符串中data segment
s db "Ada Lovelace is often regarded as "
db "the first computer programmer."
slen = $ - offset s
t db slen dup('T')
data ends
code segment
assume cs:code, ds:data
main:
mov ax, data
mov ds, ax
mov es, ax
mov si, offset s; ds:si->s[0]
mov di, offset t; es:di->t[0]
mov cx, slen ; CX = length of s
cld ; DF = 0
jcxz done ; jump if cx is zero
again:
lodsb ; AL=ds:[si], si++
cmp al, ' '
je skip
stosb ; es:[di]=AL, di++
skip:
loop again ; dec cx
; jnz again
done:
xor al, al ; AL = 00h
stosb ; es:[di]=AL, di++
mov ah, 4Ch
int 21h
code ends
end main