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

相同的功能,不同的代码

Posted by BINBIN Blog on October 22, 2019

##代码清单

总共就50行代码!

1 			 ;代码清单6-1
2 			 ;文件名:c06_mbr.asm
3 			 ;文件说明:硬盘主引导扇区代码
4 			 ;创建日期:2011-4-12 22:12 
5 		  
6 			 jmp near start
7 			 
8 	  mytext db 'L',0x07,'a',0x07,'b',0x07,'e',0x07,'l',0x07,' ',0x07,'o',0x07,\
9 				'f',0x07,'f',0x07,'s',0x07,'e',0x07,'t',0x07,':',0x07
10	  number db 0,0,0,0,0
11	  
12	  start:
13			 mov ax,0x7c0                  ;设置数据段基地址 
14			 mov ds,ax
15			 
16			 mov ax,0xb800                 ;设置附加段基地址 
17			 mov es,ax
18			 
19			 cld
20			 mov si,mytext                 
21			 mov di,0
22			 mov cx,(number-mytext)/2      ;实际上等于 13
23			 rep movsw
24		 
25			 ;得到标号所代表的偏移地址
26			 mov ax,number
27			 
28			 ;计算各个数位
29			 mov bx,ax
30			 mov cx,5                      ;循环次数 
31			 mov si,10                     ;除数 
32	  digit: 
33			 xor dx,dx
34			 div si
35			 mov [bx],dl                   ;保存数位
36			 inc bx 
37			 loop digit
38			 
39			 ;显示各个数位
40			 mov bx,number 
41			 mov si,4                      
42	   show:
43			 mov al,[bx+si]
44			 add al,0x30
45			 mov ah,0x04
46			 mov [es:di],ax
47			 add di,2
48			 dec si
49			 jns show
50			 
51			 mov word [es:di],0x0744
52
53			 jmp near $
54
55	  times 510-($-$$) db 0
56					   db 0x55,0xaa

代码分析

  • 8行-9行:这里声明了非指令的数据。一般来说,所有处理器指令都是按顺序存放,在他们中间不允许夹杂非指令的数据。但是如果有办法让处理器不执行这些数据,则又另当别论。如第6行的代码。这两行的目的是声明要显示的内容。在 NASM 里,“\”是续行符,当一行写不下时,可以在行尾使用这个符号,以表明下一行与当前行应该合并为一行。

这两行声明的是要在显示屏上显示的数据:”Label offset: “,其中0x07是每个字符的显示属性值。

  • 6行:它是一条转移指令。让处理器跳转到标号start处开始执行。这就避开了数据区。
  • 13-14行:设置数据段的基地址。DS代表数据段的基地址。

这里为什么是0x07c0呢?

由上几篇文章学过的知识知道,主引导扇区程序加载时,被加载到的位置是0x0000:0x7c00.也就是物理地址:0x07c00 这其实就是将整个物理地址空间看成是基地址0x0000,偏移地址0x7c00的分段方式。

这样的话,CPU每次访问内存的时候总是要加上0x7c00这个偏移地址。但是程序中一般访问内存的指令非常多,每一条都加上0x7c00很不现实。

但是Intel处理器的分段策略很灵活。逻辑地址0x0000:0x7c00对应的物理地址是0x07c00 ,而该地址又是另一个逻辑地址0x07c0:0x0000的地址。如下图是以两个逻辑段的视角看待同一个内存区域。

我们可以将512字节的区域看成是一个单独的段。段的基地址是:0x07c0 段长512字节。注意,该段的最大长度是64KB,但是这里我们实际上只用了512字节。尽管BIOS是将主引导扇区加载到物理地址0x07c00处,但是我们却可以认为它是从0x07c0:0x0000处开始加载的。

所以13-14行将数据段寄存器DS指向0x07c0!

  • 16-17行:使附加段寄存器ES的内容指向显存的基地址0xb800
  • 19-23行:循环movsw,直到cx寄存器内容为0(rep指令代表反复传送)。这里是循环将DS:SI所指向的数据传送到ES:DI所指定的显示缓冲区。

  • 源程序第 19 行是方向标志清零指令 cld。这是个无操作数指令,与其相反的是置方向标志指令std。 cld 指令将 DF 标志清零,以指示传送是正方向的!
  • movsb 和 movsw 指令执行时,原始数据串的段地址由 DS 指定,偏移地址由 SI 指定,简写为DS:SI;要传送到的目的地址由 ES:DI 指定;传送的字节数(movsb)或者字数(movsw) 由 CX 指定。除此之外,还要指定是正向传送还是反向传送,正向传送是指传送操作的方向是从内存区域的 低地址端到高地址端;反向传送则正好相反。正向传送时,每传送一个字节(movsb)或者一个字(movsw), SI 和 DI 加 1 或者加 2;反向传送时,每传送一个字节(movsb)或者一个字(movsw)时, SI 和 DI 减去 1 或者减去 2。不管是正向传送还是反向传送,也不管每次传送的是字节还是字,每传送一次, CX 的内容自动减一。 我们是正向传送是指传送操作的方向是从内存区域的低地址端到高地址端。

  • 26行:我们还是想像上一篇文章一样,显示字符串后将number这个标号的数值显示出来。所以先将number标号的汇编地址传送给AX寄存器保存。后面会用。

  • 29-37行:还记得上一篇文章是如何分解number的各个数位的么?如果不记得,请点击链接查看:上一篇文章 上一篇文章是一个一个分解然后保存的。这里有所改变。使用了循环,可以让我们少写很多代码。这里就不多说了,不懂的看上一篇文章,这个循环也很好理解,loop这个指令将循环次数CX减一,指导CX等于0为止。

  • 第35行 mov [bx],dl ;保存数位将 DL 中得到的余数传送到由 BX 所指示的内存单元中去! 但 mov [bx],dl 做相同的事情,那就是把 DL 中的内容,传送到以 DS 的内容为段地址,以 BX 的内容为偏移地址的内存单元中去。注意,指令中的中括号是必需的,否则就是传送到 BX 中,而不是 BX 的内容所指示的内存单元了。 所以带括号就是偏移地址的内存单元!

  • 40-49行:显示标号number的汇编地址的各个数位。同理,如何显示各个数位,可以查看上一篇文章。这里只是将重复的代码,写成了循环的形式。

jns这个指令判断SF标志位是否为0,当SF标志位不为0,继续执行show处的代码。当SF标志位为0,则跳过这条指令执行下一条指令。

dec指令会影响SF标志位,当SI寄存器的值为0的时候,SF的标志位置1!

当显示完最后一个数位后, SI 的内容是零。执行 dec si 指令后,由于产生了借位,实际的运算结果是 0xffff(SI 只能容纳 16 个比特),因其最高位是“1”,故处理器将标志位 SF 置“1”,表明当前 SI 中的结果可以理解为一个负数(-1)。于是,执行 jns show 时,条件不满足,接着执行后面第 51 行的指令。

这里唯一需要注意的是低端字节序传送的时候,寄存器的低字节传送到显示缓冲区的低地址部分,寄存器的高字节传送到显示缓冲区的高地址部分。如下图所示:

  • 51行:显示字符‘D’
  • 53行:死循环
  • 55行:计算512字节中,空字节有多少,然后将这些空字节填满0 $ 代表当前行的汇编地址 $$ 代表当前段的起始地址。由于本程序没有定义段,所以自成一个段,并且起始地址是0地址。

  • 56行:一个有效的主引导扇区,最后两字节必须是0x55 0xaa!

编译运行

将我们汇编代码编译好的二进制bin文件写到虚拟硬盘的主引导扇区中。启动虚拟机,就会运行我们写的代码,运行结果如下: