第三章 完善MBR

地址、section、vstart

什么是地址

地址只是数字,描述各种符号在源程序中的位置,它是源代码文件中各符号偏移文件开头的距离

编译器的工作就是给各符号编址。 编译器根据所在硬件平台的特性,将源代码中的每一个符号(指令和数据)都按照本硬件平台的特性分配空间,在不考虑对齐的情况下,这些符号在空间上都彼此相邻,连续分布它们在程序中距第一个符号的距离便是它们在程序中的地址

在这里插入图片描述
偏移量的本质就是地址,每个变量的地址是前一个变量的地址+前一个变量的内存空间大小。
代码编译之后,源代码中的标号会被替换为实际地址。若想看到程序编译后的各指令或标号的地址,可以用反汇编查看。
在这里插入图片描述
“地址”列中的数字和“内容”列中的内容有这样一种关系:地址等于上一个地址+上一个地址处的内容的长度。
编译器给程序中各符号(变量名或函数名等)分配地址,就是各符号相对于文件开头的偏移量。

什么是section

section用来描述汇编语言中的段,关键字section并没有对程序中的地址产生任何影响,即在默认情况下,有没有section都一个样,section中的数据的地址依然是相对于整个文件的顺延,仅仅是在逻辑上让开发人员梳理程序之用。

在这里插入图片描述
在这里插入图片描述

什么是vstart

vstart 是虚拟起始地址。
vstart的作用是为section内的数据指定一个虚拟的起始地址,也就是根据此地址,在文件中是找不到相关数据的,是虚拟的,假的,文件中的所有符号都不在这个地址上。
编译器只负责编址,它只会将数据相对于文件开头的偏移量作为该数据的地址,全是以0起始的。编译器以相对于文件开头偏移来编址的好处是利于重定位。整个文件加载到某个地址后,可以以这个地址为段基址,文件内的数据的地址是以0为开始算的,所以它们直接就可以用作段内偏移地址。
编译器只负责编址,还需要加载器将程序放到对应的地址,对于程序,程序内的符号都是从零开始的,没有改变数据本身在文件中的地址,这个地址相当于是放入程序的段基址,但不是文件中的地址。
在这里插入图片描述

CPU的实模式

实模式是指8086CPU的寻址方式、寄存器大小、指令用法等,是用来反应CPU在该环境下如何工作的概念。

CPU的工作原理

CPU:执行指令。
CPU包括控制单元、运算单元、存储单元。
控制单元是CPU的控制中心,CPU需要经过它的帮忙才知道自己下一步要做什么。控制单元根据指令指针寄存器中的IP地址指向将指令载入指令寄存器,再通过指令译码器将指令解码。
在这里插入图片描述

存储单元存储待处理的数据。
在这里插入图片描述

运算单元负责算术运算和逻辑运算,它从控制单元那里接收命令并执行。
在这里插入图片描述
总结:
在这里插入图片描述

实模式下的寄存器

寄存器是一种物理存储单元,用于数据存储。寄存器是给CPU处理数据的场所。
在这里插入图片描述

段寄存器
CPU是用“段基址:段内偏移地址”的形式来访问内存的。段寄存器的作用就是指定一片内存的起始地址,故也称为段基址寄存器。

在这里插入图片描述
CS寄存器用来存代码段段基址,IP寄存器用来存储代码段段内偏移地址
在这里插入图片描述
在这里插入图片描述

flag寄存器是计算机的窗口,展示了CPU内部各项设置、指标。任何一个指令的执行、其执行过程的细节、对计算机造成了哪些影响,都在flag寄存器中通过一些标志位反映出来。

通用寄存器
无论是实模式还是保护模式,通用寄存器有8个,分别是AX、BX、CX、DX、SI、DI、BP、SP
在这里插入图片描述
在这里插入图片描述

实模式下内存分段由来

在这里插入图片描述
让16位的寄存器访问20位的地址空间:
先把16位的段基址左移4位后变成20位(最大值为0xFFFF0),再加段内偏移地址,这样便形成了20位地址,最大的访问地址为:0xFFFF0+0xFFFF=0xFFFFF+0xFFF0=1MB+16*4KB-16-1。超过20位的地址对1MB取模(地址回卷),即超过最大范围后,从0重新开始计数。
在这里插入图片描述

实模式下CPU内存寻址方式

在这里插入图片描述
寻址就是寻找地址,CPU寻找“数”的地址,可以是源操作数,也可以是目的操作数,寻址就是找到“数”的所在地。

寄存器寻址:“数”在寄存器中,操作数在寄存器中,直接从寄存器中拿数据。

在这里插入图片描述

**立即数寻址:**立即数就是常数,在指令中直接给出,立即数免去了找数的过程。
在这里插入图片描述

**内存寻址:**操作数在内存中。
在这里插入图片描述

直接寻址:就是将直接在操作数中给出的数字作为内存地址,通过中括号的形式告诉CPU,取此地址中的值作为操作数。

在这里插入图片描述

基址寻址:就是在操作数中用bx寄存器或bp寄存器作为地址的起始,地址的变化以它为基础。bx寄存器的默认段寄存器是DS,而bp寄存器的默认段寄存器是SS,即bp和sp都是栈的有效地址(偏移地址)。

sp寄存器作为栈顶指针,相当于栈中数据的游标,这是专门给push指令和pop指令做导航用的寄存器,push指令往哪个内存压入数据,pop将哪个地址的数据弹出
在这里插入图片描述
在这里插入图片描述

也就是说,sp用来访问栈顶,进行pop和push操作,bp用来访问栈间,用偏移量来完成。

在这里插入图片描述

变址寻址:变址寻址和基址寻址类似,只是将寄存器换成了si和di。si是指源索引寄存器,di是指目的索引寄存器。这两个寄存器的默认段寄存器也是ds。变址寻址主要是用于字符搬运方面。

在这里插入图片描述

基址变址寻址:基址寻址和变址寻址的结合,即基址寄存器bx或bp加一个变址寄存器si或di。
在这里插入图片描述

栈是什么

栈是线性表的一种,结构中每个元素都有一个前驱元素和一个后继元素,且仅有一个。数据的存取都在一端进行,这一端称为栈顶,另一端作为存储单元的基本地址永远不动,称为栈底。
在这里插入图片描述
在这里插入图片描述

**上述是数据结构中的栈,是逻辑上的,而内存上的栈是物理上的。**把数据结构中的栈的概念用物理硬件来实现。它同数据段、代码段一样,是内存中的区域,也就是栈段寄存器SS和栈指针SP所指向的内存区域。

给栈指定一片内存区域,区域的起始地址作为栈基址,存入栈基址寄存器SS中,另一端是动态变化的,用栈指针寄存器SP来指定。栈在使用过程中是向下扩展的,所以栈顶指针肯定小于栈底指针。栈中内存地址和访问地址一样,用“段基址SS的值*16+栈指针SP(段内偏移地址)形成的20位地址”。

硬件提供了相应的方法来存取栈。栈的出口和入口都是栈顶,push把数据压向哪里,它得知道栈顶在哪里才行。pop指令也一样,它得知道哪里是栈顶才能从栈中取出正确的数据。这正是栈指针寄存器SP的作用,此寄存器中的值是段内偏移地址,是栈顶相对于栈底的偏移量。
在这里插入图片描述
在这里插入图片描述

实模式下的ret

指令都是存在内存中的,CPU也有访问内存才能拿到要执行的指令。
对于指令来说,CS寄存器是代码段段基址,IP寄存器中的是代码段的段内偏移地址。经过寄存器CS*16后再加上IP寄存器的值,所得的和就是指令存放的内存地址。

call指令用来执行一段新的代码。需要ret返回原来地址。
ret(return) 指令的功能是在栈顶(寄存器ss:sp所指向的地址)弹出2个字节的内容来替换IP寄存器,ret指令不管里面的内容是不是地址,它只负责把当前栈顶处的内容弹出栈并用它为IP寄存器赋值。也就是ret可以返回它所保存的代码地址。这里,ret只置换了IP寄存器(偏移地址),不用换段基址,属于近返回。ret指令需要赋值维护栈顶指针,由于栈是从高到低地址发展,所以被回收的栈顶空间应该是使sp指针值变大,故ret指令会使sp指针+2。
retf(return far) 是从栈顶取得4字节,栈顶处的2字节用来替换IP寄存器,另外的2字节用来替换CS寄存器。retf指令也要负责维护栈顶指针,retf指令会使sp+4。
在这里插入图片描述

实模式下的call

在8086处理器中,有两个指令用于改变程序流程,一个是jmp,另一个是call。jmp属于一去不复返地执行新的代码,call指令用于执行完一段分支后再回来的情况。

16位实模式相对近调用:
call指令所调用目标函数和当前代码段是同一段,即在同一个64KB的空间内,所以只给出段内偏移地址就好。
“近”就是指在同一段内,不用切换段,不用换基址,只需给出段内偏移地址。
“相对”是指在同一个代码段内,所以只要给出目标函数的相对地址即可。
指令格式是call near 立即数地址(near表示在内存或寄存器中取2字节)。
指令中的立即数地址可以是被调用的函数名、标号、立即数,函数名同标号一样,它只是地址的人性化表示方法,最终都会被编译器转换为一个实际数字地址。不过并不是转换成它们的绝对地址,而是call指令相对于目标地址的偏移量,是个地址差。
在这里插入图片描述

16位实模式间接绝对近调用:
“间接”是指目标函数的地址没有直接给出,地址要么在寄存器中,要么在内存中,总之不以立即数的形式出现。
“绝对”是指目标函数的地址是绝对地址。
指令的一般形式是“call 寄存器寻址”或“call 内存寻址”。不同指令对应不同操作码,“call 内存寻址”对应的操作码是ff16,机器码是ff16+16位内存地址。

在这里插入图片描述

16位实模式直接绝对远调用:
“直接”就是操作数在指令中直接给出,是立即数。不需要经过寄存器或内存。
“远”指跨段,目标函数和当前指令不在同一段。
指令的一般形式是“call far 段基址(立即数):段内偏移地址(立即数)”

在这里插入图片描述

16位实模式间接绝对远调用:
段基址和段偏移地址都不是立即数,段基址和段内偏移地址都在内存中。
指令格式是“call far 内存寻址”
在这里插入图片描述

实模式下的jmp

jmp转移指令只有更新CS:IP寄存器或只更新IP寄存器,不需要保存它们的指,所以跳转到新的地址后没办法再回来。

16位实模式相对短转移:
“相对”指操作数是个相对增量,操作数范围为-128~127。
“短转移”指只在段内转移,不需要跨段,只需要偏移地址。
指令格式是"jmp short 立即数地址"。
在这里插入图片描述

16位实模式相对近转移:
操作数依然是地址相对量,范围为-32768~32767.
指令格式是“jmp near 立即数地址”,其操作码是0xe9。
在这里插入图片描述
16位实模式间接绝对近转移:
其目标地址是绝对地址,并且未在指令中直接给出,存在寄存器或内存中。
指令格式是“jmp near 寄存器寻址”或“jmp near 内存寻址”。
在这里插入图片描述

16位实模式直接绝对远转移:

在这里插入图片描述
在这里插入图片描述

16位实模式间接绝对远转移:
操作数不直接给出,放在内存中。
指令格式是“ jmp far 内存寻址”。
在这里插入图片描述

标志寄存器flags

flags寄存器是16位宽,保护模式下对其扩展成32 位的eflags寄存器。
在这里插入图片描述
在这里插入图片描述

有条件转移

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

让我们直接对显示器说点什么吧

CPU如何与外设通信——IO接口

微型计算机通过外部设备与外面的世界互换信息,外部设备种类繁多,远离各异,输出信息不同,数据格式不同,工作时序也不同,不可能让CPU一一适应它们。
任何不兼容问题,都可以通过增加一“层”来解决。在CPU和外设之间的这一层就是IO接口。它的作用就是在CPU和外设之间相互做协调转换
显卡就是一种IO接口,它是用来驱动显示器的。
在这里插入图片描述
在这里插入图片描述

IO接口功能:
在这里插入图片描述

CPU通过物理链路访问IO接口。物理链路就是一组电线,这条电线用于传送信号,故称为信号线。

同一时刻,CPU只能和一个IO接口通信,输入输出控制中心(ICH),也就是南桥芯片 负责仲裁IO口的竞争,同时还要连接各种内部总线。CPU通过内部总线连接到南桥芯片的内部,这个内部总线是专用的,它只通向位于南桥中的CPU接口。
南桥用于连接pci、pci-express、AGP 等低速设备,北桥用于连接高速设备,如内存。
在这里插入图片描述

IO接口中的寄存器称为端口,IO接口通过寄存器的方式同CPU通信。访问端口可以通过内存映射来访问,把一些内存地址作为端口的映射,访问这些内存地址就相当于访问了这些端口。还有一些微机系统把端口独立编址,把所有端口从0开始编码,位于一个IO接口上的所有端口号都是连续的。

在这里插入图片描述
在这里插入图片描述

显卡概述

mbr运行在实模式下,所以在实模式下可以通过用BIOS的0x10中断打印字符串,但中断向量表只在实模式下存在,BIOS中断是要依赖于中断向量表的。但将来要在保护模式下进行,保护模式下没有中断向量表。其次,不希望有更多的依赖,这需要依赖于BIOS。

BIOS中断例程中凡是设计向屏幕打印之类的功能,必然也是通过操作显卡实现的。
某些IO接口也叫适配器,适配器是驱动某一外部设备的功能模块。显卡也称为适配器,专门用来连接CPU和显示器。
显卡是pci设备,安装在主板的pci插槽上。
之后的输出都是通过直接操作显卡来实现的,而显卡给我们的输入接口是显存端口,我们主要用显存。

显存、显卡、显示器

显存是显卡提供的,它是位于显卡内部的一块内存。显卡的工作是不断读取这块内存,随后将其内容发送到显示器。显卡可以让显示器工作在图形模式和字符模式。屏幕是由密密麻麻的像素组成的,显存中的每一位都对应屏幕上的一个像素点。

在黑白图形模式中,显存位与屏幕像素是1对1的,因为只有两种颜色,所以只要显存中的对应位置为1,屏幕上的相应像素就被点亮,呈现的是白色。若该位为0,该像素就不会被点亮,只要不管该像素就是黑色,所以用黑色壁纸当桌面,才真正在物理上保护了显示器。而在真彩中,是用24位对应一个像素,所以才呈现出彩色。

显示器分不清给它的数据是文本,还是图像,在它眼里全是图像,粒度更细致点来说,全是像素信息,即像素的位置及像素的颜色。

将字符输出为图像的方法就是一个字符对应一个字节编码,只要往显存中写入这个编码,显卡就知道这是要打印此字符,由它帮你完成像素的拼凑。
在这里插入图片描述

各外部设备都是通过软件指令的形式与上层接口通信的,显卡也是,它也有自己的BIOS。位置是0xC0000到0xC7FFFF。显卡支持三种模式,文本模式、黑白图形模式、彩色图形模式。
显存地址分布:
在这里插入图片描述

从起始地址0xB8000到0xBFFFF,这片32KB大小的内存区域是用于文本显示。我们往0xB8000处输出的字符会落到显存中,显存有了数据,显卡就将其搬到显示器屏幕上了。

显卡的文本模式也分为多种模式,用“列数 * 行数”来表示,它们的乘积是整个屏幕上可以容纳的字符数。显卡在加电后,默认就置为模式80 * 25,也就是一屏可以打印2000个字符。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

