《x86汇编语言:从实模式到保护模式》08代码-mbr加载用户代码

;启动扇区加载用户程序

app_lba_start equ 100               ;一会会将程序放在硬盘的100扇区

SECTION mbr align=16 vstart=0x7c00      ;mbr段声明,16字节对齐,初始地址7c00

;设置堆栈段和栈指针
mov ax,0
mov ss,ax
mov sp,ax

mov ax,[cs:phy_base]                ;phy_base保存的是一个32位地址,所以需要用两个寄存器存放
mov dx,[cs:phy_base+0x02]
mov bx,16
div bx
mov ds,ax                           ;十六进制左移1位,实际上是除以了16
mov es,ax                           ;将ds和es都设置成计算出的phy_base的段地址

xor di,di
mov si,app_lba_start                ;程序在硬盘上的第一个逻辑扇区号
xor bx,bx                           ;bx清零
call read_hard_disk_0               ;加载程序到DS:0x0000处

;以下判断整个程序有多大
mov dx,[2]                          ;ds:0x0002处保存的值,是用户程序声明的用户程序大小
mov ax,[0]                          ;ds:0x0000处保存的值,这时ds依然是定义的phy_base段地址
mov bx,512                          ;512每个扇区
div bx                              ;看看占了几个扇区
cmp dx,0                            ;除尽了没有
jnz @1                              ;没除尽,跳到@1
dec ax                              ;除尽了,余数ax减一,因为已经预读了一个扇区
@1:
cmp ax,0                            ;有可能长度本来就不够一个扇区,这时ax为0,所以用jz
jz direct                           ;跳到读完操作

;读取剩余扇区
push ds                             ;保存ds,因为程序传参要用ds

mov cx,ax                           ;循环次数(剩余扇区数)
@2:
mov ax,ds
add ax,0x20                         ;如果容纳不下,那就加个0x20的段间距,这样多了0x200=512
mov ds,ax

xor bx,bx
inc si                              ;下一个逻辑扇区
call read_hard_disk_0
loop @2

pop ds                              ;恢复数据段基址ds到用户程序头部段

;计算入口点代码段基址
direct:
mov dx,[0x08]                       ;此时为何是0x06和0x08?因为取的是user的section.code_1.start
mov ax,[0x06]                       ;这是一个双字地址,所以dx存高位ax存低位
call calc_segment_base
mov [0x06],ax                       ;将真正的用户代码入口的基地址写回到用户头中

;开始处理段重定位表
mov cx,[0x0a]                       ;需要重定位的个数,这里用户头部的0x0a是段重定位表项数
mov bx,0x0c                         ;把基地址定在0x0c,这里是段重定位表格的开头地址

realloc:
mov dx,[bx+0x02]                    ;32位地址的高16位
mov ax,[bx]                         ;32位地址的低16位
call calc_segment_base              ;重定位段
mov [bx],ax                         ;写回重定位后的地址
add bx,4                            ;下一个重定位项(每项4字节)
loop realloc

jmp far [0x04]                      ;跳转到用户程序

;---------------------------------------------
read_hard_disk_0:                      ;从硬盘读取一个逻辑扇区

push ax                             ;输入:DI:SI=起始逻辑扇区号
push bx                             ;DS:BX=目标缓冲区地址
push cx
push dx

mov dx,0x1f2                        ;0x1f2是保存要读取的扇区数量的端口
mov al,1
out dx,al

inc dx                              ;0x1f3
mov ax,si                           ;si传参100,0x1f3存储0-7位
out dx,al                           ;表示要读写的扇区

inc dx
mov al,ah                           ;0x1f4
out dx,al                           ;LBA地址 15-8

inc dx                              ;0x1f5
mov ax,di                           ;LBA地址23-16
out dx,al                           ;di传参为0

inc dx                              ;0x1f6
mov al,0xe0                         ;LBA28模式,主盘
or al,ah                            ;LBA地址27-24
out dx,al                           ;这里用or是因为LBA28模式设定是前四位,后四位还是扇区

inc dx                              ;0x1f7
mov al,0x20                         ;读命令
out dx,al

.waits:
in al,dx                            ;0x1f7
and al,0x88                         ;第8位表示硬盘忙,第4位表示已做好准备传输10001000
cmp al,0x08                         ;00001000表示可以传输了
jnz .waits                          ;硬盘是否不繁忙且已经准备好传输数据,否则继续等待

mov cx,256                          ;读256次
mov dx,0x1f0                        ;0x1f0是硬盘接口的数据接口

.readw:
in ax,dx                            ;读数据
mov [bx],ax                         ;将数据写到ds:bx所在的位置
add bx,2                            ;偏移两个读写下一个数据
loop .readw

pop dx
pop cx
pop bx
pop ax

ret

;--------------------------------------------
calc_segment_base:                      ;计算16位段地址
;输入:DX:AX=32位物理地址,dx=0x08 ax=0x06
;返回:ax=16位段基地址
push dx

add ax,[cs:phy_base]                ;取用户程序基址+06,也就是用户程序定义的段地址
adc dx,[cs:phy_base+0x02]           ;adc带进位的加法,基址+08,也就是用户程序定义的,这里取高位
shr ax,4                            ;0x0000,右移四位,ax存储低16位,dx高4位,总共20位地址
ror dx,4                            ;0x0001,循环右移四位 0x1000
and dx,0xf000                       ;低12位我们不需要,and取高四位
or ax,dx                            ;or得到结果0x1000

pop dx                              ;恢复现场,ax为返回值

ret

;----------------------------------------------
phy_base dd 0x10000                 ;用户程序被加载的物理起始地址

times 510-($-$$) db 0
db 0x55,0xaa

用户代码:

;===============================================================================
SECTION header vstart=0                     ;定义用户程序头部段
program_length  dd program_end          ;程序总长度[0x00]

;用户程序入口点
code_entry      dw start                ;偏移地址[0x04]
dd section.code_1.start ;段地址[0x06]

realloc_tbl_len dw (header_end-code_1_segment)/4
;段重定位表项个数[0x0a]

;段重定位表
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                        ;cl=0 ?
jz .exit                        ;是的,返回主程序
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
mov dx,0x3d5
in al,dx                        ;高8位
mov ah,al

mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
in al,dx                        ;低8位
mov bx,ax                       ;BX=代表光标位置的16位数

cmp cl,0x0d                     ;回车符?
jnz .put_0a                     ;不是。看看是不是换行等字符
mov ax,bx                       ;此句略显多余,但去掉后还得改书,麻烦
mov bl,80
div bl
mul bl
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
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
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:
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:
;初始执行时,DS和ES指向用户程序头部段
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]
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 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: