《操作系统真象还原》MBR

元气小坏坏 提交于 2019-12-04 12:04:14

  以下是读本书第三章的收获。


  如何知道一个源程序的各符号(指令和变量)地址?简单来说,地址就是该符号偏移文件开头的距离,符号的地址是按顺序编排的,所以两个相邻的符号,其地址也是相邻的。对于指令来说,指令的地址=上一个指令的地址+上一个指令的大小,最初的符号地址为0,可以根据此公式推算出所有符号的地址。

      section称为节,它是提供给程序员编排程序用的,我们可以将一段读取字符串的代码放在section A下,将读取硬盘的代码放进section B下,可以给A,B换成一个更具体的名字,来提高可读性。        例如,下图这段代码,将整个程序分成section code和section data两节,顾名思义,就是存放代码和数据的两个section,这样我们就很清楚地知道每部分代码是做什么用的。另一个值得注意的细节是section并不会对符号的编址用什么影响,去掉section和不去掉其实符号的地址都是一样的。

 

 

     vstart用于告诉编译器,之后的符号都以某个地址为初始地址来编址。如下图,像$$的地址替换成以0x7c00为初始地址的地址,符号var1和var2的地址被替换成以0x900的地址。

     当然,我们还可以通过section.节名称.start来获得在文件中真正的地址。如section.code.start值为0x0,即section code偏移文件的距离为0。那么什么时候用vstart来修饰section呢,其实只要我们保证将改源码加载到内存后,符号的地址能够指向正确的位置即可。

 

 

 

mov ax, message
message db "1MBR"

  假设我们要将这段源代码加载到0x7c00后,我们会发现message是以地址0为初始地址编址的符号,即以文件开头编址的符号,这时候我们ax得到的不是真正的message的位置。此时我们需要增加一个vstart来修饰这段代码,message才能真正地以0x7c00地址为初始地址编址,即:

SECTION MBR vstart=0x7c00
mov ax, message
message db "1MBR"

访问外设

  CPU要管理或者访问外设也不是一件容易的事情,毕竟外设多种多样,并没有一个统一地功能设计,要CPU去兼容所有外设的功能,和实现不一样的IO操作指令其实是不太现实的。所以设计者在CPU和外设之间加多了一层,即IO接口。IO接口是帮助CPU去管理外设的,不同的外设会有不一样的IO接口。IO接口可以增加缓存来缓解CPU与外设访问速度上的不匹配,还可以做数据格式转换,电平转换等等,总之,是个帮助CPU访问外设的助手。

     那么CPU是如何通过IO接口来访问外设的呢?答案就是通过IO接口上的寄存器,也称为端口。通过对不同的端口赋值,我们就可以获取外设的各种信息,如硬件是否加载好,硬盘的数据是否已经准备好等等。那么我们又是如何访问端口的呢,有两种方式,第一种是通过内存映射,将端口映射到相应的内存地址上,这样我们访问这地址就相当于访问了这个端口,像之前的ROM也是这样做的。第二种是对端口进行编码得到一套独立的端口号编址,通过in,out指令我们就可以对端口进行输入和输出,从而得到外设的信息。

   下面我们就通过这两种方式来对硬件进行IO操作。

对显存操作

   

 

 

   由上图可以看到,0xB8000-0xBFFFF这段内存区域是是映射到显存里面的,即采用内存映射方式来对显存进行操作;只要我们向内存的这块区域写进文本,显存相应位置的值就会更新,屏幕上也就会显示相应的文本了。

   下面代码将段基址寄存器gs初始化为0xB8000,定位到显存这块区域。然后下面加粗的代码直接将相应的文本数据填进显存里面。这样,屏幕上就会显示‘1 MBR’了。

SECTION MBR vstart=0x7c00
  mov ax,cs
  mov ds,ax
  mov es,ax
  mov ss,ax
  mov fs,ax
  mov sp,0x7c00
  mov ax,0xb800
  mov gs,ax

  mov ax,0x600
  mov bx,0x700
  mov cx,0
  mov dx,0x184f
  int 0x10

  mov byte [gs:0x00], '1'
  mov byte [gs:0x01], 0xA4
  mov byte [gs:0x02], ' '
  mov byte [gs:0x03], 0xA4
  mov byte [gs:0x04], 'M'
  mov byte [gs:0x05], 0xA4
  mov byte [gs:0x06], 'B'
  mov byte [gs:0x07], 0xA4
  mov byte [gs:0x08], 'R'
  mov byte [gs:0x09], 0xA4
  
  jmp $
  
  times 510-($-$$) db 0
  db 0x55, 0xaa

  编写好代码之后,编译:

nasm -o mbr.bin mbr.S

  再写进虚拟硬盘里面:

dd if=mbr.bin of=hd60M.img bs=512 count=1 conv=notrunc  

  最后再运行Bochs,结果显示如下图:

对硬盘操作

 端口用途

  接下来,我们用指令in,out来对硬盘进行操作,首先,我们得知道硬盘有哪一些端口号,下图将硬盘控制器相关的端口号列出来了,并且读操作和写操作时,端口的用途是不一样的。

 

  接下来简单地描述各个寄存器具体的含义:

  ①Data,用于读写数据的。这个寄存器就相当于缓存,读操作时需要从这个缓存里面拿数据,前提是数据已经准备好了;写操作时,需要向缓存里面存放数据,最终才能存到硬盘里面。

  ②Error,顾名思义,就是保存读操作时出现的错误信息。

  ③Features,有些指令需要用到额外的参数,这些参数就保存到这个寄存器里面。

  ④Sector count,指定读取或写入多少个扇区。

  ⑤LBA,是用来描述一个扇区的地址,在这里一个扇区的地址是28位的,LBA low,mid,high 分别保存着LBA的0-7位,8-15位,16-23位。那么另外的4位去哪呢?保存在device里面了,所以device其实也是个杂项,即保存着多个不同的信息。

  ⑥Device,0-3位保存着LBA的24-27位,第4位指定通道上的主盘或从盘,0为主盘,1为从盘,第5位固定是1,第6位指定是否启用LBA模式,第7位固定是1。

  ⑦Status,保存硬盘的状态信息。读硬盘时,第0位是ERR,如果ERR位为1,代表命令出错,错误原因看Error;第3位是data request位,如果此位为1,表示硬盘已经准备好数据了;第6位位DRDY,表示硬盘就绪,对硬盘诊断用的;第7位位BSY位,表示硬盘是否繁忙,其它位暂不用到。

  ⑧Command,写硬盘,需要在这个寄存器指定功能。我们主要用到了三个功能。存入0xEC时,代表进行硬盘识别;存入0x20,代表读扇区;存入0x30时代表进行写扇区。

  需要再强调一次的是,有的寄存器在读和写的时候作用是不一样的,但都是共用一个寄存器,如Status和Command是共用同一寄存器的。

 读写硬盘步骤

  读写硬盘需要用到多个寄存器,我们就需要考虑读写寄存器的先后顺序了,为了统一一些,我们采取以下步骤:

  (1)先选择通道,往通道sector count寄存器写入待操作的扇区数。

  (2)往通道的三个LBA寄存器写入扇区起始地址的低24位。

  (3)往device寄存器中写入LBA地址的24-27位,将第6位置为1,使其为LBA模式, 设置第4位,选择操作的硬盘(master硬盘或slave硬盘)。

  (4)往该通道上的command寄存器写入操作命令。

  (5)读取该通道的status寄存器,判断硬盘工作是否完成。

  (6)如果以上步骤是读硬盘,进入下一个步骤,否则,完工。

  (7)将硬盘数据读出。

  下面开始编写代码:

%include "boot.inc"
SECTION MBR vstart=0x7c00
  mov ax,cs
  mov ds,ax
  mov es,ax
  mov ss,ax
  mov fs,ax
  mov sp,0x7c00
  mov ax,0xb800
  mov gs,ax

  mov ax,0x600
  mov bx,0x700
  mov cx,0
  mov dx,0x184f
  int 0x10

  mov byte [gs:0x00], '1'
  mov byte [gs:0x01], 0xA4
  mov byte [gs:0x02], ' '
  mov byte [gs:0x03], 0xA4
  mov byte [gs:0x04], 'M'
  mov byte [gs:0x05], 0xA4
  mov byte [gs:0x06], 'B'
  mov byte [gs:0x07], 0xA4
  mov byte [gs:0x08], 'R'
  mov byte [gs:0x09], 0xA4

  mov eax,LOADER_START_SECTOR
  mov bx,LOADER_BASE_ADDR
  mov cx,4
  call rd_disk_m16

  jmp LOADER_BASE_ADDR
  rd_disk_m16:;步骤1,写入读写扇区数量,cx指定数量
  mov esi,eax
  mov di,cx
  mov dx,0x1f2
  mov al,cl
  out dx,al
;步骤2,写入低24位LBA地址
  mov eax, esi
  mov dx,0x1f3
  out dx,al
  mov cl,8
  shr eax,cl
  mov dx,0x1f4
  out dx,al
  
  shr eax,cl
  mov dx,0x1f5
  out dx,al
;步骤3,写入高4为LBA地址,并设置第4-7位
  shr eax,cl
  and al,0x0f
  or al,0xe0
  mov dx,0x1f6
  out dx,al
;步骤4,写入读操作
  mov dx,0x1f7
  mov al,0x20
  out dx,al
;步骤5,判断硬盘是否已经准备好数据
 .not_ready:
  nop
  in al,dx
  and al,0x88
  cmp al,0x08
  jnz .not_ready

  mov ax,di
  mov dx, 256
  mul dx
  mov cx, ax
  mov dx,0x1f0
  .go_on_read:;从端口读取数据
  in ax,dx
  mov [bx],ax
  add bx,2
  loop .go_on_read
  ret
  
  times 510-($-$$) db 0
  db 0x55,0xaa

  boot.inc如下:

LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

    编写好代码之后,编译:

nasm -o mbr.bin mbr.S

  再写进虚拟硬盘里面:

dd if=mbr.bin of=hd60M.img bs=512 count=1 conv=notrunc  

  不过,我们的工作还没完成,既然要读硬盘,那硬盘里面除了MBR外还要放点什么才行,因此,我们编写一个简单的内核加载器,从硬盘加载到内存后,再让MBR程序调到这个内核加载器即可,loader.S代码如下:

%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
  mov byte [gs:0x00], '2'
  mov byte [gs:0x01], 0xA4
  mov byte [gs:0x02], ' '
  mov byte [gs:0x03], 0xA4
  mov byte [gs:0x04], 'L'
  mov byte [gs:0x05], 0xA4
  mov byte [gs:0x06], 'O'
  mov byte [gs:0x07], 0xA4
  mov byte [gs:0x08], 'A'
  mov byte [gs:0x09], 0xA4
  mov byte [gs:0x0a], 'D'
  mov byte [gs:0x0b], 0xA4
  mov byte [gs:0x0c], 'E'
  mov byte [gs:0x0d], 0xA4
  mov byte [gs:0x0e], 'R'
  mov byte [gs:0x0f], 0xA4
  jmp $

  编译代码:

nasm -o loader.bin loader.S

  写进硬盘里面,seek=2代表写进扇区2里面,和mbr程序隔开存储:

dd if=loader.bin of=hd60M.img bs=512 count=1 seek=2 conv=notrunc

   最后,我们再运行bochs,然后屏幕就会显示'2 LOADER',离进入保护模式又近了一大步,下一节将讲述保护模式!!!!

 

 

 

 

 

       

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!