0%

马上过年了,这几天要么在帮忙,要么在玩。今天是年三十,上午没什么事,我要好好学一下。

外中断

外中断信息

有一种中断信息,来自于CPU的外部,当CPU外部有需要处理的事情发生的时候。比如说,外设的输入到达,芯片会向CPU发出相应的中断信息。CPU在执行完当前的指令后,可以检测发送过来的中断信息,引发中断过程,处理外设的输入

在PC中,外中断源一共有以下两类:

  1. 可屏蔽中断

    可屏蔽中断是CPU可以不响应的外中断。CPU是否可以响应可屏蔽中断,要看标志位寄存器IF的位的设置。当CPU检测到可屏蔽中断信息是,如果IF = 1,则CPU在执行完当前指令后响应中断,引发中断;如果IF= 0,则不响应可屏蔽

    可屏蔽中断程序的所引发的中断过程,基本和内中断的中断过程相同。因为可屏蔽中断信息来自于CPU外部,中断类型码通过数据总线送入CPU;而内中断的中断类型码在CPU内产生的

    现在,我们可以解释中断过程中将IF置为0的原因了。将IF置为0的原因就是,在进入中断和程序之后,禁止其他的可屏蔽中断。如果在中断过程中需要处理可屏蔽中断,可以用指令将IF置1。。8086CPU提供的设置IF的指令如下:

    • sti,设置IF = 1
    • cli,设置IF = 0
  2. 不可屏蔽中断

    不可屏蔽中断时CPU必须响应的外中断。当CPU检测到不可屏蔽中断信息时,则在执行完当前命令后,立即响应,并引发中断过程

    对于8086CPU,不可屏蔽中断的中断类型码固定为2,所以中断过程中,不需要取中断类型码,而是直接触发。不可屏蔽中断的中断过程为:

    • 标志寄存器入栈,IF=0,TF=0
    • CS,IP入栈
    • (IP) = (8),(CS)= (0AH)

几乎所有的外设引发的外中断,都是可屏蔽中断。当外设有需要处理的事件时(比如说键盘输入)发生时,相关芯片向CPU打出可屏蔽中断信息。不可屏蔽中断时在系统中有必须处理的紧急情况发生时用来通知CPU的中断信息

PC机的键盘处理过程

通过键盘的响应过程。我们可以感受一下外设输入的过程

键盘输入

键盘上的键相当于开关,键盘中有一个芯片。当按下一个键时,芯片就会产生一个扫描码,扫描码说明了按下的键在键盘上的位置。扫描码被送入主板上的相关接口芯片的寄存器中,该寄存器的端口地址为60h。松开按下的键时,也产生一个扫描码,扫描码说明了松开的键在键盘上的位置。松开时,产生的扫描码也送到端口60h中

  • 通码:按下一个键产生,第7位为0
  • 断码:松开一个键产生,第7位为1

一个扫描码的长度为一个字节,所以有 断码 = 通码 + 80H

引发9号中断

当键盘的输入到达60H端口时,相关的芯片就会向CPU发出中断类型码为9的可屏蔽中断信息。CPU检测到该中断信息后,如果IF=1,则响应中断,引发中断过程,转去执行int 9 中断例程

执行 int 9中断例程

BIOS提供了int 9中断例程,用来进行基本的键盘输入处理,其工作如下:

  1. 读出60h端口中的扫描码
  2. 如果是字符键的扫描码,将该扫描码和它所对应的(ASCII码)送入内存中的BIOS键盘缓冲区;如果是控制键(Ctrl)和切换键(CapsLock)的扫描码,则将其转变为状态字节(用二进制位记录控制键和切换状态的字节)写入内存中存储状态字节的单元
  3. BIOS键盘缓冲区在系统启动后,用来存放int 9中断例程中所接收的键盘输入。该内存区可以存储15个键盘输入,因为 int 9中断例程除了接受扫描码外,还要产生对应的字符码。所以在BIOS键盘缓冲区,一个键盘输入用一个字单元存放(高位存放扫描码,低位存放字符码)

关于状态码如下:

image.png

INT 9例程

编写int 9例程

我们可以编写新的键盘中断例程,进行一些特殊工作。不过设计到部分的硬件操作细节,不过我们可以通过使用已经编写好的int 9例程覆盖这些操作

比如现在我们需要:在屏幕中间依次显示“a”~“z”,并可以让人看清。在显示的过程中,按下Esc键后改变颜色

我们可以先写出循环打印的程序

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
40
41
42
43
44
assume cs:code
stack segment
db 128 dup(0)
stack ends

code segment
start:
mov ax,stack
mov ss,ax
mov sp,128

mov ax,0b800H
mov es,ax
mov ah,'a'
s:
;显示a-z
mov es:[160*12+40*2],ah
call delay ;因为我们的CPU执行速度太快了,所以需要延迟输出
inc ah
cmp ah,'z'
jna s

mov ax,4c00h
int 21h

delay:
;延迟函数
push ax
push dx
mov dx,0FH ;循环F0000h次
mov ax,0
s1:
sub ax,1
sbb dx,0
cmp ax,0
jne s1
cmp dx,0
jne s1
pop dx
pop ax
ret

code ends
end start

接着我们使用int 9中断例程去实现变色

键盘输入到达60h端口后,会引发int 9 中断例程。我们可以编写int 9例程,功能如下:

  • 从60H端口中读出键盘的输入
  • 调用BIOS的int 9中断例程,处理其他硬件细节
  • 判断是否位Esc的扫描码,如果是,改变颜色后返回;如果不是则直接返回

不过如何调用原int 9也是一个问题,在这里我们可以将其入口地址作为我们的函数入口,然后用以下方法模拟调用:

  1. 标志寄存器入栈
  2. IF=0,TF=0
  3. call dword ptr ds:[0]

对于(1),可以使用 pushf实现

对于(2),可以用下面的指令实现

1
2
3
4
5
pushf
pop ax
and ah,11111100B ;IF和TF为标志寄存器的第九位和第八位
push ax
popf

则综上的int模拟过程为:

1
2
3
4
5
6
7
pushf
pushf
pop ax
and ah,11111100B
push ax
popf ;IF=0,TF=0
call dword ptr ds:[0] ;将CS,IP入栈

知道原理之后我们可以实现:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
assume cs:code

stack segment
db 128 dup(0)
stack ends

data segment
dw 0,0 ;为原int 9中断例程的调用分配空间
data ends
code segment
start:
;栈的初始化
mov ax,stack
mov ss,ax
mov sp,128
;拷贝原int 9的中断例程
mov ax,data
mov ds,ax
mov ax,0
mov es,ax
push es:[9*4]
pop ds:[0]
push es:[9*4+2]
pop ds:[2] ;将入口地址保存在当时ds:0~2单元中
mov word ptr es:[9*4],offset int9
mov es:[9*4+2],cs
;显存初始化
mov ax,0b800H
mov es,ax
mov ah,'a'
s:
;显示a-z
mov es:[160*12+40*2],ah
call delay ;因为我们的CPU执行速度太快了,所以需要延迟输出
inc ah
cmp ah,'z'
jna s
;恢复例程
mov ax,0
mov es,ax
push ds:[0]
pop es:[9*4]
push ds:[2]
pop es:[9*4+2]

mov ax,4c00h
int 21h

delay:
;延迟函数
push ax
push dx
mov dx,0Fh ;循环F0000h次
mov ax,0
s1:
sub ax,1
sbb dx,0
cmp ax,0
jne s1
cmp dx,0
jne s1
pop dx
pop ax
ret
;------------新的int 9的中断例程-----------------
int9:
push ax
push bx
push es

in al,60h ;从60h读出键盘的输入

pushf
pushf
pop bx
and bh,11111100B
push bx
popf
call dword ptr ds:[0] ;对int指令进行模拟,调用原来的int 9中断例程

cmp al,1
jne int9ret

mov ax,0b800H
mov es,ax
inc byte ptr es:[160*12+40*2+1] ;将属性值加1,改变颜色
int9ret:
pop es
pop bx
pop ax
iret
code ends
end start

安装int 9例程

刚刚是简介使用了原int 9中断例程的功能,现在我安装一个新的int 9中断例程,使得原int9中断例程的功能得到拓展

功能:在DOS下,按F1键后改变当前屏幕的显示颜色,其他键照常处理

在开始之前需要解决几个问题,首先

  1. 改变屏幕的显示颜色

改变从B800H开始的4000个字节中的所有奇地址元素中的内容,当前屏幕的显示颜色即发生改变

1
2
3
4
5
6
7
8
	mov ax,08b00H
mov es,ax
mov bx,1
mov cx,2000
s:
inc byte ptr es:[bx]
add bx,2
loop s
  1. 其他的都是使用原int9例程处理即可,这里我们还需要调用原int9中断例程,所以需要保存原int9例程的入口地址:

由于安装程序在程序返回之后地址将丢失,所以我们将其保存在0:200后,而我们重写的int9例程,我们也将保存在0:204之后

现在准备就绪,开始编写

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
assume cs:code
stack segment
db 128 dup (0)
stack ends

code segment
start:
mov ax,stack
mov ss,ax
mov sp,128

push cs
pop ds

mov ax,0
mov es,ax
;安装程序
mov si,offset int9 ;设置ds:si指向源地址
mov di,204h ;设置es:di指向目的地址
mov cx,offset int9end-offset int9
cld
rep movsb

push es:[9*4]
pop es:[200H]
push es:[9*4+2]
pop es:[202H]

cli
mov word ptr es:[9*4],204h
mov word ptr es:[9*4+2],0
sti

mov ax,4c00H
int 21h

int9:
push ax
push bx
push cx
push es

in al,60h
pushf
call dword ptr cs:[200h] ;此时cs=0

cmp al,3bh ;F1的扫描码为3bH
jne int9ret

mov ax,0b800H
mov es,ax
mov bx,1
mov cx,2000
s:
inc byte ptr es:[bx]
add bx,2
loop s
int9ret:
pop es
pop cx
pop bx
pop ax
iret
int9end:
nop
code ends
end start

实验十五

image.png

很简单,只要在上一个代码的基础上做修改即可

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
assume cs:code
stack segment
db 128 dup (0)
stack ends

code segment
start:
mov ax,stack
mov ss,ax
mov sp,128

push cs
pop ds

mov ax,0
mov es,ax
;安装程序
mov si,offset int9 ;设置ds:si指向源地址
mov di,204h ;设置es:di指向目的地址
mov cx,offset int9end-offset int9
cld
rep movsb

push es:[9*4]
pop es:[200H]
push es:[9*4+2]
pop es:[202H]

cli
mov word ptr es:[9*4],204h
mov word ptr es:[9*4+2],0
sti

mov ax,4c00H
int 21h

int9:
push ax
push bx
push cx
push es

in al,60h
pushf
call dword ptr cs:[200h] ;此时cs=0

cmp al,9EH ;判断断码
jne int9ret

mov ax,0b800H
mov es,ax
mov bx,0
mov cx,2000
s:
mov byte ptr es:[bx],'A' ;全屏输出
add bx,2
loop s
int9ret:
pop es
pop cx
pop bx
pop ax
iret
int9end:
nop
code ends
end start

昨天学的那个太难了,再加上下午一直在玩,今天要赶赶进度

int 指令

当CPU执行 int n指令时,相当于引发一个n号中断的过程,其执行流程如下:

  • 取中断类型码n
  • 标志寄存器入栈,IF = 0,TF = 0
  • CS,IP入栈
  • (IP) = (n*4) (CS) = (n*4+2)

可以在程序中使用int指令,调用任何一个中断的中断处理程序

比如我们使用这段程序:

1
2
3
4
5
6
7
8
9
assume cs:code
code segment
start:
mov ax,0b800h
mov es,ax
mov byte ptr es:[12*160+40*2],'!'
int 0
code ends
end start

当我们运行这段指令之后我执行0号处理程序,然后回到系统

由此,我们可以看出int 和 call指令相似,都是调用一段程序

我们可以实现编译一些子程序,作为中断处理程序,然后用int进行调用,我们把这个称为中断例程

编写供应用程序调用的中断例程

书本里面给了两个例题

image.png

我们在下面给出安装程序

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
assume cs:code
code segment
start:
mov ax,cs
mov ds,ax
mov si,offset sqr ;指向源地址
mov ax,0
mov es,ax
mov di,200H
mov cx,offset sqrend - offset sqr
cld
rep movsb

mov ax,0
mov es,ax
mov word ptr es:[7ch*4],200H
mov word ptr es:[7ch*4+2],0

mov ax,4c00H
int 21h

sqr:
mul ax
iret ;中断例程的最后使用,退栈CS:IP和标志寄存器
sqrend:
nop

code ends
end start
image.png

我们在下面给出安装程序

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
40
assume cs:code
code segment
start:
mov ax,cs
mov ds,ax
mov si,offset capital ;指向源地址
mov ax,0
mov es,ax
mov di,200H
mov cx,offset capitalend - offset capital
cld
rep movsb

mov ax,0
mov es,ax
mov word ptr es:[7ch*4],的
mov word ptr es:[7ch*4+2],0

mov ax,4c00H
int 21h

capital:
push cx
push si
change:
mov cl,[si]
mov ch,0
jcxz ok
and byte ptr [si],11011111b
inc si
jmp short change
ok:
pop si
pop cx
iret
capitalend:
nop

code ends
end start

因为中断例程用到了si,cx所以我们需要用栈保存

对int,iret和栈的深入理解

我们通过一个例子,来实现int,iret,栈的深入了解

image.png

为了具备loop的功能,7cH中断例程需要具备以下功能:

  • dec cx
  • 如果(cx)!=0,转到s标号执行,否则向下执行

可是怎么实现到目的地的转移到呢?

  • 我们应该设(CS)为s的段地址,(IP)为s的偏移地址
  • 在中断例程开始之后,我们会将s标号的段地址和se的偏移地址压入栈中.此时,我们可以用之前存放在bx中的偏移位移,来得到标号s的偏移地址
  • 接着利用iret指令,我们将栈中的se的偏移地址加上bx中的转移位移,则栈中的偏移地址即变成了s的偏移地址.我们再使用iret指令,用栈中的内容设置CS,IP从而实现了跳转

由此我们可以写出中断例程:

1
2
3
4
5
6
7
8
9
lp:
push bp
mov bp,sp
dec cx
jcxz lpret
add [bp+2],bx
lpret:
pop bp
iret

这需要说明一下,因为我们要访问栈,所以使用了bp.在程序开始前,先将bp入栈保存,结束时再出栈恢复.当要修改栈中se的偏移地址时,栈中的结构是: 栈顶处是bp原来的数值,下面是se的偏移地址,在下面是s的段地址,再下面是标志寄存器此时bp中为栈顶的偏移地址,所以((ss)*16+(bp)+2)处为se的偏移地址,再加上bx中的偏移位移就变成了s的偏移地址.最后再用iret出栈返回,CS:IP此时为标号s的指令

BIOS与DOS

BIOS和DOS提供的中断例程

在系统的ROM中存放了一套程序,称之为BIOS(基本输入输出系统),BIOS中主要包含以下内容:

  • 硬件系统的检测和初始化程序
  • 外部中断和内部中断的中断例程
  • 用于对硬件设备进行I/O操作的中断例程
  • 其他和硬件系统相关的中断例程

操作系统DOS也提供了中断例程,从操作系统的角度来看,DOS的中断例程就是操作系统像程序员提供的一种编程资源

BIOS和DOS中断例程的安装过程

BIOS和DOS 的中断例程是怎么安装到内存中的呢?

  • 开机后,CPU加电.初始化(CS)=0FFFFH,(IP)=0,自动从FFFF:0单元开始执行程序.在FFFF:0单元有一条跳转指令,CPU执行这个命令之后转去执行BIOS中的硬件系统检测和初始化程序
  • 初始化程序将建立BIOS所支持的中断向量,只需要将BIOS提供的中断例程入口登记在中断向量表中.在这里需要注意,对于BIOS所提供的中断例程,只需要将入口地址登记在中断向量表中,因为他们是被固化在ROM中的程序,一直在内存中存在
  • 硬件系统检测和初始化完成之后,调用 int 19h进行操作系统的引导.从此将计算机交由操作系统控制
  • DOS启动后,除完成其他工作之外,还将它所提供的中断例程装入内存中,并建立相应的中断向量

BIOS中断例程的应用

我们可以使用int 10H中断例程,其包含了多个和屏幕输出相关的子程序.

这里可以看出,一个供程序员调用的中断例程,其中包含了多个子程序,中断例程内部用传递进来的参数来决定执行哪一个子程序.在BIOS和DOS中提供的中断例程,都是用ah来传递内部的子程序的编号

展示以下 int 10h中断例程的设置光标位置功能:

1
2
3
4
5
6
mov ah,2	;置光标
mov bh,0 ;第0页
mov dh,5 ;dh中放行号
mov dl,12 ;dl中放列号
int 10h
;上面操作的含义是:设置光标到第0页,第5行,第12列

关于页号的含义可以看看下方的图片:

image.png

我们继续试试int 10h中断例程的在光标位置显示字符功能

1
2
3
4
5
6
7
mov ah,9	;在光标位置显示字符
mov al,'a' ;字符
mov bl,7 ;颜色属性
mov bh,0 ;第0页
mov cx,3 ;字符重复个数
int 10H
;含义:在屏幕的第5行12列显示3个红底高亮闪烁绿色的"a"

我们编写一个完整的程序来查看效果:

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
start:
mov ah,2
mov bh,0
mov dh,5
mov dl,12
int 10h

mov ah,9
mov al,'a'
mov bl,11001010b
mov bh,0
mov cx,3
int 10h

mov ax,4c00H
int 21h

code ends
end start

非常成功(好耶!!!)

DOS中断例程的应用

int 21H是DOS提供的中断例程,其中包含了DOS提供给程序员编程时调用的子程序

比如我们常用的:

1
2
3
mov ah,4ch	;程序返回
mov al,0 ;程序返回值
int 21h

我们来试试另外的用法:在光标位置显示字符串的功能

1
2
3
;ds:dx指向字符串	;要显示的字符串需要用"$"作结束符
mov ah,9 ;功能号9,表示在光标位置显示字符串
int 21h

我们来试试效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
assume cs:code
data segment
db 'I love you my baby','$' ;$本身并不输出,只是起到边界作用
code segment
start:
mov ah,2
mov bh,0
mov dh,5
mov dl,12
int 10h

mov ax,data
mov ds,ax
mov dx,0
mov ah,9
int 21H

mov ax,4c00H
int 21h

code ends
end start

DOS为程序员提供了许多可以调用的子程序,包含在int 21H中断例程中.可以自行了解

端口

在PC机种,和CPU通过总线相连接的芯片除了各种储存器以外,还有以下3种芯片:

  • 各种接口卡(显卡,网卡)上的接口芯片,它们控制接口卡进行工作
  • 主板上的接口芯片,CPU通过它们对部分外设进行访问
  • 其他芯片,用来存储相关的系统信息,或进行相关的输入输出处理

在这些芯片种,都有一组可以由CPU读写的寄存器,这些寄存器有以下共同点:

  • 都和CPU的总线相连接,当然这种连接都是通过他们所在的芯片进行的
  • CPU对他们进行读写的时候都通过控制线向他们所在的芯片发出端口读写命令

所以,从CPU的角度,将这些寄存器当作端口,对他们进行统一编址,从而建立一个统一的端口地址空间。每一个端口在地址空间都有一个地址

CPU可以直接读写这三个地方的数据:

  • CPU内部的寄存器
  • 内存单元
  • 端口

端口的读写

在访问端口时,CPU通过端口地址来定位端口。因为端口所在的芯片和CPU通过总线相互连接,所以,端口地址和内存地址一样,通过地址总线来传送。在PC中CPU最多可以定位64KB个不同的端口,即端口的地址范围为0~65535

访问端口的命令只有两条 inout,我们分析下面的例子:

1
in al,60H	;从60H号端口读入一个字节

注意,在inout指令中,只能使用ax和al来存放从端口中读入的数据或要发送到端口中的数据

  • 访问8位端口要用al
  • 访问16位端口要用ax

CMOS RAM芯片

我们通过一个芯片的例子来详细的体会一下对端口的访问

这个芯片的特征如下:

  • 包含一个实时钟和一个有128个存储单元的RAM存储器
  • 该芯片靠电池供电。所以,关机后其内部的实时钟仍可以正常工作,RAM中的信息也不会丢失

芯片的作用如下:

  • 128个字节的RAM中,内部实时钟占用0~0DH单元来保存时间信息,其余大部分单元用于保存系统配置信息,供系统启动时BIOS程序读取。BIOS也提供相关的程序,使我们可以在开机的时候配置CMOSRAM中的系统信息
  • 该芯片有两个端口,端口地址为70H和71H,CPU通过这两个端口来读写CMOSRAM。其中70H为地址端口,存放要访问的CMOS的单元地址;71H为数据端口,存放选定的CMOS单元中读取的数据,或要写入其中的数据

也就是说CPU对CMOS的读写要分两步进行。比如,读CMOS的2号单元

  1. 将2送入端口70H
  2. 从端口71H读出2号单元的内容

shl和shr指令

他们两个是逻辑移位指令

他们的使用方法为

1
2
3
4
5
6
shl/shr 二进制数据,见下面的分类
;如果只是移动一位
shl al,1
;如果移动的不止一位,则将移动的次数放在cl中
mov cl,N
shl al,cl

shl

shl是逻辑左移指令,它的功能为:

  • 将一个寄存器或内存单元中的数据向左移位
  • 将最后移除的一位写为CF中
  • 最低位用0补充

将X逻辑左移一位相当于进行: X=X*2

shr

shr是逻辑右移指令,它的功能为:

  • 将一个寄存器或内存单元中的数据项右移位
  • 将最后移除的一位写入CF中
  • 最高位用0补充

将X逻辑右移一位相当于进行:X=X/2

这里举个例子,将10100100B左移三位

1
2
3
原数据:	10100100
左移后: 10100100 CF =1
最低位用0补充:00100000

CMOS RAM中存储的时间信息

这里有一个编程任务

image.png

实现这个功能,我们需要分成两个步骤进行:

  • 从CMOSRAM中的8号单元读出当前月份的BCD码
  • 将用BCD码表示的月份以十进制的形式显示到屏幕上

关键在于怎么将BCD值转换为十进制数对应的ASCII码,这里我们可以用刚刚提到的逻辑位移实现

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
start:
;从CMOS中单读出当前的月份的BCD码
mov al,8
out 70h,al
in al,71h
;分离BCD码
mov ah,al
mov cl,4
shr ah,cl
and al,00001111b
;转换为ASCII码
add ah,30H
add al,30H
;显示
mov bx,0b800H
mov es,bx
mov byte ptr es:[160*12+40*2],ah ;显示十位数码
mov byte ptr es:[160*12+40*2+2],al ;显示个位数码

mov ax,4c00H
int 21H
code ends
end start

实验十四

image.png
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
assume cs:code 
data segment
db '00/00/00 00:00:00','$'
db 9,8,7,4,2,0
data ends
stack segment
dw 8 dup(0)
stack ends
code segment
start:
;初始化
mov ax,stack
mov ss,ax
mov sp,16
mov ax,data
mov ds,ax
mov si,0
mov cx,6
mov bx,12H
;从CMOS中单读出当前的月份的BCD码
s:
mov al,[bx]
out 70h,al
in al,71h
call go
inc bx
loop s

;字符显示
mov ax,data
mov ds,ax
mov dx,0
mov ah,9
int 21H

mov ax,4c00H
int 21H

go:
translation:
;分离BCD码
push cx
mov ah,al
mov cl,4
shr ah,cl
and al,00001111b
;转换为ASCII码
add ah,30H
add al,30H
input:
;存入
mov byte ptr ds:[si],ah ;显示十位数码
mov byte ptr ds:[si+1],al ;显示个位数码
add si,3
pop cx
ret

code ends
end start

今天继续学习汇编语言,争取这个星期之内学完8086CPU

内中断

CPU 具有一种能力,可以在执行当前的命令之后,检测到从CPU外部发送过来的或者内部产生的一种特殊信息,并且可以立即对接受到的信息进行某种处理。这种特殊的信息,我们称之为”中断信息”。中断的意思是要求CPU马上进行某种处理,并向需要进行处理的提供了必备的参数的通知信息

内中断的产生

对于8086CPU,我们有四种情况会产生中断信息:

  • 除法错误,比如除法溢出
  • 单步执行
  • 执行into 指令
  • 执行int 指令

对于不同的信息,我们需要进行不同的处理,那么我们必须知道中断信息的来源,所以中断信息中需要包含识别来源的编码。在8086中我们用称为中断类型码的数据来标识中断信息的来源。中断类型码为一个字节型数据,可以标识256种中断信息的来源。以后我们将中断信息的来源称之为中断源。上述的4种中断源,在8086中的中断类型码如下:

  • 除法错误 –> 0
  • 单步执行 –> 1
  • 执行into –> 4
  • 执行int指令,指令格式为 int n指令中n为字节型立即数,是提供给CPU 的中断类型码

中断处理

中断处理程序

CPU在收到中断信息之后,需要对中断信息进行处理。而我们编写的,用来处理中断信息的程序被称之为中断处理程序。一般对不同的中断信息我们会编写不同的处理程序

当我们收到中断信息后,应该前往中断处理程序进行处理。所以我们需要根据中断信息确定处理程序的入口。我们用下面的方式来进行对中断处理程序的段地址和偏移地址的查找

中断向量表

根据CPU的设计,中断类型码的作用就是用来定位中断处理程序的

我们通过中断向量表完成对中断类型的查找,中断向量表在内存中保存,其中放了256个中段源的处理程序、

image.png

所以只要知道了中断类型码,就可以通过查找中断向量表,找到处理程序的入口

所以现在,找到中断向量表成了首要条件。

中断向量表在内存中存放,对于8086PC机,中断向量表指定存放在内存地址0处。从内存0000:00000000:03FF的1024个单元中存放着中断向量表

中断过程

当CPU收到中断信息后,要对中断信息进行处理,首先将引发中断过程。硬件在完成中断过程之后,CS:IP将指向中断处理程序的入口,CPU开始执行中断处理程序

不过在执行完中断处理程序后,我们还需要返回原来的执行点继续执行下面的指令。

为了解决这个问题,CPU在收到中断类型之后,所引发的中断过程是这样的:

  • 获取中断类型码
  • 将标志寄存器中的值入栈(因为在中断过程中要改变标志寄存器的值,所以先要保存)
  • 设置标志寄存器的第8位TF和第9位IF的值为0
  • CS的内容入栈
  • IP的内容入栈
  • 从内存地址为中断类型码4和中断类型码4+2的两个字单元读取中断处理程序的入口地址,并设置

我们用以下方式更加简洁的描述这个中断的过程:

  • 取得中断类型码N
  • pushf
  • TF = 0,IF = 0
  • push CS
  • push IP
  • (IP) = (N*4)(CS) = (N*4+2)

最后开始执行由程序员编写的中断处理程序

中断处理程序和iret指令

由于CPU随时都可能检测到中断信息,所以CPU随时都可能执行中断处理程序,所以中断处理程序必须一直存储在内存中的某段空间中。而中断处理程序的入口地址,即中断向量,必须存储在中断向量表项中

中断处理程序的编写方法和子程序的比较相似,其步骤如下:

  • 保存用到的寄存器
  • 处理中断
  • 恢复用到的寄存器
  • 用iret指令返回

iret指令的功能用汇编语法描述为:

  • pop IP
  • pop CS
  • popf

iret的出栈顺序和执行中断过程中断的入栈顺序正好相反

除法错误中断的处理

触发触发错误

编程处理0号中断

我们改变一下0号中断处理程序的功能,即重新编写一个0号中断处理程序,他的功能是在屏幕中显示”overflow”,然后返回操作系统

我们分析一下需求:

  1. 引发中断过程
  • 取得中断类型码0
  • 标志寄存器入栈,TF,IF设置为0
  • CSIP入栈
  • (IP) = (0*4) (IP) = (0*4+2)
  1. 中断处理过程(我们将此程序称为do0)
  • 相关处理
  • 向显示缓冲区送字符串”overflow!”
  • 返回DOS
  1. 存放do0到电脑内存空间中

​ 如果存储在其他内存空间中,可能会导致内存内容被覆盖。所以我们将其放在中断向量表中的后面的空余部分,这是因为中断向量表支持256个中断,但是在实际的操作过程中,后面的数据基本不会被使用。所以在中断向量表中,许多单元是空的。我们使用这些程序对我们的中断处理程序进行存放。

  1. 中断处理程序do0的存放

​ 我们将中断处理程序放到0000:0200后,此时0000:0200是我们中断处理程序的入口,我们需要把0号中断向量表的地址设置为该入口的地址

综上我们需要,进行以下的任务:

  • 编写可以显示”overflow!“的中断处理程序
  • 将do0送入内存0000:0200
  • 将do0的入口地址0000:0200存储在中断向量表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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
assume cs:code
code segment
start:
;设置es:di指向目标地址
mov ax,0
mov es,ax
mov di,200H
;设置ds:si指向源地址
mov ax,cs
mov ds,ax
mov si,offset do0
;设置cx为源程序长度
mov cx,offset do0end-offset do0;利用编译器计算do0代码字节长度
;设置方向为正
rep movsb

;设置中断向量表
mov ax,0
mov es,ax
mov word ptr es:[0*4],200H
mov word ptr es:[0*4+2],0d

mov ax,4c00H
int 21H

do0:
jmp short do0start
db "overflow!"

do0start:
;显示字符串"overflow!"
;设置ds:si指向字符串
mov ax,cx
mov ds,ax
mov si,142H
;设置es:di指向显存空间的中间位置
mov ax,0b800H
mov es,ax
mov di,12*160+36*2

mov cx,9
s:
mov al,[si]
mov es:[di],al
inc si
add di,2
loop s

mov ax,4c00H
int 21H

do0end:
nop

code ends
end start

首先我们将这段程序进行编译之后成可执行程序,我们运行程序对0号中断处理程序进行修改

然后编写一个有除法溢出错误的程序即可

由于这里的显存内容不断的被刷新,所以会出现看不到警告的问题,但是没关系

单步中断

分析以下单步中断的中断过程

我们知道当CPU检测到TF的值为1时,进行1号中断处理程序,如果此时TF仍然为1,那么在执行中断程序时,会重新进入1号中断处理程序,这样如此往复,会出现各种问题。为了解决这个问题,我们采取以下方法:

  • 取得中断类型码N
  • 标志寄存器入栈,TF=0,IF=0
  • CS,IP入栈
  • (IP)=(N*4),(CS) = (N*4+2)

通过这种方法,我们就可以实现CPU的单步中断功能

响应中断的特殊情况

一般情况下,CPU在执行完当前指令后,如果检测到中断信息,就立即响应中断,引发中断过程。

但是也有特殊情况,在执行完向ss寄存器中传送数据的指令后,即使发生中断,也不会响应。这是因为 ss:sp联合指向栈顶,所以对他们的设置应该联合完成,如果只设置了SS,而没有更新SP,那么此时指向的是一个错误的栈顶。所以CPU在执行设置ss的指令之后不会响应中断,而是向后继续执行一条指令,这样的话为连续设置栈顶提供了一个机会(当然你也可以执行其他指令)。

所以这样就可以解释为什么之前提到的,设置SS之后会继续向后执行一条命令。

今天继续学习汇编语言,感觉有点难哇

标志寄存器

我们前面介绍了13种寄存器分别的作用,现在还剩一种特殊的寄存器,它有以下功能:

  • 用来存储相关指令的某些执行结果
  • 用来为CPU执行相关指令提供行为依据
  • 用来控制CPU的相关工作方式

这种特殊的寄存器被称为 标志寄存器(flag)

它和别的寄存器不同,其他寄存器用来存放数据,具有整个的意义,而flag寄存器是按位起作用的,其每一位都有特定作用

1
2
3
4
  15   14   13   12   11   10   9    8    7    6    5    4    3    2    1    0
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
| | | | | OF | DF | IF | TF | SF | ZF | | AF | | PF | | CF |
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+

我们主要学习 CF PF ZF SF OF DF 这几种标志位

常见标志位

ZF标志位

flag的第六位是ZF,零标志位

它记录相关指令执行后数值是否为0,如果是,那么ZF = 1表示肯定;如果不是,那么ZF = 0表示否定

在8086的指令集中,并不是所有的指令都会影响标志位寄存器

以下的指令是由影响的

1
add sub mul div inc or and

以下的没有影响

1
mov push pop

PF标志

flag的第二位是PF,奇偶标志位

它记录相关指令执行后,其结果的所有bit位中1的个数是否为偶数。如果是偶数,PF = 1;如果不是,PF = 0

比如下面的指令执行后,结果为00001011B,其中有3个1,则PF = 0

1
2
mov al,1
add al,10

SF标志位

flag的第七位是SF,符号标志位

它记录相关指令执行后,其结果是否为负。如果是,那么SF = 1;如果不是,SF = 0

这里我们知道,我们进行的计算,既可以看作有符号计算,也可以看作无符号计算;当我们进行无符号计算时,无论如何它对于我们而言都是非负数,但是对于SF而言,它的结果始终是由符号的。

也就是说当我们执行相关命令的时候我们始终是会影响到SF标志位的,至于是否需要这种影响,取决于我们自己

CF标志位

flag的第零位是CF,进位标志位

在进行无符号运算时,它记录了运算结果的最高有效值向更高维的进位值,或从更高位的借位值

当两个数相加时可能向更高位进位CF = 1

1
2
mov al,98H
add al,al

当两个数相减时也有可能向更高位借位

1
2
mov al,97H
sub al,98H ;借位变成197H-98H = FFH

OF标志位

flag的第十一位是OF,溢出标志位

OF用来记载是否发生了溢出,如果发生,OF = 1;如果没有,OF = 0

这里我们需要区分一下进位与溢出:

  • 进位是针对无符号计算,溢出是针对有符号的计算
  • CF用于检测无符号运算溢出
  • OF用于检测有符号运算溢出

更多的指令

adc指令

adc是带进位的加法指令,它利用CF位上的记录的进位制

1
adc 操作对象1,操作对象2

功能:操作对象1 = 操作对象1 + 操作对象2 + CF

我们为什么要加上CF的值呢?我们可以使用其完成低位存在进位的加法,可以分成两步:

  • 低位相加 add al,bl
  • 高位进位相加 adc ah,bh

比如计算1E F000 1000H + 20 1000 1EF0H的值,结果放在ax(最高位),bx(次高位),cx(最低位)

1
2
3
4
5
6
mov ax,001EH
mov bx,F000H
mov cx,1000H
add cx,1EF0H
adc bx,1000H
adc ax,0020H

sbb指令

sbb是带借位的减法指令,它利用了CF位上记录的CF值

1
sbb 操作对象1,操作对象2

功能:操作对象1 = 操作对象1 - 操作对象2 - CF

比如计算 003E 1000H- 0020 2000H的值,结果放在ax,bx

1
2
3
4
mov bx,1000H
mov ax,003EH
sub bx,2000H
sbb bx,0020H

popf和pushf

pushf的功能时将标志寄存器的值压入栈中

popf则是从栈中弹出数据,送入标志寄存器中

比较跳转

cmp指令

cmp指令的操作相当于sub,只不过其结果不被寄存器储存,而是只影响flag中的标志寄存器

我们可以通过cmp ax,bx指令执行后,相关标志位的状态看出比较的结果:

  • 如果(ax) = (bx),则 zf = 1
  • 如果(ax) != (bx),则zf = 0
  • 如果(ax) < (bx),则必将产生借位,cf = 1
  • 如果(ax) >= (bx),则不必借位,cf = 0
  • 如果(ax) > (bx),则不必借位,且结果不为0,cf = 0 and zf = 0
  • 如果(ax) <= (bx),则可能借位,也可能结果为0,cf = 0 or zf = 0

但是在这里我们默认的进行的是无符号计算,但是在实际的比较中我们也会遇到有符号数值的比较

这个时候我们需要结合sf(进位)和of(溢出)的情况进行判断:

  • 当of = 0 时,说明没有溢出,此时 逻辑上真正结果的正负 = 实际结果的正负
  • 当of !=0 时,说明溢出,此时 逻辑上真正结果的正负 != 实际结果的正负

所以我们可以进行有符号整数的判断:

  • 如果 (ax) < (bx),则(sf = 1 and of = 0) or (sf = 0 and of = 1)
  • 如果 (ax) > (bx),则sf = 1 and of = 1
  • 如果 (ax) >= (bx),则sf = 0 and of = 0

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

我们之前使用过jcxz条件跳转指令,但是它是对(cx)进行判断

下面有常用的根据无符号数的比较结果进行转移的条件转移指令:

1
2
3
4
5
6
7
指令			 含义						检测的相关标志位
je 等于则转移(=) zf = 1
jne 不等于则转移(!=) zf = 0
jb 低于则转移(<) cf = 1
jnb 不低于则转移(>=) cf = 0
ja 高于则转移(>) cf = 0 且 zf = 0
jna 不高于则转移(<=) cf = 1 或 zf = 0

通过cmp指令和比较指令还有标志位,可以实现想要的逻辑判断

DF标志位和串传送指令

DF标志位

flag的第十位是DF,方向标志位。在串处理命令中,控制每次操作后si,di的递减

  • df = 0 每次操作后si,di递减
  • df = 1 每次操作后si,di递增

在8086CPU中提供两种方式对df进行修改:

  • cld–> 令df = 0
  • std–> 令df = 1

串传送指令

1
movsb

执行movsb指令相当于进行下面的步骤(将ds:si指向的内存单元中的字节送入es:di中,然后根据df中的值,进行增减)

1
2
3
4
5
6
7
mov es:[di],byte ptr ds:[si]
;如果df=0
inc si
inc di
;如果df=1
dec si
dec di
1
movsw

执行movsw指令相当于进行下面的步骤(将ds:si指向的内存字单元中的字送入es:di中,然后根据df中的值,进行增减)

1
2
3
4
5
6
7
mov es:[di],word ptr ds:[si]
;如果df=0
inc si
inc di
;如果df=1
dec si
dec di
1
rep movsb/movesw

rep指令的含义是根据cx的值,重复执行后面的指令。可以理解为下面的指令:

1
2
s:	movsb/movsw
loop s

当我们使用串传送时,需要为串传送指令提供以下信息:

  • 传送的原始位置
  • 传送的目的位置
  • 传送的长度
  • 传送的方向

DEBUG中的标志寄存器

image.png

图中标识了不同标志寄存器对应的位置

下面我们列出Debug对标志位的表示:

1
2
3
4
5
6
7
标志				值为1的标记				值为0的标记
of OV NV
sf NG PL
zf ZR NZ
pf PE PO
cf CY NC
df DN UP

实验11

image.png
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
assume cs:code,ds:data
data segment
db "Some day,I'll be enough so you can't hit me.",0
data ends
code segment
begin:
mov si,0
call letterc
mov ax,4c00H
int 21H

letterc:
mov ah,ds:[si]
cmp ah,30H
je return
cmp ah,41H
jnb s1
inc si
jmp letterc

s1:
cmp ah,7AH
jna s2
inc si
jmp letterc

s2:
and ah,11011111B
mov ds:[si],ah
inc si
jmp letterc

return:
ret

code ends
end begin

今天继续学习汇编语言

转移指令的学习

jmp指令

使用jmp指令需要给出两种信息:

  • 转移的目标地址
  • 转移的距离(段间转移,段内转移,段内近转移)

根据位移进行的jmp指令

先对两种jmp进行介绍:

1
jmp short 标号

功能为:(IP)=(IP)+ 八位位移(short指明)

  • 8位指令=标号处的地址-jmp指令后的第一个字节的地址
  • 八位位移的范围是-128~127
  • 位移值是在编译程序的过程中计算出来的
1
jmp near ptr 标号

功能为:(IP)=(IP) + 十六位位移(near ptr)

  • 16位指令=标号处的地址-jmp指令后的第一个字节的地址
  • 十六位位移的范围是-32768~32767
  • 位移值是在编译程序的过程中计算出来的

我们可以通过下面的图片理解位移的计算过程:

image.png

指定转移目的地址的jmp指令

1
jmp far ptr 标号

far ptr指明了指令用标号的段地址和偏移地址修改CS和IP

其机器码表现形式为指定目的地址

转移地址在寄存器中的jmp指令

1
jmp (16位reg)

功能:(IP)= (16位reg)

转移地址在内存中的jmp指令

1
jmp word ptr 内存单元地址(段内转移)

功能:从内存单元地址处开始存放一个字,是转移的目的偏移地址

1
jmp dword ptr 内存单元地址(段间转移)

功能:从内存单元地址处开始存放着两个字,高地址存放的字是转移的目的段地址,低地址是转移的目的偏移地址

  • (CS) = (内存单元地址+2)
  • (IP) = (内存单元地址)

jcxz指令

有条件转移指令,所有的有条件转移指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址,对IP修改范围为-128~127

1
jcxz 标号

功能:当(cx)= 0 时,转移到标号处执行

  • 8位位移=标号处的地址-jcxz指令后的第一个字节的地址
  • 八位位移的范围是-128~127
  • 位移值是在编译程序的过程中计算出来的

可以理解为

1
if((cx)==0) jmp short 标号;

loop 指令

所有的循环指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址

我们在之前学习过,可以理解为

1
2
(cx)--;
if((cx)!=0)jmp short 标号;

根据位移进行转移的意义

因为程序段在不同的机器中内存情况并不一样,如果指定内存地址进行跳转会发生错误

但如果根据位移进行索引,便可以准确的找到位置

地址间的相对关系是不会改变的

当然如果位移距离超出范围,会造成编译错误

offset

我们可以通过

1
offset 标号

取到标号的偏移地址

实验九

参考链接:王爽《汇编语言》(第三版)实验9解析 - nojacky - 博客园 (cnblogs.com)

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
assume cs:code
data segment
db '!!!HelloWorld!!!'
db 2,36,113
data ends
stack segment
db 16 dup (0)
stack ends
code segment
start:
;指向data单元
mov ax,data
mov ds,ax
;指向显存区域
mov ax,0B800H
mov es,ax
;设置栈段
mov ax,stack
mov ss,ax
mov sp,16
;初始化
mov bx,780H;这个是第十二行的位置
mov si,16

mov cx,3
s:
mov ah,ds:[si]
push cx
push si

mov cx,16
mov si,64;这个确保居中显示
mov di,0

s0:
mov al,ds:[di]
mov es:[si+bx],al
mov es:[si+bx+1],ah

add si,2
add di,1

loop s0

pop si
pop cx

add si,16
add bx,0A0H
loop s

mov ax,4c00h
int 21h

code ends
end start

效果图

4ad49d42c593958a5834364a12d92ea8.png

CALL和RET指令

ret与retf

ret指令用栈中的数据,修改IP的内容,从而实现近转移 retf指令用战中的数据,修改CS和IP的内容,从而实现远转移

CPU执行ret:

1
2
(IP) = ((ss)*16 + (sp))
(sp) = (sp) + 2 //pop IP

CPU执行retf:

1
2
3
4
(IP) = ((ss)*16 + (sp))
(sp) = (sp) + 2 //pop IP
(CS) = ((ss)*16) + (sp)
(sp) = (sp) + 2 //pop CS

call指令

总结一下就是:

  • 将当前的IP或CS和IP压入栈中
  • 转移

依据位移进行的call指令

1
call 标号;将当前的IP压入栈中,转到标号处执行指令

执行过程如下:

1
2
3
(sp) = (sp) -2
((ss)*16 + (sp)) = (IP)
(IP) = (IP) + 16位位移

位移的计算同上

转移到目的地址在call指令中

1
call far ptr 标号

相当于进行

1
2
3
push CS
push IP
jmp far ptr 标号

转移地址在寄存器中的call指令

1
call (16位reg)

相当于进行

1
2
push IP
jmp (16位reg)

转移地址在内存中的call指令

(1)单字节索引

1
call word ptr 内存单元地址

相当于进行:

1
2
push IP
jmp word ptr 内存单元地址

(2)双字节索引

1
call dword ptr 内存单元地址

相当于进行

1
2
3
push CS 
push IP
jmp dword ptr 内存单元地址

使用

在学会ret和call的用法之后,我们可以使用下面的框架来模拟函数的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
assume cs:code
code segment
main:
...
call sub1
...
mov ax,4c00h
int 21h
sub1:
...
call sub2
...
ret
sub2:
...
ret
code ends
end main

mul指令

使用mul指令时,我们需要注意以下几点:

  • 两个相乘的数要么都是八位,要么都是16位。如果是八位,则一个放在AL中,另一个在8位的reg或内存字节单元中;如果是16位,则一个在AX中,一个在16位的reg或者内存字单元中
  • 如果是八位乘法,结果放在AX中;如果是十六位乘法,结果高位放在DX中,低位放在AX中
1
mul reg/内存单元

实验十:编写子程序

显示字符串

image.png
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
assume cs:code
data segment
db 'Welcome to masm!',0
data ends

code segment
start:
;指向显存
mov ax,0B800H
mov es,ax

mov dh,8
mov dl,3
mov cl,2
mov ax,data
mov ds,ax
mov si,0
call show_str

mov ax,4c00h
int 21h

show_str:
mov bx,8*160
mov di,3*2
mov ah,cl
mov cx,16
s:
mov al,ds:[si]
mov es:[bx+di],al
mov es:[bx+di+1],ah
inc si
add di,2
loop s
ret

code ends
end start

解决除法溢出问题

tips:

公式 X/N = int(H/N)*65536 + [rem(H/N)*65536+L]/N

X:被除数,范围[0,FFFFFFFF] N:除数,范围[0,FFFF] H:X的高16位,范围[0,FFFF] L:X的低16位,范围[0,FFFF] int():取商 rem():取余

参考链接:汇编语言(王爽第三版)实验10:编写子程序 - 筑基2017 - 博客园 (cnblogs.com)

image.png
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:code
stack segment
dw 0,0,0,0,0,0,0,0
stack ends
code segment
start:
mov ax,stack
mov ss,ax
mov sp,16

mov ax,4240H
mov dx,000FH
mov cx,0AH
call divdw
mov ax,4c00h
int 21h

divdw:
push ax
mov ax,dx
mov dx,0
div cx
mov bx,ax

pop ax
div cx
mov cx,dx
mov dx,bx
ret

code ends
end start

数值显示

image.png

暂时写不出来

自从大学以来我一直在思考一个问题

是学技术重要还是卷绩点重要?

从学校的角度出发,因为是一个中下游的211,有一定的保研资格但是不多,所以给人一种有希望的感觉。但是僧多肉少,很多人为了保研无所不用其极,或者说是要舍弃很多的东西。对我而言,可能最让我纠结的便是在卷绩点上面所花费的时间。

从我个人的角度出发,我十分喜欢自己正在学习的东西,非常非常喜欢。我等了很久才学到我想学的东西,我很早开始就想学计算机,但是因为教育观念有一定的落后,所以我的家庭和学校并不支持我的想法。到了大学,我有很多时间去做我想做的事情,我把几乎所有的时间投入在技术的学习里面,难以想象的热爱。可是因此,也为我带来了很多烦恼,对我而言,研究生的学历是十分必要的。因为我并不满足于我现在的学历,而且专业需求的是高学历人才,所以要么保研要么考研。就现在来说,肯定是不考虑考研的,因为有保研的名额,而且考研有一定的难度,所以我还是更加倾向于保研。那么问题来了,我并没有时间去学习保研内容相关的,我的绩点岌岌可危。

就目前而言,最为理性的选择无疑是将重心转移到保研上。但是我实在不愿意做出这样的选择,12年以来的应试教育,为什么到了大学就没有选择的权力。我可以做出我想要的选择,可是环境并不会给我想要的结果,这是我内心一直以来的纠结与矛盾。我尝试过两手抓同步走,但是到了最后我才发现自己也是一个很普通的学生。这种心比天高的做法并不适合我。在大学生活的一个学期里,我已经慢慢感受到 了,很多身不由己的瞬间。不能太过于任性,要理性的去判断。

其实当我开始写这段话的时候我的心里已经有了答案,也许我确实应该花更多的时间在学习上面。这是对现实的妥协吧,但是归根到底这也是一种能力不足的体现,也许我更应该锻炼自己。真正优秀的人不会被这些问题困扰,我的心思过于分散,这就是如今局面的原因。反思过后,便是行动。也许人生就是在一次次的妥协里成长。我的高考成绩便是任性的后果,很多事情不想再经历第二遍,不能”只做自己想做”的事情,也许我也应该去做好一些不得不做的事情。

也许以后,慢慢成长,能够放下杂念,也有机会让学习成绩与技术能力并驾齐驱,但是对于现在的我而言却言之过早。不知道向谁倾诉我的烦恼,记录于此,警醒自己。

DEBUG的用法

在 DOSBox 中使用 DEBUG 工具时,可以使用以下命令及其用法,这些命令主要用于调试汇编语言程序、查看和修改寄存器与内存内容、执行代码等。以下是 DEBUG 的所有常用命令及其详细用法:

1. 进入和退出 DEBUG

  • 进入 DEBUG:在 DOSBox 中输入 debug 命令。
  • 退出 DEBUG:使用 q 命令退出 DEBUG 并返回到 DOSBox 命令行。

2. 寄存器操作

  • 查看所有寄存器:输入 r,查看所有寄存器的当前值。
  • 查看和修改单个寄存器:输入 r <寄存器名>,例如 r ax,查看 AX 寄存器的值。输入新值后按回车即可修改。

3. 内存操作

  • 查看内存内容
    • d [起始地址]:从指定地址开始查看内存内容。
    • d [起始地址] [结束地址]:查看指定范围内的内存内容。
  • 修改内存内容
    • e [内存地址]:修改指定地址的内存内容。
    • e [内存地址] '文本':直接输入文本内容。
  • 填充内存内容
    • f [起始地址] [结束地址] [值1] [值2]...:用指定值填充内存区域。

4. 汇编指令操作

  • 输入汇编指令
    • a [地址]:从指定地址开始输入汇编指令。输入完成后按回车退出。
  • 反汇编指令
    • u [地址]:从指定地址开始反汇编指令。
    • u [段地址:偏移地址]:指定段地址和偏移地址进行反汇编。

5. 程序执行

  • 单步执行
    • t:执行当前指令并进入下一步。
    • t [地址]:从指定地址开始单步执行。
  • 连续执行
    • g:从当前地址开始执行程序。
    • g=[地址]:从指定地址开始执行程序,并设置断点。
  • 运行程序至结束:使用 p 命令。

6. 其他功能

  • 计算偏移量
    • h value1 value2:计算两个十六进制值的和。
  • 保存程序到文件
    • p [文件名] [地址]:将内存中的程序保存到文件。

汇编程序中的注意点

关于下面四个指令,在汇编程序中有不同的含义:

1
2
3
4
mov al,[0]
mov al,ds:[0]
mov al,[bx]
mov al,ds:[bx]
  1. (al)= 0

  2. (al)= ((ds)*16 + 0)

  3. (al)= ((ds)*16 + bx)

  4. (al)= ((ds)*16 + bx)

所以总结得到:

  • 如果在”[ ]“里用一个常量idata直接给出内存单元的偏移地址,就要在”[ ]“前面显式的给出段地址所在的寄存器,否则会被解释为常量
  • 如果在”[ ]“里面使用寄存器,则默认段地址为ds,可以不用显式的表现

汇编程序的入口

当我们在代码段设置数据时会遇到一个问题,就是程序的入口被设置在代码段

但是这会导致程序无法执行,因为你定义的字节被反编译后可能是未被定义的或者意义不明的指令

所以我们在运行程序时需要手动调节IP值,那么有什么更好的办法呢?

我们可以在汇编代码时事先定义程序的入口:

1
2
3
4
5
6
7
8
9
10
11
assume cs:code
code segment
...
;数据
...
start:
...
;代码
...
code ends
end start

通过这种格式我们可以在start处定义函数的入口,程序将从此开始运行

那么换个角度思考,我们既然可以指定程序的入口,那么我们就可以对程序进行分段

我们将数据,代码,栈分别放入不同的段中

比如下面这个程序

1
2
3
4
5
6
7
8
9
10
11
assume cs:code,ds:data,ss:stack
data segment
;字型数据
data ends
stack segment
;定于栈空间
stack ends
code segment
start:;代码
code ends
end start

注意要分清什么是伪指令,什么是汇编指令。CPU如何处理我们定义的段空间,完全是靠程序中具体的汇编指令,这里的伪指令只是将其进行了抽象,这样的分段定义,实际上是基于段寄存器的使用。并非我们想象的直接对内存进行分段

汇编程序转换大小写

首先我们要知道ASCII字符中大小写字母之间有什么样的联系

  • 大写字母的十六进制数值比小写字母的十六进制数值小 20H
  • 大写字母和小写字母的区别在于小写字母的第五位是1(因为位数从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:code,ds:data
data segment
db 'BaSiC'
db 'iNfOrMaTiOn'
data ends

code segment
start:mov ax,data
mov ds,ax
mov bx,0
mov cx,5

s:mov al,[bx]
and al,11011111B ;小写
mov [bx],al
inc bx
loop s

mov bx,5
mov cx,11

s0:mov al,[bx]
or al,00100000B ;大写
mov [bx],al
inc bx
loop s0

mov ax,4c00h
int 21h
code ends
end start

可以看到我们使用了两个循环,分别将其转换为大小写

数组处理

我们可以用’[bx+idata]’的形式来模拟数组的行为

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
assume cs:code,ds:data
data segment
db 'BaSiC'
db 'MinIX'
data ends
code segment
start:mov ax,data
mov ds,ax
mov bx,0

mov cx,5

s: mov al,0[bx]
and al,11011111B
mov 0[bx],al
mov al,5[bx]
or al,00100000B
mov 5[bx],al
inc bx
loop s

mov ax,4c00h
int 21h
code ends
end start

这里面的 0[bx]5[bx]可以分别理解为C语言中的 a[]b[],只不过这里体现的是偏移地址

在这里我们有几种等价的表达方式:

1
[bx + idata] = idata[bx] = [bx].idata

我们可以用数组的思想去理解

汇编内存寻址的进一步理解

我们依次进行深入:

DS,SS,CS

作为一个段地址,往往用来划分一定的内存区域存放特定的数据

我们可以理解成C语言中,申请了一段空间(空间)

[BX]寄存器间接寻址

通过修改寄存器BX中的值,我们可以进一步索引到段中的某一部分内存的起点

我们可以理解成在这一片空间中划分了一部分作为数组(一维数组)

[BX + idata]寄存器相对寻址

我们以BX确定在段空间的位置后,我们可以用常量去查询指定内存的数值

此时我们可以把常量idata理解成数组的下标(二维数组)

我们可以用以下形式表达:

1
[bx + idata] = idata[bx] = [bx].idata

[BX + SI/DI + idata]相对基址变址寻址

我们先用BX确定一部分空间,再用SI/DI中的地址确定在这段空间中的位置,然后用常量去查询指定内存中的数值

此时我们可以把SI/DI中的数值理解成二维数组的首地址,而常量作为数组下标进行索引(三维数组)

当没有常量时我们这样表达:(基址变址寻址)

1
[bx + si/di] = [bx][si/di]

有常量时我们这样表达:

1
[bx + si/di + idata] = idata[bx][si/di] = [bx].idata[si/di] = [bx][si/di].idata

SI/DI

这两个寄存器是8086CPU中与bx功能相近的寄存器,这两个寄存器不能被分成两个八位的寄存器来使用

通过这些各种各样的表达方式,我们可以根据自己的需求进行各种各样的寻址

新的汇编指令

div指令

使用这个指令时我们需要注意以下几点:

  • 除数:有8位和16位两种,在一个reg或者内存单元中
  • 被除数:默认放在AX或DX和AX中,被除数的位数是除数的两倍,如果被除数是32位,那么DX存放高十六位,AX存放低十六位
  • 结果:如果除数为8位,则AL存储除数操作的商,AH存储除数操作的余数;如果为16位,那么AX存储商,DX存储余数

伪指令 dd

db,dw,dd分别代表三种不同的定义类型

1
2
3
db			;定义字节(byte)类型
dw ;定义字(word)类型
dd ;定义double类型

dup

用来重复定义同一类型的数据

1
2
3
db 重复的次数 dup (重复的字节类型)
dw 重复的次数 dup (重复的字类型)
dd 重复的次数 dup (重复的双字类型)

比如定义200个字类型

1
dw 200 dup (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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
assume cs:code
data segment
db '1975','1976','1977','1978','1979','1980','1981','1982','1983'
db '1984','1985','1986','1987','1988','1989','1990','1991','1992'
db '1993','1994','1995'
;0-84 0-54H

dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514
dd 345980,590827,803530,1183000,1843000,2759000,3753000,4649000,5937000
;85-168 55H-A9H

dw 3,7,9,13,28,38,130,220,476,778,1001,1442,2258,2793,4037,5635,8226
dw 11542,14430,15257,17800
;169-210 AAH-D4H

data ends
table segment
db 21 dup ('year summ ne ?? ')
table ends
code segment
start: mov ax,data
mov ds,ax
mov ax,table
mov es,ax

mov bp,0
mov si,0
mov di,0
mov cx,21

s: mov ax,ds:[si]
mov es:[di],ax
mov ax,ds:[si+2]
mov es:[di+2],ax

mov ax,ds:[84+si]
mov es:[di+5],ax
mov ax,ds:[84+si+2]
mov es:[di+5+2],ax

mov ax,ds:[168+bp]
mov es:[di+10],ax

mov ax,ds:[84+si]
mov dx,ds:[84+si+2]
div word ptr ds:[168+bp]
mov es:[di+13],ax

add si,4
add di,16
add bp,2

loop s

mov ax,4c00h
int 21h

code ends
end start



为了进一步的使用对内存单元进行灵活的操作,所以使用[BX]和loop指令对其进行操作

[BX]和loop指令

在开始学习之前,需要对一些符号进行讲解

  1. [bx]和内存单元的描述

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

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

    这里[bx]表示一个内存单元,段地址默认为ds,偏移地址存储在bx中

  2. loop

    loop在英文中有循环的含义,所以这个指令肯定和循环有关,我们在后面进行详细的说明

  3. 我们定义的描述性的符号:“( )”

    为了描述上的简洁我们用一个描述性的符号”()“来表示一个寄存器或者一个内存单元中的值

    现在我们可以把[bx]的物理地址表示为((ds)*16+(bx))

    “(X)”所表示的数据有两种类型:1)字节;2)字 数据类型由寄存器名称或者具体的运算决定

  4. 约定符号idata表示常量

    之前我们说在”[…]“里用一个常量0表示内存单元的偏移地址,现在我们可以把所有常量都作为[idata]

[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)

Loop指令

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

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

在此我们可以看出cx的值影响了loop指令的执行结果。通常我们用loop来实现循环功能,cx中存放循环次数

这里我们通过一个程序引出关于loop的使用:

1
2
3
4
5
6
7
8
9
10
11
12
assume cs:code
code segment
mov ax,2

mov cx,11
s: add ax,ax
loop s

mov ax,4c00h
int 21h
code ends
end

我们注意到这里使用了标号”s” ,在汇编语言中标号代表一个地址,它实际上标识了一个地址

我突然发现 这样一点一点敲很慢,所以我打算先学完再总结下来

反正这也不是教程 哈哈

今天晚上的学习任务是用汇编写一个可执行程序,接下来开始吧

第一个程序

一个源程序从写出到执行的过程

  1. 编写汇编原程序

    产生一个存储源程序的文本文件

  2. 对源程序进行编译连接

    先使用汇编语言编译程序对源程序进行编译,产生目标文件;再用连接程序对目标程序进行连接,生成可在系统中运行的可执行程序

    可执行程序包含两部分内容:

    • 程序(从源程序中的汇编指令翻译过来的机器码)和数据(源程序中定义的数据)
    • 相关的描述信息(比如,程序有多大,要占用多少内存空间)
  3. 执行可执行文件中的程序

    在操作系统中,执行可执行文件中的程序

在这些步骤中,操作系统可以根据可执行文件中的描述信息,将可执行文件中的机器码和数据加载进入内存,并进行相关的初始化(比如设置CS:IP指向的第一条执行的指令),然后由CPU执行

源程序

我们以一段汇编语言源程序为例

1
2
3
4
5
6
7
8
9
10
11
12
13
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

伪指令

在汇编语言源程序中包含两种指令。一是汇编指令,二是伪指令。

汇编指令是有对应的机器码的指令,可以被编译为机器指令,最终由CPU执行

而伪指令则是由编译器来执行的指令,编译器根据伪指令来进行相关的编译工作

我们先对这段程序中的三处伪代码进行说明:

1
2
3
XXX segment
...
XXX ends

segment 和 ends是一对成对使用的伪指令,这是在写可被编译器编译的汇编程序是,必须要用到的一对伪指令

其功能是定义一个段,segment 用来定义一个段的开始,ends用来定义一个断的结束

通常一个源程序是由多个段组成的,一个程序中所有要被处理的信息:指令,数据,栈被划分到了不同的段中

1
end

end是汇编程序结束的标记,编译器在进行编译时,如果碰到了伪指令end就结束对源程序的编译

1
assume cs:codesg

这条伪指令的含义为”假设”。它假设某一段寄存器和程序中的某一个用 segments...ends定义的段相关联

这一段程序的含义便是将 codesg段与 cs段寄存器相关联

程序返回

当一个程序结束后,将CPU的控制权交还给使它得以运行的程序,我们称这个过程为:程序返回

这两条指令实现的功能便是程序返回:

1
2
mov ax,4c00H
int 21H

我们暂时无法理解这两句的含义,不必深究

编辑源程序

我们在edit程序中编辑程序

image.png

我们将其保存至C盘中为1.asm

编译

我们使用masm对其进行编译

image.png

我们一一分析这些信息的作用:

  1. [.ASM]:提示我们默认的文件拓展名是asm,当我们输入名称XXX便在当前目录下调用XXX.asm(如果要用其他拓展名则需输入完全)
  2. [1.OBJ]:提示我们我们生成目标文件为1.obj ,我们可以在后面指定生成的路径,也可以用Enter跳过,使用当前文件夹
  3. [NUL.LST]:提示输入列表名称,这个文件是编译器翻译源程序的过程中的中间结果,使用Enter跳过
  4. [NUL.CRF]:提示输入交叉引用文件的名称,也是中间产物,跳过
  5. 当出现下面的标志后,代表编译成功结束

连接

我们在得到目标文件后,需要对目标文件进行连接,从而得到可执行程序

image.png

我们接着分析这些信息:

  1. [.OBJ]:提示我们默认的文件拓展名是obj,当我们输入名称XXX便在当前目录下调用XXX.obj(如果要用其他拓展名则需输入完全)
  2. [1.EXE]:提示我们我们生成目标文件为1.exe,我们可以在后面指定生成的路径,也可以用Enter跳过,使用当前文件夹
  3. [NUL.MAP]:提示输入映像文件的名称,这个文件是目标文件生成可执行程序的中间结果,使用Enter跳过
  4. [.LIB]:提示输入库文件的名称,库文件里面包含了一些可以调用的子程序,如果调用了某一个库文件的子程序,就需要在连接时,将这个库文件和目标文件连接在一起生成可执行程序,这里我们跳过
  5. 最后显示出现了“没有栈段”的错误,我们直接忽视,此时连接成功

在下面我们简单的介绍以下连接的作用:

  • 当源程序很大时可以分为多个源程序文件生成目标文件,最后再将目标文件连接到一起
  • 程序中调用了某个库文件的子程序,需要将这个库文件和目标文件连接到一起生成可执行程序
  • 一个源程序在编译后,得到了有机器码的目标文件,目标文件中的内容还不能直接生成可执行程序,所以需要连接程序处理

以简化的方式进行编译和连接

image.png

直接在命令后面加一个 ; 可以直接忽略中间产物的生成,实现快速的编译连接

程序执行过程的跟踪

汇编程序从写出到执行的过程:

1
2
 编程 --> 1.asm --> 编译 --> 1.obj --> 连接 --> 1.exe --> 加载 --> 内存中的程序 --> 运行
(Edit) (masm) (Link) (command) (CPU)

我们先展示一下EXE文件中程序加载的过程:

image.png

我们可以根据这副图得到以下信息:

  • 程序加载后ds中存放着程序所在内存区的段地址,这个内存区的偏移地址为0,则程序的内存区的地址为ds:0

  • 这个内存区的前256个字节存放的是PSP,DOS用来用来和程序进行通信。从256个字节之后存放的是程序

    因为PSP占256(100H)个字节,所以程序的物理地址是:

    SA * 16 + 0 + 256 = SA * 16 + 16 * 16 + 0 = (SA + 16) * 16 + 0 = (SA + 10H) + 0

    可以用段地址和偏移地址表示为 SA+10H:0

程序执行过程的跟踪

我们以刚刚的程序1.exe为例:

image.png

我们可以看到图中DS的值为075AH,则PSP的地址为075A:0 ,程序的地址为076A:0(即075A + 10:0)

同时可以看到从076A:0000~076A:000F都是我们的程序的机器码

现在我们开始单步执行跟踪:

image.png

当我们执行到 INT 21时需要用P指令退出程序,最后再使用Q指令返回command

昨天我们学习了简单寄存器的使用DEBUG程序的使用

这一篇我们将学习 寄存器的内存访问

寄存器(内存访问)

从访问内存的角度认识学习寄存器i

我们知道一个字的存储分为高字节和低字节,由于内存地址是自上而下向下递增的,所以高位字节从内存分布上看再地位字节的下面

也就是说当我们从0地址开始存放 数值20000(4E20H)

其在内存空间中的顺序为

1
2
3
4
5
+---------+----------+-----------------------------------
| 0 | 1 | ...
+---------+----------+-----------------------------------
| 20 | 4E | ...
+---------+----------+-----------------------------------

这样的分布特点我们称之为 小端序

综上所述,我们知道任何两个地址连续的内存单元,N号单元和N+1号单元,可以将它看成两个内存单元,也可以看成一个地址为N的字单元中的高位字节单元和低位字节单元

DS和[address]

8086CPU中有一个DS寄存器,通常用来存档要访问的数据的段地址。

我们用下面的例子来展示它的用法,比如读取10000H单元的内容:

1
2
3
mov bx,1000H
mov ds,bx
mov al,[0]

我们通过上面的三个指令,实现读取,接下来一一解释

首先是前两句,为什么不能直接 mov ds,1000H呢?这是8086CPU的硬件问题,我们并不支持此行为,所以我们用一个寄存器来中转

第三句的 […] 又是什么意思呢? […]表示操作对象是一个内存单元,里面的数值代表内存单元的偏移地址

mov,sub,add指令

首先我们需要知道这三个指令的特点:他们都有两个操作对象

MOV指令

我们先看看至今我们所知的mov的用法:

  • mov 寄存器,数据
  • mov 寄存器,寄存器
  • mov 寄存器,内存单元
  • mov 内存单元,寄存器
  • mov 段寄存器,寄存器

根据这些我们可以合理的猜测一些其他的用法,并使用DEBUG程序来验证:

  • mov 内存单元,段寄存器 验证通过
  • mov 寄存器,段寄存器 验证通过
  • mov 段寄存器,内存单元 验证通过

SUB ADD指令

他们也可以有以下用法:

  • add 寄存器,数据
  • add 寄存器,寄存器
  • add 寄存器,内存单元
  • add 内存单元,寄存器
  • sub 寄存器,数据
  • sub 寄存器,寄存器
  • sub 寄存器,内存单元
  • sub 内存单元,寄存器

数据段

在编程时,可以根据需要,将一组内存单元定义为一个段。我们可以将一组长度为N(N<=64KB)、地址连续、起始地址为16的倍数的内存单元作为专门存储数据的内存空间。从而定义了一个数据段

将一段内存作为数据段,是我们编程时的一种安排,我们可以在具体操作时,用DS存放数据段的段地址,从而进行访问

比如一段数据段 123B0H~123B9H 的内存单元定义为数据段 ,现在要累加这个数据段的前三个内存单元的值

1
2
3
4
5
6
mov ax,123B
mov ds,ax
mov al,0 ;给ax赋值为0
add al,[0] ;加上数据段的第一个值
add al,[1] ;......
add al,[2]

8086CPU 提供相关的指令来以栈的方式访问内存空间。这意味着,在基于8086CPU编程时,可以将一段内存作为栈来使用

8086CPU提供入栈和出栈指令,分别时POP和PUSH

  • push ax 将ax中的数据送入栈中
  • pop ax 从栈顶取出数据送入ax中

注意: 入栈和出栈的操作都是以字为单位进行的

SS:SP

CPU是怎么知道栈顶的位置呢?

在8086CPU中,有两个寄存器,分别是段寄存器SS 和 寄存器SP,栈顶的段地址存放在SS中,栈顶的偏移地址存放在SP中。任意时刻,SS:SP指向栈顶元素。执行pop和push时,CPU从SS和SP中得到栈顶的地址。

现在我们可以对pop和push进行完整的描述了:

push:

  1. SP = SP - 2,SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶
  2. 将ax中的内容送入SS:SP指向的内存单元,SS:SP 此时指向新的栈顶

pop:

  1. 将SS:SP 指向的内存单元处的数据送入ax中
  2. SP = SP + 2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶

当栈顶的数据出栈之后,其内存单元所存储的数据仍然存在,但其已经不在栈顶中。当再次进行入栈操作时,直接对其数据进行覆盖

pop push指令

栈空间也是内存空间的一部分,它只是一段可以以一种特殊的方式进行访问的内存空间

push,pop可以是指令格式:

  • push,pop 寄存器
  • push,pop 段寄存器
  • push,pop 内存空间

栈顶超界问题

我们在此讨论一个问题,虽然我们可以通过SS 和SP来确保在进行入栈和出栈时找到栈顶。可是怎么保证栈顶不会超出栈空间呢?

当我们把一个空间容量为16个字节的内存空间当作栈时,向其中压入八个字后就已经达到了栈顶,此若是再使用push操作,其数据便会溢出栈空间,覆盖栈以外的数据。

同理当我们已经达到栈底时,我们再进行一次pop操作,我们会把栈空间以下的数据弹出。

以上这些操作我们都称为 栈顶越界问题

如果CPU中有记录栈顶上限和栈底的寄存器,那么可以检测越界问题。但是,在8086CPU中,并没有这个寄存器,因此其不保证我们对栈的操作不会越界。这一点需要操作者自行考虑

栈段

如果我们设置一段内存,将它当作栈,并以栈的形式进行访问,那么我们可以称之为 栈段

这里我们有一个问题,如果将10000H~1FFFFH这段空间当作栈段,SS = 1000,SP = FFFE。也就是说,此时栈段内还有一个数据,如果我们将这个数据进行出栈操作,那么此时SP = ?,栈顶指向哪里?

由于出栈后,SP = SP + 2,栈顶指向最底部单元下面的单元。所以此时SP = 0

那么我们可以说 SP=0,此时即是空栈也是满栈

实验二:用机器指令和汇编指令编程

正如前文所言,我们使用D命令查看内存单元的命令,那么我们有以下疑问:

  • Debug是靠什么来执行D命令的? 是一段程序

  • 谁来执行这段程序? 用CPU

  • CPU在访问内存单元时从哪里得到内存单元的段地址? 从段寄存器得到

所以我们得出结论 在处理D命令的程序段中,必须有将段地址送入段寄存器的代码

段寄存器有4个:SS,ES,CS DS,那么将段地址送入那个段寄存器呢?

由于CS要用来指向处理D命令的代码,而SS要作为指向栈顶的代码。再因为一般默认段地址再DS中,所以我们将段地址送入DS中

下一条指令执行了嘛?

我们有这样一个程序:

1
2
3
4
5
6
7
mov ax,2000
mov ss,ax
mov sp,10
mov ax,3123
push ax
mov ax,3366
push ax

我们使用T命令单步执行,看一看发生了什么?

image.png

注意看,在执行 mov ss,ax之后本来应该是 mov sp,10但是却直接来到了 mov ss,ax

但通过观察SP的值,我们可以知道 mov sp,10得到了执行,这是为什么呢?

这是因为设计到了之后的一个内容:中断机制

在这里我们只需要知道,T命令在执行修改寄存器 SS 的指令时,下一条指令也被紧接着执行