[书]x86汇编语言:从实模式到保护模式 -- 第八章 硬盘和显卡的访问与控制,mbr加载并重定位应用程序

第八章 硬盘和显卡的访问与控制

          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:

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值