操作系统攻略:自己动手从零开始写操作系统——3.完善MBR

前置知识

本次开始前我们需要了解什么是地址:

地址只是数字,描述各种符号在源程序中的位置,它是源代码文件中各符号偏移文件开头的距离。由
于指令和变量所占内存大小不同,故它们相对于文件开头的偏移量参差不齐。源文件就像旅店一样,里面
的符号(指令、变量等)就像旅店里的房间,有单人间、双人间,虽然大小不同,但它们也需要被旅店管
理员编号,也就是每个房间都有房间号,这样房客通过房间号便能找到自己的住所。房间由旅店管理员给
编址,那源代码文件中各符号地址又是由谁来规划的呢?
编译器的工作就是给各符号编址。编译器根据所在硬件平台的特性,将源代码中的每一个符号(指令
和数据)都按照本硬件平台的特性分配空间,在不考虑对齐的情况下,这些符号在空间上都彼此相邻,连
续分布,它们在程序中距第一个符号的距离便是它们在程序中的地址。
本质上,程序中各种数据结构的访问,就是通过“该数据结构的起始地址+该数据结构所占内存的大
小”来实现的。题外话,这就解释了为什么定义变量要给出变量类型,因为变量类型规定了变量所占内存
大小,每种类型都有其对应的内存容量。
程序中定义的任何一个变量,在编译之后的可执行文件中都会占据一席之地。此变量在文件中的位置是编
译器来安排的。编译器是人设计的,所以如果是您来设计编译器,您会怎样规划其地址呢?
按理说,人只能创造出人的思维能想到的东西,所以无论人创造出什么,一定能够被人来理解,无非
是时间长短的问题。
编译器无论怎样安排程序中的数据,必然有个先后顺序,而占据第一位的数据,其地址便是整个程序的起始
地址,在它后面的其他数据依次排开就行了。若以整个程序的开头部分为基准,它的第一个数据所在的位置必然
也是在文件的开头,也就是偏移文件开头为0 的地方。第二个数据所在的位置是数据1 的起始(偏移为O) +数
据l 所占的内存大小。第n 个数据所在的位置便是数据n-1 的偏移+数据n-1 的内存空间。可见,数据的地址,
其实就是该数据相对整个程序开头的距离,即偏移量。

什么是section:

汇编语言中的section称为节,在有的编译器中同时支持segment和section这两个关键字,它们的功能都是在程序中宣称一个区域,这并不是Linux用户进程中的段。编译器提供的关键字section只是为了让程序员在逻辑上将程序划分为几个部分,因为它是伪指令,CPU都不知道有这个东西。所以程序员自己知道在哪个section中是什么就可以了,例如数据放在一个section中,指令放在另一个section中,这样便将指令和数据分开了。
section并不会对程序中的地址产生任何影响,在默认的情况下有没有section都一个样,section中数据的地址仍然是相对于整个文件的顺延,仅仅是在逻辑上让开发人员梳理程序之用。

什么是vstart:

section用vstart修饰后可以被赋予一个虚拟起始地址,它被用来计算在该section内的所有内存引用地址。既然是虚拟起始地址,也就是说根据此地址在文件中是找不到相关数据的。像section data vstart=0x900这句话,目的是让编译器将section中的数据的地址以vstart的值为起始,不再从程序开头算起,只有以程序开头算起的地址才是真实存在的。
其作用是,假设某段数据需要被程序以「0x900 为起始」的逻辑地址访问(比如硬件要求某数据必须映射到 0x900 处),但实际编译时该段数据可能存储在文件的其他位置(比如偏移 0x20)。此时用vstart=0x900,汇编器会自动按 0x900 为基准计算段内地址,避免手动计算偏移(如0x900 - 0x20)的麻烦。

CPU工作原理:

控制单元要取下一条待运行的指令,该指令的地址在程序计数器PC 中,在x86CPU 上,程序计数器就是CS: ip 。于是读取ip 寄存器后,将此地址送上地址总线, CPU 根据此地址便得到了指令,并将其存入到指令寄存器皿中。这时候轮到指令译码器上场了,它根据指令格式检查指令寄存器中的指令,先确定操作码是什么,再检查操作数类型,若是在内存中,就将相应操作数从内存中取回放入自己的存储单元,若操作数是在寄存器中就直接用了,免了取操作数这一过程。操作码有了,操作数也齐了,操作控制器给运算单元下令,开工,于是运算单元便真正开始执行指令了。ip 寄存器的值被加上当前指令的大小,于是ip 又指向了下一条指令的地址。接着控制单元又要取下一条指令了,流程回到了本段开头, CPU 便开始了日复一日的循环,由于CPU 特别不容易坏,所以唯一它停下来的条件就是停电。
在这里插入图片描述

缓存的应用:

