我们先来看上一章的实验十

实验十 编写子程序

一.显示字符串

image-20230402110611568

也就是我们要自己编写子程序显示的部分!

代码如下:

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
show_str:push cx
push dx
push si#先把这些跳出程序还要用的寄存器的值先入栈!

mov ax,B800h#先找到每个字符对应的偏移地址!这是段地址
mov es,ax#先把段地址放es存着,接下来还要算偏移地址
mov ax,0
mov al,160
mul dh#因为是第八行,每一行160字节
mov bx,ax#找到行偏移地址。
mov ax,0//清零
mov al,2
mul dl
add bx,ax#把行偏和列偏加起来。


mov di,0
mov al,cl#字符属性存到al
mov ch,0
show:mov cl,ds:[si]#把字符属性放到al之后,把字符放进cl
jcxz ok
#如果判断失败
mov es:[bx+di+0],cl#在显示缓冲区显示字符
mov es:[bx+di+1],al#显示字符的属性(颜色等等)
add di,2
inc si#向下一个字符了。
jmp short show#循环。这样循环省了一个寄存器cx用来存字符。

ok:pop si
pop dx
pop cx
ret

code ends
end start

二.解决除法溢出的问题

复习div可以做除法

1
2
进行8位除法的时候,al存储结果的商,ah存储结果的余数。
进行16位除法的时候,ax存储结果的商,dx存储结果的余数。

但是如果结果的商大于al或者ax能存储的最大值,那么将如何?

比如下面的程序

1
2
3
mov bh,1
mov ax,1000
div bh

显然这是8位除法,商为1000,但是1000在al中放不下。

或者比如

1
2
3
4
mov ax,1000h
mov dx,1
mov bx,1
div bx

显然这是16位除法,但是商为11000h,在ax中也放不下。

上述错误都可以称除法溢出

我们采用divdw来解决

功能:进行不会产生溢出的除法运算,被除数为dword型,除数为word型,结果为dword型。

参数:(ax)=dword型数据的低16位。

(dx)=dword型数据的高16位。

(cx)=除数

返回结果:(dx)=结果的高16位,(ax)=结果的低16位

(cx)=余数。

应用举例:计算1000000/10(F4240h/0Ah)

1
2
3
4
mov ax,4240h
mov dx 000Fh
mov cx,000Ah
call divdw

结果(dx)=0001h,(ax)=86A0h,(cx)=0

我们给出构造divdw的基本公式:

1
2
3
4
5
6
7
8
X:被除数
N:除数
H:X高16位,范围[0,FFFF]
L:X低16位,范围[0,FFFF]
int():取商
rem():取余数
公式:X/N=int(H/N)*65536 + [rem(H/N)*65536 + L]/N + 最后的余数。
验证的话两边同时✖️N就显而易见了

这样转化为不会产生溢出的除法!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ax是被除数低位,结果低位
dx是被除数高位,结果高位
cx除数和结果存的余数
divdw:push ax
mov ax,dx//高位先除,而默认用ax进行除法,所以把数放到ax
mov dx,0000h//清零
div cx//结果存到ax,余数在dx,相除的时候会自动作为高位。
mov es,ax//把高位结果放到es

pop ax//低位出来进行除法
div cx//结果存到ax
mov cx,dx//因为现在的余数在dx
mov dx,es//高16位。
ret返回了

我们尝试写一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
divdw:
push ax
mov ax,dx
mov dx,0000h
div cx#得到高16位除法的结果,我们要把商放到最后高16位上去,注意,这里我们的余数就已经在dx处了,也就是已经在高位了,而且再进行除法的时候会自动作为被除数的高位。

mov es,ax#先存起来

pop ax#把低16位放出来作为被除数的低位也就是L
div cx#再除法

mov cx,dx#余数
mov dx,es#最终的高16
ret回去。
code ends
end start

先来看标志寄存器。

第十一章 标志寄存器

有一种特殊的寄存器,具有以下三种作用。

image-20230403190650942

简记为控制CPU相关工作方式。

本章标志寄存器简称flag是我们要学习的最后一个寄存器。

之前学习了13个

1
ax,bx,cx,dx,si,di,bp,IP,cs,ss,es,ds,sp

flag寄存器是按位起作用的,每一位都有专门的含义,记录特定的信息。

8086的flag寄存器结构如图所示

image-20230403192555522

这里的

1
0,2,4,6,7,8,9,10,11位都具有特殊的含义!

