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