必读

§ 第5章 [BX]和loop指令 - 图1 [bx]和内存单元的描述

[bx]是什么?和[0]类似,[0]表示偏移地址为0的内存单元,就比如下面的指令

  1. mov ax,[0]

将一个内存单元的内容送入ax,内存单元的长度为2字节(1个字单元),该内存单元的段地址为ds,偏移地址为0.

§ 第5章 [BX]和loop指令 - 图2

将一个内存单元的内容送入al,这个内存单元的长度为1字节,存放一个字节,偏移地址为0,段地址在ds中

§ 第5章 [BX]和loop指令 - 图3

要完整描述一个内存单元,需要两种信息:

  • 内存单元的地址
  • 内存单元的长度类型

[0]表示一个内存单元时,0表示单元的偏移地址,段地址默认在ds中,单元的长度可以由具体指令中的其他操作对象(比如寄存器)指出来。

[bx]同样表示一个内存单元,它的偏移地址在bx中,比如下面的指令

  1. ; 将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移
  2. ; 地址在bx中,段地址在ds
  3. mov al,[bx]
  4. ; 将一个内存单元的内容送入al,这个内存单元的长度为1字节,存放一个字节,偏移地址在bx
  5. ; 段地址在ds
  6. mov al,[bx]

§ 第5章 [BX]和loop指令 - 图4 loop

loop指令和循环有关

§ 第5章 [BX]和loop指令 - 图5 定义的描述性的符号:**()**

为了描述上的简洁,使用一个描述性的符号()来表示一个寄存器或一个内存单元中的内容,比如

  • (ax):表示ax中的内容
  • (al):表示al中的内容
  • (20000H):表示内存20000H单元的内容
  • ((ds)*16+(bx)):表示ds中的内容为ADR1,bx中的内容为ADR2,内存ADR1x16+ADR2单元的内容

§ 第5章 [BX]和loop指令 - 图6

§ 第5章 [BX]和loop指令 - 图7 约定符号**idata**表示常量

之前写过类似的指令:mov ax,[0],表示将ds:0处的数据送入ax中,指令中[ ]中用一个常量0表示内存单元的偏移地址,以后就使用idata来表示这个偏移地址

  1. mov ax,[idata] ; 代表mov ax,[1] mov ax,[2]等
  2. mov bx, idata ; 代表mov bx,1 mov bx,2

一、[BX]

下面指令的功能

  1. mov ax,[bx]

功能:bx中存放的的数据作为一个偏移地址EA,段地址SA默认在DS中,将SA:EA处的数据送入ax中。即(ax)=((ds)*16+(bx))

  1. mov [bx],ax

功能:bx中存放的数据作为一个偏移地址EA,段地址SA默认在DS中,将ax中的数据送入内存SA:EA处,即((ds)*16+(bx))=(ax)

建议:书上的题5.1非常适合巩固

二、loop指令

loop指令的格式是:loop 标号,CPU执行到loop指令的时候,要进行两步操作

  • (cx)=(cx)-1
  • 判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行

2.1 任务1

编程计算§ 第5章 [BX]和loop指令 - 图8,结果存在ax中。

分析:设(ax)=2,可计算(ax)=(ax)*2,最后(ax)中为§ 第5章 [BX]和loop指令 - 图9的值,§ 第5章 [BX]和loop指令 - 图10可用§ 第5章 [BX]和loop指令 - 图11实现,程序如下。

  1. assume cs:code
  2. code segment
  3. mov ax,2
  4. add ax,ax
  5. mov ax,4c00h
  6. int 21h
  7. code ends
  8. end

2.2 任务2

编程计算§ 第5章 [BX]和loop指令 - 图12

分析:§ 第5章 [BX]和loop指令 - 图13,若设(ax)=2,可计算(ax)=(ax)*2*2,最后(ax)中的值为§ 第5章 [BX]和loop指令 - 图14的值,§ 第5章 [BX]和loop指令 - 图15可用§ 第5章 [BX]和loop指令 - 图16实现,程序如下

  1. assume cs:code
  2. code segment
  3. mov ax,2
  4. add ax,ax
  5. add ax,ax
  6. mov ax,4c00h
  7. int 21h
  8. code ends
  9. end

2.3 任务3

编程计算§ 第5章 [BX]和loop指令 - 图17

  1. assume cs:code
  2. code segment
  3. mov ax,2
  4. ; 需要11 add ax,ax
  5. mov ax,4c00h
  6. int 21h
  7. code ends
  8. end

写11个add ax,ax,这个有点累人啊 § 第5章 [BX]和loop指令 - 图18

