以下是读本书第三章的收获。
如何知道一个源程序的各符号(指令和变量)地址?简单来说,地址就是该符号偏移文件开头的距离,符号的地址是按顺序编排的,所以两个相邻的符号,其地址也是相邻的。对于指令来说,指令的地址=上一个指令的地址+上一个指令的大小,最初的符号地址为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',离进入保护模式又近了一大步,下一节将讲述保护模式!!!!
来源:oschina
链接:https://my.oschina.net/u/4383329/blog/3347199