x86汇编语言-从实模式到保护模式笔记

比高斯更快的计算

Posted by BINBIN Blog on October 23, 2019

##代码清单 其实主要就是讲堆栈,前面汇编语言有一章专门讲堆栈的!

还记得前几篇文章,我们学会了编写主引导扇区代码,在显示屏显示字符串。最开始我们的做法是一个字符一个字符的传送给显存。后来发现可以先将所有需要传送的字符先存放到一块内存中,然后使用movsw连续传送这些字符串到显存更加方便。

今天我们的目的是,我们将我们想要显示的数字,先暂时存放到一种称为栈的结构中。最后我们再从栈中取出这些数字发送给显存。

1 			 ;代码清单7-1
2 			 ;文件名:c07_mbr.asm
3 			 ;文件说明:硬盘主引导扇区代码
4 			 ;创建日期:2011-4-13 18:02
5 			 
6 			 jmp near start
7 		
8 	 message db '1+2+3+...+100='
9 			
10	 start:
11			 mov ax,0x7c0           ;设置数据段的段基地址 
12			 mov ds,ax
13
14			 mov ax,0xb800          ;设置附加段基址到显示缓冲区
15			 mov es,ax
16
17			 ;以下显示字符串 
18			 mov si,message          
19			 mov di,0
20			 mov cx,start-message
21		 @g:
22			 mov al,[si]
23			 mov [es:di],al
24			 inc di
25			 mov byte [es:di],0x07
26			 inc di
27			 inc si
28			 loop @g
29
30			 ;以下计算1到100的和 
31			 xor ax,ax  ; xor 指令将寄存器 AX 清零
32			 mov cx,1
33		 @f:
34			 add ax,cx
35			 inc cx
36			 cmp cx,100
37			 jle @f
38
39			 ;以下计算累加和的每个数位 
40			 xor cx,cx              ;设置堆栈段的段基地址
41			 mov ss,cx
42			 mov sp,cx
43
44			 mov bx,10
45			 xor cx,cx
46		 @d:
47			 inc cx
48			 xor dx,dx
49			 div bx
50			 or dl,0x30   ; 分解出的数位将来要显示在屏幕上,为了方便,源程序第 50 行,直接将 AL 中的商“加上”
0x30,以得到该数字所对应的 ASCII 码。
51			 push dx
52			 cmp ax,0
53			 jne @d
54
55			 ;以下显示各个数位 
56		 @a:
57			 pop dx
58			 mov [es:di],dl
59			 inc di
60			 mov byte [es:di],0x07
61			 inc di
62			 loop @a
63		   
64			 jmp near $ 
65		   
66
67	times 510-($-$$) db 0
68					 db 0x55,0xaa

代码分析

这里分析会比较简洁,因为大部分代码的意思跟前几篇文章内容是一个意思,无非就是设置代码段数据段基地址与偏移地址,设置显存的基地址与偏移地址。然后将要显示的字符串经过计算得出结果并存起来。最后将这些字符串传送到显示缓冲区。

那么下面就开始分析:

  • 8行:就是想要显示‘1+2+3+…+100’,只不过这里先要将它存储在这里,好方便下面的循环传送。message是标号,代表它当前位置的汇编地址

  • 11-14行:设置数据段基地址与附加段基地址(也就是显存的基地址),这里前几篇文章已经讲了很多,不懂的可以回头看前面的文章。

  • 18-28行:将字符串‘1+2+3+…+100’显示出来。这里同样使用了循环的方法将字符串循环传送到显存。CX这里代表计数器,表示要传送的字符串的字节数。inc指令代表加1的意思。

  • 31-37行:计算1-100的和。这里将计算结果存到AX寄存器。CX每次加1是代表下一次要加的数。第31行是 xor 指令将寄存器 AX 清零。源程序第 32 行,将第一个被累加的数“1”传送到寄存器 CX。 源程序第 34 行就开始累加了,每次相加之后,源程序第 35 行,将 CX 的内容加一,以得到下一个将要累加的数。 源程序第 36 行,将 CX 的内容同 100 进行比较,看是不是已经累加到 100 了。如果小于等于100,则继续重复累加过程,如果大于 100,就不再累加,直接往下执行!需要特别说明的是, AX 可以容纳的无符号数最大是 65535,再大就不行了。由于我们已经知道最终的结果是 5050,所以很放心地使用了寄存器 AX!
  • 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行所做的事。其他指令较为简单治理不再赘述。 第51行 push dx,处理器在执行 push 指令时,首先将堆栈指针寄存器 SP 的内容减去操作数的字长(以字节为单位的长度,在 16 位处理器上是 2),然后,把要压入堆栈的数据存放到逻辑地址 SS:SP 所指向的内 存位置(和其他段的读写一样,把堆栈段寄存器 SS 的内容左移 4 位,加上堆栈指针寄存器 SP 提供的偏移地址)!

如图 7-3 所示,代码段和堆栈段是同一个段,所以段寄存器 CS 和 SS 的内容都是 0x0000。而且,堆栈指针寄存器 SP 的内容在源程序第 42 行被置为 0。所以,当 push 指令第一次执行时, SP的内容减 2,即 0x0000-0x0002=0xFFFE,借位被忽略。于是,被压入堆栈的数据,在内存中的位 置实际上是 0x0000:0xFFFE。 push 指令的操作数是字,而且 Intel 处理器是使用低端字节序的,故低字节在低地址部分,高字节在高地址部分,正好占据了堆栈段的最高两个字节位置。 这只是第一次压栈操作时的情况。以后每次压栈时, SP 都要依次减 2。很明显,不同于代码段,代码段在处理器上执行时,是由低地址端向高地址端推进的,而压栈操作则正好相反,是从高地址端向低地址端推进的。 push 指令不影响任何标志位。

  • 57-62行:出栈,并显示各个数位。 这几行都比较简单。pop指令的意思是将逻辑地址SS:SP处的一个字弹出到寄存器DX中,然后将寄存器SP的内容加上操作数的字长(2)。

  • 64行:为了让我们看到显示屏的显示效果,这里是一个死循环,防止程序退出。

  • 67-68行:填充空的字节区间。然后最后的0x55和0xaa是主引导扇区的有效标志。

进一步认识堆栈

关于堆栈,这里有几点说明。 第一, push 指令的操作数可以是 16 位寄存器或者指向 16 位实际操作数的内存单元地址,push 指令执行后,压入堆栈中的仅仅是该寄存器或者内存单元里的数值,与该寄存器或内存单元不再相干。所以,下面的指令是合法而且正确的: push cs pop ds 这两条指令的意思是, 将代码段寄存器的内容压栈,并弹出到数据段寄存器 DS。如此一来,代码段和数据段将属于同一个内存段。实际上,这两条指令的执行结果,和以下指令的执行结果相同: mov ax,cx mov ds,ax 第二,堆栈在本质上也只是普通的内存区域,之所以要用 push 和 pop 指令来访问,是因为你把它看成堆栈而已。实际上,如果你把它看成是普通的数据段而忘掉它是一个堆栈,那么它将不再神秘。 引入堆栈和 push、 pop 指令只是为了方便程序开发。临时保存一个数值到堆栈中,使用 push指令是最简洁、最省事的,但如果你不怕麻烦,可以不使用它。所以,下面的代码可以用来取代 push ax 指令:

sub sp,2
mov bx,sp
mov [ss:bx],ax

同样, pop ax 指令的执行结果和下面的代码相同:

mov bx,sp
mov ax,[ss:bx]
add sp,2

但是显而易见, push 和 pop 指令更方便,毕竟与堆栈访问有关的一切都是由处理器自动维护的。

第三,要注意保持堆栈平衡,防止数据访问越界。尤其是在编写程序前,必须充分估计所需要的堆栈空间, 以防止破坏有用的数据。特别是在堆栈段和其他段属于同一个段的时候。

如图 7-3 所示, 堆栈段和代码段属于同一个内存段,段地址都是 0x0000,段的长度都是 64KB。主引导程序的 长度是 512(0x200)字节,从偏移地址 0x7c00 延伸到 0x7e00。堆栈是向下增长的,它们之间有 0xffff -0x7e00+1=0x8200 字节的空档。通常来说,我们的程序是安全的,因为不可能压入这么多的数据!

运行程序

运行结果如下:

8086 处理器的寻址方式

多数指令操作的是数值。比如:

mov ax,0x55aa

这条指令执行时,把 0x55aa 传送到寄存器 AX。再如:

add dx,cx

这是把寄存器 DX 中的数据和寄存器 CX 中的数据相加,并把结果保留在 DX 中,同时保持 CX中原有的内容不变。

既然操作和处理的是数值,那么,必定涉及数值从哪里来,处理后送到哪里去,这称为寻址方式(Addressing Mode)。简单地说,寻址方式就是如何找到要操作的数据,以及如何找到存放操作结果的地方。

多数的寻址方式我们都已经使用过,现在所做的只是一个完整的总结。当然,这里的讲解仅限于 16 位的处理器。

具体请看书中所说,不在详细描述了!