• 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位入口程序逻辑段地址。

  • 相关阅读:
    Springmvc:(八)拦截器
    单例模式最终推荐写法-线程安全
    mybatis源码解析-日志适配器
    java基本类型与byte字节数组的转换(包含大端,小端)
    spring boot mybatis 报错Invalid bound statement (not found)解决过程
    mysql中联合索引中的自增列的增长策略
    软件设计六大原则个人理解记录
    spring boot mybatis+ vue 使用POI实现从Excel中批量导入数据
    spring boot 使用POI导出数据到Excel表格
    spring boot 整合JPA多数据源
  • 原文地址:https://www.cnblogs.com/michaelle/p/4023342.html
Copyright © 2020-2023  润新知