11.1 ZF标志(见0置1)

flag的6位是ZF,零标志位。它记录执行相关指令之后,其结果是否为0。如果结果位0,那么zf=1;如果结果不

为0,那么zf=0

比如:

1
2
mov ax,1
sub ax,1

执行后,结果为0,则zf=1

1
2
mov ax,2
sub ax,1

执行后结果不为0,zf=0

再看

1
2
mov ax,1
or ax,0

执行后结果不为0,则zf=0,表示“结果非0”

11.2 PF标志(见偶1置1)

这是奇偶标志位,它记录相关指令执行之后,其结果的所有bit位中1的个数是否为偶数,偶数的话pf=1,奇数的话pf=0.

比如:

1
2
mov al,1
add al,10

执行后,结果为00001011b,显然1有奇数个,则pf=0

或者

1
2
mov al,1
or al,2

执行后为00000011b,显然偶数个1,pf=1

11.3 SF标志(讲解了补码运算!见负置1)

符号标志位。它记录相关指令执行之后,其结果是否为负。如果结果为负,sf=1;结果为正,sf=0

计算机中通常用补码来表示有符号数据。计算机中的一个数据可以看成有符号数,也可以看成是无符号数。比如:

1
2
00000001B,可以看成无符号数1,或者有符号+1
10000001B,可以看成无符号数129,或者有符号数-127(数字是取反+1得到的,符号是一开始的1决定的)

对于同一个二进制数据,计算机可以将它当作无符号数来运算,也可以当作有符号数来运算。比如

1
2
mov al,10000001B
add al,1

结果,(al)=10000010B

当作无符号的话,add相当于计算129+1=130。当作有符号的话,相当于-127+1=-126。

SF标志就是CPU对有符号数运算结果的一种记录,记录数据的正负。在我们将数据当作有符号数来计算的时候,可以通过它来得知结果的正负。如果当作无符号sf就没有意义。

比如

1
2
mov al,10000001B
add al,1

当作有符号,执行后,结果为10000010B,即-126,结果为负,则sf=1。

下面看特殊情况:

1
2
mov al,10000001B有符号数-127
add al,01111111B有符号数127

执行后结果为0,按正数来算,sf直接为0。

11.4 CF标志(见进为1)

flag的第0位为CF。一般情况下,在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位进位的值,或从更高位的错位值

比如

1
2
3
4
5
mov al,98h
add al,al#因为al是8位,最高表示到256。加完这个结果已经是130h,进位给cf,也就是
cf=1,al=30h妙
如果再来一次
add al,al此时al=60h,cf=0因为此时没有进位。

现在举个例子看减法错位

1
2
3
mov al,97h
sub al,98h相当于97-98,借位相当于197h-98h=FFh,此时cf=1
sub al,al执行后相当于(al)=0,CF=0因为没有借位。

11.5 OF 标志(有符号数见溢为1)

我们看两个溢出的例子,也就是运算结果超出了机器所能表达的范围

1
2
mov al,98
add al,99

执行后将产生溢出,因为al = al + 99=98+99=197,而8位有符号数能表示-128~127。(看来寄存器默认存有符号数)

再来看

1
2
mov al,00F0h也就是有符号数-16的补码(因为11110000为负数,计算机计算时取补码为00010000也就是-16,符号由初始数决定)
add al,0088h;88h(10001000取补码为01111000-120)显然是-120的补码,加在一起就是-136超出了-128所以结果将不正确。

flag的11位是OF,溢出标志位。一般情况下OF记录了有符号数运算结果是否发生溢出(这里我们也复习了负数计算时先取补码再计算)

溢出,OF=1,否则OF=0。

注意:CF对无符号数的进位!OF对有符号数的溢出!

(其实就是有符号数叫溢出,无符号数叫进位)

举例:

1
2
3
4
mov al,0F0h
add al,78h
显然执行之后无符号运算应该得168h,这里al=68h,cf=1。
按照有符号运算后16进制68h,不发生溢出!所以of=0

补充:计算机有符号运算,正数和负数的加法,就是正数和负数补码的异或运算!

11.6 adc指令

带进位加法指令。比如指令adc ax,bx实现的功能是(ax) = (ax) + (bx) + CF

例:

1
2
3
4
mov ax,2
mov bx,1
sub bx,ax
adc ax,1

显然bx-ax时存在进位所以cf = 1。最后执行adc ax,1时执行ax + 1 + cf = 4。

例:

1
2
3
mov al,98h
add al,al#这里加完应该是130h,所以al=30h,因为al最大8位,cf=1
adc al,3#此处执行时al = cf + 3 + al = 30h + 1 + 3 = 34h

其实这个指令是用来分解大寄存器加法的,因为ax等寄存器都是16位,16位的加法,如下:

1
2
3
4
0198h 
0183h +
--------
031bh

我们可以通过adc来实现,如下:

1
2
add al,bl
adc ah,bh

显然我们在做完低位加法之后会产生进位cf=1,之后我们高位加法并且要加上进位。

我们来做一个更大的运算

1
2
3
4
1ef000h
201000h +
------------
3f0000h

我们可以分成两步计算,两位两位做加法,每一步都加上前一步的进位即可!

1
2
3
4
mov ax,001eh
mov bx,f000h
add bx,1000h
adc ax,0020h

再来看

1
2
3
4
1ef0001000h
2010001ef0h +
---------------
3f00001ef0h

首先加低16位,记录cf值;再加次高16位,加上cf值同时记录新的cf值;最后加最高16位,加上cf值。

1
2
3
4
5
6
mov ax,001eh
mov bx f000h
mov cx,1000h#提出问题,,要是位数再大寄存器不够用了怎么办
add cx,1ef0h
adc bx,1000h
adc ax,0020h

下面编写一个程序实现两个128位数相加。

128位显然需要8个字单元。由低地址到高地址单元依次存放128位数据由低到高的各个字。结果存储在第一个数的存储空间即可。

用ds:si指向第一个数的存储空间,ds:di指向第二个数的存储空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
add128:push ax
push cx
push si
push di#先入栈,因为退出函数之后还可能用得到

sub ax,ax#这是将cf设置为0的方式
mov cx,8#循环8个字

s:mov ax,ds:[si]#找到第一个字
adc ax,ds:[di]#与第二个数的第一个字相加
#初始cf=0,后面可就不一定了,所以用adc
mov ds:[si],ax#$加完的结果依次存在第一个数的位置
inc si#自增进行下一个字节的运算,所以说,这里是一个字一个字的进行运算。所以每次自增两个字节。
inc si
inc di
inc di
loop s

pop di
pop si
pop cx
pop ax
ret

这里有个问题,自增语句inc si用了两次,可不可以直接用add si,2来代替?其实是可以的,只不过inc si只占一个字节,用两次才占两个字节。但是add si,2一次就三个字节,占空间很大。

11.7 sbb指令

带借位减法指令。指令sbb ax,bx实现的功能是(ax) = (ax) - (bx) - cf

比如计算

1
2
3
4
003e1000h
00202000h -
-------------
001effffh#我没算错吧,我也不清楚哈哈

我们采用如下指令来做

1
2
3
4
mov bx,1000h
mov ax,003eh
sub bx,2000h
sbb ax,0020h

可以看到sub产生进位cf=1然后再用sbb将cf也算上。

11.8 cmp指令

比较指令,相当于减法,只是不保存结果。执行后对标志寄存器产生影响。

比如cmp ax,axax-ax运算结果为0,但不在ax中保存,执行后,zf=1,pf=1,sf=0,cf=0,of=0。

因为zf是判断0的,见0置1,所以为1,其他同理,pf见偶置1确实为1,of见溢出置1,没有溢出所以为0。

比如下面指令

1
2
3
mov ax,8
mov bx,3
cmp ax,bx

执行后ax=8,但是结果5中有两个1所以pf=1,zf=1,因为非0,of见溢出置1,没有溢出所以of=0。

其实我们也可以通过看标志寄存器的值来看比较的结果!

比如

1
2
3
4
如果ax=bx显然ax-bx=0所以zf=1
如果ax不等bx显然ax-bx不等0,所以zf=0
如果ax<bx则ax-bx产生借位,所以cf=1
如果ax>bx则不产生借位并且结果不为0,所以cf=0而且zf=0

所以我们可以通过标志寄存器来看ax与bx的关系。

当然上面是指无符号运算,我们下面来看有符号运算。(减法用补码表示!负数也用补码表示!)

ah<bh可能引起sf=1因为为负。

1
2
3
4
5
6
ah=1
bh=2
ah-bh=1-2=01h-02h=0ffh因为结果应该为-1也就是101有符号数,用补码表示了。
ah=0feh
bh=0ffh
ah-bh=0feh-offh=-1用补码表示也就是0ffh,或者算的时候用0feh的补码减去0ffh的补码也就是-2-(-1)结果为负sf=1

不过sf=1并不能说明 操作对象1< 操作对象2

因为

1
2
3
4
5
6
ah=22h,bh=a0h,ah-bh=34-(-96)=82h是-126的补码
22h=00100010
a0h=10100000补码也就是01011111 + 1 = 01100000 = -96
82h=10000010是负数也就是-126
最后结果-126
所sf=1但是这里34显然>96!

得到相应结果的正负并不能说明运算所应该得到结果的正负。因为在运算过程中可能存在溢出。

比如

1
2
3
4
mov ah,22h
mov bh,a0h
sub ah,bh
结果sf=1,但是在数学上34-(-96)=130因为这个结果超出了127-128这个范围!在ah中不能表示了!ah中的结果被cpu解释成了有符号数-126

如果没有发生溢出,计算出的结果和数学上真正的结果应该一致了!

所以sf与其无关!

我们考查sf的同时也关注of,看是否存在溢出,即可知道cmp比较的结果了

1
2
3
4
1.sf=1而of=0也就是没有溢出!这是最好的情况,逻辑结果和计算结果相等!所以直接判断cmp即可。
2.sf=1,of=1有溢出计算结果和逻辑结果两个结果不相等,此时sf结果和cmp结果相反,也就是sf=1时ax>bx
3.sf=0而of=1显然就是ah<bh
4.sf=0并且of=0显然没进位,正常比较sf=0也就是ah>bh

11.9 检测比较结果的 条件转移指令

比如jcxz通过检测cx来修改ip注:条件转移指令的转移位移范围[-128,127]

下面是一些根据无符号数比较结果进行转移的指令根据有符号数的比较结果进行转移的条件转移指令。

image-20230427172031898

可以简记below,above等等。带n表示否定,not equal, not below, not above等。

并且检测 等于 只需要zf,检测大于或小于则需要cf。

实现如下功能:

如果ah=bh则ah = ah + ah,否则ah = ah + bh

1
2
3
4
5
6
cmp ah,bh
je s
add ah,bh
jmp short ok
s:add ah,ah
ok:..

这里也就是je去检测zf是否为1,如果为1代表ah=bh直接跳转到s执行。

这里注意,我们是否使用cmp指令要看需不需要,和je的执行无关,因为je只检测zf

1
2
3
4
5
mov ax,0
add ax,0
je s//检测上一步ax是否为0
inc ax
s:inc ax

因为执行add ax,0使得zf=1所以je直接执行跳转到s。

剩下的其他指令执行原理也类似。

下面来做题:

image-20230427173724776

image-20230427173730964

image-20230427173751488

采用比较的方式!

1
2
3
4
5
6
7
8
9
10
11
mov ax,data
mov ds,ax
mov bx,0
mov ax,0
mov cx,8#循环八次因为有8个字节。每个字节都和8比较一下
s:cmp byte ptr [bx],8#每个字节都和8比较
jne next#如果不相等,也就是zf=0就跳转到next继续循环
inc ax#如果相等就不跳转,ax+1计数一次。

next:inc bx
loop s

或者也可以这样

1
2
3
4
5
6
7
8
9
10
11
12
mov ax,data
mov ds,ax
mov bx,0#ds:bx指向第一个字节
mov ax,0#初始化计数器
mov cx,8
s:cmp byte ptr [bx],8#和8进行比较
je ok#相等就跳转到计数程序
jmp short next

ok:inc ax
next:inc bx
loop s

image-20230427174243454

和上面类似,我们来编程一下

1
2
3
4
5
6
7
8
9
10
11
12
mov ax,data
mov ds,ax
mov bx,0
mov ax,0
#计数器清零
mov cx,8
s:cmp byte ptr [bx]:8
jna next如果不大于就跳转到next
inc ax#如果大于8就计数

next:inc bx
loop s

或者也用ok的语句,判断大于就跳转到ok

1
2
3
4
5
6
7
8
9
10
11
12
13
mov ax,data
mov ds,ax
mov bx,0
mov ax,0
//都清零
mov cx,8//8次循环
s:cmp byte ptr [bx]:8
ja ok如果大于就到ok
jmp short next如果不大于就到next
next:inc bx
loop s

ok:inc ax

11.10 DF标志和串传送指令

df=0,每次操作后si,di递减。

df=1,每次操作后di,si递增。

下面看一个串传送指令

1
2
3
4
5
6
7
#movsb
#功能:
1.((es)*16 + (di)) = ((ds)*16 + (si))
2.如果df = 0,(si) = (si) + 1
(di) = (di) + 1
如果df = 1,(si) = (si) - 1
(di) = (di) - 1

用汇编语言描述如下

1
2
3
4
5
6
7
8
mov es:[di], byte ptr ds:[si]//注意实际8086并不支持这个指令,这只是一个描述
如果df = 0:
inc si
inc di

如果df = 1:
dec si
dec di//自减命令

也就是传送一个字节到es:[di],当然也可以传送一个字。然后根据寄存器df的值将si和di自增或者自减2。

1
mov es:[di], word ptr ds:[si]

记作movsw

一般来说movsbmovsw都和rep配合使用,格式如下:

1
2
3
4
5
rep movsb
原理就是
s:movsb
loop s
所以rep就是根据cx的值,重复执行后面的串传送指令。最终实现cx个字节或者字(rep movsw)的传送。

因为df决定着为i和di的增还是减,这也叫df是决定方向的。而cpu中对df进行设置的指令如下

1
2
cld: df = 0
std: df = 1

来看一个例子

(1)用串传送指令,将data段中第一个字符串复制到其后面的空间中。

1
2
3
4
data segment
db 'Welcome to masm!'
db 16 dup (0)
data ends

我们知道串传送指令默认的方向为:

1
2
3
4
传送的起始位置ds:[si]
目的位置es:[di]
传送长度cx
传送方向df

在本题中参数如下:

1
2
3
4
原始位置 data:0
目的位置data:0010//也就是第二行
传送长度16
传送方向df这里是依次正向传送应该为递增合适

所以可以编写程序如下:

1
2
3
4
5
6
7
8
9
mov ax,data
mov ds,ax
mov si,0//指向第一行
mov es,ax
mov di,16//这样es:[di]就指向第二行
mov cx,16
//循环16次,传送16次
cld//传送方向为正向
rep movsb//每次传送一个字节。

(2)用串传送指令,将F000H段中最后16个字符(显然最后一个字符也就是F000:FFFF)复制到data段中。

1
2
3
data segment
db 16 dup(0)
data ends

这里我们可以使用逆向传送

1
2
3
4
原始位置:F000:FFFF
目的位置:data:000F
传送长度:16
传送方向:逆向传送,si,di每次递减1。df = 1

程序可以如下:

1
2
3
4
5
6
7
8
9
mov ax,f000h
mov ds,ax//确定原始数据位置
mov si,ffffh//确定到最后一个字符
mov ax,data
mov es,ax//确定目标位置
mov di,000F
mov cx,16//共传送16个字符
std//方向逆向
rep movsb

11.11 pushf 和popf

pushf是将标志寄存器的值压入栈中,而popf是从栈中弹出数据,送入标志寄存器中。

也就是为简介访问标志寄存器提供了一种方法。

一道例题,执行之后(ax)=?

1
2
3
4
5
6
7
8
9
mov ax,0
push ax
popf//弹出0送入标志寄存器,此时flag全0
mov ax,fff0h
add ax,0010h//此时变成0000,cf=1,zf=1,pf=0
pushf//将flag压入栈中。
pop ax//出栈到ax,
and al,11000101b
and ah,00001000b

回忆标志寄存器图

image-20230403192555522

所以这里flag = 0041h出栈到ax。

然后进行与运算两次即可。

11.12 标志寄存器在debug中表示

image-20231203192927171

如图所示,有对应的表示。

下面看之前的一个题目

编写程序,统计F000:0处32个字节中,大小在[32,128]的数据的个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mov ax,0f00h
mov ds,ax

mov bx,0
mov dx,0
mov cx,32
//遍历32个字符
s: mov al,[bx]//每次取出一个字节
cmp al,32//如果小于32
jb s0
cmp al,128//如果大于128
ja s0
inc dx
s0: inc bx
loop s也就是不符合条件,继续判断下一个字符

编写程序,统计32个字节在区间在(32,128)也就是开区间的字节的个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mov ax,0f00h
mov ds,ax

mov bx,0
mov dx,0
mov cx,32

s:mov al,[bx]
cmp al,32
//如果小于等于,也就是不大于
jna s0
cmp al,128//如果大于等于,也就是不小于
jnb s0
inc dx

s0:inc bx
loop s

本章最后的实验十一单独找时间写一篇。