(1 )浏览器内部都有也s 客户端,它先查询本地也s 缓存中是否有该域名的ip,如果有就直接去访问
该ip。如果没有,该由s 客户端先要查找自己主机所设置的dns 服务器,然后去该dns 服务器去查询ip 。
(2 )如果该dns 服务器本地缓存中有该域名的A 记录(域名与ip 地址的对应记录),则直接返回给浏
览器中的dns 客户端。没有该域名的A 记录,就通过递归的方式向上询问其他dns 服务器,也许问到了根
dns 服务器才找到了答案。于是这路上所有被询问过的dns 服务器,都将此域名对应的A 记录缓存到自己
的cache 中,以备下次再有相同域名查询时好直接返回。
(3 )浏览器中的dns 客户端得到此域名的ip 地址后,也将该域名和ip 放在自己的缓存中,以备下次
用户再键入同一域名时,避免再查一次ip 。
(4 )浏览器开始通过网络用h即协议访问该ip 地址的80 端口(默认是80 端口,除非特别指定)。
(5 ) 一般情况下该ip 对应的设备不是最终的Web 服务器,很少有人把Web 服务器直接暴露在公网。
假设该ip 对应的设备是台网关( 一般是硬件路由设备),该网关检查本地缓存中是否有相关Web 服务器的
缓存,若有则直接将该htφ 请求分配给缓存中的Web 服务器。否则从服务器列表中重新分配一台Web 服
务器,将该http 请求转发给该Web 服务器处理。随后将该Web 服务器的ip 地址(一般是内网地址)和端
口号缓存起来,以备下次该用户的请求到来时,依然给该Web 服务器。有的网关可以识别用户cookie 信
息,从而可以将请求再次落到上一个请求的Web 服务器上。
(6) Web 服务器拿到请求后,如果是静态请求,先检查自己的缓存中是否有该页面的记录,否则直接
从硬盘上取出页面,将其返回后,再存入本地静态缓存中。如果动态请求,先交给自己的cgi 去处理。
( 7) cgi 拿到请求后,先检查自己的缓存系统,如memcache ,如果缓存中没有,与数据库建立连接,
向数据库发出请求。
( 8 )数据库也是先检查自己的缓存,若没有结果集,则从表中检索到数据后返回,并将结果集缓存起来。
(9) cgi 拿到数据后,返回给Web 服务器,并将数据缓存到memcache 中。
(10) Web 服务器拿到了数据后,将数据返回给网关。由于是动态数据,不需要缓存。
( 11 )网关拿到数据后,直接返回给浏览器。
(12 )如果浏览器发现其中有静态数据,如图片,也将静态数据缓存到用户的internet 临时目录。

SRAM:

静态随机访问存储器是使用寄存器存储数据的,所以它快,而寄存器为什么快,是因为寄存器是使用触发器实现的,这也是一种存储电路,工作速度极快,是纳秒级别的。
在这里插入图片描述

实模式下内存分段的由来:

CPU本来没有实模式这一称呼,因为有了保护模式后,为了与老的模式区别开来,所以称老的模式为实模式。
x86中的x是个变量,它指代Intel所有86系列的产品,例如286、386、486等等。
在 8086 之前的 CPU 没有 “段” 的概念,程序访问内存时地址需 “硬编码” 写死,导致程序无法重定位,必须加载到内存固定位置。若该位置被占用,程序只能等待;即便修改地址重新编译,也可能因新地址被占用而无法运行,严重限制了内存使用效率和程序灵活性。而 8086 的出现改变了这一状况,因此被视为里程碑。
16 位寄存器(如 bx)最大只能表示0xFFFF(64KB),而 8086 需要访问 20 位地址空间(1MB),单一寄存器无法覆盖。即使段基址和偏移地址都取最大值0xFFFF,直接相加也只能得到 17 位地址(0x1FFFE),远不足 20 位,无法访问 1MB 空间。工程师的解决方案是将 16 位段基址左移 4 位(相当于乘以 16)扩展为 20 位,再加上 16 位偏移地址,最终形成 20 位物理地址。
按上述方法,当段基址和偏移地址都取最大值0xFFFF时:
段基址左移 4 位后为0xFFFF0(20 位),加上偏移地址0xFFFF,得到物理地址0xFFFF0 + 0xFFFF = 0x10FFEF。
但 8086 只有 20 条地址线(A0~A19),最大可访问地址为0xFFFFF(1MB),0x10FFEF超出了这一范围。由于 8086 没有第 21 条地址线(A20),超出 20 位的地址会被 “截断”,相当于对 1MB 空间取模,即地址回卷:
例如0x100001(21 位)会被截断为0x00001(只保留低 20 位)。
因此0x10FFEF实际等效于0x0FFEF,超出 1MB 的部分(0x100000到0x10FFEF)被称为高端内存区(HMA),但这部分内存物理上不存在,无需额外处理。
在这里插入图片描述

实模式下CPU的寻址方式:

寄存器寻址是指操作数在寄存器中,指令直接对寄存器进行操作(无论作为源或目的操作数),如将数据存入 ax、dx 寄存器或用 mul 指令计算 ax 与 dx 的乘积;立即数寻址的操作数是直接包含在指令中的常数(即立即数),因可直接使用、效率高而得名,宏和标号在编译后转换为数字也属于此类,例如 mov ax, 0x18 中 0x18 为立即数,该指令同时涉及寄存器寻址;内存寻址则是操作数在内存中,通过 “段基址:段内偏移地址”(默认段基址在 DS 寄存器,段内偏移地址为有效地址)访问,这是由于寄存器数量有限且多数操作数位置仅知地址未知值,使得内存寻址成为必要。
其中内存寻址又分为四种。直接寻址是用操作数中给出的数字作为内存地址(段内偏移地址),结合段地址计算最终地址,需注意与立即数寻址区分;基址寻址以 bx 或 bp 为基址寄存器(实模式下),bx 默认段寄存器为 DS,bp 默认为 SS,sp 是栈顶指针,专用于 push/pop 指令导航,不可随意修改,bp 可用于访问栈中数据,32 位环境下 ebp 应用于堆栈框架;变址寻址类似基址寻址,使用 si 和 di 作为变址寄存器,多用于字符搬运,单纯的变址寻址没什么新鲜的,它只是为了配合基址寻址,用来实现基址变址寻址;基址变址寻址则是基址寄存器与变址寄存器的结合,实模式下各有特定寄存器限制,保护模式下更灵活。
CPU 访问数据的方式看上去很死板(有些寄存器是规定的),原因是一种寻址方式对应一种电路实现,
增加一种寻址方式,会增加硬件电路的复杂性,所以寻址方式是有限的。

ret/retf:

call 指令执行目标函数后需返回,会提前将程序计数器(PC,x86 中为 CS:IP)中的返回地址存入内存的栈中(因栈后进先出的特性适配函数嵌套调用,且内存空间充足),具体保存内容取决于是否跨段访问。返回需依赖目标函数中的 ret 或 retf 指令:ret 为近返回,从栈顶弹出 2 字节赋给 IP,同时使 sp 指针 + 2;retf 为远返回,弹出 4 字节分别赋给 IP 和 CS,使 sp 指针 + 4,两者均不检查内容正确性,需程序员保证。call 与 ret/retf 需配合使用,近调用对应 ret,远调用对应 retf,否则会出错。

call:

8086处理器中,jmp和call是改变程序流程的指令,jmp执行新代码后不返回,适用于“交接”场景,call则用于执行分支后返回,需配合ret或retf,可实现函数调用。在实模式下,call指令调用函数有四种方式,前两种为近调用,后两种为远调用。其中第一种是16位实模式相对近调用,“近”指调用的目标函数与当前代码段相同,只需段内偏移地址,可用near修饰(可省略,默认取2字节,同word);“相对”指操作数是目标函数地址相对于当前call指令地址的偏移量(计算方式为目标地址减当前call指令地址再减该指令长度3字节),是有符号数,范围为-32768~32767,且因x86小端字节序,操作数低位在低地址、高位在高地址。该指令机器码为3字节(操作码0xe8占1字节,操作数占2字节),CPU执行时需将此相对增量还原为绝对地址,编译器会自动将函数地址转换为这种相对地址以适配硬件设计。
call调用方式的后三种中,第二种是近调用的直接近调用,其操作数来自寄存器或内存单元,直接获取段内偏移地址(同属近调用,无需段基址,在同一段内),调用时仅将IP压栈,对应ret返回。第三种是直接远调用,属于远调用,操作数为立即数形式的远地址(包含段基址和段内偏移),调用时会将当前CS(段基址)和IP(偏移)压栈,因目标函数在不同段,需用retf返回。第四种是间接远调用,同样为远调用,操作数来自内存单元,从内存中获取段基址和段内偏移,调用时也会压栈CS和IP,跨段调用,对应retf返回。这三种调用中,近调用均在同一段内,远调用则涉及跨段,且远调用的两种方式分别通过立即数和内存获取完整地址信息。

jmp:

无条件跳转指令jmp通过修改段寄存器CS和指令指针寄存器IP(或仅修改IP)改变程序流程,无需保存原寄存器值,属于“一去不回头”的转移。按是否跨段可分为近转移、远转移,还有更近的短转移,共5类,其中第一种是16位实模式相对短转移。其指令格式为jmp short 立即数地址(立即数可为标号,编译时转为地址),操作数是有符号的相对增量,机器码为2字节(操作码0xeb占1字节,操作数占1字节),跳转范围限于-128~127。操作数由编译器转换而来,计算方式为目标地址减当前jmp地址再减指令大小(2字节),CPU执行时需将操作数加IP值再加2字节得到绝对地址并载入IP,因在同一段内无需修改CS。关键字short指定编译为此形式,若操作数超出范围则编译失败,省略时编译器会根据操作数大小决定转移形式。
jmp后四类转移方式中,第二类是16位实模式相对近转移,指令操作数为2字节有符号数,跳转范围-32768~32767,机器码占3字节(操作码0xe9占1字节,操作数占2字节),操作数计算方式为目标地址减当前jmp地址再减3字节,CPU通过操作数加IP值加3字节得到绝对地址载入IP,无需修改CS,编译器会根据操作数大小自动选择短转移或近转移;第三类是直接近转移,操作数来自寄存器或内存单元,直接获取段内偏移地址,仅修改IP,在同一段内转移;第四类是直接远转移,操作数为立即数形式的远地址(含段基址和偏移),会同时修改CS和IP,实现跨段转移;第五类是间接远转移,操作数来自内存单元,从内存中获取段基址和偏移,同样修改CS和IP,用于跨段转移。这四类中,近转移(第二、三类)在同一段内仅改IP,远转移(第四、五类)跨段需同时改CS和IP。

有条件转移指令的判断条件来自标志寄存器,实模式下为 16 位的 flags,保护模式下扩展为 32 位的 eflags(兼容 flags),这些条件是上一条指令执行结果的特征标志,而非结果本身。eflags 寄存器包含多个标志位,不同 CPU 型号支持的标志位不同:

位位置标志位英文全称中文名称功能说明支持的CPU型号
0CFCarry Flag进位标志记录运算中最高位的进位或借位,有则为1,否则为0;用于检测无符号数加减法溢出8088以上
1、3、5、15---无专门标志位,空着占位-
2PFParity Flag奇偶标志标记结果低8位中1的个数,偶数为1,否则为0;用于数据传输的奇偶校验8088以上
4AFAuxiliary Carry Flag辅助进位标志记录运算结果低4位的进、借位情况,有则为1,否则为08088以上
6ZFZero Flag零标志位计算结果为0时为1,否则为08088以上
7SFSign Flag符号标志位运算结果为负时为1,否则为08088以上
8TFTrap Flag陷阱标志位为1时CPU进入单步运行方式,为0时连续工作;用于debug单步调试8088以上
9IFInterrupt Flag中断标志位为1时开启中断,CPU可响应外部可屏蔽中断;为0时关闭,不响应外部可屏蔽中断(仍响应内部异常)8088以上
10DFDirection Flag方向标志位用于字符串操作指令,为1时操作数地址自动减少一个单位,为0时自动增加;单位大小取决于指令8088以上
11OFOverflow Flag溢出标志位标识有符号数运算结果是否超出数据类型表示范围,超出为1,否则为08088以上
12~13IOPLInput Output Privilege LevelI/O特权级表示4种任务特权级(0、1、2、3),占用2位80286以上
14NTNest Task任务嵌套标志位任务中嵌套调用另一个任务时为1,否则为080286以上
16RFResume Flag恢复标志位用于程序调试,与调试寄存器配合,为1时忽略调试故障,为0时接受80386以上
17VMVirtual 8086 Model虚拟8086模式标志为1时可在保护模式下运行实模式程序,提供虚拟实模式环境80386以上
18ACAlignment Check对齐检查标志为1时进行地址对齐检查(检查地址是否为偶数、16/32的整数倍等),为0时不检查80486以上
19VIFVirtual Interrupt Flag虚拟中断标志位虚拟模式下的中断标志80586(奔腾)以上
20VIPVirtual Interrupt Pending虚拟中断挂起标志位多任务下为操作系统提供虚拟中断挂起信息,需与VIF配合80586(奔腾)以上
21IDIdentification识别标志位为1时表示CPU支持CPU id指令,可获取CPU型号、厂商等信息;为0时不支持80586(奔腾)以上
22~31---无实际用途,用于将来扩展-

有条件转移:

有条件转移是一个指令族(简称jxx),其格式为jxx 目标地址,功能是若条件满足则跳转到目标地址执行,否则顺序执行下一条指令。目标地址仅为段内偏移地址,实模式下编译器会根据当前指令与目标地址的偏移量编译为短转移或近转移,保护模式下因寄存器为32位,偏移地址可访问4GB内存,不再区分转移方式。条件转移的“条件”基于上一条指令对标志寄存器中标志位的影响,只有能影响标志位的指令才能作为其前置条件,jxx中的“xx”代表不同条件分类,对应不同转移指令。
有条件转移指令的“条件”来源于其前一条指令执行后对标志寄存器中标志位的改变,这些标志位的状态成为判断是否跳转的依据;同时,只有那些能对标志寄存器标志位产生影响的指令(如算术运算、比较等指令),才能作为有条件转移指令的前置指令,因为若前置指令不影响标志位,转移判断便会失去有效依据,无法实现正确的条件跳转。
在这里插入图片描述

I/O接口:

CPU与种类繁多、特性各异(如速度、信号电平、数据格式、时序不同)的外部设备直接交流存在诸多困难,因此在两者之间引入了I/O接口作为中间层。I/O接口形式多样(如电路板、芯片等),分为硬件和软件部分,硬件负责协调CPU与外设的不匹配(如数据缓冲、电平转换、格式转换、时序同步、地址译码),软件包括驱动程序等,可通过编程(如in/out指令)控制接口功能和工作模式,还可分为可编程与不可编程两类(前者功能更多,支持定制)。其核心作用是解决CPU与外设的速度、信号、格式、时序等不兼容问题,简化CPU访问外设的工作,例如显卡、声卡就是分别驱动显示器和音响的I/O接口,部分已集成在主板芯片组中。
CPU通过总线(即连接设备的公共线路)与I/O接口通信,同一时刻仅能与一个I/O接口交互,此时由输入输出控制中心(ICH,即南桥芯片)负责仲裁I/O接口的竞争并连接各种内部总线,南桥连接PCI等低速设备,北桥(部分CPU已集成)连接内存等高速设备,且南桥还集成必要I/O接口并通过PCI接口支持设备扩展。I/O接口通过内部寄存器(称为端口,区别于CPU内部寄存器,有8位、16位、32位等宽度,各端口用途不同)与CPU通信,访问端口的方式有内存映射(将内存地址作为端口映射)和独立编址(端口从0开始编号,同一I/O接口的端口号连续)两种,最终通过软件驱动硬件。
MBR在实模式下可使用BIOS的0x10中断打印字符串,因实模式存在中断向量表供BIOS中断依赖,但保护模式无中断向量表,故无法使用;且为减少依赖、直接与显卡交互,需通过操作显卡实现输出。显卡作为连接CPU和显示器的I/O接口(又称显示适配器),市面上分A卡(AMD阵营)和N卡(nvidia阵营),其核心由对应厂商研发,其他品牌多做本地化开发。早期显卡为PCI设备(并行总线),并行数据就要保证数据发送后必须同时到达目的地,因为这关系到数据的顺序,不能发过去后成一团乱麻。例如8 位并行总线就需要同时发送这8 位,接收方也要同时接收这8 位才行。虽然貌似并行传输是高效的,但对于要保证同时接收n 位数据,这是有困难的,随着并行数据的位宽越来越大,这种困难也越来越明显。于是串行传输很好地解决了这一问题。现多为PCI Express(PCIe,串行总线)设备,虽串行一次传1位,但传输频率高,速率高。操作显卡主要通过显存(将输出内容写入显存,显卡会据此向屏幕打印)和端口,后续输出将直接通过操作显卡实现。

显卡:

显卡作为外部设备,通过软件指令与上层通信,自身有BIOS(位于0xC0000到0xC7FFF),支持文本、黑白图形、彩色图形三种模式,我们主要关注文本模式以实现类Linux终端的字符界面。为提升效率,可直接向显存写入数据,显存地址范围为0xB8000到0xBFFFF(32KB),数据会由显卡显示到屏幕。
显卡文本模式以“列数×行数”表示,默认是80×25模式(一屏可显示2000个字符)。文本模式下每个字符由2个连续字节表示,低字节为字符ASCII码,高字节为属性信息:低4位是字符前景色,高4位是背景色(均通过RGB组合,第4位控制亮度),第7位控制字符是否闪烁。32KB显存可容纳约8屏80×25模式的字符数据(每屏字符占用4000字节)。

在文本模式下(如这段代码所操作的VGA文本模式),显卡会将特定显存区域的内容映射到屏幕上显示,但并非显存中所有内容都会被输出,具体取决于显卡的工作模式和显存布局:

  1. 文本模式的显存映射规则
    代码中使用的0xb8000(通过gs=0xb800作为段基址,实际访问地址为gs:偏移量,即0xb8000 + 偏移量)是VGA显卡文本模式下的默认显存地址。在这种模式下,显卡硬件会固定将该区域的内容解析为屏幕显示的字符和属性(每个字符占2字节:1字节ASCII码、1字节颜色/背景属性),并按顺序映射到屏幕的行和列(例如代码中向0xb8000开始的偏移量写入数据,对应屏幕左上角开始的位置)。

  2. 显存区域的划分
    显存并非单一连续区域都用于显示。除了文本模式的0xb8000区域,显存可能还有其他区域用于图形模式(如0xa0000等)、显卡内部寄存器等。只有当前工作模式对应的显存区域(如文本模式的0xb8000)会被显卡读取并输出到屏幕,其他区域的内容不会被显示。

  3. 显卡的硬件设计
    显卡的显示逻辑由硬件电路固化,在文本模式下,它会按照固定的规则扫描0xb8000区域的内容,将其转换为像素信号发送到显示器。这种映射关系是标准化的(如VGA标准),确保不同硬件和软件能兼容工作。

因此,并非显存中所有内容都会被输出,而是显卡根据当前工作模式(如文本模式),只读取对应显存区域的内容并显示——这段代码正是利用了文本模式下0xb8000区域的固定映射规则,才能让写入的字符在屏幕上显示出来。

正文

下面我们将操纵显卡来显示内容(上一篇的显示功能完全依赖 BIOS 的 0x10 中断服务,无需直接操作显存,而是通过 BIOS 封装的接口实现字符输出):

SECTION MBR vstart=0x7c00 ;起始地址编译在0x7c00
    mov ax,cs
    mov ds,ax
    mov es,ax
    mov ss,ax
    mov fs,ax
    mov sp,0x7c00
    mov ax,0xb800 ; ax为文本模式显存起始段地址
    mov gs,ax     ; gs = ax 作为访问显存的段基址

    ;清屏操作
    mov ax,0600h 
    mov bx,0700h
    mov cx,0
    mov dx,184fh
    int 0x10 

    ; 字符闪烁的关键:属性字节最高位(第7位)设为10x80; 每个属性字节 = 0x80(闪烁位) + 背景色(高4位) + 前景色(低4位)
    
    ; 'y':闪烁,黑底绿字(0x80 + 0x10 + 0x02 = 0x92)
    mov byte [gs:0x00],'y'
    mov byte [gs:0x01],0x92
    
    ; 'u':闪烁,蓝底青字(0x80 + 0x20 + 0x03 = 0xA3)
    mov byte [gs:0x02],'u'
    mov byte [gs:0x03],0xA3
    
    ; 'e':闪烁,青底红字(0x80 + 0x30 + 0x04 = 0xB4)
    mov byte [gs:0x04],'e'
    mov byte [gs:0x05],0xB4
    
    ; 'b':闪烁,红底紫字(0x80 + 0x40 + 0x05 = 0xC5)
    mov byte [gs:0x06],'b'
    mov byte [gs:0x07],0xC5
    
    ; 'u':闪烁,紫底黄字(0x80 + 0x50 + 0x06 = 0xD6)
    mov byte [gs:0x08],'u'
    mov byte [gs:0x09],0xD6
    
    ; 'y':闪烁,棕底白字(0x80 + 0x60 + 0x07 = 0xE7)
    mov byte [gs:0x0A],'y'
    mov byte [gs:0x0B],0xE7
    
    ; 'a':闪烁,白底蓝字(0x80 + 0x70 + 0x01 = 0xF1)
    mov byte [gs:0x0C],'a'
    mov byte [gs:0x0D],0xF1
    
    ; 'n':闪烁,灰底黑字(0x80 + 0x80 + 0x00 = 0x100?实际取低8位为0x00,结合高4位灰底为0x80,结果0x80)
    mov byte [gs:0x0E],'n'
    mov byte [gs:0x0F],0x80  ; 0x80 = 闪烁位1 + 灰底(0x80的高4位) + 黑字0x00
    
    jmp $ ;死循环保持显示
    
    times 510 - ($ - $$) db 0 
    db 0x55,0xaa
    

闪烁位原理:
显存中每个字符的属性字节(8 位)格式为:
bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
bit7:闪烁控制位(1 = 闪烁,0 = 不闪烁)
bit6~bit4:背景色(3 位,可结合 bit7 形成 8 种背景色)
bit3~bit0:前景色(4 位,支持 16 种颜色)
因此,只需将属性字节的最高位(bit7)设为1(即加上0x80),字符就会闪烁。
老样子:

nasm -o mbr.bin mbr.s
sudo dd if=/home/cooiboi/bochs/mbr.bin of=/home/cooiboi/bochs/hd60M.img bs=512 count=1 conv=notrunc
bin/bochs -f bochsrc.disk
c

在这里插入图片描述

磁盘:

结构上,主轴上有多个盘片(示意画两个),盘片随主轴高速转动(主流个人电脑硬盘转速 7200 转 / 分钟),每个盘片上下两面各有一磁头,磁头固定在磁头臂上,由步进电机驱动呈钟摆式弧线运动,磁头号从上到下以 0 开始计数代表盘面;存储逻辑上,盘面磁性介质被格式化,划分为同心环(磁道,有横截面用于存数据)和弧状区域(扇区,是 512 字节的基本存储单位),相同编号磁道组成柱面,利用柱面存取可减少寻道时间(盘面越多硬盘越快),扇区以 1 为起始编号且仅限本磁道有效,其头部包含磁头号、磁道号和扇区号以实现定位,通过盘片自转与磁头摆动的合成运动,磁头可读取盘片任意位置数据。
在这里插入图片描述

磁盘操作参考:

对于硬盘操作,由于指令众多且用法不同,需参考ATA手册,书中将其抽象出公共步骤:先选择通道并设置待操作扇区数,写入扇区起始地址低24位,在device寄存器中设置LBA地址相关位、模式及目标硬盘,写入操作命令,查询状态判断是否完成,读硬盘还需读出数据,且command寄存器需最后写入。

磁盘数据传输方式:

数据传送方式有五种:无条件传送方式(适用于随时准备好数据的设备,如寄存器、内存);查询传送方式(程序先检测设备状态,适用于低速设备,硬盘可用此方式);中断传送方式(设备准备好数据后发中断通知 CPU,效率较高);直接存储器存取方式(DMA,需硬件支持,设备与内存直接传输,不占用 CPU);I/O 处理机传送方式(需专用硬件,专门处理 I/O 及相关数据处理,彻底解放 CPU)。硬盘不适用无条件传送方式,DMA和I/O处理机方式需单独硬件支持故暂不考虑,书中系统采用查询传送和中断传送这两种软件方式。

对 MBR 的改进计划,目标是让 MBR 能够读取硬盘,其核心使命是从硬盘加载 loader 程序到内存并移交控制权。由于 MBR 受限于 512 字节大小,无法完成初始化环境和加载内核的任务,因此需要由 loader 来完成这些工作。计划将 loader 放在硬盘第 2 扇区(LBA 编号),从该扇区读取后加载到内存的 0x900 地址(选择此地址是考虑到 loader 中的数据结构不能被覆盖,且需为内核预留空间,该地址属于可用区域)。后续将基于此规划实现改进后的 MBR 代码。
mbr.s:

%include "boot.inc"  ; 包含外部常量定义(如LOADER_START_SECTORLOADER_BASE_ADDRSECTION MBR vstart=0x7c00 ;起始地址编译在0x7c00
    mov ax,cs
    mov ds,ax
    mov es,ax
    mov ss,ax
    mov fs,ax
    mov sp,0x7c00
    mov ax,0xb800 ; ax为文本模式显存起始段地址
    mov gs,ax     ; gs = ax 作为访问显存的段基址

    ;清屏操作
    mov ax,0600h 
    mov bx,0700h
    mov cx,0
    mov dx,184fh
    int 0x10 

    ; 字符闪烁的关键:属性字节最高位(第7位)设为10x80; 每个属性字节 = 0x80(闪烁位) + 背景色(高4位) + 前景色(低4位)
    
    ; 'y':闪烁,黑底绿字(0x80 + 0x10 + 0x02 = 0x92)
    mov byte [gs:0x00],'y'
    mov byte [gs:0x01],0x92
    
    ; 'u':闪烁,蓝底青字(0x80 + 0x20 + 0x03 = 0xA3)
    mov byte [gs:0x02],'u'
    mov byte [gs:0x03],0xA3
    
    ; 'e':闪烁,青底红字(0x80 + 0x30 + 0x04 = 0xB4)
    mov byte [gs:0x04],'e'
    mov byte [gs:0x05],0xB4
    
    ; 'b':闪烁,红底紫字(0x80 + 0x40 + 0x05 = 0xC5)
    mov byte [gs:0x06],'b'
    mov byte [gs:0x07],0xC5
    
    ; 'u':闪烁,紫底黄字(0x80 + 0x50 + 0x06 = 0xD6)
    mov byte [gs:0x08],'u'
    mov byte [gs:0x09],0xD6
    
    ; 'y':闪烁,棕底白字(0x80 + 0x60 + 0x07 = 0xE7)
    mov byte [gs:0x0A],'y'
    mov byte [gs:0x0B],0xE7
    
    ; 'a':闪烁,白底蓝字(0x80 + 0x70 + 0x01 = 0xF1)
    mov byte [gs:0x0C],'a'
    mov byte [gs:0x0D],0xF1
    
    ; 'n':闪烁,灰底黑字(0x80 + 0x80 + 0x00 = 0x100?实际取低8位为0x00,结合高4位灰底为0x80,结果0x80)
    mov byte [gs:0x0E],'n'
    mov byte [gs:0x0F],0x80  ; 0x80 = 闪烁位1 + 灰底(0x80的高4位) + 黑字0x00

    ;读取硬盘中的加载器
    mov eax,LOADER_START_SECTOR  ; eax存放loader所在的硬盘扇区(从boot.inc获取)
    mov bx,LOADER_BASE_ADDR      ; bx存放loader加载到内存的目标地址(从boot.inc获取)
    mov cx,1                     ; cx=1:读取1个扇区
    call rd_disk_m_16            ; 调用读盘函数
    jmp LOADER_BASE_ADDR         ; 读取完成后,跳转到loader的内存地址执行

;读盘函数
;备份寄存器并设置扇区数
rd_disk_m_16:
    mov esi,eax  ; 备份eax(LBA地址)
    mov di,cx    ; 备份cx(扇区数)
    ;0x1F2端口(扇区计数寄存器)写入要读取的扇区数
    mov dx,0x1F2  
    mov al,cl    ; al=要读取的扇区数(cx的低8位)
    out dx,al    ; 通过out指令向端口写入数据
    mov eax,esi  ; 恢复eax(LBA地址)

    ;设置LBA地址(定位扇区)
    mov cl,0x8   ; cl=8(用于右移8位)
    ;0x1F3端口(LBA8位寄存器)写入LBA地址的0~7位
    mov dx,0x1F3  
    out dx,al    
    ;0x1F4端口(LBA8位寄存器)写入LBA地址的8~15位(eax右移8位)
    mov dx,0x1F4  
    shr eax,cl    
    out dx,al    
    ;0x1F5端口(LBA8位寄存器)写入LBA地址的16~23位(eax再右移8位)
    mov dx,0x1F5  
    shr eax,cl    
    out dx,al    

    ;设置设备寄存器(指定硬盘和寻址模式)
    ;0x1F6端口(设备寄存器)写入LBA地址的24~27位,及模式设置
    shr eax,cl          ; eax右移8位,获取LBA24~27位
    and al,0x0f         ; 保留低4位(LBA24~27位)
    or al,0xe0          ; 设置高3位:0xe0=11100000(第6=1启用LBA模式;第4=0选主盘)
    mov dx,0x1F6        
    out dx,al           

    ;发送读扇区命令
    ;0x1F7端口(命令寄存器)写入读扇区命令(0x20)
    mov dx,0x1F7  
    mov ax,0x20    
    out dx,al      

    ;等待硬盘准备就绪
    .not_ready:
        nop                 ; 空指令延时
        in al,dx            ;0x1F7端口(状态寄存器)读取状态
        and al,0x88         ; 保留关键位:第7位(BSY,硬盘忙)和第3位(DRQ,数据就绪)
        cmp al,0x08         ; 若状态为0x08BSY=0DRQ=1),表示硬盘就绪
        jne .not_ready      ; 未就绪则继续等待

    ;读取数据到内存
    ; 计算读取次数:1个扇区=512字节,每次读2字节,需512/2=256次
    mov ax,di           
    mov dx,256          
    mul dx              ; ax=di*256(di=1,故ax=256)
    mov cx,ax           ; cx=256(循环次数)
    mov dx,0x1F0        ; 0x1F0端口(数据寄存器,16位,每次读2字节)
    .go_read_loop:
        in ax,dx            ; 从数据端口读取2字节
        mov [bx],ax         ; 写入bx指向的内存地址
        add bx,2            ; bx递增2(下一个2字节位置)
        loop .go_read_loop  ; 循环直到读完
        ret                 ; 函数返回

    jmp $ ;死循环保持显示
    
    times 510 - ($ - $$) db 0 ; 填充剩余空间到510字节
    db 0x55,0xaa  ; MBR结束标志(必须以0x55aa结尾,BIOS才认为是有效引导扇区)

rd_disk_m_16 是一个基于ATA协议的16位实模式硬盘读操作函数,以下从硬件原理和必要性角度详细剖析:

一、寄存器备份:mov esi,eaxmov di,cx

作用:
  • 备份 eax(存储LBA扇区地址)和 cx(存储要读取的扇区数)到 esidi
为什么需要:
  • 后续步骤中,eaxcx 的值会被修改(如拆分LBA地址、计算循环次数),而原始的LBA地址和扇区数在读取数据阶段还需使用(如计算总读取次数)。
  • 实模式下寄存器数量有限,必须通过备份保留原始参数,避免被中间操作覆盖。

二、设置扇区数:向0x1F2端口写入扇区数

mov dx,0x1F2  ; 扇区计数寄存器端口
mov al,cl     ; cl是cx的低8位(扇区数≤255)
out dx,al     ; 写入要读取的扇区数
作用:
  • 告知硬盘控制器需要读取的扇区数量。
为什么需要:
  • 硬盘控制器的 0x1F2端口 是「扇区计数寄存器」,专门用于存储待操作的扇区数(8位,最大值255,0表示256)。
  • 扇区数通过 clcx 的低8位)传递,因为硬盘控制器仅支持8位扇区数输入,且 out 指令要求数据必须通过 al(8位)传递。

三、拆分LBA地址:向0x1F3~0x1F5端口写入LBA低24位

; 0x1F3端口:LBA地址0~7位
mov dx,0x1F3  
out dx,al    
; 0x1F4端口:LBA地址8~15位(eax右移8位)
mov dx,0x1F4  
shr eax,cl    ; cl=8,右移8位取出8~15位
out dx,al    
; 0x1F5端口:LBA地址16~23位(eax再右移8位)
mov dx,0x1F5  
shr eax,cl    
out dx,al    
作用:
  • 向硬盘控制器传递LBA(逻辑块地址)的低24位,定位要读取的扇区。
为什么需要:
  • 函数使用 LBA28寻址模式(28位地址,支持最大128GB硬盘),但硬盘控制器的 0x1F3~0x1F5 端口仅能存储8位数据,因此需要将28位LBA地址拆分为3个8位部分,分别写入这三个端口:
    • 0x1F3:存储LBA的0~7位(最低8位);
    • 0x1F4:存储LBA的8~15位;
    • 0x1F5:存储LBA的16~23位。
  • 右移操作(shr eax,cl)是为了从32位寄存器 eax 中依次提取出这3个8位部分。

四、配置设备寄存器:向0x1F6端口写入LBA高4位及模式

shr eax,cl          ; 提取LBA的24~27位(高4位)
and al,0x0f         ; 保留低4位(仅需LBA的24~27位)
or al,0xe0          ; 设置高3位:启用LBA模式+主盘
mov dx,0x1F6        
out dx,al           
作用:
  • 指定硬盘(主盘/从盘)、启用LBA模式,并传递LBA地址的最高4位(24~27位)。
为什么需要:
  • 0x1F6端口是「设备寄存器」,8位结构定义如下(高4位关键):
    • 第6位:LBA模式开关(1=启用LBA,0=CHS模式);
    • 第4位:主从盘选择(0=主盘,1=从盘);
    • 第7、5位:固定为1(称为MBS位,硬件要求);
    • 低4位:存储LBA的24~27位(补全28位地址)。
  • and al,0x0f 确保只保留LBA的24~27位(低4位),or al,0xe0(二进制11100000)设置高3位为固定值(启用LBA+主盘),符合设备寄存器的硬件规范。

五、发送读命令:向0x1F7端口写入0x20

mov dx,0x1F7  ; 命令寄存器端口
mov ax,0x20    ; 读扇区命令码
out dx,al      
作用:
  • 向硬盘控制器发送「读扇区」命令,触发硬盘开始读取操作。
为什么需要:
  • 0x1F7端口在「写操作」时是「命令寄存器」,用于接收硬盘操作指令。
  • ATA协议中,0x20 是标准的「读扇区」命令码,硬盘控制器收到后会根据之前设置的LBA地址和扇区数执行读取。

六、等待硬盘就绪:查询0x1F7端口状态

.not_ready:
    nop                 ; 轻微延时
    in al,dx            ; 读取状态寄存器(0x1F7端口读操作时为状态寄存器)
    and al,0x88         ; 保留关键位:BSY(第7位)和DRQ(第3位)
    cmp al,0x08         ; 检查:BSY=0(不忙)且DRQ=1(数据就绪)
    jne .not_ready      ; 未就绪则循环等待
作用:
  • 等待硬盘完成扇区读取并准备好传输数据。
为什么需要:
  • 0x1F7端口在「读操作」时是「状态寄存器」,关键位定义:
    • BSY(第7位):1=硬盘忙(无法传输数据),0=空闲;
    • DRQ(第3位):1=数据就绪(可传输),0=未就绪。
  • 硬盘读取扇区需要时间(机械硬盘寻道+转动),CPU必须等待「BSY=0且DRQ=1」才能开始读取数据,否则会读到无效数据。
  • nop 是为了减少CPU查询频率,避免资源浪费;and al,0x88 仅保留BSY和DRQ位,简化状态判断。

七、读取数据:从0x1F0端口读取数据到内存

; 计算总读取次数:扇区数 × 256(每个扇区512字节,每次读2字节)
mov ax,di           
mov dx,256          
mul dx              ; ax = 扇区数 × 256
mov cx,ax           ; 循环次数存入cx(loop指令依赖cx)
mov dx,0x1F0        ; 数据寄存器端口(16位)
.go_read_loop:
    in ax,dx            ; 从数据端口读取2字节
    mov [bx],ax         ; 写入内存(bx指向目标地址)
    add bx,2            ; 内存地址+2(下一个2字节)
    loop .go_read_loop  ; 循环直到读完
作用:
  • 将硬盘控制器缓冲区中的数据读取到内存指定位置。
为什么需要:
  • 0x1F0端口是「数据寄存器」,16位宽度(每次可传输2字节),硬盘读取的扇区数据会先存入控制器内部缓冲区,再通过该端口传输给CPU。
  • 每个扇区512字节,因此每个扇区需要读取 512/2=256 次;若读取多个扇区(如cx=2),总次数为 2×256=512 次。
  • mul dx 计算总读取次数(扇区数×256),loop 指令通过cx递减控制循环,确保完整读取所有扇区数据。

总结:函数设计的核心逻辑

整个函数严格遵循ATA硬盘控制器的 「端口操作规范」「数据传输流程」,每一步都对应硬件要求:

  1. 先设置参数(扇区数、LBA地址、设备信息);
  2. 发送操作命令;
  3. 等待硬件就绪;
  4. 传输数据。

boot.inc:

LOADER_START_SECTOR equ 2
LOADER_BASE_ADDR equ 0x600

为了更加整洁,我们后面将按照这种方式组织目录
在这里插入图片描述

sudo nasm -I include/ -o ./project/c3/b/boot/mbr.bin ./project/c3/b/boot/mbr.s
sudo dd if=/home/cooiboi/bochs/project/c3/b/boot/mbr.bin of=/home/cooiboi/bochs/hd60M.img bs=512 count=1 conv=notrunc 

整体流程大概是:初始化环境→清屏→显示内容→从硬盘指定扇区读取 loader 到内存→跳转到 loader 执行。
然后编写loader.s:

%include "boot.inc"
SECTION MBR vstart=LOADER_BASE_ADDR
    mov byte [gs:0x00],0x00
    mov byte [gs:0x01],0xA4    
    
    mov byte [gs:0x02],'L' 
    mov byte [gs:0x03],0xA4
    
    mov byte [gs:0x04],'E' 
    mov byte [gs:0x05],0xA4
    
    mov byte [gs:0x06],'T' 
    mov byte [gs:0x07],0xA4
    
    mov byte [gs:0x08],'S' 
    mov byte [gs:0x09],0xA4
    
    mov byte [gs:0x0A],' ' 
    mov byte [gs:0x0B],0xA4
    
    mov byte [gs:0x0C],'G' 
    mov byte [gs:0x0D],0xA4
    
    mov byte [gs:0x0E],'O' 
    mov byte [gs:0x0F],0xA4
    
    mov byte [gs:0x10],' ' 
    mov byte [gs:0x11],0xA4
    
    mov byte [gs:0x12],'L' 
    mov byte [gs:0x13],0xA4
    
    mov byte [gs:0x14],'O' 
    mov byte [gs:0x15],0xA4
    
    mov byte [gs:0x16],'A' 
    mov byte [gs:0x17],0xA4
    
    mov byte [gs:0x18],'D' 
    mov byte [gs:0x19],0xA4
    
    mov byte [gs:0x1A],'E' 
    mov byte [gs:0x1B],0xA4
    
    mov byte [gs:0x1C],'R' 
    mov byte [gs:0x1D],0xA4
    
    mov byte [gs:0x1E],'!' 
    mov byte [gs:0x1F],0xA4
    
    jmp $
sudo nasm -I include/ -o ./project/c3/b/boot/loader.bin ./project/c3/b/boot/loader.s
sudo dd if=/home/cooiboi/bochs/project/c3/b/boot/loader.bin of=/home/cooiboi/bochs/hd60M.img bs=512 count=1 seek=2 conv=notrunc
sudo bin/bochs -f bochsrc.disk

在这里插入图片描述

总结

到此结束,下一篇进入保护模式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

钺不言

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值