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