概述 本期的重点在于使用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
时,ZF
置1
; op1 != op2
时,ZF
置0
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
零标志位,要用到的跳转函数是ja
和jb
ja
: Jump if Above ,当op1 > op2
时跳转
jb
: Jump if Below ,当op1 < op2
时跳转
这样我们就能只取'0'
和'9'
中间的字符了,
至于数字存入,我们则是把暂存的结果(这里是bx
)乘 以10
再加 上ax
里的尾数的数值
而如何实现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
这里的重点就是利用栈
在递归调用前暂存需要保护的 变量
,并在合适的时候重新取出来。
模块整合,完整代码 之后我们便可以把几个模块整合起来了。
因为现在有了额外需求:
所以我们在略微修改一下程序主体内容,并在最后另外设置一个专门程序退出的标签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