3.1 地址、section、vstart浅尝辄止
本质上,程序中的各种数据结构的访问,就是通过“该数据结构的起始地址+该数据结构所占内存的大小”,所谓数据的地址,就是该数据相对整个程序开头的距离,即偏移量。
section只是为了让程序员在逻辑上将程序划分为几个部分,section本身并没有被分配地址,它是伪指令。
vstart是虚拟起始地址,作用是为section内的数据制定一个虚拟的起始地址。mbr之所以用vstart=0x7c00来修饰,是因为我们人为的规定mbr要被BIOS加载到物理地址0x7c00。
3.2.1 CPU的工作原理
控制单元要取下一条待运行的指令,该指令的地址在程序计数器PC中,在x86CPU上,程序计数器就是cs:ip。读取ip寄存器后,将此地址送上地址总线,CPU根据次地址得到了指令,并将其存入到指令寄存器IR中。译码器根据指令格式检查指令寄存器中的指令,先确定操作码是什么,再检查操作数类型,运算单元开始真正执行指令。ip寄存器的值被加上当前指令的大小,于是ip又指向了下一条指令的地址,循环。
3.2.2 实模式下的寄存器
寄存器是给CPU处理数据的场所,虽然有些寄存器对程序员不可见,但我们需要对他们进行初始化。
CPU是使用“段基址:段内偏移地址”来访问内存的,地址被分开存储到代码段CS寄存器和指令指针IP寄存器。在不跨段的前提下,CPU会将“当前ip寄存器中的值+指令长度”作为新的ip寄存器中的值,若需要跨段,则需要加载新的段基址到CS寄存器中。
3.2.3 实模式下内存分段由来
实模式,即程序中用到的地址都是真实的物理地址。CPU20位地址,但寄存器只有16位,所以通过“基地址:偏移地址”的形式处理,先把16为的段基址左移4位变成20位再加上段内偏移地址。
3.2.4 实模式下CPU内存寻址方式
1.寄存器寻址
从寄存器里直接拿数据
2.立即数寻址
即常数
3.内存寻址
1)直接寻址
内存地址写入中括号中
mov ax,[0x1234]
2)基址寻址
在操作数中用bx寄存器或寄存器作为地址的气势,地址的变化以它为基础
bx寄存器的默认段寄存器是DS,而bp寄存器的默认段寄存器是SS
3)变址寻址
mov [si+0x1234],ax
3.2.5 什么是栈
CPU中有栈段SS寄存器和栈指针Sp寄存器,他们是用来指定当前使用的栈的物理地址。
栈是线性表中的一种,后进先出,也就是从上往下。在内存中指定一块区域为栈区域,其起始地址存入栈基址寄存器SS,其指针用寄存器sp来指定。栈在使用过程中是向下扩展的,所以栈顶地址小于栈底地址
push指令,先将SP减去字长,所得新的SP,再讲数据压入。POP指令相反,先弹出数据,才讲SP指令加上字长在更新SP
代码3-4 mbr.S
SECTION MBR vstart=0x7c00 ;MBR程序地址0x7c00
mov ax,cs ;由于cs为0,,所以可以用cs来初始化ax
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800 ;文本模式显示适配器
mov gs,ax
;利用INT 0x10 功能号:0x06来上卷全部行
mov ax,0x0600 ;AH 功能号 = 0x06 AL = 行卷的行数(0表示全部)
mov bx,0x0700 ;BH表示上卷行属性,属性是指背景色
mov cx,0x0000 ;cx=(CL,CH)=窗口左上角的(X,Y)
mov dx,0x184f ;右下角(80,25)VGA文本模式,一行80个字符,一共25行
;下标从0开始,所以0x18=24,0x4f=79
int 0x10
; 输出背景色绿色,前景色红色,并且跳动的字符串"1 MBR"
mov byte [gs:0x00],'1'
mov byte [gs:0x01],0xA4 ; A表示绿色背景闪烁,4表示前景色为红色
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=/home/moyao/Desktop/bochs/boot/mbr.bin of=/home/moyao/Desktop/bochs/hd60M.img bs=512 count=1 conv=notrunc
执行
bin/bochs -f bochsrc.disk
3.4 bochs调试方式
由于bochs是虚拟机,所以支持硬件级别上的调试,其硬件级别的调试,体现在
1、调试时可以查看页表、gdt、idt等数据结构
2、可以查看栈中数据
3、可以反汇编任意内存
4、实模式、保护模式互相变换是提醒
5、中断发生时提醒
由于bochs已经将选项[6]作为默认的行为,进入bochs后可以直接回车
具体看p116页介绍
3.5 硬盘介绍
针对硬盘的IO接口是硬盘控制器,与显卡和显示器不同的是,硬盘控制器是和硬盘链接在一起的
IO接口与端口
IO接口:向CPU提供I/O设备的状态信息和进行命令译码。对传送数据提供缓冲,以消除计算机与外设在“定时”或数据处理速度上的差异。
端口:是指接口电路中的一些寄存器,这些寄存器分别用来存放数据信息、控制信息和状态信息。
data寄存器管理数据。在读硬盘时,硬盘准备好数据后,硬盘控制器将其放在内部的缓冲区中,不断读此寄存器便是读出缓冲区中的全部数据。在写硬盘时,我们要把数据源源不断地运送到此端口,数据便被存入缓冲区里,硬盘控制器发现这个缓冲区中有数据了,便将此处的数据写入相应的扇区中。(16位)
读硬盘时,端口0x171或者0x1F1的寄存器名字叫Error寄存器,只有读取硬盘失败时有用,会记录失败的信息,尚未读取的扇区数在Sector count寄存器中。写硬盘时此寄存器有了别的用途,叫Feature 寄存器。有些命令需要额外参数,这些参数就写入此端口中。注意Error 和 Feature本质上是同一个端口,只不过不同环境下有不同用途。(8位)
Sector count 寄存器用来指定待读取或者待写入的硬盘个数。8位所以最大值为255,若设置为0则说明要操作256个扇区。
LBA(逻辑块地址),对人来说直观的磁盘寻址方式。分为LBA28和LBA48两类,前者用28为比特来描述一个扇区的地址,后者用48位。LBA28有三个寄存器,LBA low,LBA mid,LBA high,每个寄存器都是8位,这里三个寄存器只有24位,还有4位在哪呢?放到了device寄存器上。
device寄存器前四位为LBA地址的第23~27位;第四位用来指定通道上的主盘或从盘,0代表主盘,1代表从盘;第六位用来设置是否启用LBA方式,1代表启用,0代表启用CHS模式即“煮面-磁头-扇区”模式;第五位和第七位固定式一,成为MBS位,无需关注。
读硬盘时,端口0x1F7和0x177的寄存器名称是status,它是8位的寄存器,用来给出硬盘的状态信息。第0位是ERR位,若为1,表示命令出错,具体原因可看Error寄存器;第3位是data request位,若为1,表示硬盘已经把数据准备好了,主机现在可以把数据读出来。第6位是DRDY,表示硬盘就绪,此位是对硬盘诊断时用的,表示硬盘检测正常,可以执行一些命令。第7位是BSY位,表示硬盘是否繁忙,若为1则繁忙,此寄存器中的其他位无效。另外的四位暂不关注。
约定步骤:
1)先选择通道,往该通道的sector count寄存器中写入代操作的扇区数。
2)往该通道上的三个LBA寄存器写入扇区起始地址的低24位。
3)往device寄存器中写入LBA地址的24~27位,并将第6位为1,使其为LbA模式,设置第4位,选择操作的硬盘。
4)往该通道的command寄存器写入操作命令(状态寄存器)。
5)读取该通道的status寄存器,判断硬盘工作是否完成。
6)如果以上步骤是读硬盘,进入下一个步骤。完成,完工。
7)将硬盘数据读出。
硬盘完成工作后,我们选取查询传送方式和中断传送方式将数据读出
查询传送方式,也称为程序I/O,PIO,是指在传输之前,由程序先去检测设备的状态。需要满足一定的条件才能传输,适合低速的设备,由于磁盘有status寄存器,所以可以使用此方式。
中断传送方式,也称为中断驱动I/O,由于查询传送方式需要不断查询设备状态,这意味着只有最后一刻的查询才有意义,所以效率不高。设备准备好数据后,主动通知CPU来取,这样效率就高了。
3.6 改造MBR
MBR只有512字节,在这么小的空间中,没法为内核准备好环境,更没法将内核成功加载到内存并运行。所以我们会通过另一个程序完成初始化环境及加载内核的任务,这个程序,我们称为loader,即加载器。MBR占据的是扇区的第0扇区第1扇区是空闲的,我们不妨把loader放到第2扇区。
首先loader中要定义一些数据结构,且这些数据结构在之后的保护模式中要使用,所以loaer加载到内核后不能被覆盖。所以loader的加载地址选为0x900,当然也可以0x500
目的:我们将loader的代码写入磁盘中的第二个扇区,并在mbr中将loader的代码提取到地址为0x900处
代码实现
库代码实现 myos/boot/include/boot.inc
;------------- loader和kernel ----------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2
MBR代码实现 myos/boot/mbr.S
;主引导程序
;---------
%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
; 清屏
;利用0x06号功能,上卷全部行,则可清屏。
; ---------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;----------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
mov ax, 0600h
mov bx, 0700h
mov cx, 0 ; 左上角: (0, 0)
mov dx, 184fh ; 右下角: (80,25),
; 因为VGA文本模式中,一行只能容纳80个字符,共25行。
; 下标从0开始,所以0x18=24,0x4f=79
int 10h ; int 10h
mov eax,LOADER_START_SECTOR ;初始扇区lba地址,第2个扇区,即0x2
mov bx,LOADER_BASE_ADDR ;写入的地址 0x900
mov cx,1 ;待读入的扇区数
call rd_disk_m_16 ; 以下读取程序的起始部分(一个扇区)
jmp LOADER_BASE_ADDR ;来到loader部分0x900
;-------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;-------------------
; eax=LBA扇区号
; ebx=将数据写入的内存地址
; ecx=读入的扇区数
mov esi,eax ;备份eax,LBA的扇区号
mov di,cx ;备份cx,读入的扇区数
;读写硬盘
;第一步:选择通道,往该通道的sector count寄存器写入待操作的扇区数
mov dx,0x1f2
mov al,cl ;在8086中,cx高位两个8位寄存器,高8位的ch和低8位的cl
out dx,al ;往sector count寄存器中打入1,代表待操作一个扇区
mov eax,esi ;回复ax
;第二步,往通道上的三个LBA寄存器写入扇区起始地址的低24位
;LBA地址的7~0位写入端口0x1f3
mov dx,0x1f3
out dx,al
;LBA地址15~8位写入端口0x1f4
mov cl,8
shr eax,cl ;逻辑右移8位
mov dx,0x1f4
out dx,al
;LBA地址23~16位写入端口0x1f5
shr eax,cl
mov dx,0x1f5
out dx,al
shr eax,cl
and al,0x0f ;lba第24~27位
or al,0xe0 ;设置7~4位为1110,表示lba模式 主盘,LBA模式
mov dx,0x1f6
out dx,al
;第3步:向0x1f7端口写入读命令,0x20 0x20读扇区命令
mov dx,0x1f7
mov al,0x20
out dx,al
;第4步:检测硬盘状态
.not_ready:
;同一端口,写时表示写入命令字,读时表示读入硬盘状态
nop ;相当于sleep一下
in al,dx ;这里dx不需要改变command和status是同一个寄存器
and al,0x88 ;将寄存器信息读取到al中
cmp al,0x08 ;与第四位相减,若第四位都等于1,则zf=0
jnz .not_ready ;若未准备好,继续等。如果zf!=0则跳转
;第5步:从0x1f0端口读数据
mov ax, di ;di当中存储的是要读取的扇区数
mov dx, 256 ;每个扇区512字节,一次读取两个字节因为data寄存器是16位,所以一个扇区就要读取256次,与扇区数相乘,就等得到总读取次数
mul dx
mov cx, ax ;得到了要读取的总次数,然后将这个数字放入cx中
mov dx, 0x1f0
.go_on_read:
in ax,dx
mov [bx],ax
add bx,2
loop .go_on_read ;loop循环时会将cx-1
ret
编译代码mbr.S
nasm -I include/ -o mbr.bin mbr.S
写入磁盘
dd if=/home/moyao/Desktop/bochs/boot/mbr.bin of=/home/moyao/Desktop/bochs//hd60M.img bs=512 count=1 conv=notrunc
LOADER代码实现 myos/boot/loader.S
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
; 输出背景色绿色,前景色红色,并且跳动的字符串"1 MBR"
mov byte [gs:0x00],'2'
mov byte [gs:0x01],0xA4 ; A表示绿色背景闪烁,4表示前景色为红色
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 $ ; 通过死循环使程序悬停在此
编译代码LOADER
nasm -I include/ -o loader.bin loader.S
写入磁盘
dd if=/home/moyao/Desktop/bochs/boot/loader.bin of=/home/moyao/Desktop/bochs//hd60M.img bs=512 count=1 seek=2 conv=notrunc