16位cpu下主引导扇区及用户程序的编写

 一些约定
  • 主引导扇区代码(0面0道1扇区)加载至0x07c00处
  • 用户程序头部代码需包含以下信息:程序总长度、程序入口、重定位表等信息
 用户程序

当虚拟机启动时,在屏幕上显示以下两句话: This is user program,it just to display basic information.This contents is written in 2014-06-01.

定义各程序段

 1 ;用户程序头部信息
 2 SECTION header align=16 vstart=0
 3 
 4 ;代码段1
 5 SECTION code_1 align=16 vstart=0
 6 ;代码段2
 7 SECTION code_2 align=16 vstart=0
 8 
 9 ;数据段1
10 SECTION data_1 align=16 vstart=0
11 
12     msg0 db '  This is user program,it just to display basic information',0x0d,0x0a
13          db 0
14 
15 ;数据段2
16 SECTION data_2 align=16 vstart=0
17 
18     msg1 db '  This contents is written in 2014-06-01'
19          db 0
20 
21 ;256字节栈段
22 SECTION stack align=16 vstart=0
23     resb 256
24 stack_end:
25 
26 ;用于统计程序长度
27 SECTION trail align=16
28 program_end:

编写用户程序头部信息

 1 ;用户程序头部信息
 2 
 3 SECTION header align=16 vstart=0
 4     ;程序长度
 5     program_length dw program_end
 6     
 7     ;用户程序入口
 8     code_entry dw start
 9                dd section.code_1.start
10                
11     ;重定位表项数
12     realloc_tbl_len dw (header_end-code_1_segment)/4
13     
14     ;段重定位表
15     code_1_segment  dd section.code_1.start
16     code_2_segment  dd section.code_2.start 
17         data_1_segment  dd section.data_1.start 
18         data_2_segment  dd section.data_2.start 
19         stack_segment   dd section.stack.start  
20     
21     header_end:

代码段1及代码段2需要实现显示字符功能,下面分解开了一点点实现。当用户程序获得cpu使用权后,第一步要做的是初始化各寄存器的指向,此时,ds和es都是指向用户程序头部,即程序第一个字节处。

 1 ;代码段1
 2 SECTION code_1 align=16 vstart=0
 3     start:
 4         ;设置栈段
 5         mov ax,[stack_segment]
 6         mov ss,ax
 7         mov sp,stack_end
 8         
 9         ;设置ds指向数据段1
10         mov ax,[data_1_segment]
11         mov ds,ax

初始化寄存器后,就需要调用显示字符例程以在屏幕上打印字符

;ds:bx指向数据段开始的第一个字符
mov bx,msg0
call put_string

下面编写put_string例程,put_string首先需要判断是否是字符串结尾,若到达结尾则返回主程序,否则调用put_char例程打印字符。jz的意思是说zf表示为等于1则转移,zf标志位的结果受上一条代码影响,若or cl,cl执行后,cl=0则zf=1

  put_string:
    
        mov cl,[bx]
        or cl,cl
        jz .exit    
        call put_char
        inc bx
        jmp put_string
        
    .exit
        ret

接下来编写put_char例程,他的功能是显示ds:bx处的一个字符,在编写之前需先了解VGA标准下光标的获取与回车换行的处理。

光标在屏幕上的位置是存储在两个8为寄存器中的,这两个寄存器位于显卡中,为了提高I/O效率,一般通过索引寄存器方位显卡中的寄存器,索引寄存器的端口号是0x3d4,两个8为寄存器的索引值分别为0x0e和0x0f,读写操作需要通过数据端口0x3d5来进行。

 1 put_char:
 2          push ax
 3          push bx
 4          push cx
 5          push dx
 6          push ds
 7          push es
 8          
 9          ;获取光标位置的高8位,存储在ah中
10          mov dx,0x3d4
11          mov al,0x0e
12          out dx,al
13          mov dx,0x3d5
14          in al,dx
15          mov ah,al
16          
17          ;获取光标位置的低8位,存储在al中
18          mov dx,0x3d4
19          mov al,0x0f
20          out dx,al
21          mov dx,0x3d5
22          in al,dx
23          
24          ;bx中存储光标位置
25          mov bx,ax

光标位置获取以后,需要进行下一步判断即想要显示的字符是否是回车或换行符这样的控制字符,回车符(0x0d)、换行符(0x0a)。

 1 put_char:
 2          push ax
 3          push bx
 4          push cx
 5          push dx
 6          push ds
 7          push es
 8          
 9          ;获取光标位置的高8位,存储在ah中
10          mov dx,0x3d4
11          mov al,0x0e
12          out dx,al
13          mov dx,0x3d5
14          in al,dx
15          mov ah,al
16          
17          ;获取光标位置的低8位,存储在al中
18          mov dx,0x3d4
19          mov al,0x0f
20          out dx,al
21          mov dx,0x3d5
22          in al,dx
23          
24          ;bx中存储光标位置
25          mov bx,ax
26          
27          cmp cl,0x0d
28          ;不是回车,跳转到判断是不是换行处
29          jnz .put_0a
30          mov bl,80
31          div bl
32          ;此时al中是光标所在行数,再乘以80即得到
33          ;回车后光标在屏幕上的位置
34          mul bl
35          mov bx,ax
36          ;重新设置光标位置
37          jmp .set_cursor
38          
39     .put_0a:
40         cmp cl,0x0a
41         jnz .put_other
42         add bx,80
43         ;判断是否滚动屏幕
44         jmp .roll_screen

下面是重新设置光标的例程.set_cursor

 1 .set_cursor:
 2         ;高8位对应bh
 3         mov dx,0x3d4
 4         mov al,0x0e
 5         out dx,al
 6         mov dx,0x3d5
 7         mov al,bh
 8         out dx,al
 9         ;低8位对应bl
10         mov dx,0x3d4
11         mov al,0x0f
12         out dx,al
13         mov dx,0x3d5
14         mov al,bl
15         out dx,al

.put_others的工作是显示字符,就不细说了

 1  .put_other:                             
 2          mov ax,0xb800
 3          mov es,ax
 4          ;bx是光标的位置,一个字符在显存中是2字节显示
 5          ;所以光标位置*2是字符的显示位置
 6          shl bx,1
 7          mov [es:bx],cl
 8 
 9          ;将光标位置推进一个字符
10          shr bx,1
11          add bx,1

接下来就是处理滚屏时的操作,滚屏可以理解为屏幕整体向上一行且最后一行清空

 1 .roll_screen:
 2          cmp bx,2000                    
 3          jl .set_cursor
 4 
 5          mov ax,0xb800
 6          mov ds,ax
 7          mov es,ax
 8          cld
 9          mov si,0xa0
10          mov di,0x00
11          mov cx,1920
12          rep movsw
13          mov bx,3840                     
14          mov cx,80
15     .cls:
16          mov word[es:bx],0x0720
17          add bx,2
18          loop .cls
19 
20          mov bx,1920

代码段1执行完毕后需要转到代码段2继续执行

push word [es:code_2_segment]
         mov ax,begin
         push ax                          
         
         retf 

代码段2

SECTION code_2 align=16 vstart=0          ;定义代码段2(16字节对齐)

  begin:
         push word [es:code_1_segment]
         mov ax,continue
         push ax                          
         
         retf 

continue例程实现显示第二段信息的功能

continue:
         mov ax,[es:data_2_segment]       ;段寄存器DS切换到数据段2 
         mov ds,ax
         
         mov bx,msg1
         call put_string                  ;显示第二段信息 

         jmp $ 

至此,用户程序编写完毕

 主引导扇区代码

首先要做的是定义读取用户程序的逻辑扇区编号、加载到的内存地址以及主引导扇区代码段

SECTION mbr align=16 vstart=0x7c00

;用户程序所在逻辑扇区编号
app_lba_start equ 100
;用户程序将要加载的内存地址
phy_base dd 0x10000

下一步编写引导代码,我们电脑加点启动后主引导扇区代码会被加载到内存地址0x07c00处,所以上面的代码中有vstart=0x7c00语句方便下面的操作。引导扇区代码第一步要做是获取用户程序头部信息,根据程序长度从逻辑扇区把用户程序字节码加载到指定的内存地址处

 1 ;主引导扇区代码
 2 SECTION mbr align=16 vstart=0x7c00
 3     mov ax,0
 4     mov ss,ax
 5     mov sp,ax
 6     
 7     ;20位内存地址高16位存储在dx中
 8     mov ax,[cs:phy_base]
 9     mov dx,[cs:phy_base+02]
10     ;除以16得到逻辑段地址
11     mov bx,16
12     div bx
13     ;ds,es指向16位用户程序逻辑段地址
14     mov ds,ax
15     mov es,ax

下一步,从硬盘中读取用户程序字节码至指定的内存地址处

    ;清空di,ds:si代表逻辑扇区编号
    xor di,di
    mov si,app_lba_start
    ;清空bx,ds:bx指向加载内存地址
    xor bx,bx
    call read_hard_disk_0

read_hard_disk_0例程用于读取硬盘上的内容,硬盘内容的读写也是通过端口进行的,具体见下面的代码

 1 read_hard_disk_0:                        ;从硬盘读取一个逻辑扇区
 2                                          ;输入:DI:SI=起始逻辑扇区号
 3                                          ;      DS:BX=目标缓冲区地址
 4          push ax
 5          push bx
 6          push cx
 7          push dx
 8       
 9          mov dx,0x1f2
10          mov al,1
11          out dx,al                       ;读取的扇区数
12 
13          inc dx                          ;0x1f3
14          mov ax,si
15          out dx,al                       ;LBA地址7~0
16 
17          inc dx                          ;0x1f4
18          mov al,ah
19          out dx,al                       ;LBA地址15~8
20 
21          inc dx                          ;0x1f5
22          mov ax,di
23          out dx,al                       ;LBA地址23~16
24 
25          inc dx                          ;0x1f6
26          mov al,0xe0                     ;LBA28模式,主盘
27          or al,ah                        ;LBA地址27~24
28          out dx,al
29 
30          inc dx                          ;0x1f7
31          mov al,0x20                     ;读命令
32          out dx,al
33 
34   .waits:
35          in al,dx
36          and al,0x88
37          cmp al,0x08
38          jnz .waits                      ;不忙,且硬盘已准备好数据传输 
39 
40          mov cx,256                      ;总共要读取的字数
41          mov dx,0x1f0
42   .readw:
43          in ax,dx
44          mov [bx],ax
45          add bx,2
46          loop .readw
47 
48          pop dx
49          pop cx
50          pop bx
51          pop ax
52       
53          ret

用户程序头部信息读取后,就可以根据头部信息判断程序大小然后读取剩余的字节码

 1 mov dx,[2]                     
 2          mov ax,[0]
 3          mov bx,512                      ;512字节每扇区
 4          div bx
 5          cmp dx,0
 6          jnz @1                          ;未除尽,因此结果比实际扇区数少1 
 7          dec ax                          ;已经读了一个扇区,扇区总数减1 
 8    @1:
 9          :实际长度小于512字节,直接计算入口程序入口段地址
10          cmp ax,0                        
11          jz direct
12          
13          ;读取剩余的扇区
14          push ds                         ;以下要用到并改变DS寄存器 
15 
16          mov cx,ax                       ;循环次数(剩余扇区数)
17    @2:
18          mov ax,ds
19          add ax,0x20                     ;得到下一个以512字节为边界的段地址
20          mov ds,ax  
21                               
22          xor bx,bx                       ;每次读时,偏移地址始终为0x0000 
23          inc si                          ;下一个逻辑扇区 
24          call read_hard_disk_0
25          loop @2                         ;循环读,直到读完整个功能程序 
26 
27          pop ds                          ;恢复数据段基址到用户程序头部段
direct例程实现入口代码段地址的计算
 1          mov dx,[0x08]
 2          mov ax,[0x06]
 3 
 4          push dx                          
 5          add ax,[cs:phy_base]
 6          adc dx,[cs:phy_base+0x02]
 7          shr ax,4
 8          ror dx,4
 9          and dx,0xf000
10          or ax,dx
11          pop dx
12 
13          mov [0x06],ax                   ;回填修正后的入口点代码段基址 

下面处理段重定位表,原理和处理入口地址一样

 1 ;开始处理段重定位表
 2          mov cx,[0x0a]                   ;需要重定位的项目数量
 3          mov bx,0x0c                     ;重定位表首地址
 4           
 5  realloc:
 6          mov dx,[bx+0x02]                ;32位地址的高16位 
 7          mov ax,[bx]
 8 
 9          push dx                          
10          add ax,[cs:phy_base]
11          adc dx,[cs:phy_base+0x02]
12          shr ax,4
13          ror dx,4
14          and dx,0xf000
15          or ax,dx
16          pop dx
17 
18          mov [bx],ax                     ;回填段的基址
19          add bx,4                        ;下一个重定位项(每项占4个字节) 
20          loop realloc 
21       
22          jmp far [0x04]                  ;转移到用户程序 

注意最有一行代码jmp far [0x04],此时ds是指向用户程序首地址的,取出[ds:0x04]处的2个字数据,分别赋予cs和ip.[0x04]处是一个字数据即用户程序开始的标号的偏移地址,下一个数据是回填以后的16位入口程序逻辑段地址。

原文地址:https://www.cnblogs.com/michaelle/p/4023342.html