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

2023-06-12,,

一些约定

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

用户程序

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

定义各程序段

 ;用户程序头部信息
SECTION header align= vstart= ;代码段1
SECTION code_1 align= vstart=
;代码段2
SECTION code_2 align= vstart= ;数据段1
SECTION data_1 align= vstart= msg0 db ' This is user program,it just to display basic information',0x0d,0x0a
db ;数据段2
SECTION data_2 align= vstart= msg1 db ' This contents is written in 2014-06-01'
db ;256字节栈段
SECTION stack align= vstart=
resb
stack_end: ;用于统计程序长度
SECTION trail align=
program_end:

编写用户程序头部信息

 ;用户程序头部信息

 SECTION header align= vstart=
;程序长度
program_length dw program_end ;用户程序入口
code_entry dw start
dd section.code_1.start ;重定位表项数
realloc_tbl_len dw (header_end-code_1_segment)/ ;段重定位表
code_1_segment dd section.code_1.start
code_2_segment dd section.code_2.start
data_1_segment dd section.data_1.start
data_2_segment dd section.data_2.start
stack_segment dd section.stack.start header_end:

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

 ;代码段1
SECTION code_1 align= vstart=
start:
;设置栈段
mov ax,[stack_segment]
mov ss,ax
mov sp,stack_end ;设置ds指向数据段1
mov ax,[data_1_segment]
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来进行。

 put_char:
push ax
push bx
push cx
push dx
push ds
push es ;获取光标位置的高8位,存储在ah中
mov dx,0x3d4
mov al,0x0e
out dx,al
mov dx,0x3d5
in al,dx
mov ah,al ;获取光标位置的低8位,存储在al中
mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
in al,dx ;bx中存储光标位置
mov bx,ax

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

 put_char:
push ax
push bx
push cx
push dx
push ds
push es ;获取光标位置的高8位,存储在ah中
mov dx,0x3d4
mov al,0x0e
out dx,al
mov dx,0x3d5
in al,dx
mov ah,al ;获取光标位置的低8位,存储在al中
mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
in al,dx ;bx中存储光标位置
mov bx,ax cmp cl,0x0d
;不是回车,跳转到判断是不是换行处
jnz .put_0a
mov bl,
div bl
;此时al中是光标所在行数,再乘以80即得到
;回车后光标在屏幕上的位置
mul bl
mov bx,ax
;重新设置光标位置
jmp .set_cursor .put_0a:
cmp cl,0x0a
jnz .put_other
add bx,
;判断是否滚动屏幕
jmp .roll_screen

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

 .set_cursor:
;高8位对应bh
mov dx,0x3d4
mov al,0x0e
out dx,al
mov dx,0x3d5
mov al,bh
out dx,al
;低8位对应bl
mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
mov al,bl
out dx,al

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

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

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

 .roll_screen:
cmp bx,
jl .set_cursor mov ax,0xb800
mov ds,ax
mov es,ax
cld
mov si,0xa0
mov di,0x00
mov cx,
rep movsw
mov bx,
mov cx,
.cls:
mov word[es:bx],0x0720
add bx,
loop .cls mov bx,

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

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

代码段2

SECTION code_2 align= vstart=          ;定义代码段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= vstart=0x7c00

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

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

 ;主引导扇区代码
SECTION mbr align= vstart=0x7c00
mov ax,
mov ss,ax
mov sp,ax ;20位内存地址高16位存储在dx中
mov ax,[cs:phy_base]
mov dx,[cs:phy_base+]
;除以16得到逻辑段地址
mov bx,
div bx
;ds,es指向16位用户程序逻辑段地址
mov ds,ax
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例程用于读取硬盘上的内容,硬盘内容的读写也是通过端口进行的,具体见下面的代码

 read_hard_disk_0:                        ;从硬盘读取一个逻辑扇区
;输入:DI:SI=起始逻辑扇区号
; DS:BX=目标缓冲区地址
push ax
push bx
push cx
push dx mov dx,0x1f2
mov al,
out dx,al ;读取的扇区数 inc dx ;0x1f3
mov ax,si
out dx,al ;LBA地址7~0 inc dx ;0x1f4
mov al,ah
out dx,al ;LBA地址15~8 inc dx ;0x1f5
mov ax,di
out dx,al ;LBA地址23~16 inc dx ;0x1f6
mov al,0xe0 ;LBA28模式,主盘
or al,ah ;LBA地址27~24
out dx,al inc dx ;0x1f7
mov al,0x20 ;读命令
out dx,al .waits:
in al,dx
and al,0x88
cmp al,0x08
jnz .waits ;不忙,且硬盘已准备好数据传输 mov cx, ;总共要读取的字数
mov dx,0x1f0
.readw:
in ax,dx
mov [bx],ax
add bx,
loop .readw pop dx
pop cx
pop bx
pop ax ret

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

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

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

 ;开始处理段重定位表
mov cx,[0x0a] ;需要重定位的项目数量
mov bx,0x0c ;重定位表首地址 realloc:
mov dx,[bx+0x02] ;32位地址的高16位
mov ax,[bx] push dx
add ax,[cs:phy_base]
adc dx,[cs:phy_base+0x02]
shr ax,
ror dx,
and dx,0xf000
or ax,dx
pop dx mov [bx],ax ;回填段的基址
add bx, ;下一个重定位项(每项占4个字节)
loop realloc jmp far [0x04] ;转移到用户程序

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

16位cpu下主引导扇区及用户程序的编写的相关教程结束。

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

下载本文的Word格式文档,以方便收藏与打印。