概述

本期的重点在于使用8086汇编语言实现函数递归无符号数字输出

程序框图

模块图

初始化准备

我们先把代码段数据段栈段准备好相关的数据并完成相关的初始化

声明段

我们先声明三个段

1
assume cs:codesg,ds:datasg,ss:stacksg

装载数据

数据段我们准备填入如下数据

  • num表示我们要求斐波那契数列的第num个数
  • choose 字符串,用于输出提示信息
  • result 字符串,用于输出提示信息
  • enter 字符串,用于输出回车换行,即\n\r

这里写入num我们使用dw指令写入一个字长(2字节)

而写入字符串必须一个字节一个字节写入,所以使用db指令

特别的,在8086中,$用于表示一段字符串的结尾

1
2
3
4
5
6
7
;初始化数据段,并填入数字
datasg segment
num dw 10h ;选择斐波那契数列的第num个数
choose db 'Please choose a num from 1 to 100 : ','$'
result db 'The result is : ','$'
enter db 13, 10,'$'
datasg ends

而对于栈段,其实并不需要做什么特别的初始化,当然我们可以给它额外做个置零

这里对连续内存置零我们使用了dup指令配合db进行重复写入

1
2
3
4
;初始化栈段,置空
stacksg segment
db 25565 dup(0) ;我们选择栈的大小为25565个字节
stacksg ends

最后我们再在代码段把整个程序的初始化框架搭出来

对于初始化我们要把这些段地址装载到对应的寄存器里,以及初始化栈指针

再在程序的主体部分末尾加入退出程序的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
codesg segment
start:
mov ax,datasg
mov ds,ax;
mov ax,stacksg;
mov ss,ax;
mov sp,25565;初始化栈

mov ah,4ch ;输入退出程序对应的中断代码
int 21h ;启动中断程序,这里就是退出程序,并返回al里存的值

codesg ends
end start

组合后的代码如下

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:codesg,ds:datasg,ss:stacksg

;初始化数据段,并填入数字
datasg segment
num dw 10h ;选择斐波那契数列的第num个数
choose db 'Please choose a num from 1 to 100 : ','$'
result db 'The result is : ','$'
wrongRange db 'wrong range, try again'
enter db 13, 10,'$'
datasg ends

;初始化栈段,置空
stacksg segment
dw 25565 dup(0)
stacksg ends

codesg segment
start:
mov ax,datasg
mov ds,ax;
mov ax,stacksg;
mov ss,ax;
mov sp,25565;初始化栈

mov ah,4ch
int 21h

codesg ends
end start

无符号数字输出

为了更好地输出计算结果或者输出调试,我们先实现无符号数字的输出模块。

为什么选无符号?因为斐波那契数列里只有正数,而且无符号数的打印要比有符号的更简单。本着简单问题简单做的原则,我们就只写只打印无符号数的模块

注:接下来会出现比较多的标签用于跳转

初始化&现状保存

如同C语言函数的传值传参一样,我们也模拟函数的行为做传值传参。为此我们就要做现状保存。

现状保存的具体做法就是按一定顺序把需要保存的寄存器数据存入堆栈,函数返回前我们再逆序取出来。

至于具体的传参,我们这里约定使用ax

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PrintNum:
;现状保存
push ax ;设计为传值传参,所以要保存ax~dx
push bx
push cx
push dx
;初始化
mov bx,10 ;这个bx存了除数10
mov cx,0 ;cx用于记录数字位数/循环次数

;.... 中间代码....
;....函数功能完成,即将返回....
;开始恢复函数调用前的状态

pop dx; 恢复现场
pop cx;
pop bx;
pop ax;
ret ;函数返回

解决了模块/模拟函数的框架设计问题后,我们再来实现主体部分

解析数字

接下来我们按十进制把多位十进制数转换成一个个独立的数字字符并存在

因为我们传入的数不是定长的,所以我们要用循环来处理

1
2
3
4
5
6
7
8
ParseLoop:  ;解析数字串
mov dx,0 ;dx置零
div bx ;ax/10取余数在dx中,商在ax中
add dx,30h;转换成数字字符
push dx ;将余数压栈(正好最后一个数字先压栈,后出栈)
inc cx ;增加打印计数
cmp ax,0 ;判断ax是否为0
jnz ParseLoop ;循环调用

在上面我们使用模10的方式来获取数字的最后一位数。这里的具体实现用到了汇编语言的特点,div bx时,ax作为被除数,运算结果中会把存在ax中,余数存在dx中。

所以每一次循环我们都获取一次余数dx,然后把它加上'0'ASCLL码值30h把个位数转成数字字符

然后循环的出口是把ax除到0为止,所以使用了cmp指令配合jnz指令。

  • cmp当会比较两个操作数(做一次op1 - op2运算,但不储存结果),并设定ZF,CF,SF,OF,AF等标志位,这里只用到AF
    • ZF:Zero Flag,零标志位。 两个操作数op1 == op2时,ZF1; op1 != op2时,ZF0
  • jnz指令是跳转指令的一种,它会检测ZF标志位,即非零则跳转(这里指的是cmp的运算结果)
    • ZF == 1:这句语句不执行,程序自动往下执行
    • ZF == 0:跳转到jnz后面的标签位置处

像这里的两句就是把循环执行到ax变为0才退出循环,往下执行下一个模块

打印堆栈内的数字

接下来我们就要逐个出栈(这个顺序正好是逆序),然后逐个打印字符

这里就可以把前面的cx用上了。所以我们使用loop指令

至于输出字符,它也有自己的中断指令02h

1
2
3
4
5
PrintNumStr:
pop dx ;出栈
mov ah,02h;打印字符指令
int 21h ;开启中断
loop PrintNumStr

完整代码

我们把整个打印无符号数字的代码整合一下,得到如下代码

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
;==============数字打印模块=============
PrintNum:
;现状保存
push ax ;设计为传值传参,所以要保存ax~dx
push bx
push cx
push dx
;初始化
mov bx,10 ;这个bx存了除数10
mov cx,0 ;cx用于记录数字位数/循环次数

ParseLoop: ;解析数字串
mov dx,0 ;dx置零
div bx ;ax/10取余数在dx中
add dx,30h;转换成数字字符
push dx ;将余数压栈(正好最后一个数字先压栈,后出栈)
inc cx ;增加打印计数
cmp ax,0 ;判断ax是否为0
jnz ParseLoop ;循环调用

PrintNumStr:
pop dx ;出栈
mov ah,02h;打印字符指令
int 21h ;开启中断
loop PrintNumStr

pop dx; 恢复现场
pop cx;
pop bx;
pop ax;
ret ;函数返回

无符号数字输入模块

在解决了输出模块后,我们就可以更方便直观地观察输入处理是否正确了

所以我们接下来再实现一下输入模块

初始化&现状保存

如同C语言函数的传值传参一样,我们也模拟函数的行为做传值传参。为此我们就要做现状保存。

现状保存的具体做法就是按一定顺序把需要保存的寄存器数据存入堆栈,函数返回前我们再逆序取出来。

至于如何传出获取到的数字,我们这里约定使用ax作为输出型参数输出寄存器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
;================数字键入模块============
InputNum:
;现状保存,除了ax要输出参数
;设计为传值传参,所以要保存bx~dx
push bx
push cx
push dx

mov bx,0 ;要用bx所以提前置0,用于暂存结果
mov cx,0 ;同理,但用于中间计算

;.... 中间代码....
;....函数功能完成,即将返回....
;开始恢复函数调用前的状态

InputNumEnd:
mov ax,0
mov al,bl ;向ax存入结果
pop dx; 恢复现场
pop cx;
pop bx;
ret ;函数返回

非法输入处理和数字存入

我们采用死循环的方式逐个字符即时读取

为了防止Q被解析成非法字符,所以我们先判断输入是否为Q,若是,我们就跳转到程序退出。但是因为这里的代码长度问题,使用je可能会越界,所以我们使用了二级跳跃:je 配合 jmp far ptr使用

然后再判断到非法字符(非0~9数字字符)时模块跳出循环到模块结束部分InputNumEnd

这里的判断非法字符我们要配合之前用过的cmp,这里用到的标志位是CF进位标志位ZF零标志位,要用到的跳转函数是jajb

  • ja: Jump if Above,当op1 > op2时跳转
  • jb: Jump if Below,当op1 < op2时跳转

这样我们就能只取'0''9'中间的字符了,

至于数字存入,我们则是把暂存的结果(这里是bx10ax里的尾数的数

而如何实现x10操作呢?我们采用移位操作/乘以2的n次方 配合加法,即把bx*=10拆成bx = bx*2^3 + bx + bx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
InputLoop:
mov ah,1h ;使用1号中断输入字符
int 21h
;非法字符判断,包括回车时结束输入
cmp al,'0' ;和字符'0'比较
jb InputNumEnd ;jb用于比小,当al < '0'小时跳转
cmp al,'9'
ja InputNumEnd ;ja用于比大,当al > '0'时跳转

sub al,'0' ;减去'0'获得真实数值 字符->数字
;实现bx*=10,即 bx = bx*2^3 + bx + bx
mov cl,bl ;备份bl的值
shl bx,1 ;不能一次性移3位,会报错
shl bx,1 ;
shl bx,1 ;左移3位,相当于*8
add bl,cl ;
add bl,cl ;加两次
add bl,al ;把尾数al加上去

jmp InputLoop ;重新循环

模块的完整代码

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
;================数字键入模块============
InputNum:
;现状保存,除了ax要输出参数
;设计为传值传参,所以要保存bx~dx
push bx
push cx
push dx

mov bx,0 ;要用bx所以提前置0,用于暂存结果
mov cx,0 ;同理,但用于中间计算
InputLoop:
mov ah,1h ;使用1号中断输入字符
int 21h
;判断是否为字符Q,为Q则退出程序
cmp al,'Q'
je ProcExit
;非法字符判断,包括回车时结束输入
cmp al,'0' ;和字符'0'比较
jb InputNumEnd ;jb用于比小,当al < '0'小时跳转
cmp al,'9'
ja InputNumEnd ;ja用于比大,当al > '0'时跳转

sub al,'0' ;减去'0'获得真实数值 字符->数字
;实现bx*=10,即 bx = bx*2^3 + bx + bx
mov cl,bl ;备份bl的值
shl bx,1 ;不能一次性移3位,会报错
shl bx,1 ;
shl bx,1 ;左移3位,相当于*8
add bl,cl ;
add bl,cl ;加两次
add bl,al ;把尾数al加上去

jmp InputLoop ;重新循环

InputNumEnd:
mov ax,0
mov al,bl ;向ax存入结果
pop dx; 恢复现场
pop cx;
pop bx;
ret ;函数返回

斐波那契数列递归模块

对于这一模块,我们采用ax输入参数,ax输出参数,中间变量使用来暂存,函数结束前出栈中间变量防止造成空间浪费。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
;===========斐波那契数列递归模块========规定用堆栈输出
Fib:
cmp ax,3 ;ax <3 ,或者说 ax <= 2时
jb FibBaseRet

dec ax
push ax ;暂存 n - 1
call Fib ;递归调用求Fib(n - 1); 规定Fib调用前后栈的状态不变
;下面几句顺序 有严格要求
mov bx,ax ;bx暂存数据
pop ax ;取出暂存的ax-1
push bx ;bx的内容存入栈内暂存
dec ax ; n - 2 ;后面就不用n了,所以就不保存了
call Fib ;递归调用求Fib(n - 2)

pop bx ;取出暂存的bx的值
add ax,bx ;运算前ax = Fib(n-2), bx = Fib(n-1)
ret

FibBaseRet:
;Fib(1) = Fib(2) = 1
mov ax,1 ;ax输出参数
ret

这里的重点就是利用在递归调用前暂存需要保护的变量,并在合适的时候重新取出来。

模块整合,完整代码

之后我们便可以把几个模块整合起来了。

因为现在有了额外需求:

  • 循环输入,输入Q退出
  • 输入范围错误时输出错误信息

所以我们在略微修改一下程序主体内容,并在最后另外设置一个专门程序退出的标签ProcExit

后面就不多赘述了,直接放完整代码

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
assume cs:codesg,ds:datasg,ss:stacksg

;初始化数据段,并填入数字
datasg segment
num dw 10h ;选择斐波那契数列的第num个数
choose db 'Please choose a num from 1 to 100 : ','$'
result db 'The result is : ','$'
pressQ db 'Presss Q to exit','$'
wrongRange db 'wrong range, try again'
enter db 13, 10,'$'
datasg ends

;初始化栈段,置空
stacksg segment
db 25565 dup(0)
stacksg ends

codesg segment
start:
mov ax,datasg
mov ds,ax;
mov ax,stacksg;
mov ss,ax;
mov sp,25565;初始化栈


;打印输入
mov ah,09h
lea dx,choose
int 21h

;获取输入,并存到ax里
call InputNum
push ax

mov ah,09h
lea dx,enter;打印回车
int 21h

pop ax
cmp ax,0 ;判断是否为0
je ProcWrongRange
cmp ax,99 ;判断是否比100大
ja ProcWrongRange

call Fib
push ax ;暂存结果

mov ah,09h
lea dx,result
int 21h

pop ax
call PrintNum

mov ah,09h
lea dx,enter;打印回车
int 21h

mov ah,09h
lea dx,pressQ
int 21h

mov ah,09h
lea dx,enter;打印回车
int 21h

jmp start ;改成循环,循环的退出在字符输入模块内

ProcWrongRange:
mov ah,09h
lea dx,wrongRange
int 21h

mov ah,09h
lea dx,enter;打印回车
int 21h

jmp start
;===========斐波那契数列递归模块========规定用堆栈输出
Fib:
cmp ax,3 ;ax <3 ,或者说 ax <= 2时
jb FibBaseRet

dec ax
push ax ;暂存 n - 1
call Fib ;递归调用求Fib(n - 1); 规定Fib调用前后栈的状态不变
;下面几句顺序 有严格要求
mov bx,ax ;bx暂存数据
pop ax ;取出暂存的ax-1
push bx ;bx的内容存入栈内暂存
dec ax ; n - 2 ;后面就不用n了,所以就不保存了
call Fib ;递归调用求Fib(n - 2)

pop bx ;取出暂存的bx的值
add ax,bx ;运算前ax = Fib(n-2), bx = Fib(n-1)
ret

FibBaseRet:
;Fib(1) = Fib(2) = 1
mov ax,1 ;ax输出参数
ret

;================数字键入模块============
InputNum:
;现状保存,除了ax要输出参数
;设计为传值传参,所以要保存bx~dx
push bx
push cx
push dx

mov bx,0 ;要用bx所以提前置0,用于暂存结果
mov cx,0 ;同理,但用于中间计算
InputLoop:
mov ah,1h ;使用1号中断输入字符
int 21h
;判断是否为字符Q,为Q则退出程序
cmp al,'Q'
je ProcExit
;非法字符判断,包括回车时结束输入
cmp al,'0' ;和字符'0'比较
jb InputNumEnd ;jb用于比小,当al < '0'小时跳转
cmp al,'9'
ja InputNumEnd ;ja用于比大,当al > '0'时跳转

sub al,'0' ;减去'0'获得真实数值 字符->数字
;实现bx*=10,即 bx = bx*2^3 + bx + bx
mov cl,bl ;备份bl的值
shl bx,1 ;不能一次性移3位,会报错
shl bx,1 ;
shl bx,1 ;左移3位,相当于*8
add bl,cl ;
add bl,cl ;加两次
add bl,al ;把尾数al加上去

jmp InputLoop ;重新循环

InputNumEnd:
mov ax,0
mov al,bl ;向ax存入结果
pop dx; 恢复现场
pop cx;
pop bx;
ret ;函数返回

;==============数字打印模块=============
PrintNum:
;现状保存
push ax ;设计为传值传参,所以要保存ax~dx
push bx
push cx
push dx
;初始化
mov bx,10 ;这个bx存了除数10
mov cx,0 ;cx用于记录数字位数/循环次数

ParseLoop: ;解析数字串
mov dx,0 ;dx置零
div bx ;ax/10取余数在dx中
add dx,30h;转换成数字字符
push dx ;将余数压栈(正好最后一个数字先压栈,后出栈)
inc cx ;增加打印计数
cmp ax,0 ;判断ax是否为0
jnz ParseLoop ;循环调用

PrintNumStr:
pop dx ;出栈
mov ah,02h;打印字符指令
int 21h ;开启中断
loop PrintNumStr

pop dx; 恢复现场
pop cx;
pop bx;
pop ax;
ret ;函数返回
;=========程序出口======
ProcExit:
mov ax, 4c00h
int 21h
codesg ends
end start