第八章 硬盘和显卡的访问与控制
mbr加载、重定位用户程序
PART 1 >> VirtualBox显示最终效果
===================================================================================================
PART 2 >> 部分说明
1、应用程序的头部
加载器和用户程序之间需要一个协定,比如说,用户程序的一些基本的结构信息必须放在某个固定位置,加载器就可以从这个固定位置读取。经验表明,把这个约定的地方放在用户程序的起始位置,对双方,特别是对加载器来说比较方便,这就是用户程序的头部。
(1)用户程序的大小,以字节为单位。加载器需要根据这个信息来决定读取多少个逻辑扇区。
(所有程序在硬盘上所占用的逻辑扇区不一定是连续的 ?)
(2)应用程序入口点(Entry Point),包括段地址和偏移地址。即,第一条要执行的指令的段地址和偏移地址。
(3)段重定位表。程序通常会包含多个代码段和数据段。加载器要根据程序加载到内存的实际位置,来重新计算每个段的地址,即重定位。
2、内存空间范围
物理地址0x0 FFFF以下,是加载器及其栈的范围;
0xA 0000以上,是BIOS和外围设备的范围,有很多传统的老式设备将自己的存储器和只读存储器映射到这段空间。
===================================================================================================
PART 3 >> 源代码
3.1 c08_mbr.asm
mbr程序
; FILE: c08_mbr.asm
; DATE: 20191211
; jmp near mbr
app_lba_begin equ 100 ; 将配套的的用户程序从磁盘lba扇区100开始写入
; 若从硬盘启动,BIOS读取主引导扇区并加载到内存0x0000:0x7c00,即0x07c00
; 即,主引导程序mbr的实际加载地址是0x0000:0x7c00
; vstart, 指定段内汇编地址的开始点, 段内所有元素的汇编地址都将从0x7c00开始计算
SECTION mbr align=16 vstart=0x7c00
; 设置堆栈段ss和段指针
mov ax, 0
mov ss, ax
mov sp, ax
; 设置数据段ds和es
; 计算得到加载用户程序的逻辑段地址
; 本程序中,数据段和代码段是分离的,而且代码段定义部分使用了"vstart=0x7c00"
mov ax, [cs:phy_base] ; 从标号phy_base的位置读取
mov dx, [cs:phy_base + 2]
mov bx, 16
div bx ; 右移4位, 即0x1000
mov ds, ax
mov es, ax
; 读取硬盘中用户程序所在的第1个扇区
; 包含了头部信息:程序大小、入口点、段重定位表
xor di, di
mov si, app_lba_begin ; 程序在硬盘上的逻辑扇区号
xor bx, bx ; 加载到ds:0x0000处
call read_hard_disk_0
; 根据用户程序的头部信息,判断整个程序的大小
mov dx, [2]
mov ax, [0]
mov bx, 512
div bx
cmp dx, 0
jnz @1
dec ax ; 余数dx为0则商ax减1,已读取一个扇区
@1:
cmp ax, 0
jz direct ; 实际长度小于512字节,则已读取完
; 读取剩余的扇区
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] ; 用户程序头部内偏移为0x06处的双字,存放着入口点代码段地址
call calc_segment_base
mov [0x06], ax ; 回填修正后的入口点代码段地址
; 开始处理用户程序的段重定位表
mov cx, [0x0a] ; 用户程序头部中,存放的重定位表项个数
mov bx, 0x0c ; 重定位表首地址
realloc:
mov dx, [bx+2]
mov ax, [bx]
call calc_segment_base
mov [bx], ax ; 回填修正后的段基址
add bx, 4 ; 下一个重定位表项
loop realloc
jmp far [0x04] ; 转移到用户程序
; ===============================================================================
; Function: 读取主硬盘的1个扇区
; Input: 1) di:si 起始逻辑扇区号 2) ds:bx 目标缓冲区地址
read_hard_disk_0:
push ax
push bx
push cx
push dx
; 1) 设置要读取的扇区数
; ==========================
; 向0x1f2端口写入要读取的扇区数。每读取一个扇区,数值会减1;
; 若读写过程中发生错误,该端口包含着尚未读取的扇区数
mov dx, 0x1f2 ; 0x1f2为8位端口
mov al, 1 ; 1个扇区
out dx, al
; 2) 设置起始扇区号
; ===========================
; 扇区的读写是连续的。这里采用早期的LBA28逻辑扇区编址方法,
; 28个比特表示逻辑扇区号,每个扇区512字节,所以LBA25可管理128G的硬盘
; 28位的扇区号分成4段,分别写入端口0x1f3 0x1f4 0x1f5 0x1f6,都是8位端口
inc dx ; 0x1f3
mov ax, si
out dx, al
inc dx ; 0x1f4
mov al, ah
out dx, al ; in和out 操作寄存器只能是al或者ax
inc dx ; 0x1f5
mov ax, di
out dx, al
; 8bits端口0x1f6,低4位存放28位逻辑扇区号的24~27位;
; 第4位指示硬盘号,0为主盘,1为从盘;高3位,111表示LBA模式
inc dx ; 0x1f6
mov al, 0xe0 ; al 高4位设为 1110
or al, ah ; al 低4位设为 LBA的的高4位
out dx, al
; 3) 请求读硬盘
; ==========================
; 向端口写入0x20,请求硬盘读
inc dx ; 0x1f7
mov al, 0x20
out dx, al
.wait:
; 4) 等待硬盘读写操作完成
; ===========================
; 端口0x1f7既是命令端口,又是状态端口
; 通过这个端口发送读写命令之后,硬盘就忙乎开了。
; 0x1f7端口第7位,1为忙,0忙完了同时将第3位置1表示准备好了,
; 即0x08时,主机可以发送或接收数据
in al, dx ; 0x1f7
and al, 0x88 ; 取第8位和第3位
cmp al, 0x08
jnz .wait
; 5) 连续取出数据
; ============================
; 0x1f0是硬盘接口的数据端口,16bits
mov cx, 256 ; loop循环次数,每次读取2bytes
mov dx, 0x1f0 ; 0x1f0
.readw:
in ax, dx
mov [bx], ax
add bx, 2
loop .readw
pop dx
pop cx
pop bx
pop ax
ret
; ===============================================================================
; Function: 计算段地址
; Input: 1) dx:ax 32位物理地址
; Output: 1) ax 16位段地址
calc_segment_base:
push dx
add ax, [cs:phy_base] ; 标号phy_base处存放着的0x1 0000
adc dx, [cs:phy_base + 2]
shr ax, 4
ror dx, 4
and dx, 0xf000
or ax, dx
pop dx
ret
; ; 直接除以16可以吗 ???
; push bx
; push dx
; add ax, [cs:phy_base] ; 标号phy_base处存放着的0x1 0000
; adc dx, [cs:phy_base + 2]
; mov bx, 16
; div bx
; pop dx
; pop bx
; ret
; 物理内存分布
; 0x0 0000 ~ 0x0 FFFF, 加载器及其栈
; 0xA 0000 ~ 0xF FFFF, BIOS和外围设备的映射空间
; 0x1 0000 ~ 0x9 FFFF, 可用用户空间,约500KB
phy_base dd 0x10000 ; 设定一个程序加载的起始内存地址0x1 0000,标号phy_base
times 510-($-$$) db 0
db 0x55, 0xaa
3.2 c08.asm
用户程序
; FILE: c08.asm
; DATE: 20191211
; ===============================================================================
SECTION head vstart=0 ; 定义用户程序头部段
; 用户程序可能很大,16位可能不够
program_length dd program_end ; 程序总长度[0x00]
; 程序入口点(Entry Point)
program_entry dw beginning ; 偏移地址[0x04]
; 只是编译阶段确定的汇编地址。程序加载到内存后,需要根据加载的实际位置重新计算
; 尽管在16位的环境中,一个段最长为64KB,但它却可以起始于任何20位的物理地址处。
; 不可能用16位来保存20位的地址,所以需要32位
dd section.code_1.start ; 汇编地址[0x06]
realloc_tbl_size dw (head_end-segment_code_1)/4 ; 段重定位表项个数[0x0a]
segment_code_1 dd section.code_1.start ; [0x0c]
segment_code_2 dd section.code_2.start ; [0x10]
segment_data_1 dd section.data_1.start ; [0x14]
segment_data_2 dd section.data_2.start ; [0x18]
segment_stack dd section.stack.start ; [0x1c] ; 这里section 和 start 不能用大写 ???
head_end:
; ===============================================================================
SECTION code_1 align=16 vstart=0
beginning:
; 设置用户程序自己的堆栈段
; ds和es依然指向着用户程序头部head段
mov ax, [segment_stack]
mov ss, ax
mov sp, stack_end
; 设置用户程序自己的数据段
; 如果先初始化数据段ds和附加段es,那么头部head段中的数据将无法访问
mov ax, [segment_data_1]
mov ds, ax
mov bx, msg0
call show_string
; 巧用retf跳转到code_2的begin
push word [es:segment_code_2] ; ds指向了用户程序自己的数据段
mov ax, begin
push ax
retf ; retf相当于"pop ip" "pop cs"
continue:
mov ax, [es:segment_data_2]
mov ds, ax ; ds指向数据段2
mov bx, msg1
call show_string ; 显示第2段文本
jmp $
; Function: 频幕上显示文本
; Input: ds:bx 字符串起始地址,以0结尾
show_string:
mov cl, [bx]
or cl, cl
jz .exit
call show_char
inc bx
jmp show_string
.exit:
ret
; Function:
; Input: cl 字符
show_char:
push ax
push bx
push cx
push dx
push ds
push es
; 读取当前光标位置
; 索引寄存器端口0x3d4,其索引值14(0x0e)和15(0x0f)分别用于提供光标位置的高和低8位
; 数据端口0x3d5
mov dx, 0x3d4
mov al, 0x0e
out dx, al
mov dx, 0x3d5
in al, dx
mov ah, al
mov dx, 0x3d4
mov al, 0x0f
out dx, al
mov dx, 0x3d5
in al, dx
mov bx, ax ; 此处用bx存放光标位置的16位数
; 判断是否为回车符0x0d
cmp cl, 0x0d ; 0x0d 为回车符
jnz .show_0a ; 不是回车符0x0d,再判断是否换行符0x0a
mov ax, bx ; 是回车符,则将光标置位到行首
mov bl, 80
div bl
mul bl
mov bx, ax
jmp .set_cursor
; ; 将光标位置移到行首,可以直接减去当前行吗??
; mov ax, bx
; mov dl, 80
; div dl
; sub bx, ah
; jmp .set_cursor
; 判断是否为换行符0x0a
.show_0a:
cmp cl, 0x0a ; 0x0a 为换行符
jnz .show_normal; 不是换行符,则正常显示字符
add bx, 80 ; 是换行符,再判断是否需要滚屏
jmp .roll_screen
; 正常显示字符
; 在写入其它内容之前,显存里全是黑底白字的空白字符0x0720,所以可以不重写黑底白字的属性
.show_normal:
mov ax, 0xb800 ; 显存映射在 0xb8000~0xbffff
mov es, ax
shl bx, 1 ; 光标指示字符位置,显存中一个字符占2字节,光标位置乘2得到该字符在显存中得偏移地址
mov [es:bx], cl
shr bx, 1 ; 恢复bx
inc bx ; 将光标推进到下一个位置
; 判断是否需要向上滚动一行屏幕
.roll_screen:
cmp bx, 2000 ; 25行x80列
jl .set_cursor
mov ax, 0xb800
mov ds, ax ; movsw的源地址ds:si
mov es, ax ; movsw的目的地址es:di
mov si, 0xa0
mov di, 0
cld ; 传送方向cls std
mov cx, 1920 ; rep次数 24行*每行80个字符*每个字符加显示属性占2字节 / 一个字为2字节
rep movsw
; 清除屏幕最底一行,即写入黑底白字的空白字符0x0720
mov bx, 3840 ; 24行*每行80个字符*每个字符加显示属性占2字节
mov cx, 80
.cls:
mov word [es:bx], 0x0720
add bx, 2
loop .cls
mov bx, 1920 ; 重置光标位置为最底一行行首
; 根据bx重置光标位置
; 索引寄存器端口0x3d4,其索引值14(0x0e)和15(0x0f)分别用于提供光标位置的高和低8位
; 数据端口0x3d5
.set_cursor:
mov dx, 0x3d4
mov al, 0x0e
out dx, al
mov dx, 0x3d5
mov al, bh ; in和out 只能用al或者ax
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
;===============================================================================
SECTION code_2 align=16 vstart=0
; 巧用retf跳转到code_1的continue
begin:
push word [es:segment_code_1]
mov ax, continue
push ax
retf ; retf相当于"pop ip" "pop cs"
;===============================================================================
SECTION data_1 align=16 vstart=0
msg0 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 BigBao. '
db '2019-12-14'
db 0
; ===============================================================================
SECTION stack align=16 vstart=0
resb 256
stack_end:
; ===============================================================================
SECTION tail align=16 ; 这里用于计算程序大小,不需要vstart=0
program_end: