本章主要讲加载器代码如何一步步把用户程序代码从硬盘加载到内存的,然后把CPU交给用户程序,执行用户程序代码。用户程序功能就是显示两段字符串到屏幕上。作者讲解了CPU如何访问显卡和硬盘这样的外围设备,抽象出来说的话就是CPU通过端口向外围设备发号施令以及读写数据。笔者阅读并根据自己理解稍微更改并重新注释了作者的源码(具体细节书中说明比较详细,结合我的注释更易理解),具体如下。
;文件名:c08_01_mbr.asm
;文件说明:硬盘主引导扇区代码
;参考书:《x86汇编语言:从实模式到保护模式》李忠 著
;代码功能:从硬盘加载用户程序到内存,把CPU交给用户程序
;(用户程序头部包含用户程序的长度、入口地址、段重定位表等信息)
app_lba_start equ 100 ;声明常数(用户程序所在的硬盘逻辑扇区号,人为指定)
;常数声明不占用汇编地址
SECTION mbr align=16 vstart=0x7c00 ;段mbr,16字节对齐,相对段起始偏移从0x7c00开始
;设置堆栈及用户程序被加载的段地址(用户程序被加载的物理地址人为指定,见本程序末phy_base)
xor ax,ax
mov ss,ax
mov sp,ax
mov ax,[cs:phy_base] ;用户程序被加载的物理起始地址的低16位
mov dx,[cs:phy_base+0x02] ;用户程序被加载的物理起始地址的高4位是数的低4位
mov bx,16
div bx ;除以16,即右移4位,得到用户程序被加载的段地址
mov ds,ax
mov es,ax
;先把用户程序头部从对应硬盘扇区(事先写入的指定扇区app_lba_start)加载到指定的内存区域
;读写硬盘需要通过DI:SI提供扇区号
xor di,di
mov si,app_lba_start
xor bx,bx ;提供偏移地址,用户程序加载到DS:0x0000
call read_hard_disk_0
;利用刚读入的用户程序头部,计算还剩下多少扇区,依次读取(用户程序是连续扇区存放的)
mov dx,[2]
mov ax,[0]
mov bx,512
div bx
cmp dx,0 ;如果除以512字节,余数如果是0,那么剩余扇区数是ax-1,否则就是ax
jnz @1
dec ax
@1:
cmp ax,0
jz direct ;剩余扇区数是0,就不用再读了,直接进入下面步骤
;剩余扇区数不是0,就把剩余扇区读进来
push ds
mov cx,ax
@2:
mov ax,ds
add ax,0x20 ;段地址加0x20,相当于物理内存地址增加512字节,保证存得下1个扇区
mov ds,ax
xor bx,bx
inc si ;前提得保证扇区号不会加到超过16位表示的数
call read_hard_disk_0
loop @2
pop ds
;更新被加载后的用户程序头部信息
direct:
mov dx,[0x08]
mov ax,[0x06]
call calc_segment_base
mov [0x06],ax ;更新用户程序入口代码段的段地址
mov cx,[0x0a] ;段重定位表项数
mov bx,0x0c ;段重定位表开始地址
realloc:
mov dx,[bx+0x02] ;高4位地址是dx的低4位
mov ax,[bx] ;低16位地址在ax中
call calc_segment_base
mov [bx],ax ;更新该重定位表项段的段地址
add bx,4
loop realloc
jmp far [0x04] ;远转移到用户程序入口地址(当前CS,IP压栈,然后CS=DS,IP=0x04)
;-------------------------------------------------------------------------------
read_hard_disk_0: ;从硬盘读取1个逻辑扇区(通过端口操作)
;一个逻辑扇区号(28位)需要四个端口(8位)来提供
;0x1f3~0x1f5各提供8位,0x1f6的后四位用来提供4位
;输入:DI:SI=逻辑扇区号
; DS:BX=目标缓冲区地址
push ax
push bx
push cx
push dx
mov dx,0x1f2
mov al,1
out dx,al ;往端口0x1f2写入al,表示要从硬盘读取al个扇区
inc dx
mov ax,si
out dx,al ;往端口0x1f3写入al,提供LBA的7~0位(si的低8位)
inc dx
mov al,ah
out dx,al ;往端口0x1f4写入ah,提供LBA的15~8位(si的高8位)
inc dx
mov ax,di
out dx,al ;往端口0x1f5写入al,提供LBA的23~16位(di的低8位)
inc dx ;端口0x1f6
mov al,0xe0
or al,ah ;0xe0与ah(di高8位)或运算,保证di的4位和0xe0高4位不变
out dx,al ;di的低4位提供LBA的27~24位,0xe0的高4位表示主盘及LBA模式
inc dx
mov al,0x20
out dx,al ;往端口0x1f7写入0x20,发送读命令
.waits:
in al,dx ;从端口0x1f7读状态信息
and al,0x88 ;硬盘系统准备好后,第7位是0,第3位是1,即0x08
cmp al,0x08
jnz .waits ;硬盘系统忙,准备中...
mov cx,256 ;总共要读取的字数(512字节对应256字)
mov dx,0x1f0 ;0x1f0是数据端口,端口号要用dx传输
.readw:
in ax,dx ;利用端口0x1f0每次从扇区SI:DI读一个字到ax中
mov [bx],ax
add bx,2
loop .readw
pop dx
pop cx
pop bx
pop ax
ret
;-------------------------------------------------------------------------------
calc_segment_base: ;计算16位段地址
;输入:DX:AX=32位物理地址(原始)
;返回:AX=16位段基地址(重定位之后)
push dx
add ax,[cs:phy_base]
adc dx,[cs:phy_base+0x02]
shr ax,4
ror dx,4
and dx,0xf000
or ax,dx
pop dx
ret
;-------------------------------------------------------------------------------
phy_base dd 0x10000 ;用户程序被加载到的物理起始地址
;20位地址,所以用双字存放存的下
times 510-($-$$) db 0
db 0x55,0xaa
;文件名:c08_02_user.asm
;文件说明:用户程序
;参考书:《x86汇编语言:从实模式到保护模式》李忠 著
;代码功能:把两段字符显示到屏幕,光标随着字符显示移动,支持滚屏
;-------------------------------------------------------------------------------
SECTION header vstart=0 ;定义用户程序头部段
program_length dd program_end ;程序总长度[0x00]
;用户程序入口点
code_entry:
dw start ;偏移地址[0x04]
dd section.code_1.start ;入口程序所在段的物理地址[0x06]
;段重定位表项个数[0x0a]
realloc_tbl_len dw (header_end-code_1_segment)/4
;段重定位表
code_1_segment dd section.code_1.start ;[0x0c]
code_2_segment dd section.code_2.start ;[0x10]
data_1_segment dd section.data_1.start ;[0x14]
data_2_segment dd section.data_2.start ;[0x18]
stack_segment dd section.stack.start ;[0x1c]
header_end:
;-------------------------------------------------------------------------------
SECTION code_1 align=16 vstart=0 ;定义代码段1(16字节对齐)
put_string: ;显示串(0结尾)到屏幕。
;输入:DS:BX=串地址
mov cl,[bx]
or cl,cl
jz .exit ;最后一个字符是0,是0就退出
call put_char
inc bx
jmp put_string
.exit:
ret
;-------------------------------------------------------------------------------
put_char: ;显示一个字符
;输入:cl=字符ascii
push ax
push bx
push cx
push dx
push ds
push es
;以下取当前光标位置
mov dx,0x3d4
mov al,0x0e
out dx,al ;向端口0x3d4表明要从寄存器0x0e得到光标高8位
mov dx,0x3d5
in al,dx ;从端口0x3d5读取高8位到al
mov ah,al ;并存放到ah中
mov dx,0x3d4
mov al,0x0f
out dx,al ;向端口0x3d4表明要从寄存器0x0f得到光标低8位
mov dx,0x3d5
in al,dx ;从端口0x3d5读取低8位到al
mov bx,ax ;BX=代表光标位置的16位数
cmp cl,0x0d ;回车符?
jnz .put_0a ;不是。看看是不是换行等字符
mov bl,80
div bl ;是回车符,除以80得到光标所在行号放入al中
mul bl ;行号乘以80得到当前行的行首位置
mov bx,ax
jmp .set_cursor
.put_0a:
cmp cl,0x0a ;换行符?
jnz .put_other ;不是,那就正常显示字符
add bx,80
jmp .roll_screen
.put_other: ;正常显示字符
mov ax,0xb800
mov es,ax
shl bx,1 ;光标位置乘2代表字符的偏移地址(1个字符占2字节)
mov [es:bx],cl
;以下将光标位置推进一个字符
shr bx,1
add bx,1
.roll_screen:
cmp bx,2000 ;光标超出屏幕?滚屏
jl .set_cursor
mov ax,0xb800
mov ds,ax
mov es,ax
cld
mov si,0xa0 ;整体上移一行(80字符160字节)
mov di,0x00
mov cx,1920
rep movsw
mov bx,3840 ;清除屏幕最底一行
mov cx,80
.cls:
mov word[es:bx],0x0720
add bx,2
loop .cls
mov bx,1920
.set_cursor: ;设置光标在屏幕上的显示位置
;输入:BX=光标位置
mov dx,0x3d4
mov al,0x0e
out dx,al
mov dx,0x3d5
mov al,bh
out dx,al
mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
mov al,bl
out dx,al
pop es
pop ds
pop dx
pop cx
pop bx
pop ax
ret
;-------------------------------------------------------------------------------
start:
;已经经过加载器代码加载并更新头部信息
mov ax,[stack_segment] ;设置用户程序自己的堆栈
mov ss,ax
mov sp,stack_end
mov ax,[data_1_segment] ;设置用户程序自己的数据段
mov ds,ax
mov bx,msg0
call put_string ;显示第一段信息
push word [es:code_2_segment] ;从代码段1跳到代码段2。。。。。。。。
;把代码段2的地址压入栈,retf后,弹栈,
;将用该地址设置CS:IP,从而跳至代码段2,
;这会导致代码段2执行完后回不到加载器,
;可以像记录代码段1的入口地址一样,记录
;代码段2的入口地址,代码段1执行完回到
;加载器,再转移到代码段2,代码段2执行
;完就能回到加载器了
mov ax,begin
push ax ;可以直接push begin,80386+
retf ;转移到代码段2执行
continue:
mov ax,[es:data_2_segment] ;段寄存器DS切换到数据段2
mov ds,ax
mov bx,msg1
call put_string ;显示第二段信息
jmp $
;-------------------------------------------------------------------------------
SECTION code_2 align=16 vstart=0 ;定义代码段2(16字节对齐)
begin:
push word [es:code_1_segment]
mov ax,continue
push ax ;可以直接push continue,80386+
retf ;转移到代码段1接着执行
;-------------------------------------------------------------------------------
SECTION data_1 align=16 vstart=0
msg0: ;0x0d,0x0a回车换行标志。0是字符串结束标志
db ' This is NASM - the famous Netwide Assembler. '
db 'Back at SourceForge and in intensive development! '
db 'Get the current versions from http://www.nasm.us/.'
db 0x0d,0x0a,0x0d,0x0a
db ' Example code for calculate 1+2+...+1000:',0x0d,0x0a,0x0d,0x0a
db ' xor dx,dx',0x0d,0x0a
db ' xor ax,ax',0x0d,0x0a
db ' xor cx,cx',0x0d,0x0a
db ' @@:',0x0d,0x0a
db ' inc cx',0x0d,0x0a
db ' add ax,cx',0x0d,0x0a
db ' adc dx,0',0x0d,0x0a
db ' inc cx',0x0d,0x0a
db ' cmp cx,1000',0x0d,0x0a
db ' jle @@',0x0d,0x0a
db ' ... ...(Some other codes)',0x0d,0x0a,0x0d,0x0a
db 0
;-------------------------------------------------------------------------------
SECTION data_2 align=16 vstart=0
msg1:
db ' The above contents is written by LeeChung. '
db '2011-05-06'
db 0
;-------------------------------------------------------------------------------
SECTION stack align=16 vstart=0
resb 256 ;未初始化哦!有可能是历史遗留数据
stack_end:
;-------------------------------------------------------------------------------
SECTION trail align=16
program_end:
把编译后的mbr代码写入0扇区,把编译后的user代码写入100扇区,之后运行虚拟机结果:
参考资料
[1] 《x86汇编语言:从实模式到保护模式》李忠 著
[2] 《汇编语言》王爽 著