本篇简单介绍了MBR程序是怎么从0x7c00加载执行的?MBR如何控制显卡来进行显示?MBR如何读写硬盘数据?以及完善MBR的功能。
MBR的功能是:将内核加载器Loader加载到内存中并跳转过去。
本篇难点:
- 通过端口IO操作显卡、读写硬盘
地址、Section、Vstart
这里解释了为什么MBR程序会加载在0x7C00并进行执行。
首先,地址是什么?编译器是干嘛的?
地址是个数字,表示各种符号(变量名,函数名等)在源程序中的位置。
编译器负责给各符号编址,就是符号相对于文件头的偏移量
其次,Section是什么?
Section称为节,就是用来人为将汇编程序分成多个部分来使用的,比如一个节存储程序,一个节存储数据。
最后,Vstart是什么?
nasm编译器中,将Section用vstart修饰,可以给Section赋予虚拟地址,例如上一篇程序中的:
SECTION MBR vstart=0x7c00
让这个区段以0x7c00位起始地址。
那么,MBR程序是怎么被加载执行的呢?
MBR程序位于0盘0道1扇区的位置,BIOS在运行过程中,会将MBR加载到0x7c00的位置上,为了让程序正常的运行,那么代码里的地址偏移量也都应该是以0x7c00为起始地址,所以在编码的时候,指定vstart=0x7c00让编译器按照0x7c00为起始地址为程序进行编址,这样,程序被BIOS加载好跳转过去就能直接执行了。
CPU的工作原理
实模式说的是8086CPU的寻址方式、寄存器大小、指令用法等,是用来反应CPU在该环境下如何工作的概念。
CPU的工作是执行指令,CPU的工作原理,其实就是CPU如何执行指令的原理;
CPU分为三个部分:控制单元、运算单元、控制单元。
控制单元是CPU的控制中心,CPU需要通过控制单元来知道下一条执行的指令是什么。
控制单元由指令寄存器IR、指令译码器ID、操作控制器OC组成。
程序运行过程中,指令指针寄存器IP(指令的地址在程序计数器PC上,也就是x86CPU上的cs:ip)指向下一条执行的地址,控制单元根据IP的指向,将位于内存中的指令逐个装载到指令寄存器IR中,然后指令译码器ID将位于指令寄存器IR中的指令按照指令格式来解码,指令格式都是固定的:
IA-32指令格式
前缀 操作码 寻址方式、操作数类型 立即数 偏移量
存储单元指的是CPU内部的L1、L2、L3缓存和寄存器,待处理的数据(指令中的操作数)就放在这里头,寄存器分为程序可见寄存器(通用寄存器,段寄存器等)和程序不可见寄存器
为了提高CPU运行效率,从而出现了缓存,缓存采用SRAM(静态随机存储器),是比内存DRAM(动态随机存储器)性能更好的存储器,但集成度低,容量不大。DRAM需要每隔一段时间去刷新电路,否则数据会消失,SRAM则不用
运算单元负责逻辑运算和算术运算,负责从控制单元那里读取数据,执行计算
实模式下的寄存器
寄存器有一部分是对程序不可见的,如:全局描述符表寄存器GDTR、中断描述符表寄存器IDTR、局部描述符表寄存器LDTR、任务寄存器TR、控制寄存器CR0-3、指令指针寄存器IP、标志寄存器flags、调试寄存器DR0-7;
还有一部分是对程序可见的,如通用寄存器、段寄存器;
对于不可见那一类寄存器,虽然不能直接使用,但其中一部分还得由命令来初始化,这些内容在保护模式会涉及到。
在实模式下,寄存器都是16位宽。
实模式下,内存访问通过“段基址:段内偏移地址”来进行访问,这里的段基址,就是用段寄存器来存储的,不管在实模式还是保护模式下,段寄存器的功能都是指定一片内存区域的起始地址;
段有很多种:
- CS代码段,把所有指令都连续放在一起,是一个只有指令的区域
- DS数据段,类似CS段,不过存的都是数据
- SS栈段,只在内存中有
- ES、FS、GS附加段,给程序提供的多几个段来用
flags寄存器用来存各种状态,后面专门写一篇来介绍这flags寄存器
通用寄存器,就那些功能,懒得介绍了
实模式被取代的原因
实模式的实指的是它用的是真实的物理地址,现在实模式已经被保护模式取代了,实模式仅存在于引导系统启动的这个阶段,引导完成后,控制器交给保护模式。
实模式被淘汰的主要原因是,安全问题,实模式下,用户程序和系统是同一特权级别,用户程序可以轻易执行一些破坏性的指令,而保护模式则引入了特权级,用户级和系统级不是一个级别,系统的安全性得到了提升。
CPU与外设
CPU通过IO接口与外设通信,IO接口位于CPU和外设之间,用于协助CPU与外设通信,常见的IO接口,如显卡(驱动显示器)、声卡(驱动音响)等;
IO接口可以分为硬件部分和软件部分;
硬件部分,为了简化CPU访问外设的工作,能够轻易同任何外设通信,所以IO的功能是约定好的:
- 设置数据缓冲,解决CPU与外设速度不匹配的问题
- 设置信号电平转换电路,将外设的机电电平转换成CPU的TTL电平
- 设置数据格式转换,A/D、D/A转换器进行数模转换
- 设置时序控制电路来同步CPU和外设
- 提供地址译码,使CPU可以选中某个IO端口进行通信
同一时间CPU只能同一个IO接口通信,负责控制CPU同哪一个IO接口通信的设备叫输入输出控制中心,也就是南桥芯片,CPU通过内部总线连接南郊芯片的内部,南桥负责和低速设备通信(北桥负责高速设备),如并口硬盘PATA、串口硬盘SATA、USB、PCI、电源管理等接口,其中,PCI接口是南桥芯片专门用于扩展的接口
软件部分,IO接口是通过寄存器与CPU通信的,为区别CPU的寄存器,所以叫做端口,端口是IO接口开放给CPU的接口,一般IO设备都有一组端口,每个端口都有自己的功能
IA-32体系中,系统把IO接口的端口独立编址,可以使用in和out指令来进行读写操作:
- in ax dx
- out dx bx
这里dx是端口号
小结:
- IO接口负责连接外设和CPU
- IO接口传输前后会对数据进行处理
显存、显卡、显示器
MBR运行在实模式下,可以用BIOS的0x10中断打印字符串,但在进入保护模式之后,就不能这么操作了,因为BIOS中断依赖中断向量表,保护模式下没中断向量表;
某些IO接口也叫适配器,显卡也可以叫做显示适配器;
显卡是pci设备,插在pci接口上,pci总线是并行传输的,并行传输需要同时接收位宽x的数据,位宽越大越困难,于是有了pcie总线,这是串行传输的,现在的显卡都是串口的了
传输速度取决于并行数据量和传输频率,由于频率很大,目前串行传输速率极高
显卡提供的可编程接口是显存和IO端口,显存是显卡提供的,位于显卡内部的一块内存;
显卡的工作就是不断的读取显存,将其内容发送到显示器,显示器不区分显示的内容是什么,在显示器眼中,显示的东西全都是像素点的排列组合,为了便于显示文字,于是有了编码,例如ASCII编码,这是一种约定,显卡根据编码来显示编码所对应的字符。
实模式的显存分布中,0xB8000~0xBFFFF这32kb空间用于文本模式显示适配器,往0xb8000输出的字符会落入显存中,显存中有了数据就会往显示器上显示;
显卡的文本模式也分很多种模式,用“列*行”来表示,默认是80*25;
每个字符在屏幕上都由两个字符表示,低字节是字符的ASCII码,高字节是属性元信息,低4位是前景色,高4位是字符背景色,第4位控制亮度,第7位控制是否闪烁。
改进MBR,直接操作显卡
主引导程序
;-----------------------------------------------------
SECTION MBR vstart=0x7c00 ;告诉编译器,起始地址是0x7c00
mov ax,cs ;因为BIOS执行完毕后cs:ip为0x0:0x7c00,所以用cs初始化各寄存器(此时cs=0)
mov ds,ax ;ds、es、ss、fs不能给立即数初始化,需要用ax寄存器初始化
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00 ;初始化堆栈指针,因为目前0x7c00以下的内存暂时可用
mov ax,0xb800 ;选择显卡的文本模式
mov gs,ax ;使用GS段寄存器作为显存段基址
;清屏利用0x06号功能,上卷全部行,则可清屏
;-----------------------------------------------------
;INT 0x10
-----------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
mov ax,0x600 ;上卷行数:全部 功能号:06
mov bx,0x700 ;上卷属性
mov cx,0 ;左上角:(0,0)
mov dx,0x184f ;右下角:(80,25)
;VGA文本模式中,一行只能容纳80字节,共25行
;下标从0开始,所以0x18=24,0x4f=79
int 0x10 ;int 0x10
;输出背景色为蓝色,前景色为红色,并且跳动的字符串“1 MBR”
;--------------------------------------------------------------------------
mov byte [gs:0x00], '1'
mov byte [gs:0x01], 0x92
mov byte [gs:0x02], ' '
mov byte [gs:0x03], 0x12
mov byte [gs:0x04], 'M'
mov byte [gs:0x05], 0x12
mov byte [gs:0x06], 'B'
mov byte [gs:0x07], 0x12
mov byte [gs:0x08], 'R'
mov byte [gs:0x09], 0x12
;--------------------------------------------------------------------------
jmp $ ;使用程序悬停在此
message db "hello world"
times 510-($-$$) db 0
db 0x55,0xaa
硬盘简介
这里简单介绍一下机械硬盘是怎么工作的;
结构简介:
机械硬盘上有大量盘片,每个盘片上都有两面,每一面都涂有磁性介质,用来存储数据;
每个盘面上都有磁头,磁头臂由步进电机驱动带着磁头进行移动,进行访问磁盘数据;
磁盘被划分为多个区域,以一定半径为圆,划分出多个同心圆环,这个圆环叫做磁道,磁道上以一定大小为基准进行进一步划分,分成一个一个扇形,叫做扇区,一般一个磁道上有63个扇区,磁盘里有大量盘片,不同盘片的同一磁道称为柱面。
为了减少磁头移动的时间,数据存储是以柱面为单位进行的,在当前磁道的扇区里写入数据,写不下了向下一个盘面的当前磁道的这个扇区进行写入;
每个扇区都有自己的Header,上面会记录当前扇区所在的磁头号,磁道号,扇区号。
IO简介:
CPU同磁盘进行控制使用的IO接口叫做硬盘控制器,硬盘控制器和硬盘是连在一起的,这种接口称为集成设备电路IDE,后来改叫ATA,串行接口SATA,并行接口PATA
PATA接口一边插向主板,另一边插向两个硬盘,一个是主盘一个是从盘;主板上如果支持4块这样的硬盘,则会有两个PATA插槽,这个插槽称为通道,分别为Primary通道和Secondary通道;
使用IO:
让硬盘工作,需要通过读写硬盘控制器上的端口(寄存器)来进行:
- Data寄存器是用于管理数据的,我们读取和写入的数据都通过这个寄存器获取,此寄存器被设计为16位。
- Error寄存器,只在读取硬盘失败时才有效,里面才有失败的信息。
- 尚未读取的扇区数载Sector count寄存器中。
- LBA寄存器有LBA low、LBA mid、LBA high 三个,它们三个都是8位宽度的。LBA low寄存器用来u才能出28位地址的0 ~ 7位,LBA mid寄存器用来存储第 8 ~ 15 位,LBA high寄存器存储第16 ~ 23位。剩下不够的位数用Device寄存器。
- device寄存器,它的宽度是8位。在此寄存器的低4位用来存储LBA地址的24~27位。第4位用来指定通道上的主盘或从盘,0代表主盘,1代表从盘。第6位用来设置是否启用LBA方式,1代表启动LBA模式,0代表启用CHS模式。
这里主要使用的命令是:
- identify:0xEC,硬盘识别
- read sector: 0x20,读扇区
- write sector: 0x30,写扇区
硬盘操作方法:
- 先选择通道,往该通道的sector count写入待操作扇区数
- 往该通道上三个LBA寄存器写入扇区起始地址的低24位
- 往device寄存器中写入LBA地址的24~27位,并置第6位为1,使其为LBA模式,设置第4位选择主盘或从盘
- 往该通道上command寄存器中写入操作命令
- 读取该通道上的status寄存器,判断硬盘工作是否完成
- 如果以上步骤是读硬盘进入下一个步骤,否则完工。
- 将硬盘数据读出
完善MBR,使用硬盘
操作系统由BIOS引导,BIOS执行完毕后,将位于0盘0道1扇区的MBR程序加载到0x7C00内存中,然后将控制权交给MBR,MBR只有512字节大小,加载不了内核,所以需要在另一个地方加载内核(加载器),MBR的工作就是将加载器从硬盘读取到内存空间中,然后将控制权交给它,就像BIOS将控制权交给MBR一样。
为了安全一点,把加载器程序放在第二扇区上,加载到内存0x900的位置上;
配置文件boot.inc:
SE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2
主引导程序
;-----------------------------------------------------
%include "boot.inc"
SECTION MBR vstart=0x7c00 ;告诉编译器,起始地址是0x7c00
mov ax,cs ;因为BIOS执行完毕后cs:ip为0x0:0x7c00,所以用cs初始化各寄存器(此时cs=0)
mov ds,ax ;ds、es、ss、fs不能给立即数初始化,需要用ax寄存器初始化
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00 ;初始化堆栈指针,因为目前0x7c00以下的内存暂时可用
mov ax,0xb800 ;选择显卡的文本模式
mov gs,ax ;使用GS段寄存器作为显存段基址
;清屏利用0x06号功能,上卷全部行,则可清屏
;-----------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;-----------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
mov ax,0x600 ;上卷行数:全部 功能号:06
mov bx,0x700 ;上卷属性
mov cx,0 ;左上角:(0,0)
mov dx,0x184f ;右下角:(80,25)
;VGA文本模式中,一行只能容纳80字节,共25行
;下标从0开始,所以0x18=24,0x4f=79
int 0x10 ;int 0x10
;输出背景色为蓝色,前景色为红色,并且跳动的字符串“1 MBR”
;--------------------------------------------------------------------------
mov byte [gs:0x00], '1'
mov byte [gs:0x01], 0x92
mov byte [gs:0x02], ' '
mov byte [gs:0x03], 0x12
mov byte [gs:0x04], 'M'
mov byte [gs:0x05], 0x12
mov byte [gs:0x06], 'B'
mov byte [gs:0x07], 0x12
mov byte [gs:0x08], 'R'
mov byte [gs:0x09], 0x12
;--------------------------------------------------------------------------
mov eax ,LOADER_START_SECTOR ;起始扇区lba地址
mov bx ,LOADER_BASE_ADDR ;写入的地址
mov cx ,1 ;待读入的扇区数
call rd_disk_m_16 ;以下读取程序的起始部分
jmp LOADER_BASE_ADDR ;跳转到Loader
;--------------------------------------------------------------------------
;功能:读取eax=LBA扇区号
rd_disk_m_16:
mov esi ,eax ;备份eax
mov di ,cx ;备份cx
;读写硬盘
;1---设置要读取的扇区数
mov dx ,0x1f2 ;设置端口号,dx用来存储端口号的
mov al ,cl
out dx ,al ;读取的扇区数
mov eax ,esi ;恢复eax
;2---将LBA地址存入0x1f3~0x1f6
;LBA 7~0位写入端口0x1f3
mov dx ,0x1f3
out dx ,al
;LBA 15~8位写入端口0x1f4
mov cl ,8
shr eax ,cl ;逻辑右移8位,将eax的最低8位移掉,让最低8位al的值变成接下来8位
mov dx ,0x1f4
out dx ,al
;LBA 24~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模式
mov dx ,0x1f6
out dx ,al
;3---向0x1f7端口写入读命令0x20
mov dx ,0x1f7
mov al ,0x20
out dx ,al
;4---检测硬盘状态
.not_ready:
;同写入命令端口,读取时标示硬盘状态,写入时是命令
nop
in al ,dx
and al ,0x88 ;第三位为1表示已经准备好了,第7位为1表示硬盘忙
cmp al ,0x08
jnz .not_ready
;5---0x1f0端口读取数据
mov ax ,di ;要读取的扇区数
mov dx ,256 ;一个扇区512字节,一次读取2字节,需要读取256次
mul dx ;结果放在ax里
mov cx ,ax ;要读取的次数
mov dx ,0x1f0
.go_on_read:
in ax, dx
mov [bx], ax ;bx是要读取到的内存地址
add bx, 0x02
loop .go_on_read ;循环cx次
ret
times 510-($-$$) db 0
db 0x55,0xaa
编译:
nasm -I include/ -o mbr3.bin mbr3.S
-I参数用来指定include文件位置
加载到硬盘中:
dd if=./boot/mbr3.bin of=hd60M.img bs=512 count=1 conv=notrunc
随便写个程序来代替loader
主要是来试试看这个MBR将控制权交给Loader程序是否成功,Loader程序:
%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 $
就是上面操作显卡的时候的显示字符串,这里不用手动设置gs段寄存器是因为在这个程序执行之前,段寄存器已经是设置好的了
编译:
nasm -I include/ -o loader.bin loader.S
dd:
dd if=boot/loader.bin of=hd60M.img bs=512 count=1 seek=2 conv=notrunc
通过seek选择跳过块的数量,因为使用第二个扇区,所以跳过0和1两个块(块指定的大小是512字节和扇区相同)
运行Bochs
程序也写完了,来看看效果吧: