上一篇文章以一种更加高效的方法编写了主引导扇区的代码。主要是引入了循环和跳转指令。点击链接查看上一篇文章:编写主引导扇区代码-另一种更高效的写法
本篇文章,继续上一篇文章的学习。同样还是编写汇编代码加载到主引导扇区让CPU直接执行。但是我们以一个简单程序,实现1加到100,来引出8086中的栈结构。了解处理器访问栈需要哪些支持。
1、回顾
还记得前几篇文章,我们学会了编写主引导扇区代码,在显示屏显示字符串。最开始我们的做法是一个字符一个字符的传送给显存。后来发现可以先将所有需要传送的字符先存放到一块内存中,然后使用movsw连续传送这些字符串到显存更加方便。
今天我们的目的是,我们将我们想要显示的数字,先暂时存放到一种称为栈的结构中。最后我们再从栈中取出这些数字发送给显存。
2、代码清单
与前几篇文章一样,我们先提供汇编代码。遇到汇编代码不要害怕,脑子里将CPU,寄存器,内存这三个结构与他们的关系都勾勒出来,然后分析指令的执行,就会很清晰。
如果不懂,看后面的分析,如果你有一点基础,就一定能够学会!!!
;代码清单7-1
;文件名:c07_mbr.asm
;文件说明:硬盘主引导扇区代码
;
jmp near start
message db '1+2+3+...+100='
start:
mov ax,0x7c0 ;设置数据段的段基地址
mov ds,ax
mov ax,0xb800 ;设置附加段基址到显示缓冲区
mov es,ax
;以下显示字符串
mov si,message
mov di,0
mov cx,start-message
@g:
mov al,[si]
mov [es:di],al
inc di
mov byte [es:di],0x07
inc di
inc si
loop @g
;以下计算1到100的和
xor ax,ax
mov cx,1
@f:
add ax,cx
inc cx
cmp cx,100
jle @f
;以下计算累加和的每个数位
xor cx,cx ;设置堆栈段的段基地址
mov ss,cx
mov sp,cx
mov bx,10
xor cx,cx
@d:
inc cx
xor dx,dx
div bx
or dl,0x30 ;实际上应该是add指令,但是这可以是or指令,因为dl高四位为0,0x30低四位位0
push dx
cmp ax,0
jne @d
;以下显示各个数位
@a:
pop dx
mov [es:di],dl
inc di
mov byte [es:di],0x07 ;显示字符的颜色属性
inc di
loop @a
jmp near $
times 510-($-$$) db 0
db 0x55,0xaa
代码不长,大部分内容,在前几篇文章都学过。
3、代码分析
强烈建议先将上一篇文章学会,再阅读下面的代码解释会更加轻松:点击链接查看上一篇文章
这里分析会比较简洁,因为大部分代码的意思跟前几篇文章内容是一个意思,无非就是设置代码段数据段基地址与偏移地址,设置显存的基地址与偏移地址。然后将要显示的字符串经过计算得出结果并存起来。最后将这些字符串传送到显示缓冲区。
那么下面就开始分析:
-
8行:就是想要显示‘1+2+3+…+100’,只不过这里先要将它存储在这里,好方便下面的循环传送。message是标号,代表它当前位置的汇编地址
-
11-14行:设置数据段基地址与附加段基地址(也就是显存的基地址),这里前几篇文章已经讲了很多,不懂的可以回头看前面的文章。
-
18-28行:将字符串‘1+2+3+…+100’显示出来。这里同样使用了循环的方法将字符串循环传送到显存。CX这里代表计数器,表示要传送的字符串的字节数。inc指令代表加1的意思。
-
31-37行:计算1-100的和。这里将计算结果存到AX寄存器。CX每次加1是代表下一次要加的数。
-
40-53行:计算累加和的各个数位。毕竟我们要显示这个累加和嘛,又不能直接将它发送到显示缓冲区直接显示,直接将它的各个数位拆解出出来显示。这几行,是我们今天要重要研究的汇编代码。它涉及到一个新的概念----栈
得到了累加和之后,前两篇文章,是将各个数位保存在数据段中。现在我们将各个数位保存在一个叫做栈的地方。
栈----是一种特殊的数据存储结构,数据的存取只能从一端进行。这样先进去的数据只能最后出来。后进去的数据倒是最先出来。
如下图:
和代码段,数据段和附加段一样,栈也是一种内存段,叫做栈段。由栈寄存器SS指向。
针对栈有两种操作方式:push和pop。这个应该大家都理解。压栈和出栈只能在一端进行。所以需要用栈指针寄存器SP来指示下一个数据应当压入到什么位置,或者数据从哪里弹出。
定义栈需要两个步骤。即指定SS和SP寄存器。为此40-42行,设置了SS和SP。他们都是指向0地址。
到目前为止,我们已经定义了3个段。如下图是我们当前程序的内存布局:
总内存容量是1MB,物理地址范围是0x00000-0xFFFFF
其中数据段长度是64KB(实际上它的长度无关紧要)占据的物理地址范围是0x07C00-0x17BFF
,对应的逻辑地址为范围为 0x07C0:0x0000-0x7C00:0xFFFF;
代码段和栈段是同一个段,占据着物理地址0x00000-0x0FFFF
,对应的逻辑地址的范围是0x0000:0x0000-0x0000:0xFFFF
。
虽然代码段和栈段在本质上指向同一块内存区域,但是通过后面的学习我们会知道,他们互不干扰。
分解各个数位还是要靠除法来做,44行将除数10传送给寄存器BX。
由于每次分解得到的数位都是压栈的,所以后面再出栈的时候,我们需要记住总共有多少个。这里用CX寄存器记录个数。所以45行,先将CX寄存器清零。
源程序第47-53行也是一个循环体,没执行一次,分解出一个数位。每次分解时,CX加1,表明数位又多了一个,这是源程序47行所做的事。其他指令较为简单治理不再赘述。
- 57-62行:出栈,并显示各个数位。
这几行都比较简单。pop指令的意思是将逻辑地址SS:SP处的一个字弹出到寄存器DX中,然后将寄存器SP的内容加上操作数的字长(2)。
-
64行:为了让我们看到显示屏的显示效果,这里是一个死循环,防止程序退出。
-
67-68行:填充空的字节区间。然后最后的0x55和0xaa是主引导扇区的有效标志。
4、进一步认识栈
上述我们从代码层面第一次接触到栈这种结构。那么下面我们就来总结一下,做几点说明。
- push指令的操作数可以是16位寄存器或者16位内存单元。push指令执行后,压入栈中的仅仅是该寄存器或者内存单元中的数值。
- 栈在本质上也只是普通的内存区域,之所以用push和pop指令来访问,是因为你把它看成栈而已。引入栈和push、pop只是方便程序开发而已。
- 要注意保持栈平衡。push多少,pop多少。
- 在编写程序时,必须充分估计所需的栈空间,以防止破坏有用的数据。
- 尽管不能完全阻止程序中的错误,但是,通过将栈定义到一个单独的64KB内存段,可以使错误仅局限于栈段,而不破坏其他段的有用数据。
5、运行程序
运行结果如下;
本次程序运行很顺利!!!
笔记记得不是很全,像汇编的语法以及如何将代码写到虚拟硬盘的主引导扇区这些都没有写。如果又不懂的可以加我联系方式一起交流。
学习探讨加个人:
qq:1126137994
微信:liu1126137994