这时候,就可以使用loop来简化程序

  1. assume cs:code
  2. code segment
  3. mov ax,2
  4. mov cx,11
  5. s: add ax,ax
  6. loop s
  7. mov ax,4c00h
  8. int 21h
  9. code ends
  10. end

下面分析一下上述程序

  1. 标号
    在汇编语言中,标号代表一个地址,上述程序中有一个标号s,它实际上标识了一个地址,这个地址处有一条指令:add ax,ax
  2. loop s
    CPU在执行loop s的时候,要进行两步操作
    • (cx)=(cx)-1
    • 判断cx中的值,不为0则转至标号s所标识的地址处执行(这里的指令是add ax,ax),如果为0则执行下一条指令(下一条指令是mov ax,4c00h

从上面的过程中,我们可以总结出cxloop指令相配合实现循环功能的3个要点:

  1. cx中存放循环次数
  2. loop指令中的标号所标识地址要在前面
  3. 要循环执行的程序段,写在标号和loop指令的中间

实现循环功能的框架

  1. mov cx,循环次数
  2. s:
  3. ; 循环执行的程序段
  4. loop s

2.4 问题一

用加法计算§ 第5章 [BX]和loop指令 - 图19,结果存放在ax中

  1. assume cs:code
  2. code segment
  3. mov ax,7BH ; 当然这里也可以用123表示
  4. mov cx,0EBH ; 同理,这里也可用236十进制表示
  5. s:add ax,ax
  6. loop s
  7. mov ax,4c00h
  8. int 21h
  9. code ends
  10. end

注意在写16进制的数据的时候,如果第一个字符是a~f或A~F,应该在前面加个0。

例如,要把eb07h改成0eb07h

三、Debug中跟踪用loop指令实现的循环程序

考虑这样一个问题,计算ffff:0006单元中的数乘以3,结果存储在dx中。

§ 第5章 [BX]和loop指令 - 图20

这里有3个问题需要拿出来分析一下

问题1:运算后的结果是否会超过dx所能存储的范围?

ffff:0006单元中存放的是字节型的数据,而一个字节型的数据是8位,数据大小为§ 第5章 [BX]和loop指令 - 图21#card=math&code=0%5Ctext%7B~%7D8%5C%3B%282%5E8%3D256%29&id=QYkRI),而一个dx是16位的字单元,其大小为§ 第5章 [BX]和loop指令 - 图22#card=math&code=0%5Ctext%7B~%7D65535%5C%3B%20%282%5E%7B16%7D%3D65536%29&id=yGIpR),所以完全没问题。

问题2:用循环累加来实现乘法,用哪个寄存器进行累加?

将ffff:0006单元中的数赋值给ax,用dx进行累加。先设(dx)=0,然后再做3次(dx)=(dx)+(ax)

问题3:ffff:6单元是一个字节单元,ax是一个16位寄存器,数据长度不一样,如何赋值?

一个是8位的,一个是16位的,如果想实现ffff:0006处的单元向ax赋值,应该令(ah)=0, (al)=(ffff6H)

接下来,就开始编写程序

  1. assume cs:code
  2. code segment
  3. mov ax,0ffffh
  4. mov ds,ax
  5. mov bx,6 ; 以上程序,设置ds:bx指向ffff:6
  6. mov al,[bx]
  7. mov ah,0 ; 以上,设置(al)=((ds*16)+(bx)),(ah)=0
  8. mov dx,0 ; 累加器清0
  9. mov cx,3 ; 循环3
  10. s:add dx,ax
  11. loop s ; 以上累加计算(ax)*3
  12. mov ax,4c00h
  13. int 21h ; 程序返回
  14. code ends
  15. end

四、汇编编译器masm对指令的不同处理

  1. assume cs:code
  2. code segment
  3. mov ax,2000H
  4. mov ds,ax
  5. mov ax,2 ; 关键点1
  6. mov ax,[2] ; 关键点2
  7. mov ax,ds:[2] ; 关键点3
  8. mov ax,4c00h
  9. int 21h ; 程序返回
  10. code ends
  11. end

编译、连接后,使用Debug查看编译器masm生成的机器码如下:

§ 第5章 [BX]和loop指令 - 图23

这里有一个关键的地方:mov ax,[2]本应该表示的是ds:2中的数据,但是现在它和mov ax,2的作用却是一样的。正确的实现方式是3这种写法,mov ax,ds:[2],所以不能只用[idata]来表示一个偏移地址idata,需要用ds:[idata]来表示。

五、loop和[bx]的联合应用

考虑这样一个问题,计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中,在写程序之前,需要进行一下分析

问题1:运算后的结果是否会超出dx所能存储的范围?

ffff:0006单元中存放的是字节型的数据,而一个字节型的数据是8位,数据大小为§ 第5章 [BX]和loop指令 - 图24#card=math&code=0%5Ctext%7B~%7D8%5C%3B%282%5E8%3D256%29&id=dcvkd),而一个dx是16位的字单元,其大小为§ 第5章 [BX]和loop指令 - 图25#card=math&code=0%5Ctext%7B~%7D65535%5C%3B%20%282%5E%7B16%7D%3D65536%29&id=SjPJW),所以完全没问题。

问题2:我们能否将ffff:0~ffff:b中的数据直接累加到dx中?

当然不行,因为ffff:0~ffff:b中的数据是8位的,不能直接加到16位寄存器dx中。

问题3:我们能否将ffff:0~ffff:b中的数据累加到**dl**中,并设置**(dh)=0**,从而实现累加到**dx**中?

这也不行,因为dl是8位寄存器,能容纳的数据的范围在0~255之间,ffff:0~ffff:b中的数据也都是8位,如果仅向dl中累加12个8位数据,很有可能造成进位丢失。

上面的问题总结下就是在做加法的时候,会有两种方法

  • (dx)=(dx)+内存中的8位数据
  • (dl)=(dl)+内存中的8位数据

第一种方法的问题是运算对象的类型不匹配,第二种方法的问题是结果有可能越界。

§ 第5章 [BX]和loop指令 - 图26

如何解决上述的看似矛盾的问题?

目前的方法就是用一个16位寄存器来做中介,将内存单元中的8位数据赋值到一个16位寄存器ax中,再将ax中的数据加到dx上,从而使得两个运算对象的类型匹配且不超界。

§ 第5章 [BX]和loop指令 - 图27

上述问题想清楚后,就开始编写程序了。

  1. assume cs:codesg
  2. codesg segment
  3. mov ax,0ffffh
  4. mov ds,ax
  5. mov dx,0
  6. mov al,ds:[0]
  7. mov ah,0
  8. add dx,ax
  9. mov al,ds:[1]
  10. mov ah,0
  11. add dx,ax
  12. mov al,ds:[2]
  13. mov ah,0
  14. add dx,ax
  15. mov al,ds:[3]
  16. mov ah,0
  17. add dx,ax
  18. mov al,ds:[4]
  19. mov ah,0
  20. add dx,ax
  21. mov al,ds:[5]
  22. mov ah,0
  23. add dx,ax
  24. mov al,ds:[6]
  25. mov ah,0
  26. add dx,ax
  27. mov al,ds:[7]
  28. mov ah,0
  29. add dx,ax
  30. mov al,ds:[8]
  31. mov ah,0
  32. add dx,ax
  33. mov al,ds:[9]
  34. mov ah,0
  35. add dx,ax
  36. mov al,ds:[0a]
  37. mov ah,0
  38. add dx,ax
  39. mov al,ds:[0b]
  40. mov ah,0
  41. add dx,ax
  42. mov ax,4c00h
  43. int 21h
  44. codesg ends
  45. end

上述程序太长了,所以需要使用loop指令帮助它减减肥

减肥思路

上面程序中,有12个相似的程序段,我们将它们一般化地描述为

  1. mov al,ds:[X]
  2. mov ah,0 ; ffff:X单元处的值赋给ax
  3. add dx,ax ; dx中加上ffff:X单元的数值

上面的代码段就是循环体中需要执行的操作,另外还有一句话就是inc bx来对bx寄存器中的数进行自增运算

  1. assume cs:codesg
  2. codesg segment
  3. mov ax,0ffffh
  4. mov ds,ax
  5. mov bx,0
  6. mov dx,0
  7. mov cx,12
  8. s: mov al,[bx]
  9. mov ah,0
  10. add dx,ax
  11. inc bx ; 在这里给bx增加数值
  12. loop s
  13. mov ax,4c00h
  14. int 21h
  15. codesg ends
  16. end

六、段前缀

指令mov ax,[bx]中,内存单元的偏移地址由bx给出,而段地址默认在ds中,我们可以在访问内存单元的指令中显示地给出内存单元的段地址所在的段寄存器。

这些出现在访问内存单元的指令中,用于显示地指明内存单元的段地址的ds, cs, ss, es等的就称为段前缀。

§ 第5章 [BX]和loop指令 - 图28

七、一段安全的空间

在8086模式中,随意向一段内存空间中写入内容是很危险的,因为这段空间中可能存放着重要的系统数据或代码。比如下面的指令:

  1. mov ax,1000h
  2. mov ds,ax
  3. mov al,0
  4. mov ds:[0],al

这样做的结果就是向1000:0的内存单元中写入数据,但是如果1000:0中存放着重要的系统数据或代码,这句话就会引发错误。

可见,在不确定一段内存空间是否存放着重要的数据或代码的时候,不能随意向其中写入内容。

但是不要忘记,我们是在操作系统的环境中工作,操作系统管理所有的资源,也包括内存。如果我们需要向内存空间中写入数据的话,要使用操作系统分配给我们的空间,而不应该直接使用地址任意指定内存单元

我们学习的汇编语言,要通过它来获得底层的编程体验,理解计算机底层的基本工作原理。我们要尽量对硬件编程,而不去理会操作系统,但是我们是在Win10的操作系统学习的,这个时候也要遵守操作系统给我们的规矩。

§ 第5章 [BX]和loop指令 - 图29

在一般的PC机中,DOS方式下,DOS和其他合法的程序一般都不会使用 0:200~0:2ff (00200h~002ffh)的256自己的空间,所以我们使用这段空间是安全的。不过为了谨慎起见,在进入DOS后,我们可以用Debug查看一下,如果0:200~0:2ff单元的内容都是0的话,则证明DOS和其他合法的程序没有使用这里。

§ 第5章 [BX]和loop指令 - 图30

八、段前缀的使用

现在考虑一个问题,将内存ffff:0~ffff:b单元中的数据复制到0:200~0:20b单元中

注意:0:200~0:20b和0020:0~0020:b描述的是同一段内存中的空间

§ 第5章 [BX]和loop指令 - 图31

实现上述过程的程序如下

  1. assume cs:code
  2. code segment
  3. mov bx,0 ; 要使用寄存器实现增加的效果
  4. mov cx,12 ; 要循环12
  5. s: mov ax,0ffffh
  6. mov ds,ax
  7. mov dl,[bx]
  8. mov ax,0020h
  9. mov ds,ax
  10. mov [bx],dl
  11. inc bx
  12. loop s
  13. mov ax,4c00h
  14. int 21h
  15. code ends
  16. end

因为源始单元ffff:X和目标单元0020:X相距大于64KB,在不同的64KB段里,程序里要设置两次ds,这样做虽然正确但是效率却不高。我们可以使用两个寄存器分别存放源始单元ffff:X和目标单元0020:X的段地址,这样就省略了要做的12次设置ds的操作了。

  1. assume cs:code
  2. code segment
  3. mov bx,0 ; 要使用寄存器实现增加的效果
  4. mov cx,12 ; 要循环12
  5. ; 源始内存
  6. mov ax,0ffffh
  7. mov ds,ax
  8. ; 目标内容
  9. mov ax,0020h
  10. mov es,ax
  11. s:
  12. mov dl,[bx]
  13. mov es:[bx],dl ; 这里指定段寄存器为es而不是默认的ds
  14. inc bx
  15. loop s
  16. mov ax,4c00h
  17. int 21h
  18. code ends
  19. end

实验

实验1

编程,向内存0:200~0:23F依次传送数据0~63(3FH)

  1. ; 这里不使用段地址:偏移地址的方式来表示内存单元
  2. ; 而是直接使用物理地址来表示
  3. assume cs:code
  4. code segments
  5. mov bx,200h
  6. mov cx,3FH
  7. mov ax,0
  8. s: mov [bx],ax
  9. inc bx
  10. inc ax
  11. loop s
  12. mov ax,4c00h
  13. int 21h
  14. code ends
  15. end

实验2

编程,向内存0:200~0:23F依次传送数据0~63(3FH),程序中只能使用9条指令,9条指令中包括mov ax,4c00hint 21h

上面实验1的指令刚好是9条,所以实验1的解答正好符合实验2的要求

实验3

下面的程序的功能是将mov ax,4c00h之前的指令复制到内存0:200处,补全程序。上机调试,跟踪运行结果。

  1. assume cs:code
  2. code segment
  3. mov ax,cs ; 2个字节
  4. mov ds,ax ; 2个字节
  5. mov ax,0020h ; 3个字节
  6. mov es,ax ; 2个字节
  7. mov bx,0 ; 3个字节
  8. mov cx,17h ; 3个字节
  9. ; 有一个隐藏的指令ES,这个指令的大小为1个字节
  10. s: mov al,[bx] ; 2个字节
  11. mov es:[bx],al ; 2个字节
  12. inc bx ; 1个字节
  13. loop s ; 2个字节
  14. mov ax,4c00h
  15. int 21h
  16. code ends
  17. end
  1. 复制的是什么?从哪里到哪里?
  2. 复制的是什么?有多少个字节?你如何知道要复制的字节的数量

注意:一定要做完这个实验才能进行下面的课程

实验步骤如下:

首先,查看内存0:200处是否有程序或数据

§ 第5章 [BX]和loop指令 - 图32

其次,查看这段程序的大小,下图中的17H表示这句话之前的指令所占据的大小为17个字节

§ 第5章 [BX]和loop指令 - 图33