注:本文档为“《汇编语言(第3版) 》王爽著”阅读过程中记的笔记。
参考视频:通俗易懂的汇编语言(王爽老师的书)_哔哩哔哩_bilibili
一个汇编语言源程序编写到执行的过程:
1)编写汇编源程序
2)先对源程序进行编译连接,编译产生目标文件;再用连接程序对目标文件进行连接,生成可在操作系统中直接运行的可执行程序。
可执行文件包括两个部分:
3)执行可程序文件。
CPU按照可执行文件中的描述信息,将可执行文件中的机器码和数据加载入内存,并进行相关的初始化(比如CS:IP执行第一条要执行的指令),然后由CPU执行程序。
一个简单的源程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | assume cs:codesg ; cs和codesg关联 codesg segment ; 定义一个段codesg开始 mov ax,0123H ; 由CPU执行的汇编指令 mov bx,0456H add ax,bx add ax,ax mov ax,4c00H ; 程序返回 int 21H codesg ends ; 定义一个段codesg结束 end ; 汇编程序结束标记 |
(1)伪指令:汇编语言中,包括两种指令,一种是汇编指令,一种是伪指令。汇编指令有对应的机器码的指令,可以被编译为机器指令,最终由CPU执行。伪指令没有对应的机器指令,由编译器执行,编译器根据伪指令来进行相关的编译工作。
1 2 3 | 段名 segment ... 段名 ends |
segment和ends是一个成对使用的伪指令,segment和ends定义一个段,segment说明一个段开始;ends说明一个段结束。一个段必须有一个名称来标识。
段:一个汇编程序由多个段组成,这些段用来存放代码、数据或当作栈空间来使用。指令、数据、栈,被划分到不同的段中。一个汇编程序必须至少有一个段,用来存放代码。
(2)end
end是一个汇编程序的结束标记,编译器碰到伪指令end,就结束对源程序的编译。
(3)assume
assume将有特定用途的段和相关的段寄存器关联起来。
(4)程序返回
一个程序结束后,将CPU的控制权交还给使它得以运行的程序,这个过程称为程序返回。
1 2 | mov ax,4c00H ; 程序返回 int 21H |
使用masm.exe程序进行编译,有以下几种方式:
1 2 | masm 文件名 然后输入文件名 |
1 | masm 文件名; |
对源程序进行编译得到目标文件后,需要对目标文件进行连接,得到可执行文件。
连接使用的是LINK.exe程序,使用方式:
1 | link 文件名 |
1 | link 文件名; |
连接的几个作用:
1)当源程序很大时,可以将它分为多个源程序文件来编译,每个源程序编译成为多个目标文件后,再用连接程序将它们连接到一起,生成一个可执行执行。
2)程序中调用了某个库文件中的子程序,需要将这个库文件和该程序生成的目标连接到一起,生成一个可执行文件。
3)一个源程序编译后,得到了存有机器码的目标文件,目标文件中的有些内容还不能直接用来生成可执行文件,连接程序将这些内容处理为最终的可执行信息。所以,在只有一个源程序文件,又不需要调用某个库中的子程序时,也必须用连接程序对目标文件进行处理,生成可执行程序。
直接执行即可:
当前执行之后没有任何输出,所以没有什么提示。
DOS中的程序command.com是命令解释器,也就是DOS系统的shell。
如果用户要执行一个程序,输入可执行文件的名称,command会根据名称找到可执行文件,然后将这个可执行文件中的程序载入内存,设置CS:IP指向程序的入口。然后command暂停运行,CPU运行程序,程序返回后,返回到command中。
可以使用debug.exe来跟踪程序的执行步骤:
1)debug.exe加载调试程序进入内存,进行相关的初始化后设置CS:IP指向程序的入口;CX寄存器为程序的长度。
2)DOS系统中EXE文件中的程序的加载过程:
程序加载后,DS寄存器中存放着程序所在内存区的段地址,这个内存区的偏移地址为0,则程序所在的内存区地址为DS:0。
内存区的前256个字节中存放的是PSP,DOS用来和程序进行通信,从256字节处向后的空间存放的是程序。
3)使用t命令单步执行程序中的每一条指令,并观察每条指令的执行结果。
执行到了int 21,使用p命令执行。
1 2 | # 将一个内存单元的内容送入ax,长度为2字节(字单元),存放一个字,偏移地址在bx中,段地址在ds中。 mov ax, [bx] |
1 2 | # 将一个内存单元的内容送入al中,内存单元的长度为1字节,存放一个字节,偏移地址在bx中,段地址在ds中。 mov al, [bx] |
[...]:汇编语法规定,表示一个内存单元
指令 | 段地址 | 偏移地址 | 操作单位 |
---|---|---|---|
mov ax, [0] | 在DS中 | 在[0]中 | 字 |
mov al, [0] | 在DS中 | 在[0]中 | 字节 |
mov ax, [bx] | 在DS中 | 在[bx]中 | 字 |
mov al, [bx] | 在DS中 | 在[bx]中 | 字节 |
(...):为学习方便而做出的约定,表示一个内存单元或寄存器中的内容。
“(...)”中的元素可以是三种类型:寄存器名、段寄存器名、内存单元的物理地址(20位)。
"(X)"所表示的数据有两种类型:字节、字。是哪种类型由寄存器名或具体的运算决定。
描述对象 | 描述方法 | 描述对象 | 描述方法 |
---|---|---|---|
ax中的内容为0010H | (ax)=0010H | 2000:1000处的内容为0010H | (21000H)=0010H |
mov ax, [2]的功能 | (ax)=((ds)*16+2) | mov [2], ax的功能 | ((ds)*16+2)=(ax) |
add ax, 2的功能 | (ax)=(ax)+2 | add ax,bx的功能 | (ax)=(ax)+(bx) |
push ax的功能 | (sp)=(sp)-2 ((ss)*16+(sp))=(ax) |
pop ax的功能 | (ax)=((ss)*16+(sp)) (sp)=(sp)+2 |
为了方便,约定idata表示常量。
1 2 3 | mov ax, [idata] ; 代表mov ax, [1]、mov ax, [2] ... mov bx, idata mov ds, idata |
loop指令的格式:
1 | loop 标号 |
CPU执行loop指令时,需要进行两步:①(cx)=(cx)-1;②判断cx中的值,不为0则跳转到标号处执行程序,如果为0则向下执行。
比如,设(ax)=2,N*2可用N+N实现,计算2^12如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | assume cs:code code segment mov ax, 2 mov cx, 11 s: add ax, ax ; 标号代表一个地址,这个地址处有一条指令:add ax, ax loop s ; (cx)=(cx)-1,判断cx中的值,不为0则跳转到s继续执行 mov ax, 4c00H ; (cx)为0,则执行下一条语句 int 21H code ends end |
debug调试时,可以使用p命令一次将循环执行完毕,直到(cx)=0为止:
也可以使用g命令,执行到某个地址处:
目的:将内存2000:0、2000:1、2000:2、2000:3单元中的数据送入al、bl、cl、dl中。
(1)debug.exe中编程实现
1 2 3 4 5 6 | mov ax, 2000 mov ds, ax mov al, [0] mov bl, [1] mov cl, [2] mov dl, [3] |
(2)汇编程序实现
错误的方式:
1 2 3 4 5 6 7 8 9 10 11 12 | assume cs:code code segment mov ax, 2000H mov ds, ax mov al, [0] ; 实际当作了mov al, 0处理 mov bl, [1] ; 实际当作了mov bl, 1处理 mov cl, [2] ; 实际当作了mov cl, 2处理 mov dl, [3] ; 实际当作了mov dl, 3处理 code ends end |
可看出,处理方式是不一样的,汇编程序可选择的方式1:
1 2 3 4 | mov ax, 2000H mov ds, ax ; 段地址2000H送入ds mov bx, 0 ; 偏移地址0送入bx mov al, [bx]; ds:bx单元中的数据送入al |
可选择的方式2:
1 2 3 4 5 6 7 8 9 10 11 12 | assume cs:code code segment mov ax, 2000H mov ds, ax ; 段地址2000H送入ds mov al, ds:[0] ; ds:0单元中的数据送入al mov bl, ds:[1] ; ds:1单元中的数据送入bl mov cl, ds:[2] ; ds:2单元中的数据送入cl mov dl, ds:[3] ; ds:3单元中的数据送入dl code ends end |
总结:(1)在汇编程序中,如果用指令访问一个内存单元,则在指令中必须使用"[...]"来表示内存单元,但如果在“[]"里用一个常量idata直接给出内存单元的偏移地址,则需要在”[]“的前面显示地给出段地址所在的段寄存器,比如:
1 | mov al, ds:[0] |
(2)如果在"[]"里用寄存器,比如bx,间接给出内存单元的偏移地址,则段地址默认在ds中。
实验:计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中。
实现方式:将内存单元中的8位数据赋值到一个16位寄存器ax中,再将ax中的数据加到dx上,达到两个运算对象的类型匹配并且结果不会超界。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | assume cs:code code segment mov ax, 0ffffH ; 汇编语言写法,必须以0开头 mov ds, ax ; 设置(ds)=ffffH mov bx, 0 ; 初始化ds:bx指向ffff:0 mov dx, 0 ; 初始化累加寄存器dx, (dx)=0 mov cx, 12 ; 初始化循环计数寄存器cx, (cx)=12 s: mov al, [bx] mov ah, 0 add dx, ax ; 间接向dx中加上((ds)*16+(bx))单元的数值 inc bx ; ds:bx指向下一个单元 loop s mov ax, 4c00H ; (cx)为0,则执行下一条语句 int 21H code ends end |
访问内存单元时,可以显示的给出内存单元的段地址所在的段寄存器。
1 2 | ; 将一个内存单元的内容送入ax,这个内存单元的长度为2字节,存放一个字,偏移地址在bx中,段地址在ds中 mov ax, ds:[bx] |
1 2 | ; 将一个内存单元的内容送入ax,这个内存单元的长度为2字节,存放一个字,偏移地址在bx中,段地址在cs中 mov ax, cs:[bx] |
1 2 | ; 将一个内存单元的内容送入ax,这个内存单元的长度为2字节,存放一个字,偏移地址在bx中,段地址在ss中 mov ax, ss:[bx] |
1 2 | ; 将一个内存单元的内容送入ax,这个内存单元的长度为2字节,存放一个字,偏移地址在bx中,段地址在es中 mov ax, es:[bx] |
1 2 | ; 将一个内存单元的内容送入ax,这个内存单元的长度为2字节,存放一个字,偏移地址为0,段地址在ss中 mov ax, ss:[0] |
1 2 | ; 将一个内存单元的内容送入ax,这个内存单元的长度为2字节,存放一个字,偏移地址为0,段地址在cs中 mov ax, cs:[0] |
这些用在访问内存单元的指令中,用于显示地指明内存单元的段地址的”ds:“,”cs:“,”ss:“,”es:“,在汇编语言中称为段前缀。
使用段前缀完成:将内存ffff:0ffff:b单元中的数据复制到0:2000:20b单元中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | assume cs:code code segment mov ax, 0ffffH mov ds, ax ; 设置(ds)=ffffH mov ax, 0020H mov es, ax ; (es)=0020H mov bx, 0 ; (bx)=0,ds:bx指向ffff:0,es:bx指向0020:0 mov cx, 12 ; (cx)=12,循环12次 s: mov dl, [bx] ; (dl)=((ds)*16+(bx)),将ffff:bx中的数据送入dl mov es:[bx], dl ; ((es)*16+(bx))=(dl),将dl中的数据送入0020:bx inc bx ; (bx)=(bx)+1 loop s mov ax, 4c00H ; (cx)为0,则执行下一条语句 int 21H code ends end |
向内存0:2000:23F依次写入数据063(3FH)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | assume cs:code code segment mov ax, 0020H mov ds, ax ; 段地址 mov bx, 0 ; 初始值 mov cx, 64 ; 循环64次 s: mov ds:[bx], bx inc bx loop s mov ax, 4c00H int 21H code ends end |
程序需要存放数据,需要取得内存空间。
获取空间的方法有两种,一是加载程序的时候为程序分配;二是程序执行过程中向系统申请。本文主要讨论第一种。
如果需要在程序被加载的时候取得所需的空间,则必须在源程序中做出说明,通过定义段来进行内存空间的获取。
内存地址空间的分配一般是系统来分配的,在源程序中定义需要处理的数据,这些数据就会被编译、连接程序作为程序的一部分写到可执行文件中,当可执行文件中的程序被载入内存时,这些数据也同时被加载入内存,需要处理的数据自然就获得了存储空间。
实验:计算下面8个数据的和,结果存放在ax寄存器中。
1 | 0123H、0456H、0789H、0abcH、0defH、0fedH、0cbaH、0987H |
希望通过循环的方式来进行累加,在累加前,要将这些数据存储在一组地址连续的内存单元中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | assume cs:code code segment ; 定义字型数据“define word”,这些数据是在代码段中,在代码段的开头 dw 0123H, 0456H, 0789H, 0abcH, 0defH, 0fedH, 0cbaH, 0987H mov bx, 0 mov ax, 0 mov cx, 8 s: add ax, cs:[bx] ; 数据在代码段中,偏移地址起始为0 add bx, 2 loop s mov ax, 4c00H int 21H code ends end |
debug运行程序,可以看到数据存放在代码段的前16个字节:
运行指令方式,需要将IP指向0010偏移地址处。
这样比较麻烦,可以在程序中指明程序的入口位置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | assume cs:code code segment ; 定义字型数据“define word”,这些数据是在代码段中,在代码段的开头 dw 0123H, 0456H, 0789H, 0abcH, 0defH, 0fedH, 0cbaH, 0987H start: mov bx, 0 mov ax, 0 mov cx, 8 s: add ax, cs:[bx] ; 数据在代码段中,偏移地址起始为0 add bx, 2 loop s mov ax, 4c00H int 21H code ends end start ; 通知编译器程序的入口在start |
通过end指明程序指令的入口,可以得到程序的框架为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | assume cs:code code segment ; ; 数据 ; start: ; ; 代码 ; code ends end start ; 通知编译器程序的入口在start |
实验:利用栈,将程序中定义的数据逆序存放。
1 | 0123H、0456H、0789H、0abcH、0defH、0fedH、0cbaH、0987H |
思路:程序运行时,定义的数据存放到CS:0~CS:F中,依次将这8个字节单元中的数据入栈,然后再依次出栈到这8个字单元中,从而实现数据的逆序存放。
问题:首先需要一段可作为栈的空间,这段空间可以由系统分配,可以定义在程序中通过定义数据来取得一段空间,将这段空间当作栈空间来用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | assume cs:codesg code segment ; 定义字型数据“define word”,这些数据是在代码段中,在代码段的开头 ; 这16个字型数据,在程序加载时将获取16个字的内存空间,存放16个数据,有多余的空间 ; 在后面的程序中将这段空间当作栈来使用 dw 0123H, 0456H, 0789H, 0abcH, 0defH, 0fedH, 0cbaH, 0987H dw 0, 0, 0, 0, 0, 0, 0, 0 start: mov ax, cs mov ss, ax mov sp 30H ; 设置栈顶ss:sp指向cs:30,将CS:10~CS:2F当作栈使用 mov bx, 0 mov cx, 8 s: push cs:[bx]; 压栈,从CS:30开始 add bx, 2 loop s ; 将代码段0~15单元中的8个字型数据依次入栈 mov bx, 0 mov cx, 8 s0: pop cs:[bx] ; 出栈,放到CS:0开始 add bx, 2 loop s0 ; 依次出栈8个字型数据到代码段0~15单元中 mov ax, 4c00H int 21H codeseg ends end start ; 指明程序的入口在start |
前面编程时,我们需要注意用到数据和栈,将数据和栈放到一个段里面。编程的时候,需要注意何处是数据,何处是栈,何处是代码。这样存在的问题:
(1)把它们放到一个段中使程序显得混乱;
(2)如果数据、栈和代码需要的空间超过64KB,就不能放在一个段中。
需要考虑如何存放数据、代码和栈?
可以定义需要的数据,或通过定义数据来取得栈空间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | 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 stack ends code segment start: ; 初始化栈段寄存器 mov ax, stack ; 将名称为stack的段的段地址送入ax mov ss, ax mov sp, 20H ; 设置栈顶ss:sp指向stack:20 ; 初始化数据段寄存器 mov ax, data mov ds, ax ; ds指向data段 ; 入栈 mov bx, 0 ; ds:bx指向data段中的第一个单元 mov cs, 8 s: push [bx] add bx, 2 loop s ; 将data段中的0~15单元中的8字型数据依次入栈 ; 出栈 mov bx, 0 mov cx, 8 s0: pop [bx] add bx, 2 loop s0 ; 依次出栈8个字型数据到data段的0~15单元中 mov ax, 4c00H int 21h code ends end start |
代码段、数据段、栈段完全是用户的安排。如何让CPU执行段呢?
(1)源程序中为3个段起名,比如程序中的data、code、stack;
(2)使用伪指令”assume cs:code, ds:data, ss:stack“将cs、ds、ss分别和code、data、stack段相连。
(3)使用段的方式,比如:
1 2 3 | mov ax, stack ; 将名称为stack的段的段地址送入ax mov ss, ax mov sp, 20H ; 设置栈顶ss:sp指向stack:2 |
前面访问内存的方式主要有:[0]、[bx]。本章讲解一些其它方式。
(1)and指令:逻辑与指令,按位进行与运算。
1 2 3 | mov al, 01100011B and al, 00111011B ; 执行后得到al=00100011B |
通过and指令可将操作对象的相应位设为0,其它位不变。
(2)or指令:逻辑或指令,按位进行或运算。
1 2 3 | mov al, 01100011B or al, 00111011B ; 执行后得到al=01111011B |
通过or指令可将操作对象的相应位设位1,其它位不变。
在汇编程序中,以“......”的方式指明数据是以字符的形式给出的,编译器将把它们转换为对应的ASCLL码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | assume cs:code, ds:data data segment db 'unIX' ; 相当于'db 75H, 6EH, 49H, 58H' db 'foRK' ; 相当于'db 66H, 6FH, 52H, 48H' data ends code segment start: mov al, 'a' ; 相当于'mov al, 61H' mov bl, 'b' ; 相当于'mov al, 62H' mov ax, 4c00H int 21h code ends end start |
实验:将datasg中的第一个字符串转换为大写,第二个字符串转换为小写。
1 2 3 4 5 6 7 8 9 10 11 12 | assume cs:codesg, ds:datasg datasg segment db 'BaSic' db 'iNfOrMaTiOn' datasg ends codesg segment start: codesg ends end start |
思路:大写字母的ASCLL码值比小写字母的ASCLL码值小20H。
大写字母的ASCLL码加20H,就转换为了小写,如果已经是小写,就不需要转换;
小写字母的ASCLL码减20H,就转换为了大写,如果已经是大写,就不需要转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | assume cs:codesg, ds:datasg datasg segment db 'BaSic' db 'iNfOrMaTiOn' datasg ends codesg segment start: mov ax, datasg mov bx, 0 mov cx, 5 s: mov al, [bx] ; 如果(al)>61H,则为小写字母的ASCLL码,则: sub al, 20H mov [bx], al inc bx loop s codesg ends end start |
问题:如何进行值的判断?
解决方式:大写字母的ASCLL码的第5位为0,小写字母的ASCLL码的第5位为1,转换为小写字母的方式为直接将第5位置为1,转换为大写字母的方式为直接将第5位置为0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | assume cs:codesg, ds:datasg datasg segment db 'BaSic' db 'iNfOrMaTiOn' datasg ends codesg segment start: mov ax, datasg mov ds, ax mov bx, 0 mov cx, 5 s: mov al, [bx] and al, 11011111B ; 将ASCLL码的第5位置为0,变为大写字母 mov [bx], al inc bx loop s mov bx, 5 mov cx, 11 s0: mov al, [bx] or al, 00100000B ; 将ASCLL码的第5位置为1,变为小写字母 mov [bx], al inc bx loop s0 mov ax, 4c00h int 21h codesg ends end start |
除了可以用[bx]来指明一个内存单元,还可以用一种更灵活的方式来指明内存:
[bx+idata]表示一个内存单元,它的偏移地址为(bx)+idata(bx的数值加上idata)。
1 2 3 4 5 | mov ax, [bx+200] ; (ax)=((ds)*16+(bx)+200) ; 也可以写成 mov ax, [200+bx] mov ax, 200[bx] mov ax, [bx].200 |
实验:将datasg中的第一个字符串转换为大写,第二个字符串转换为小写。
思路:将两个字符串看作两个数组,一个从0地址开始存放,另一个从5开始存放。可以使用[0+bx]和[5+bx]的方式在同一个循环中定位这两个字符串中的字符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | assume cs:codesg, ds:datasg datasg segment db 'BaSic' ; 起始地址为0 db 'iNfOrMaTiOn' ; 起始地址为5 datasg ends codesg segment start: mov ax, datasg mov ds, ax mov bx, 0 mov cx, 5 s: mov al, [bx] ; 或者写为'mov al, 0[bx]' and al, 11011111b mov [bx], al ; 或者写为'mov 0[bx], al' mov al, [5+bx] ; 或者写为'mov al, 5[bx]' or al, 00100000b mov [5+bx], al ; 或者写为'mov 5[bx], al' inc bx loop s codesg ends end start |
si和di是8086CPU中和bx功能相近的寄存器。si和di不能分为两个8位寄存器使用。
1 2 3 4 5 6 7 8 9 | mov bx, 0 mov ax, [bx] ; 等同于 mov si, 0 mov ax, [si] mov di, 0 mov ax, [di+123] |
实验:用si和di实现将字符串'welcome to masm!'复制到它后面的数据区中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | codesg segment datasg segment db 'welcome to masm!' ; 起始地址;datasg db '................' ; 起始地址;datasg + 16 datasg ends start: mov ax, datasg mov ds, ax mov si, 0 mov di, 16 mov cx, 8 s: mov ax, [si] mov [di], ax add si, 2 add di, 2 loop s mov ax, 4c00h int 21h codesg ends end start |
[bx+si]和[bx+di]的含义类似。
[bx+si]:表示一个内存单元,它的偏移地址是(bx)+(si)(即bx中的数值加上si中的数值),称为基址+变址寻址方式。
1 2 3 4 5 | ; 将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为bx中的数值加上si中的数值,段地址在ds中 mov ax, [bx+si] ; (ax)=((ds)*16+(bx)+(si)) ; 也可以写成 mov ax, [bx][si] |
[bx+si+idata]和[bx+di+idata]类似,以[bx+si+idata]为例进行讲解。
[bx+si+idata]:表示一个内存单元,它的偏移地址为(bx)+(si)+idata
1 2 3 4 5 6 7 8 9 | ; 将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为bx中的数值加上si中的数值再加上idata,段地址在ds中。 mov ax, [bx+si+idata] ; (ax)=((ds)*16+(bx)+(si)+idata) ; 等同于下面的格式 mov ax, [bx+idata+si] mov ax, [idata+bx+si] mov ax, idata[bx][si] mov ax, [bx].idata[si] mov ax, [bx][si].idata |
几种定位内存地址的方法(寻址方式):
形式 | 名称 | 特点 | 意义 | 示例 |
---|---|---|---|---|
[idata] | 直接寻址 | 用一个常量表示地址 | 可用于直接定位一个内存单元 | mov ax, [200] |
[bx] | 寄存器间接寻址 | 用一个变量来表示内存地址 | 可用于间接定位一个内存单元 | mov bx, 0 mov ax, [bx] |
[bx+idata] | 寄存器相对寻址 | 用一个变量和常量表示地址 | 可在一个起始地址的基础上用变量间接定位一个内存单元 | mov bx, 4 mov ax, [bx+200] |
[bx+si] | 基址变址寻址 | 用两个变量表示地址 | mov ax, [bx+si] | |
[bx+si+idata] | 相对基址变址寻址 | 用两个变量和一个常量表示地址 | mov ax, [bx+si+200] |
实验1:将datasg段中的每个单词的头一个字母改为大写字母。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | assume cs:codesg, ds:datasg datasg segment db '1.file ' db '2.edit ' db '3.switch ' db '4.view ' db '5.options ' db '6.help ' datasg ends codeseg segment start: mov ax, datasg mov ds, ax mov bx, 0 mov cx, 6 s: mov al, [bx+3] and al, 11011111b mov [bx+3], al add bx, 16 loop s mov 4c00h int 21h codesg ends end start |
实验2:将datasg段中的每个单词都改为大写字母。
思路:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | assume cs:codesg, ds:datasg datasg segment db 'ibm ' db 'dec ' db 'dos ' db 'vax ' datasg ends codeseg segment start: mov ax, datasg mov ds, ax mov bx, 0 ; 每一行的起始地址 mov cx, 4 s0: mov dx, cx ; 暂存cx的值 mov si, 0 mov cx, 3 s: mov al, [bx+si] and al, 11011111b mov [bx+si], al inc si loop s add bx, 8 mov cx, dx ; 恢复外层循环的计数 loop s0 mov 4c00h int 21h codesg ends end start |
上面用寄存器dx暂存cx的值比较浪费寄存器,可以用固定的内存空间保存数据。
1 2 3 4 5 6 7 8 9 10 11 | s0: mov ds:[40h], cx ; 暂存cx的值 mov si, 0 mov cx, 3 s: mov al, [bx+si] and al, 11011111b mov [bx+si], al inc si loop s add bx, 8 mov cx, ds:[40h] ; 恢复外层循环的计数 loop s0 |
用内存空间保存数据,可能存在内存被修改的风险,因此可以考虑用栈保存数据:
一般来说,在需要暂存数据的时候,我们都应该使用栈。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | assume cs:codesg, ds:datasg stacksg segment dw 0, 0, 0, 0 stacksg ends datasg segment db 'ibm ' db 'dec ' db 'dos ' db 'vax ' datasg ends codeseg segment start: mov ax, stacksg mov ss, ax mov sp, 8 mov ax, datasg mov ds, ax mov bx, 0 ; 每一行的起始地址 mov cx, 4 s0: push, cx ; 暂存cx的值,将cx压栈 mov si, 0 mov cx, 3 s: mov al, [bx+si] and al, 11011111b mov [bx+si], al inc si loop s add bx, 8 pop cx ; 恢复外层循环的计数,恢复cx loop s0 mov 4c00h int 21h codesg ends end start |