跳转至

硬盘和显卡的访问与控制

硬盘和显卡的访问与控制

编译好的程序放在硬盘上,加载到内存才能执行,接下来把主引导扇区代码改为一个加载器,用来加载硬盘上的程序并将处理器的控制权交给该程序

用户程序代码

NASM 编译器使用 SECTION 或 SEGMENT 来设置分段,align=16 表示是 16 字节对齐的

用户程序涉及到在屏幕上显示内容,需要用到显卡,显卡的操作很复杂很多寄存器只能通过索引寄存器访问(端口号 0x3d4)通过向他写入一个值来指定一个内部寄存器,比如两个8位的光标寄存器(0xe 和 0xf)他俩分别提供光标位置的高八位和第八位

         ;代码清单8-2
         ;文件名:c08.asm
         ;文件说明:用户程序 
         ;创建日期:2011-5-5 18:17

;===============================================================================
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]                     ;从DS:BX取字符
         or cl,cl                        ;判断是不是/x00
         jz .exit                        ;是0,返回主程序 
         call put_char                   ;否则调用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                    ;索引寄存器端口号0x3d4
         mov al,0x0e                     ;通过索引寄存器告诉显卡要操作0x0e寄存器
         out dx,al                       ;
         mov dx,0x3d5                    ;从数据端口读
         in al,dx                        ;得到高8位读到了al寄存器 
         mov ah,al                       ;光标位置的高八位放在ah寄存器

         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                       ;每行80个字符                       
         div bl                          ;当前光标位置除以80可以得到当前行号
         mul bl                          ;在乘以80得到当前行行首的光标数值,与ax相乘,放在DX:AX
         mov bx,ax                       ;放在ax中
         jmp .set_cursor                 ;跳转到设置光标的代码段

 .put_0a:
         cmp cl,0x0a                     ;比较一下是不是换行符
         jnz .put_other                  ;不是,那就正常显示字符 
         add bx,80                       ;是的话直接设置光标数值加80
         jmp .roll_screen                ;跳转到滚屏

 .put_other:                             ;正常显示字符
         mov ax,0xb800                   
         mov es,ax                       ;设置显存地址
         shl bx,1                        ;光标位置占用了1个字符(2个字节)的位置,左移1位相当于乘以2
         mov [es:bx],cl                  ;移动到显示器上

         ;以下将光标位置推进一个字符
         shr bx,1                        ;右移一位还原回正常的光标位置
         add bx,1                        ;写完之后光标向后移动一位

 .roll_screen:
         cmp bx,2000                     ;比较是不是小于2000光标,是否超出屏幕
         jl .set_cursor                  ;是的话没有超出光标直接移动光标就行了

         mov ax,0xb800                   ;设置显存地址
         mov ds,ax
         mov es,ax
         cld                             ;设置位零,从低地址往高地址复制
         mov si,0xa0                     ;源地址的偏移地址
         mov di,0x00                     ;目的地址的偏移地址
         mov cx,1920                     ;要传送的单位,24*80
         rep movsw                       ;循环传送
         mov bx,3840                     ;第25行第1列在显存中的偏移地址是3840
         mov cx,80                       ;设置循环修次数
 .cls:
         mov word[es:bx],0x0720          ;往偏移地址写的内容是0x0720表示空格,这样就有一行空的
         add bx,2
         loop .cls

         mov bx,1920                     ;最后把光标位置换回来

 .set_cursor:                            ;设置光标
         mov dx,0x3d4                    ;索引寄存器端口号
         mov al,0x0e                     
         out dx,al                       ;通过索引寄存器告诉显卡要操作0x0e寄存器
         mov dx,0x3d5                    
         mov al,bh                       ;bx寄存器是目前光标位置,bh是高八位
         out dx,al                       
         mov dx,0x3d4
         mov al,0x0f
         out dx,al                       ;通过索引寄存器告诉显卡要操作0x0e寄存器
         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:

section.header.start 表示 header 相对于整个程序开头的汇编地址

section.code.start 表示 code 相对于整个程序开头的汇编地址

其中,段声明语句中的 vstart 表示该段里面的标号是从该段开始计算的,比如 code_2 的 begin 是相对于 code_2 开头的汇编地址

主引导扇区代码(加载器)

加载器需要从硬盘中读取程序代码,处理器通过端口与外围设备交流,这些端口就是位于I/O接口电路中的一些寄存器,每个I/O接口可能有好多个端口,连接硬盘的PATA/SATA接口有个命令端口,当向该端口写入 0x20 时表示从硬盘读取数据;写入 0x30 时表示向硬盘写入数据,体现在汇编代码中 in 指令表示从端口读,out 表示从端口向外围设备发送数据,源操作数是 dx,目的操作数是 ax 或 al

主硬盘接口分配的端口号是 0x1f0~0x1f7,副硬盘接口分配的端口号是 0x170~0x177

硬盘端口详解 - 思维导图

想要从硬盘上读逻辑扇区

首先要设置读取扇区的数量,写在 0x1f2,如果写入的是 0 表示读 256 个,每读一个扇区 0x1f2 的值就减 1,对应 87-89 行代码

然后设置起始 LBA 扇区号,从 1f3 到 1f5 分别存放 LBA 参数的 0-7、8-15、16-23,1f6 比较特别,高三位是 111,第四位 0 表示主盘 1 表示从盘,第三位是 LBA 参数的 24-27,对应 91-106 行代码

然后向 0x1f7 写入 0x20 请求读硬盘,硬盘从0x1f7端口收到0x20后将第七位置为1进行准备操作,准备结束后再将第七位置为 0,第三位置为 1,所以使用 0x88 进行与运算,看看是不是准备好了,对应 113-116 行

0x20   0010 0000
0x88   1000 1000
----------------------and
0x00   0000 0000

0xa0   1010 0000
0x88   1000 1000
----------------------and
0x80   1000 0000

0x28   0010 1000
0x88   1000 1000
----------------------and
0x08   0000 1000

接下来开始读数据,0x1f0 是硬盘接口的数据端口,使用 in 进行读取每次读取一字,对应代码 118-124 行

         ;代码清单8-1
         ;文件名:c08_mbr.asm
         ;文件说明:硬盘主引导扇区代码(加载程序) 
         ;创建日期:2011-5-5 18:17

         app_lba_start equ 100           ;声明常数(用户程序起始逻辑扇区号,这个是烧录程序时自己决定的,后面我们烧录在第100号扇区了,所以就得写100)
                                         ;常数的声明不会占用汇编地址

SECTION mbr align=16 vstart=0x7c00       ;段内所有汇编地址都从0x7c00开始计算                                  

         ;初始化栈段寄存器和栈指针 
         mov ax,0      
         mov ss,ax
         mov sp,ax

         mov ax,[cs:phy_base]            ;加载phy_base,用户程序的逻辑段地址
         mov dx,[cs:phy_base+0x02]
         mov bx,16        
         div bx                          ;dx:ax的值即0x00010000除以16,相当于右移4位得到用户程序的段地址
         mov ds,ax                       ;令DS和ES指向该段以进行操作
         mov es,ax                        

         ;读取程序的起始部分 
         xor di,di
         mov si,app_lba_start            ;程序在硬盘上的起始逻辑扇区号,烧录时定义的
         xor bx,bx                       ;加载到DS:0x0000处
         call read_hard_disk_0           ;调用read_hard_disk_0,同时将下一条指令的地址压到栈上

         ;以下判断整个程序有多大
         mov dx,[2]                      ;ds:2读取程序的大小即program_end(高16位)
         mov ax,[0]                      ;读取程序的大小低16位
         mov bx,512                      ;每扇区512字节
         div bx                          ;做个除法,看一下需要读取多少扇区
         cmp dx,0                        ;如果有余数表示有不够1扇区的内容
         jnz @1                          ;跳到@1继续读取
         dec ax                          ;已经读了一个扇区,扇区总数减1 
   @1:                                   ;当余数不为0时
         cmp ax,0                        ;若ax值为0表示读取完了
         jz direct                       ;跳转到direct

         ;读取剩余的扇区
         push ds                         ;保存当前ds的值,构造一个新的段用来读取一个扇区的数据

         mov cx,ax                       ;循环次数(剩余扇区数)
   @2:                                   
         mov ax,ds
         add ax,0x20                     ;段地址加上0x20,到时候*0x10就相当于以下一个扇区为段地址
         mov ds,ax                       ;设置段地址 

         xor bx,bx                       ;偏移地址始终为0x0000 
         inc si                          ;逻辑扇区号+1,表示读取下一个逻辑扇区 
         call read_hard_disk_0           ;调用读取的函数
         loop @2                         ;循环读,直到读完整个功能程序 

         pop ds                          ;用户程序段地址恢复到寄存器

         ;计算入口点代码段基址 
   direct:                               
         mov dx,[0x08]                   ;获取用户程序header记录的程序入口偏移地址
         mov ax,[0x06]                   ;dd双字,需要用两个寄存器存放
         call calc_segment_base          ;调用calc_segment_base
         mov [0x06],ax                   ;ax作为calc_segment_base的返回值,存放着入口点代码段基址

         ;开始处理段重定位表
         mov cx,[0x0a]                   ;需要重定位的项目数量
         mov bx,0x0c                     ;重定位表首地址

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

         jmp far [0x04]                  ;转移到用户程序的code_entry记录的start这里  

;-------------------------------------------------------------------------------
read_hard_disk_0:                        ;从硬盘读取一个逻辑扇区
                                         ;输入:DI:SI=起始逻辑扇区号
                                         ;      DS:BX=目标缓冲区地址
         push ax                         ;保存变量
         push bx
         push cx
         push dx

         mov dx,0x1f2                    ;向0x1f2端口写入要读取的扇区数
         mov al,1                        ;1表示读取1个,0表示读取全部256个
         out dx,al                       ;使用out向端口写数据

         inc dx                          ;dx加一,0x1f3,设置起始LBA扇区号,一共28位
         mov ax,si                       ;si是程序在硬盘上的起始逻辑扇区号100 
         out dx,al                       ;写入LBA地址7~0

         inc dx                          ;0x1f4
         mov al,ah                       ;写入0x00
         out dx,al                       ;LBA地址15~8

         inc dx                          ;0x1f5
         mov ax,di                       ;写入0x00n
         out dx,al                       ;LBA地址23~16

         inc dx                          ;0x1f6
         mov al,0xe0                     ;LBA28模式,主盘
         or al,ah                        ;这里如果ah低四位中存放的是LBA地址27~24才有用
         out dx,al                       ;LBA地址27~24

         inc dx                          ;0x1f7,设置从硬盘读取
         mov al,0x20                     ;0x20表示读命令
         out dx,al                       ;向端口写入

  .waits:
         in al,dx                        ;硬盘从0x1f7端口收到0x20后将第七位置为1进行准备
         and al,0x88                     ;准备结束后再将第七位置为0,第三位置为1,用0x88与一下
         cmp al,0x08                     ;看看是不是第七位为0,第三位为1
         jnz .waits                      ;不忙,且硬盘已准备好数据传输 

         mov cx,256                      ;总共要读取256字(一个扇区512字节),用作循环计数器
         mov dx,0x1f0                    ;设置端口
  .readw:
         in ax,dx                        ;从端口读一个字
         mov [bx],ax                     ;写到ds:bx
         add bx,2                        ;读的是字,两个字节,加2
         loop .readw                     ;循环读,一直到cx位0,即读完一个扇区

         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]            ;先加低16位
         adc dx,[cs:phy_base+0x02]       ;现在dx:ax是代码段起始地址,adc表示带进位假发  
         shr ax,4                        ;逻辑右移,移出来的位放在CF寄存器,左边补零
         ror dx,4                        ;循环右移,移出来的位既放在CF也补到左边的空位
         and dx,0xf000                   ;只留dx的高四位,从低四位补过来的(为啥不直接and 0xf呢?)
         or ax,dx                        ;返回结果放在ax中

         pop dx                          ;还原寄存器的值

         ret

;-------------------------------------------------------------------------------
         phy_base dd 0x10000             ;用户程序被加载的物理起始地址,低四位必须为0要16字节对齐

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

原文: https://www.yuque.com/hxfqg9/bin/we4onx