汇编程序设计
汇编程序
汇编程序的全过程:
- 汇编程序示例:
assume cs:codesg
codesg segment
mov ax, 0123h
mov bx, 0456h
add ax, bx
add ax, ax
mov ax, 4C00h
int 21h
codesg ends
end
编译连接后生成的可执行
.exe
文件主要包含两部分内容:
- 程序(从源程序中的汇编指令翻译过来的机器码)和数据(源程序中定义的数据)
- 相关的描述信息(程序的大小、内存空间的占用等)
伪指令
在汇编语言中,有两种指令:汇编指令、伪指令
汇编指令:有对应的机器码的指令,可以被编译为机器指令,最终被 CPU 执行
伪指令:没有对应的机器指令,不会被 CPU 执行,由编译器来执行
segment 和 ends
segment
和ends
是一对成对使用的伪指令,是写汇编程序必需要用到的
segment
说明一个段开始,ends
说明一个段结束,一个段必须要有一个名称来标识
段名 segment
......
段名 ends
- 一个汇编程序由多个段组成(分别用于划分指令、数据、栈),并且一个有意义的汇编程序至少要有一个段(代码段)
注意不要弄混
ends
和end
ends 表示一个段的结束,与 segment 配对使用(ends 可以理解为 end segment)
end 表示整个程序的结束
proc 和 endp
proc 指令可以用于子程序的定义。将具有某种功能的程序段看作一个过程(子程序),它可以被别的程序调用(
call
),类似于 C 语言的函数
- proc 中属性
near
和far
决定调用程序和子程序是否在同一代码段
过程名 proc near/far
...
ret
过程名 endp
属性 | 意义 |
---|---|
near (段内近调用) | 调用程序和子程序在同一代码段中,只能被相同代码段的其他程序调用 |
far (段间远调用) | 调用程序和子程序不在同一代码段中,可以被相同或不同代码段的程序调用 |
end
汇编结束命令。该伪指令是汇编语言结束的标志,对于在
end
之后的汇编指令不予处理,不和其他伪指令成对使用,且只可以有一个end
指令,否则编译器会报错
org
汇编起始指令。
org
可以用于规定该伪指令下面的程序数据的起始偏移地址,数据被连续存放在此后的地址内,直到下一条org
指令为止汇编程序中若没有
org
伪指令,则程序执行时,指令代码被放到自由内存空间的 CS:0 处;若有org
伪指令,则编译器把其后的指令代码放到 org 伪指令指定的偏移地址处。两个org
伪指令之间,除了指令代码,若有自由空间,则用 0 填充
org
指令可放在程序的任何位置。但是注意:**org
指令按出现的顺序其后的地址必须依次增大,且不能重叠,否则编译器可能报错**
用法:
org 16位地址
示例:
assume cs:code, ds:data
data segment
org 1500h
Test1 db 12h, 34h ; Test1 变量的偏移地址为 1500h,即:ds:1500
org 3000h
Test2 dw 3040h, 2830h ; Test2 变量的偏移地址为 3000h,即:ds:3000
data ends
code segment
org 200h ; 此段代码段起始地址偏移量为 200h,即:cs:200
start: mov ax, data
mov ds, ax
mov ax, 4C00h
int 21h
code ends
end start
- Debug 测试:
- 可以看到不加
org 200h
时,程序入口从 CS:0 开始:
加上 org 200h
后,将程序入口的偏移地址从 CS:0 修改到 CS:200 处:
- 可以看到不加
org 1500h
和org 3000h
时,Test1 和 Test2 从 DS:0 处依次存放:
加上 org 1500h
和 org 3000h
后
Tset1 被存放在 DS:1500 处,Test2 被存放在 DS:3000 处:
equ
代码替换指令。类似于 C 语言中的
#define
,equ
可以用于把一个符号名称与一个整数表达式或一个任意文本连接起来用法:
名字 EQU 表达式
- equ 指令的使用大致有三种,当汇编器在程序后面遇到
name
时,它就用整数值或文本来代替符号:name equ expression
,expression 必须是一个有效整数表达式name equ symbol
,symbol 是一个已存在的符号名称,已经用 = 或 equ 定义过了name equ <text>
,任何文本都可以岀现在 <······> 内
; 以下指令等价于 mov cx, [bx + si]
s equ bx+si
mov cx,[s]
---------------------------------------------------------------
word1 equ this word ; 给后面的字节存储单元取一个字属性的符号名
byte1 db 12h, 21h
flag dw 1234h
flag1 equ byte ptr flag ; 给 flag 的低字节取一个字节属性的符号名
flag2 equ byte ptr flag + 1 ; 给 flag 的高字节取一个字节属性的符号名
; 有了上述定义后,可编写如下语句:
mov ax, word1 ; 执行后,(ax) = 2112h
mov bl, flag1 ; 执行后,(bl) = 34h
---------------------------------------------------------------
pressKey equ <"Press any key to continue...", 0>
PI equ <3.1415926>
assume
assume
表示假设,它假设某一段寄存器和程序中的某一个segment
段相关联
- 例如,定义一个段 codesg,如果这个段我想用来存放代码,那么它就是一个代码段,但是编译器并不知道我想用来作为代码段,所以需要使用
assume
来假设
assume cs:codesg ; 将 codesg 段与 cs 段寄存器关联起来
codesg segment
......
codesg ends
db/dw/dd/df/dq/dt
db、dw、dd、df、dq、dt 用于定义数据类型,区别在于每一个数据的长度不一样,占用大小不一样
参数 | 每个数据占用大小 | 示例 |
---|---|---|
db | 1 字节 | db 1Ah, 2Bh, 3Ch, 4Dh, 5Eh, 6Fh, 77h, 88h, 99h, 00h |
dw | 2 字节 | dw 1A2Bh, 3C4Dh, 5E6Fh, 7788h, 9900h |
dd | 4 字节 | dd 1A2B3C4Dh, 5E6F7788h |
df | 6 字节 | df 1A2B3C4D5E6Fh |
dq | 8 字节 | dq 1A2B3C4D5E6F7788h |
dt | 10 字节 | dt 1A2B3C4D5E6F77889900h |
- 示例
data segment
db 1
dw 1
dd 1
data ends
01 01 00 01 00 00 00 00 00 00 00
第一个数据为:01h,在 data:0
处,占 1 个字节
第一个数据为:0001h,在 data:1
处,占 1 个字,2 个字节
第一个数据为:00000001h,在 data:3
处,占 2 个字,4 个字节
dup
dup
是一个操作符,与 db、dw、dd 一样,由编译器识别处理dup 与 db、dw、dd 等数据定义伪指令配合使用,用来进行数据的重复
- 用法:
db 重复的次数 dup (重复的字节型数据)
dw 重复的次数 dup (重复的字型数据)
dd 重复的次数 dup (重复的双字型数据)
- 示例:
db 3 dup (0)
; 定义了 3 个字节,每个字节值都为 0,相当于 db 0, 0, 0
db 3 dup(0, 1, 2)
; 定义了 9 个字节,相当于 db 0, 1, 2, 0, 1, 2, 0, 1, 2
db 3 dup('abc', 'ABC')
; 定义了 18 个字节,相当于 db 'abcABCabcABCabcABC'
type/seg/length/size/offset
用法 | 含义 |
---|---|
type 变量名 | 获取变量的类型,如果是 db 定义的则为 1,dw 定义的则为 2,以此类推 |
seg 变量名 | 获得变量的段地址 |
length 变量名 | 如果变量是 dup 复制的,返回分配的单元数,其他情况都为 1,但是嵌套的 dup 复制的数据不能据此得到正确的变量数 |
size 变量名 | size = type * length |
offset 变量名 | 获得变量的偏移地址 |
- 示例 1:
assume cs:codesg, ds:dataseg
dataseg segment
str db 'hello'
arry db '4ss1du0us'
dataseg ends
codesg segment
start:
mov ax, dataseg
mov ds, ax
mov bx, type str ; bx = 1
mov bx, seg arry ; bx = (ds)
mov bx, size str ; bx = 1
mov bx, offset arry ; bx = 5
mov ax, 4c00h
int 21h
codesg ends
end start
- Debug 测试 1:
- 示例 2:
assume cs:codesg
codesg segment
start:
mov ax, offset start ; 标号 start 偏移地址为 0
s:
mov ax, offset s ; 标号 s 偏移地址为 3,第一条指令长度为 3 字节
mov ax, 4c00h
int 21h
codesg ends
end start
- Debug 测试 2:
byte/word/dword
byte、word、dword 用于对存储单元的类型进行规定
- 示例:
mov byte ptr [di], 00h ; 使 DI 所指向的字节单元清 0
mov word ptr [1000h], 00h ; 使 ds:1000 所指向的字单元清 0
jmp dword ptr [2000h] ; 使程序跳转到 ds:2000 开始的 2 个字单元对应的转移地址处
一些约定符号
描述符号 ()
为了描述简洁,可以使用一个描述性符号
"()"
来表示一个寄存器或一个内存单元中的内容
例如:
(ax)
表示 ax 寄存器中的内容(al)
表示 al 寄存器中的内容(2000h)
表示内存 20000h 单元的内容【() 中的内存单元的地址为物理地址】在
"()"
中的元素可以有三种类型:
① 寄存器名;② 段寄存器名;③ 内存单元的物理地址(一个 20 位数据)用
"()"
所表示的数据可以有两种类型:
① 字节型;② 字型
数据类型由寄存器名或具体的计算决定,例如:(al)
为字节型;(ax)
为字型;(al) = (20000h)
,则(20000h)
得到的是字节型;(ax) = (20000h)
,则(20000h)
得到的是字型
常量符号 idata
为了方便,用
idata
表示常量
- 例如:
mov ax, [idata]
就代表mov ax, [1]
、mov ax, [2]
、mov ax, [3]
等mov bx, idata
就代表mov bx, 1
、mov bx, 2
、mov bx, 3
等
访问内存单元
[BX]
[BX]
表示一个内存单元,它的偏移地址在 BX 中,段地址默认在 DS 中
- 内存单元的长度(类型)可以由具体指令中的其他操作对象指出:
mov ax, [bx] ; 将一个内存单元的内容送入ax,这个内存单元长度为2字节(字单元)
mov al, [bx] ; 将一个内存单元的内容送入al,这个内存单元长度为1字节(字节单元)
- 利用
[BX]
进行数据的传送:
; bx中存放的数据作为偏移地址EA,段地址SA默认在DS中
mov ax, [bx]
; 将 SA:EA 处的数据送入ax,即:(ax) = ((ds) * 16 + (bx))
mov [bx], ax
; 将ax中的数据送入 SA:EA 处,即:((ds) * 16 + (bx)) = (ax)
- 执行如下代码,内存中的变化如图:
mov ax, 2000h
mov ds, ax
mov bx, 1000h
mov ax, [bx]
inc bx
inc bx
mov [bx], ax
inc bx
inc bx
mov [bx], ax
inc bx
mov [bx], al
inc bx
mov [bx], al
注意,编译器和 Debug 对
mov al, [idata]
这类指令的解释是不同的:
- 编译器将
[idata]
解释为常数 idata- Debug 将
[idata]
解释为一个内存单元,idata 是内存单元的偏移地址在源程序中访问
2000:0
内存单元的两种方法:mov ax, 2000h mov ds, ax ; 段地址 2000h 送入ds mov bx, 0 ; 偏移地址 0 送入bx mov al, [bx] ; ds:bx 单元中的数据送入al
mov ax, 2000h mov ds, ax ; 段地址2000h送入ds mov al, ds:[0] ; ds:[0] 单元中的数据送入al
汇编源程序中一些指令的含义:
mov al, [0] ; (al) = 0,将常量 0 送入 al,与 mov al, 0 含义相同 mov al, ds:[0] ; (al) = ((ds) * 16 + 0),将内存单元的数据送入 al mov al, [bx] ; (al) = ((ds) * 16 + bx),将内存单元的数据送入 al mov al, ds:[bx] ; 与 mov al, [bx] 含义相同
- 如果直接使用
[idata]
来表示偏移地址,那么[idata]
前必须要显式给出段地址所在的段寄存器,例如:mov al, ds:[idata]
否则,编译器会将mov al, [idata]
解释为mov al, idata
,只是表示一个常数,而不是偏移地址- 如果使用寄存器
[bx]
来表示偏移地址,则段地址默认在 DS 中,可以显式给出段地址所在的段寄存器,也可以不给出,例如:mov al, [bx]
[BX + idata]
除了使用
[BX]
以外,还可以更灵活地使用[BX + idata]
来表示一个内存单元,它的偏移地址为(BX) + idata
,即:BX 中的数据加上 idata段地址默认在 DS 中
例如,指令
mov ax, [bx + 200]
表示将一个段地址在 DS 且偏移地址为 BX 中的数据加上 200 的地址处的两字节数据内容送入 AX。除此之外,该指令还可以写为:
①mov ax, [200 + bx]
②mov ax, 200[bx]
③mov ax, [bx] . 200
示例如下:
2000:1000 BE 00 06 00 6A 22
mov ax, 2000h
mov ds, ax
mov bx, 1000h
mov ax, [bx] ; ax = 00BEh
mov cx, [bx + 1] ; cx = 0600h
add cx, [bx + 2] ; cx = 0600h + 0006h = 0606h
[BX + SI] 和 [BX + DI]
除了
[BX]
和[BX + idata]
以外,还可以配合 SI 和 DI 实现更为灵活的方式:[BX + SI]
和[BX + DI]
例如,指令
mov ax, [bx + si]
表示将一个段地址在 DS 且偏移地址为 BX 中的数据加上 SI 中的数据的地址处的两字节数据内容送入 AX。除此之外,该指令还可以写为:mov ax, [bx][si]
示例如下:
2000:1000 BE 00 06 00 6A 22
mov ax, 2000h
mov ds, ax
mov bx, 1000h
mov si, 0
mov ax, [bx + si] ; ax = 00BEh
inc si
mov cx, [bx + si] ; cx = 0600h
inc si
mov di, si
add cx, [bx + di] ; cx = 0600h + 0006h = 0606h
[BX + SI + idata] 和 [BX + DI + idata]
与上面的其他方式类似,不再赘述
例如,指令
mov ax, [bx + si + idata]
还可以写为:
①mov ax, [bx + 200 + si]
②mov ax, [200 + bx + si]
③mov ax, 200[bx][si]
④mov ax, [bx] . 200[si]
⑤mov ax, [bx][si] . 200
示例如下:
2000:1000 BE 00 06 00 6A 22
mov ax, 2000h
mov ds, ax
mov bx, 1000h
mov si, 0
mov ax, [bx + 2 + si] ; ax = 0006h
inc si
mov cx, [bx + 2 + si] ; cx = 6A00h
inc si
mov di, si
add cx, [bx + 2 + di] ; cx = 6A00h + 226Ah = 8C6Ah
操作数寻址方式
总结一下各种定位内存地址的方法,这些不同的方法也可以称为寻址方式
注意:在 8086CPU 中,只有 BX、SI、DI、BP 这四个寄存器可以用在 [···] 中进行内存单元的寻址,且只有 BX 与 SI、BX 与 DI、BP 与 SI、BP 与 DI 四种组合,即:BX 与 BP、SI 与 DI 的组合是不合法的
例如,以下指令都是正确的:
mov ax, [bx]
mov ax, [bx + si]
mov ax, [bx + di]
mov ax, [bp]
mov ax, [bp + si]
mov ax, [bp + di]
mov ax, [si]
mov ax, [di]
以下指令都是错误的:mov ax, [cx]
mov ax, [ax]
mov ax, [dx]
mov ax, [ds]
mov ax, [bx + bp]
mov ax, [si + di]
如果使用 BX 寄存器,段地址默认在 DS 中;如果使用 BP 寄存器,段地址默认在 SS 中
立即寻址
操作数就在指令中(紧跟在操作码之后)。立即数只能作为源操作数,并且长度要与目的操作数(寄存器)的长度一致
操作数作为指令的一部分存放在代码段里,当机器从内存取指令到 CPU 时,操作数就连同一起被取走,当 CPU 执行这条指令时就可以立即得到操作数,而不用再到内存中去取,因此称为立即寻址
- 示例:
mov al, 6h ; 执行指令后 al = 06h
mov ax, 12AFh ; 执行指令后 ax = 12AFh,其中 ah = 12h,al = AFh
寄存器寻址
操作数是寄存器中存放的值。在指令中给出寄存器名,对于 16 位操作数可以是:AX、BX、CX、DX、SI、DI、SP、BP 等,对于 8 位操作数可以是:AH、AL、BH、BL、CH、CL、DH、DL
由于寄存器寻址中,操作数在 CPU 内部的寄存器中,指令在执行时不需要访问内存,因此执行速度更快(与立即寻址不同的是,立即数是指令的一部分,而寄存器寻址中的操作数在 CPU 内部的寄存器中)
- 示例:
mov al, bl ; 执行指令后 al = bl,bl 不变
mov ax, bx ; 执行指令后 ax = bx,bx 不变
直接寻址
指令中直接给出操作数的偏移地址。指令中直接给出了操作数的偏移地址,当指令被取到 CPU 执行时,CPU 就可以马上从指令中获取偏移地址。
如果指令中没有使用段前缀指明操作数的段地址,则默认为 DS 寄存器,CPU 会根据段地址和偏移地址计算出物理地址,再从物理地址中取出操作数
- 例如:
2000:4045 BE 00 06 00 6A 22
mov ax, 2000h
mov ds, ax
mov al, [4050h] ; 执行指令后 al = BEh(一字节)
mov ax, [4050h] ; 执行指令后 ax = 00BEh(两字节)
寄存器间接寻址
操作数的偏移地址存放在寄存器中。与寄存器寻址不同,寄存器间接寻址不是将寄存器中的内容直接作为操作数,而是将寄存器中的内容作为偏移地址,操作数存放在内存中。
寄存器间接寻址只支持 BX、BP、SI、DI(BX、SI、DI 默认 DS 作为段地址,BP 默认 SS 作为段地址)
可以用寄存器间接指向一个内存单元,寄存器的值不同,指向的内存单元地址就不同,常用于循环
- 例如:
mov ax, [bx] ; 默认 DS 寄存器作为段地址
mov dx, [bp] ; 默认 SS 寄存器作为段地址
mov es:[di], ax ; 指定 ES 寄存器作为段地址
寄存器相对寻址
操作数的偏移地址是一个寄存器和位移量之和。与寄存器间接寻址不同,偏移地址的构成除了寄存器以外,还要加上位移量
寄存器相对寻址只支持 BX、BP、SI、DI(BX、SI、DI 默认 DS 作为段地址,BP 默认 SS 作为段地址)
特别适用于访问一维数组,寄存器可以作为数组的下标,利用修改寄存器的值来定位数组中的元素
- 例如,以下三条指令是等效的:
mov ax, arry[bx]
mov ax, [arry][bx]
mov ax, [arry + bx]
其中,位移量 arry
通常是 16 位(与 16 位寄存器匹配)的变量(也可以是常量),操作数的偏移地址由 arry
的偏移地址加上 bx 的值构成
- 例如,在如下指令中,
buf
是一个 8 位(与 8 位寄存器匹配)的变量(也可以是常量):
mov al, buf[bx]
mov al, [bx + 8]
- 示例:
assume cs:codesg, ds:dataseg
dataseg SEGMENT
arry db 1Ah, 2Bh, 3Ch, 4Fh, 5Eh, 6Dh
dataseg ENDS
codesg SEGMENT
start:
mov ax, dataseg
mov ds, ax
; 以下写法都是等效的
mov dl, arry[0] ; dl = 1Ah
mov dl, [arry][1] ; dl = 2Bh
mov dl, [arry + 2] ; dl = 3Ch
mov dl, [arry].3 ; dl = 4Fh
mov dl, 4[arry] ; dl = 5Eh
mov dl, [5 + arry] ; dl = 6Dh
mov ah, 2
int 21h
codesg ENDS
END start
基址变址寻址
操作数的偏移地址是一个基址寄存器和一个变址寄存器的内容之和。
支持的基址寄存器为:BX、BP,变址寄存器为:SI、DI(BX、SI、DI 默认 DS 作为段地址,BP 默认 SS 作为段地址)
这种寻址方式可用于数组的处理,将数组的首地址放在基址寄存器,修改变址寄存器,以此来定位数组中的元素。由于基址寄存器和变址寄存器都可以修改,所以访问数组元素更加灵活
- 例如:
mov ax, [bx][si] ; 默认 DS 寄存器作为段地址
mov ax, [bp][di] ; 默认 SS 寄存器作为段地址
mov ax, es:[bx][di] ; 指定 ES 寄存器作为段地址
相对基址变址寻址
操作数的偏移地址是一个基址寄存器和一个变址寄存器的内容以及一个位移量之和。位移量可以是一个常量,也可以是一个符号地址
支持的基址寄存器为:BX、BP,变址寄存器为:SI、DI(BX、SI、DI 默认 DS 作为段地址,BP 默认 SS 作为段地址)
这种寻址方式可用于二维数组的处理,数组的首地址为 arry,基址寄存器指向数组的行,变址寄存器指向该行的某个元素,以此来定位数组中的元素。
- 例如:
mov ax, arry[bx][si] ; 默认 DS 寄存器作为段地址
包含多个段的程序
在代码段中使用数据
将数据定义在程序最前面,为避免编译器将数据当成指令执行,需要使用
"标号"
、"end 标号"
来指明程序入口程序框架如下:
assume cs:code code segment ... 数据 ... start: ... 代码 ... code ends end start
- 示例,使用 loop 循环对 8 个字型数据进行累加:
assume cs:code
code segment
dw 0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h
start: mov bx, 0
mov ax, 0
mov cx, 8
s: add ax, cs:[bx]
add bx, 2
loop s
mov ax, 4c00h
int 21h
code ends
end start
由于
dw
(define word)定义的数据位于代码段的最开始(这里是CS:0 ~ CS:F
),所以需要使用"end 标号"
来指明汇编程序的入口(这里是end start
),即:将start
处的mov bx, 0
作为程序的第一条指令,从这里开始运行程序(如果不加标号指明程序入口,编译器会将dw
定义的数据也当作指令的机器码来进行翻译,导致结果错误)
"end 标号"
指明汇编程序的入口的实现原理:
- 在编译、链接后,由
end start
指明的程序入口会被转化为一个入口地址,存储在可执行文件的描述信息中;- 当程序被加载入内存后,加载者从可执行文件的描述信息中读到程序的入口地址,从而设置
CS:IP
指向该地址,CPU 就会从我们所希望的地址处开始执行
在代码段中使用栈
使用 SS 段寄存器,将栈顶单元的偏移地址放在 SP 中(初始时指向栈底),即可将一段空间作为栈来使用
- 示例,使用 loop 循环配合入栈、出栈将数据逆序存放
assume cs:codesg
codesg segment
dw 0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h
dw 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
; 用 dw 定义16个字型数据,在程序加载后,将取得16个字的内存空间
; 在后面的程序中将这段空间当作栈来使用
; 数据在 CS:0 ~ CS:F,栈空间在 CS:10 ~ CS:2F
start: mov ax, cs
mov ss, ax
mov sp, 30h ; 初始时栈顶指针 SS:SP 指向栈底,即 CS:30
mov bx, 0
mov cx, 8
s: push cs:[bx]
add bx, 2
loop s ; 依次入栈
mov bx, 0
mov cx, 8
s0: pop cs:[bx]
add bx, 2
loop s0 ; 依次出栈
mov ax, 4c00h
int 21h
codesg ends
end start
将数据、代码、栈放入不同的段
在 8086CPU 中,一个段的容量不能大于 64KB,所以如果数据、栈和代码需要的空间超过 64 KB 就不能放在一个段中(这是 8086CPU 的限制,但并不是所有处理器都这样)
- 将上一小节的代码进行改写:
assume cs:code, ds:data, ss:stack
data segment ; 数据段
dw 0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h
data ends
stack segment ; 栈段
dw 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
; 用 dw 定义16个字型数据,在程序加载后,将取得16个字的内存空间
; 在后面的程序中将这段空间当作栈来使用
; 数据在 CS:0 ~ CS:F,栈空间在 CS:10 ~ CS:2F
stack ends
code segment ; 代码段
start: mov ax, stack
mov ss, ax
mov sp, 20h ; 初始时栈顶指针 SS:SP 指向 stack:20,栈中共 32 字节数据,从 0 ~ 31,栈顶指针指向 32
mov ax, data
mov ds, ax ; ds 指向 data 段
mov bx, 0 ; ds:bx 指向 data 段中的第一个单元
mov cx, 8
s: push [bx]
add bx, 2
loop s ; data 段中的数据依次入栈
mov bx, 0
mov cx, 8
s0: pop [bx]
add bx, 2
loop s0 ; 依次出栈到 data 段的 0~15 单元
mov ax, 4c00h
int 21h
code ends
end start
数据表示和数据处理
以字符形式给出数据
在汇编程序中,可以用 ‘···’ 的方式指明数据是以字符的形式给出的,编译器将把这些字符转换为相对应的 ASCii 码
- 示例:
assume cs:code, ds:data
data segment ; 数据段
db 'unIX'
db 'foRK'
data ends
code segment ; 代码段
start: mov al, 'a'
mov bl, 'b'
mov ax, 4c00h
int 21h
code ends
end start
- 在程序中,
db 'unIX'
相当于db 75h, 6Eh, 49h, 58h
(分别对应'u'
、'n'
、'I'
、'X'
的 ASCii 码),db 'foRK'
相当于db 66h, 6Fh, 52h, 4Bh
(分别对应'f'
、'o'
、'R'
、'K'
的 ASCii 码) - 在程序中,
mov al, 'a'
相当于mov al, 61h
('a'
的 ASCii 码为 61h),mov bl, 'b'
相当于mov bl, 62h
('b'
的 ASCii 码为 62h)
字母的大小写转换
根据 ASCii 码来看
大写字母与小写字母的 ASCii 码间隔 20h,即:'a' - 'A' = 20h
所以可以根据 ASCii 码的加减来将字母进行大小写转换
但是,这样必须事先判断该字母是大写还是小写,若是小写则 - 20h,若是大写则 + 20h根据二进制形式来看
大写字母与小写字母的 ASCii 码间隔 20h,也就是 20h = 32d = $2 ^ 5$,仅二进制形式的第 5 位不相同
例如:'a' = 61h = 01100001b
,'A' = 41h = 01000001b
,所以:
① 将小写字母转换为大写,只需要将第 5 位的 1 置为 0,其他位不变
即:and 11011111b(and DFh)
② 将大写字母转换为小写,只需要将第 5 位的 0 置为 1,其他位不变
即:or 00100000b(or 20h)
示例,将 'BaSiC'
中的小写字母变为大写,将 'MinIX'
中的大写字母变为小写
① 以 C 语言来描述:
char a[5] = 'BaSiC'
char b[5] = 'MinIX'
int main()
{
int i = 0;
do
{
a[i] = a[i] & 0xDF; ; 将 'BaSiC' 中的小写字母变为大写
b[i] = b[i] | 0x20; ; 将 'MinIX' 中的大写字母变为小写
i++;
} while(i < 5);
return 0;
}
② 以 [BX] 为例:
assume cs:code, ds:data
data segment ; 数据段
db 'BaSiC'
db 'MinIX'
data ends
code segment ; 代码段
start: mov ax, data
mov ds, ax ; 将 ds 指向 data 段
mov bx, 0 ; 设置 bx = 0,ds:bx 指向 'BaSiC' 的第一个字母
mov cx, 5 ; 循环 5 次,因为 'BaSiC' 长度为 5
s: mov al, [bx] ; 将 ds:bx 所指向的内存单元的数据(ASCII 码)送往 al
and al, 11011111b ; 将小写字母变为大写
mov [bx], al ; 将转变后的 ASCII 码写回原单元
inc bx ; bx 加一,ds:bx 指向下一个字母
loop s
mov bx, 5 ; 设置 bx = 5,ds:bx 指向 'MinIX' 的第一个字母
mov cx, 5 ; 循环 5 次,因为 'MinIX' 长度为 5
s0: mov al, [bx] ; 将 ds:bx 所指向的内存单元的数据(ASCII 码)送往 al
or al, 00100000b ; 将大写字母变为小写
mov [bx], al ; 将转变后的 ASCII 码写回原单元
inc bx ; bx 加一,ds:bx 指向下一个字母
loop s0
mov ax, 4c00h
int 21h
code ends
end start
③ 以 [BX + idata] 为例:
assume cs:code, ds:data
data segment ; 数据段
db 'BaSiC'
db 'MinIX'
data ends
code segment ; 代码段
start: mov ax, data
mov ds, ax ; 将 ds 指向 data 段
mov bx, 0 ; 设置 bx = 0,ds:bx 指向 'BaSiC' 的第一个字母
mov cx, 5 ; 循环 5 次,因为 'BaSiC' 和 'MinIX' 长度都为 5,可以同时处理
s: mov al, [bx] ; 将 ds:bx 所指向的内存单元的数据(ASCII 码)送往 al
and al, 11011111b ; 将小写字母变为大写
mov [bx], al ; 将转变后的 ASCII 码写回原单元
mov al, [5 + bx] ; 将 ds:bx+5 所指向的内存单元的数据(ASCII 码)送往 al
or al, 00100000b ; 将大写字母变为小写
mov [5 + bx], al ; 将转变后的 ASCII 码写回原单元
inc bx ; bx 加一,ds:bx 指向下一个字母
loop s
mov ax, 4c00h
int 21h
code ends
end start
④ 以 [BX + idata] 为例,还可以写为 idata[BX]:
assume cs:code, ds:data
data segment ; 数据段
db 'BaSiC'
db 'MinIX'
data ends
code segment ; 代码段
start: mov ax, data
mov ds, ax ; 将 ds 指向 data 段
mov bx, 0 ; 设置 bx = 0,ds:bx 指向 'BaSiC' 的第一个字母
mov cx, 5 ; 循环 5 次,因为 'BaSiC' 和 'MinIX' 长度都为 5,可以同时处理
s: mov al, 0[bx] ; 将 ds:bx 所指向的内存单元的数据(ASCII 码)送往 al
and al, 11011111b ; 将小写字母变为大写
mov 0[bx], al ; 将转变后的 ASCII 码写回原单元
mov al, 5[bx] ; 将 ds:bx+5 所指向的内存单元的数据(ASCII 码)送往 al
or al, 00100000b ; 将大写字母变为小写
mov 5[bx], al ; 将转变后的 ASCII 码写回原单元
inc bx ; bx 加一,ds:bx 指向下一个字母
loop s
mov ax, 4c00h
int 21h
code ends
end start
在 C 语言中,数组:a[i],b[i]
在汇编语言中,数组:0[bx],5[bx]
其中 0 和 5 给定了两个字符串的起始偏移地址,BX 给定了从起始偏移地址开始的相对地址
数据位置的表达
绝大部分机器指令都是进行数据处理的指令,可分为:读取、写入、运算三类
在指令执行前,所要处理的数据可以在三个地方:CPU 内部、内存、端口
汇编语言中,用三个概念来表达数据的位置:立即数、寄存器、段地址和偏移地址(SA 和 EA)例如:
mov bx, [0]
数据在内存中,ds:0 内存地址单元mov bx, ax
数据在 CPU 内部,AX 寄存器mov bx, 1
数据在 CPU 内部,指令缓冲器
立即数
立即数 idata 是直接包含在指令中的数据(指令执行前在 CPU 的指令缓冲器中),在汇编指令中直接给出
- 例如:
mov ax, 1
add bx, 2000h
or bx, 00010000b
mov al, 'a'
寄存器
指令要处理的数据在寄存器中,在汇编指令中给出相应的寄存器名
- 例如:
mov ax, bx
mov ds, ax
push bx
mov ds:[0], bx
push ds
mov ss, ax
mov sp, ax
段地址和偏移地址
指令要处理的数据在内存中,可用 [X] 的格式给出偏移地址 EA,段地址 SA 在某个段寄存器中
- 例如:
; 段寄存器默认在 DS
mov ax, [0]
mov ax, [di]
mov ax, [bx + 8]
mov ax, [bx + si]
mov ax, [bx + si + 8]
; 段寄存器默认在 SS
mov ax, [bp]
mov ax, [bp + 8]
mov ax, [bp + si]
mov ax, [bp + si + 8]
; 段寄存器显式给出
mov ax, ds:[bp]
mov ax, es:[bx]
mov ax, ss:[bx + si]
mov ax, cs:[bx + si + 8]
数据的长度
在 8086CPU 中,指令可以处理两种尺寸的数据:
byte
和word
所以在机器指令中,需要指明是字操作还是字节操作
用寄存器指明
; 使用 16 位寄存器指明字操作
mov ax, 1
mov bx, ds:[0]
mov ds, ax
mov ds:[0], ax
inc ax
add ax, 1000
; 使用 8 位寄存器指明字节操作
mov al, 1
mov al, bl
mov al, ds:[0]
mov ds:[0], al
inc al
add al, 100
用 X ptr 指明
在没有寄存器参与的内存单元访问指令中,使用
word ptr
和byte ptr
显性指明所要访问的内存单元的长度是很有必要的,否则 CPU 无法得知所要访问的单元是字单元还是字节单元
; 用 word ptr 指明指令访问的内存单元是字单元
mov word ptr ds:[0], 1
inc word ptr [bx]
inc word ptr ds:[0]
add word ptr [bx], 2
; 用 byte ptr 指明指令访问的内存单元是字节单元
mov byte ptr ds:[0], 1
inc byte ptr [bx]
inc byte ptr ds:[0]
add byte ptr [bx], 2
- 示例:
2000:1000 FF FF FF FF FF FF FF ······
mov ax, 2000h
mov ds, ax
mov byte ptr [1000h], 1
; 执行后:
; 2000:1000 01 FF FF FF FF FF FF ······
mov ax, 2000h
mov ds, ax
mov word ptr [1000h], 1
; 执行后:
; 2000:1000 01 00 FF FF FF FF FF ······
用指令指明
有一些指令默认了访问的是字单元还是字节单元,例如:
push
、pop
等,因为push
、pop
只进行字操作
push [1000h]
pop ax