改进的MBR,直接操作显卡

将之前的MBR改造一下,只修改有关输出的部分,即把通过BIOS的输出改为通过显存。

;主引导程序 
;
;LOADER_BASE_ADDR equ 0xA000 
;LOADER_START_SECTOR equ 0x2
;------------------------------------------------------------
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

   ; 输出背景色绿色,前景色红色,并且跳动的字符串"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


显存文本模式中,其内存地址是0xb8000。我们目前是在实模式下编程,实模式下内存分段访问策略是“段基址*16+段内偏移地址”。注意,要考虑到最终的段基址要乘以16,所以我们选择的段基址必须除以16以后的值。目标地址是0xb8000,除以16后是0xb800,偏移地址是0。这里是往gs寄存器中存入段基址。
显存段基址放在哪个寄存器中都没关系,对于访问的是数据来说,如果不用ds做段基址寄存器,就要在寻址中“显式地”指明要用哪个段寄存器的值作为段基址。

mov ax,0xb800
mov gs,ax

这里执行的mov操作都是往显存中写字符,如mov byte [gs:0x00],‘1’,就是往以gs为数据段基址,以0为偏移地址的内存中写入字符1的ASCII码。关键词byte用于指定操作数所占空间。byte占1字节,word占2字节,dword占4字节。
在这里插入图片描述

   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

编译MBR文件

nasm -o mbr.bin mbr.S

将生产的mbr.bin 写入虚拟硬盘

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

在这里插入图片描述

bochs调试方法

bochs一般方法

bochs是一个开源x86虚拟机软件。

在这里插入图片描述
下图中的第五行显示的是下一条待执行的指令,这是程序计数器(PC)中的值,在x86上的程序计数器是指cs:ip。cs是0xf000,ip是fff0,最终地址是0xffff0,这是BIOS的入口地址,即低端1M内存最顶端的16字节。在右边的jmp far f000:e05b,是指内存0xffff0处的内容,在内存中,内容不是普通数据,就是指令,在此次就是条跳转指令。1M内存最顶端只有16字节,肯定容不下完整BIOS代码,必然是条跳转指令。
在这里插入图片描述
xp或x指令可查看内容是以“显示单元”为单位,不是字节。“xp/nuf< addr >”,nuf是指一个数字序列,这三个参数,n用来分别指定要显示的“显示单元”数,u指“显示单元”大小,f是指要用哪种进制显示。最终要显示几个字节,是以“显示单元大小u*显示单元个数n”来决定的。addr 可以是以上三种进制的数字。
这里,我们通过xp/2 0xffff0显示0xffff0处的内存内容。由于默认xp以4字节来显示,所以、xp中斜杠后面指定的数字2,最终会让xp显示8个字节。注意,这是小端字节序。只看1个4字节,先从低地址看,最低位是ea,这是直接绝对远转移jmp far机器码,高位的是0xe05b,这是jmp far的操作数,待跳转到的地址。这与程序计数器(cs:ip)中指定的内容是吻合的

在这里插入图片描述

在这里插入图片描述

bochs的调试命令大体上分为“Debugger control”,“Execution control”,“Breakpoint management”,“CPU and memory contents”,在每个大类别中的关键字都是一个调试命令。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

bochs调试实例

调试思路:
首先,打开show int ,此命令是让bochs在中断发生时输出提示。这样就知道是在执行哪个命令时引起的中断
在这里插入图片描述
这个中断所调用的中断处理函数,其选择子时08,段内地址是0xc0001elb。(在保护模式下)

可以看看这个地址是哪个函数。
nm命令用来列出可执行文件的符号表及其地址,在不加参数的情况下,默认输出“地址”“符号类型”“函数名”。用法:nm二进制可执行文件。
grep命令是在输入信息中匹配出含有“参数”的行并打印出来。用法:grep “参数字符串” 输入文件或通过管道“|”。
由于nm会输出文件所有符号信息,所以用grep将此地址过滤出来。
在这里插入图片描述
t表示这是位于代码段的符号,intr##0x0d##entry是符号名。0x0d代表13号中断处理函数。这里是GP错误。

使用lb指令验证指令数是否正确。
在这里插入图片描述

sba指令设置时间点断点。
在这里插入图片描述
在这里插入图片描述

硬盘介绍

硬盘工作原理

盘片固定在主轴上随主轴高速转动,目前主流个人电脑硬盘上的转速是7200转/分钟。每个盘片分上下两面,每面都存储数据,每个盘面各由一个磁头来读取数据,故一个盘片上对应2个磁头。由于盘面与磁头是一一对应关系,故用磁头号来表示盘面。一方面盘片的自转,另一方面磁头的摆动,这两种动作的合成,使磁头能够读取盘片任意位置的数据。
盘片表明是用于存储数据的磁性介质,为了更有效管理磁盘,这些磁性介质被“格式化”成易于管理的格局,即将整个盘面划分为多个同心环,以圆心画扇形,扇形与每个同心环相交的弧状区域作为最基本的数据存储单元。这个同心环就称为磁道,而同心环上的弧状区域是扇形的一部分,故称之为扇区,它作为我们向硬盘存储数据的最基本单位,大小是512字节。我们写入的数据最终是写进了磁道上的扇区中。
磁道的编号和磁头一样也是从0开始的。相同编号的磁道组成的管状区域就称为柱面。当0面上的 某磁道空间不足时,其他数据写入第1面相同编号的磁道上。若新磁道空间不足,再写第2面相同编号的磁道上,直到同一柱面上的磁道都不都用时才会写到新的柱面上。
扇区编号与盘面和磁道不同,各磁道内的扇区是以1为起始编号的,并且只限于本磁道内有效,所以各个磁道间的扇区编号相同,下限都是以0起。
扇区-磁道-柱面

在这里插入图片描述

硬盘控制器端口

CPU针对硬盘的IO接口是硬盘控制器,不过硬盘和硬盘控制器是连接在一起的。
让硬盘工作,我们需要读写硬盘控制器的端口,也就是硬盘控制器的寄存器。

在这里插入图片描述
data寄存器负责管理数据,作用是读取写入数据,16位。在读硬盘时,硬盘准备好数据后,硬盘控制器将其放在内部缓冲区中,不断读此寄存器便是读出缓冲区中的全部数据。在写硬盘时,我们要把数据源源不断地输送到此端口,数据便被存入缓冲区里,硬盘控制器发现缓冲区中有数据了,便将此处的数据写入相应的扇区中。

Error寄存器只有在读取硬盘失败时有用,里面会记录失败的信息,尚未读取的扇区数在Sector count寄存器中。在写硬盘时,此寄存器叫Feature寄存器,有些命令需要指定额外参数,这些参数就写在Feature寄存器中。这两个寄存器虽然名字不同,但指的是同一个寄存器,8位寄存器。
Sector count寄存器用来指定待读取或待写入的扇区数。硬盘每完成一个扇区,就会将此寄存器的值减1,所以如果中间失败了,此寄存器中的值便是尚未完成的扇区。8位寄存器,最大值255,若指定为0,则表示要操作256个扇区。

LAB寄存器用来描述一个是扇区的地址。LABlow、LABmid、LABhigh都是8位。LABlow用来存储28位地址的第0-7位,LABmid存储8-15位,LABhigh存储16-23位。

device寄存器,8位。低4位用来存储LAB地址的第24-27位。第4位用来指定通道上的主盘或从盘,0代表主盘,1代表从盘。第6位用来设置是否启用LAB方式,1代表启用LAB方式(磁盘中扇区从0开始依次递增编码),0代表启用CHS模式(柱面-磁头-扇区来定位)。另外两位第5位和第7位是固定1,称为MBS位。

在这里插入图片描述
在这里插入图片描述

常用的硬盘操作方法

硬盘操作顺序:
在这里插入图片描述

数据传输方法:
在这里插入图片描述
在这里插入图片描述

让MBR使用硬盘

改造MBR

负责从硬盘上把loader加载到内存,并使用loader为内核准备好环境,将内核成功加载到内存并运行。

由于MBR是占据了硬盘的第0扇区(以逻辑LBA方式,扇区从0开始编号,若以物理CHS方式,扇区则从1开始编号),第1扇区是空闲的,但离得太近,所以把loader放到第2扇区。MBR从第2扇区中把它读出来。

;主引导程序 
;------------------------------------------------------------
%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

   ; 输出字符串:MBR
   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	   ;A表示绿色背景闪烁,4表示前景色为红色

   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	 ; 起始扇区lba地址
   mov bx,LOADER_BASE_ADDR       ; 写入的地址
   mov cx,1			 ; 待读入的扇区数
   call rd_disk_m_16		 ; 以下读取程序的起始部分(一个扇区)
  
   jmp LOADER_BASE_ADDR
       
;-------------------------------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:	   
;-------------------------------------------------------------------------------
				       ; eax=LBA扇区号
				       ; ebx=将数据写入的内存地址
				       ; ecx=读入的扇区数
      mov esi,eax	  ;备份eax
      mov di,cx		  ;备份cx
;读写硬盘:
;1步:设置要读取的扇区数
      mov dx,0x1f2
      mov al,cl
      out dx,al            ;读取的扇区数

      mov eax,esi	   ;恢复ax

;2步:将LBA地址存入0x1f3 ~ 0x1f6

      ;LBA地址7~0位写入端口0x1f3
      mov dx,0x1f3                       
      out dx,al                          

      ;LBA地址15~8位写入端口0x1f4
      mov cl,8
      shr eax,cl
      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	   ; 设置74位为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	   ;4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
      cmp al,0x08
      jnz .not_ready	   ;若未准备好,继续等。

;5步:从0x1f0端口读数据
      mov ax, di
      mov dx, 256
      mul dx
      mov cx, ax	   ; di为要读取的扇区数,一个扇区有512字节,每次读入一个字,
			   ; 共需di*512/2次,所以di*256
      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

这个%include是nasm编译器中的预处理指令,意思是让编译器在编译之前把boot.inc文件包含进来。

%include "boot.inc"

boot.inc中写的是关于加载器的配置文件。nasm中的语法是:宏名 equ 值,LOADER_BASE_ADDR和LOADER_START_SECTOR是两个宏名。LOADER_BASE_ADDR定义了loader在内存中的位置,MBR要把loader从硬盘读入后放到此处。LOADER_START_SECTOR定义了loader在硬盘上的逻辑扇区地址,即LAB地址。

;-------------	 loader和kernel   ----------
LOADER_BASE_ADDR equ 0x900 
LOADER_START_SECTOR equ 0x2

为函数rd_disk_m_16传递参数。由于汇编语言能够直接操作寄存器,所以其传递参数可以用寄存器,也可以用栈。这里,选择用eax、bx、cx寄存器来传递参数。函数名rd_disk_m_16的意思是“在16位模式下读硬盘”。

   mov eax,LOADER_START_SECTOR	 ; 起始扇区lba地址
   mov bx,LOADER_BASE_ADDR       ; 写入的地址
   mov cx,1			 ; 待读入的扇区数

跳到了rd_disk_m_16这个函数,之后还会返回

call rd_disk_m_16		 ; 以下读取程序的起始部分(一个扇区)

rd_disk_m_16函数

;-------------------------------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:	   
;-------------------------------------------------------------------------------
				       ; eax=LBA扇区号
				       ; ebx=将数据写入的内存地址
				       ; ecx=读入的扇区数
      mov esi,eax	  ;备份eax,将exa中的值先备份到esi,因为al在out指令中会被用到,会影响eax的低8位
      mov di,cx		  ;备份cx,cx的值会在读取数据时用到
;读写硬盘:
;1步:设置要读取的扇区数
      mov dx,0x1f2    ;sector count寄存器是用0x1f2来访问的
      mov al,cl
      out dx,al            ;读取的扇区数,out指令用于往端口中写入数据,dx寄存器是用来存储端口号的,al表示寄存器中的值

      mov eax,esi	   ;恢复ax

;2步:将LBA地址存入0x1f3 ~ 0x1f6

      ;LBA地址7~0位写入端口0x1f3
      mov dx,0x1f3    ;LABlower                   
      out dx,al                          

      ;LBA地址15~8位写入端口0x1f4
      mov cl,8
      shr eax,cl  ;逻辑右移指令,置换出地址的相应部分,写入相应的LBA
      mov dx,0x1f4   ;LABmid
      out dx,al

      ;LBA地址23~16位写入端口0x1f5
      shr eax,cl
      mov dx,0x1f5  ;LABhigh
      out dx,al

      shr eax,cl
      and al,0x0f	   ;lba第24~27位
      or al,0xe0	   ; 设置74位为1110,表示lba模式
      mov dx,0x1f6
      out dx,al

;3步:向0x1f7端口写入读命令,0x20 
      mov dx,0x1f7   ;Status寄存器
      mov al,0x20                        
      out dx,al

;4步:检测硬盘状态,由于寄存器还是0x1f7,所以不用为dx赋值
  .not_ready:    ;是个标号,循环从这里开始
      ;同一端口,写时表示写入命令字,读时表示读入硬盘状态
      nop  ;空操作,只是为了增加延迟
      in al,dx ;将Status寄存器中的值读入al寄存器
      and al,0x88	   ;4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
      cmp al,0x08      ;0x08做减法
      jnz .not_ready	   ;若未准备好,继续等。判断结果是否等于0,等于0读数据,不等于0继续循环读状态

;5步:从0x1f0端口读数据
      mov ax, di
      mov dx, 256
      mul dx       ;被乘数隐含在ax中
      mov cx, ax	   ; di为要读取的扇区数,一个扇区有512字节,每次读入一个字,
			   ; 共需di*512/2次,所以di*256
      mov dx, 0x1f0
  .go_on_read:
      in ax,dx  ;读数
      mov [bx],ax
      add bx,2	;每次读2字节,地址+2	  
      loop .go_on_read
      ret

创建/include/boot.inc文件

sudo mkdir include
ls
cd include/
sudo vim boot.inc

编译
在这里插入图片描述

nasm -I include/ -o mbr.bin mbr.S

写入硬盘,dd第三行显示了实际写入硬盘的数据大小,是512字节

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

在这里插入图片描述
在这里插入图片描述

实现内核加载器

loader是要经过实模式到保护模式的过渡,并最终在保护模式下加载内核

%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR ;0x900

; 输出背景色绿色,前景色红色,并且跳动的字符串"2 LOADER"
mov byte [gs:0x20],'2'
mov byte [gs:0x21],0xA4     ; A表示绿色背景闪烁,4表示前景色为红色

mov byte [gs:0x22],' '
mov byte [gs:0x23],0xA4

mov byte [gs:0x24],'L'
mov byte [gs:0x25],0xA4   

mov byte [gs:0x26],'O'
mov byte [gs:0x27],0xA4

mov byte [gs:0x28],'A'
mov byte [gs:0x29],0xA4

mov byte [gs:0x2a],'D'
mov byte [gs:0x2b],0xA4

mov byte [gs:0x2c],'E'
mov byte [gs:0x2d],0xA4

mov byte [gs:0x2e],'R'
mov byte [gs:0x2f],0xA4

jmp $		       ; 通过死循环使程序悬停在此


vim loader.S
sudo nasm -I include/ -o boot/loader.bin boot/loader.S

sudo dd if=/usr/local/boot/loader.bin of=/usr/local/boot/hd60M.img bs=512 count=1 seek=2 conv=notrunc


sudo bin/bochs -f boot/bochsrc.disk

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值