news 2026/8/13 15:50:15

深入Linux 0.11引导启动:从实模式到保护模式的内核加载全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入Linux 0.11引导启动:从实模式到保护模式的内核加载全解析

1. 项目概述:从零启动一个操作系统内核

如果你对计算机底层感兴趣,想亲手触摸到操作系统最原始的脉搏,那么从零开始剖析一个经典内核的引导启动过程,无疑是最好的一课。Linux 0.11,这个由Linus Torvalds在1991年发布的早期版本,代码量适中,结构清晰,是无数人学习操作系统原理的“启蒙老师”。而“引导启动程序boot”,正是这个庞大系统被唤醒的第一个动作,它决定了计算机从按下电源键到屏幕出现字符之间,究竟发生了什么。

这个项目,或者说这个实验(常被称为HITOSLAB实验一),核心目标就是深入Linux 0.11内核的引导扇区代码,理解计算机加电后,CPU如何从实模式切换到保护模式,如何将内核从磁盘加载到内存,并最终将控制权交给内核的main函数。这不仅仅是读几行汇编代码,更是理解现代计算机体系结构、x86 CPU工作模式以及操作系统自举原理的绝佳实践。无论你是计算机专业的学生,还是对底层技术充满好奇的开发者,通过这个实验,你都能获得对“程序如何运行在裸机之上”这个问题最直观、最深刻的答案。

2. 核心需求与实验目标拆解

这个实验看似只是分析一段512字节的引导程序,但其背后串联的知识体系非常庞大。我们不能仅仅满足于“代码能跑通”,更要理解每一个字节、每一条指令背后的设计意图和硬件原理。

2.1 理解计算机加电后的初始状态

在深入代码之前,我们必须搞清楚我们的“舞台”是什么。当x86 PC加电或复位后,CPU处于一种被称为**实模式(Real Mode)**的古老状态。在这个状态下:

  • CPU只能访问最低端的1MB物理内存(地址范围0x00000到0xFFFFF)。
  • 没有虚拟内存、没有内存保护、没有特权级概念。
  • CPU从物理地址0xFFFF0(CS:IP = 0xF000:0xFFF0)开始执行指令,这个位置通常映射到主板ROM中的BIOS(Basic Input/Output System)程序。

BIOS会进行一系列硬件自检(POST),然后按照预设的启动顺序(如软盘、硬盘、光盘、网络)寻找可启动设备。对于硬盘,BIOS会读取其第一个扇区(512字节,即0柱面0磁头1扇区),这个扇区被称为主引导记录(MBR)。BIOS将这512字节内容加载到物理内存地址0x7C00处,然后跳转到那里开始执行。这512字节,就是我们的引导程序(bootsect.s)。理解这个“约定俗成”的地址0x7C00是分析所有引导代码的起点。

2.2 引导程序的三大核心任务

基于上述硬件约定,我们的引导程序(bootsect.s)需要完成三个核心任务,这也是本实验要逐行分析的:

  1. 将自己从0x7C00搬运到0x90000:这是为了给后续加载的操作系统内核腾出低端内存空间。0x7C000x90000之间有大约500KB的空隙,足够存放后续的setup模块和内核。
  2. 加载操作系统的第二个部分(setup模块)到内存:setup模块(setup.s)负责获取机器参数(如内存大小、显示模式)并完成从实模式到保护模式的切换。bootsect需要从磁盘上将其读入内存的指定位置(通常是0x90200)。
  3. 加载操作系统内核(system模块)到内存:这是最主要的部分。内核映像(由head.s和主要C代码编译链接而成)被加载到内存地址0x10000处。在Linux 0.11中,这里有一个精巧的设计:它先加载到0x10000,然后在适当的时候再移动到0x0000

2.3 实验的延伸目标:从理解到修改

对于HITOSLAB这类实验,分析理解原始代码是第一步。更深入的一步是进行修改和验证,例如:

  • 修改bootsect.s中打印的启动提示信息。
  • 调整内核加载的内存位置。
  • 甚至尝试支持从不同的设备(如虚拟软盘镜像)启动。 通过动手修改、重新编译、运行测试,你才能确信自己真正理解了每一行代码的作用,而不是停留在“好像懂了”的层面。

3. 环境搭建与工具链准备

工欲善其事,必先利其器。分析一个三十年前为特定硬件编写的汇编代码,我们需要一个能够准确模拟当时环境的工具链。

3.1 选择模拟器:Bochs vs QEMU

我们需要一个x86硬件模拟器来运行我们的内核。两个最流行的选择是Bochs和QEMU。

  • Bochs:这是一个高度可配置、专注于精确模拟x86硬件(包括芯片组、BIOS)的模拟器。它的调试功能极其强大,可以单步执行汇编指令、查看任意内存地址和寄存器状态。对于操作系统内核学习,尤其是需要深入跟踪硬件状态变化的场景,Bochs几乎是首选。它的缺点是速度相对较慢。
  • QEMU:这是一个快速的处理器模拟器,支持多种架构。它通常使用动态翻译来获得接近原生的速度,非常适合快速启动和测试。其调试功能(通过GDB stub)也很强大,但在某些硬件状态细节的呈现上不如Bochs直观。

对于Linux 0.11的学习,我强烈推荐使用Bochs。因为它能提供一个更“纯粹”、更符合当年硬件环境的模拟,并且其内置的调试器是理解引导过程的神器。你可以在Ubuntu/Debian上通过sudo apt-get install bochs bochs-sdl bochsbios vgabios来安装。

3.2 获取并准备Linux 0.11源码

Linux 0.11的源码很容易在网上找到。一个经典的版本是linux-0.11.tar.gz。解压后,你主要需要关注以下几个目录和文件:

  • boot/:引导程序目录,包含bootsect.ssetup.s
  • kernel/:内核核心代码。
  • tools/:包含构建工具,如build.c,用于生成最终的磁盘镜像。
  • Makefile:项目的编译构建脚本。

在开始之前,你可能需要根据现代编译环境做一些微小调整,比如将汇编器as和链接器ld的旧版本参数进行更新。不过,网上已有大量适配好的版本,例如“Linux 0.11实验环境”包,它通常已经配置好了编译脚本和Bochs配置文件,开箱即用。

3.3 配置Bochs运行环境

Bochs需要一个配置文件(通常命名为bochsrcbochsrc.bxrc)来定义虚拟机的硬件参数。一个针对Linux 0.11的最小化配置关键部分如下:

# bochsrc for Linux 0.11 megs: 16 # 模拟16MB内存,对0.11来说绰绰有余 romimage: file=/usr/share/bochs/BIOS-bochs-latest vgaromimage: file=/usr/share/bochs/VGABIOS-lgpl-latest floppya: 1_44=Image, status=inserted # 使用编译生成的Image文件作为软盘A boot: a # 从软盘A启动 log: bochsout.txt panic: action=ask error: action=report info: action=report debug: action=ignore

这里的关键是floppya,它指定了我们的启动镜像文件。Image文件是由源码编译后,通过tools/build.c程序将bootsectsetupsystem三个模块依次写入一个软盘镜像文件的前部生成的。Bochs会把这个文件当作一张虚拟软盘来读取。

注意:确保Image文件的路径在配置文件中正确。第一次运行时,Bochs可能会提示找不到键盘映射表等,根据提示安装相应包或调整配置即可。

4. 引导启动程序(bootsect.s)逐行精析

现在,让我们穿上“显微镜”,深入到bootsect.s这个仅有512字节的汇编文件中。我将结合代码、注释和硬件原理进行讲解。

4.1 第一阶段:自举与重定位

! bootsect.s (Linux 0.11) .globl begtext, begdata, begbss, endtext, enddata, endbss .text begtext: .data begdata: .bss begbss: .text SETUPLEN = 4 ! setup.s占用的扇区数(nr of setup-sectors) BOOTSEG = 0x07c0 ! bootsect的原始地址(BIOS加载地址) INITSEG = 0x9000 ! 我们将把bootsect移到这里 SETUPSEG = 0x9020 ! setup.s将被加载到这里 SYSSEG = 0x1000 ! system模块加载到0x10000(64KB处) ENDSEG = SYSSEG + SYSSIZE ! 停止加载的段地址 entry start start: mov ax,#BOOTSEG mov ds,ax ! ds = 0x07c0 mov ax,#INITSEG mov es,ax ! es = 0x9000 mov cx,#256 ! 计数器,移动256个字(512字节) sub si,si ! 源偏移 si = 0 sub di,di ! 目的偏移 di = 0 rep movw ! 重复执行movw,直到cx=0 jmpi go,INITSEG ! 段间跳转,CS=INITSEG, IP=go

代码解读与原理

  1. 定义常量SETUPLENBOOTSEG等定义了内存布局的关键地址。注意,在实模式下,段寄存器(如CS、DS)的值需要左移4位再加上偏移地址才能得到物理地址。所以BOOTSEG=0x07c0对应的物理地址是0x07c0 << 4 = 0x7c00
  2. 搬运自身rep movw是核心。ds:si指向源(0x7c00),es:di指向目标(0x90000)。cx=256表示移动256次,每次移动一个字(2字节),总计512字节。这是一次内存到内存的块复制。
  3. 跳转到新位置jmpi go, INITSEG。这条指令执行后,CS被设置为INITSEG0x9000),IP被设置为标号go的偏移地址。CPU接下来将从CS:IP0x9000:go处取指执行,也就是我们刚刚搬运过去的代码的go标签处。至此,bootsect成功将自己从0x7c00搬运到了0x90000,并继续执行。

实操心得:为什么是jmpi而不是jmp?因为jmp是段内跳转,只改变IP。而我们需要同时改变CS和IP,切换到新的代码段,所以必须用jmpi(跳转至中断,这里指段间跳转)。这是实模式编程的一个关键点。

4.2 第二阶段:加载setup模块

执行流跳转到go标签后,代码继续:

go: mov ax,cs ! 此时cs=0x9000 mov ds,ax ! 让ds, es, ss都指向我们所在的段(0x9000) mov es,ax ! 设置栈指针sp。栈向低地址增长,我们将其设在0x9ff00处,离代码有足够空间。 mov ss,ax mov sp,#0xFF00 ! 任意一个足够大的值,如0x9FF00 ! 接下来使用BIOS中断0x13来加载setup.s load_setup: mov dx,#0x0000 ! 驱动器0(DL),磁头0(DH) mov cx,#0x0002 ! 柱面0(CH),扇区2(CL)(注意:扇区号从1开始) mov bx,#0x0200 ! 缓冲区ES:BX = 0x9000:0x0200 -> 物理地址0x90200 mov ax,#0x0200+SETUPLEN ! AH=0x02(读扇区),AL=扇区数(SETUPLEN=4) int 0x13 ! 调用BIOS磁盘服务 jnc ok_load_setup ! 如果成功(CF=0),则跳转 mov dx,#0x0000 ! 如果失败,复位磁盘驱动器 mov ax,#0x0000 int 0x13 j load_setup ! 重试

代码解读与原理

  1. 初始化段寄存器与栈:将DS、ES、SS都设置为CS的值(0x9000),这样数据段、附加段和栈段都与代码段重合,简化了地址计算。栈指针SP被设置为0xFF00,这意味着栈顶在0x9FF00,栈向下(低地址)增长。
  2. BIOS磁盘读取:这是引导程序与硬件交互的关键。通过int 0x13调用BIOS的磁盘服务。
    • AH=0x02:功能号,表示“读扇区”。
    • AL:要读取的扇区数量,这里是SETUPLEN(4)。
    • CH:柱面号(0)。
    • CL:扇区号(2)。注意:BIOS的扇区编号从1开始,所以2表示磁盘上的第2个扇区(紧挨着引导扇区)。
    • DH:磁头号(0)。
    • DL:驱动器号(0表示第一个软盘驱动器)。
    • ES:BX:数据缓冲区地址。ES=0x9000BX=0x0200,所以物理地址是0x90200。这正是为setup模块预留的位置。
  3. 错误处理:读取成功后,进位标志CF会被清零(jnc跳转)。如果失败(CF=1),则先尝试复位磁盘驱动器(AH=0x00),然后重试。这种简单的重试机制在早期引导程序中很常见。

4.3 第三阶段:加载system模块与获取磁盘参数

成功加载setup后,代码继续加载内核的system模块,并获取磁盘参数以备后用。

ok_load_setup: ! 获取磁盘驱动器参数,特别是每个磁道的扇区数,后续加载system会用到。 mov dl,#0x00 ! 驱动器0 mov ax,#0x0800 ! AH=8,获取驱动器参数 int 0x13 mov ch,#0x00 ! 清空CX的高位(柱面数) and cl,#0x3f ! 取CL的低6位,即每磁道扇区数 mov sectors,cx ! 保存每磁道扇区数到变量`sectors` mov ax,#INITSEG mov es,ax ! 恢复ES为0x9000 ! 在屏幕上打印信息“Loading system ...” mov ah,#0x03 ! 功能3,读取光标位置 xor bh,bh ! 页号0 int 0x10 mov cx,#24 ! 字符串长度 mov bx,#0x0007 ! 页号0,属性7(浅灰色) mov bp,#msg1 ! 字符串地址 mov ax,#0x1301 ! 功能13,写字符串并移动光标 int 0x10 ! 现在开始加载system模块 mov ax,#SYSSEG ! SYSSEG = 0x1000 mov es,ax ! ES = 0x1000,目标段 call read_it ! 调用子程序read_it call kill_motor ! 关闭软驱马达 jmpi 0,SETUPSEG ! 跳转到setup.s开始执行(0x9020:0)

代码解读与原理

  1. 获取磁盘参数int 0x13, AH=0x08。返回的参数中,CL寄存器的低6位包含了每磁道的扇区数。这个值被保存到变量sectors中,因为后续的read_it子程序需要知道这个参数来计算读取位置,避免跨磁道时出错。
  2. 打印信息:通过int 0x10BIOS视频服务,在屏幕上显示“Loading system ...”。这提供了基本的用户反馈,是操作系统启动过程中我们看到的第一行信息。
  3. 加载system模块:这是最复杂的部分,被封装在read_it子程序中。它需要将内核的system模块从磁盘连续读取到内存0x10000(ES:0)开始的位置。由于内核可能很大(超过64KB),read_it需要处理多次磁盘读取,并妥善管理ES段寄存器(每次加载64KB后,ES需要增加0x1000)。
  4. 收尾与跳转:加载完成后,kill_motor子程序发送命令关闭软驱马达(省电并减少噪音)。最后,jmpi 0, SETUPSEG这条指令进行了一次关键的跳转:CS被设为SETUPSEG0x9020),IP被设为0。这意味着CPU将跳转到物理地址0x90200处执行——这正是我们刚刚加载的setup.s代码的起始位置。至此,bootsect.s的使命全部完成,控制权移交给了setup.s。

4.4read_it子程序:内核加载的核心

read_it是bootsect.s中最长的子程序,其逻辑是循环读取磁盘,直到将整个system模块加载完毕。其核心伪代码如下:

read_it: mov ax, es ! 检查当前ES是否已超过ENDSEG(加载结束边界) cmp ax, #ENDSEG jb ok1_read ret ! 如果已加载完毕,则返回 ok1_read: ! 根据当前已加载的段地址(ES),计算对应的磁头、柱面、起始扇区 ! 这个计算依赖于之前保存的每磁道扇区数(sectors) ! 计算公式是线性的:逻辑扇区号 = (ES - SYSSEG) * 32 (因为每个段64KB/512字节=128扇区?这里需要根据代码具体计算) ! 实际上,Linux 0.11的build工具在生成Image时,已经将system模块放在连续的扇区中。 ! 计算本次读取的扇区数:不能跨磁道读取。 ! 如果本次请求的起始扇区+欲读扇区数 > 每磁道扇区数,则只能读到当前磁道结束。 mov ax, #sectors sub ax, [当前起始扇区] mov [本次读取扇区数], ax ! 调用read_track子程序,读取一个磁道的数据 call read_track ! 读取完成后,调整ES(目标内存地址)和起始扇区号 ! 如果起始扇区号归零,说明进入下一个磁头或柱面 ! 循环,直到全部加载完成。

read_track子程序则封装了具体的int 0x13, AH=0x02调用。这里有一个关键细节:错误处理。在读取磁盘时,可能会遇到可恢复的错误(如磁头未就位)。read_track中通常包含一个重试循环,例如尝试5次,如果都失败才报错。

注意事项:理解read_it的关键在于理解实模式下的“段:偏移”地址与磁盘“柱面/磁头/扇区”(CHS)寻址之间的换算关系。虽然现代操作系统使用LBA(逻辑块地址),但BIOS时代必须使用CHS。bootsect.s中的换算逻辑是早期操作系统与硬件直接对话的经典范例。

5. setup.s:承上启下的桥梁

当bootsect跳转到0x90200后,setup.s开始执行。它的工作比bootsect更复杂,承担着从实模式到保护模式切换的重任。

5.1 获取并保存硬件参数

setup.s做的第一件事是利用BIOS中断获取机器系统数据,并保存在内存0x90000-0x901FF这块原本由bootsect使用的区域(bootsect已搬走,可复用)。这些参数包括:

  • 光标位置、扩展内存大小(>1MB的部分)、显示页面等。
  • 硬盘参数表(这对后续内核访问硬盘至关重要)。
  • 这些参数通过int 0x15int 0x10int 0x11int 0x12int 0x16等BIOS调用获得,并保存在内存的固定位置,以便内核的C代码在初始化时读取。

5.2 将system模块移动到0x0000

这是一个关键操作。在bootsect中,我们将system模块加载到了0x10000(64KB)处。现在,setup.s需要将它移动到物理内存最低端0x0000。为什么?

  1. 历史兼容性:实模式下,中断向量表(IVT)位于0x00000x03FF。在进入保护模式后,我们会使用全新的中断描述符表(IDT),这个区域就可以被覆盖用于存放内核代码。
  2. 简化寻址:内核代码和数据从物理地址0开始,使得线性地址到物理地址的映射在初期非常简单直接。 移动是通过rep movsw指令块完成的,目标地址是0x0000,源地址是0x10000

5.3 切换到保护模式

这是setup.s最核心、最精彩的部分。保护模式开启了现代操作系统内存管理、特权级保护的大门。切换步骤是标准化的:

  1. 关闭中断cli。在切换关键数据结构(如GDT)时,不能被中断打断。
  2. 加载全局描述符表寄存器(GDTR)
    lidt idt_48 ! 先加载一个空的IDT(因为目前还没启用中断) lgdt gdt_48 ! 加载GDT
    gdt_48是一个6字节的数据结构,包含GDT的线性基地址(32位)和界限(16位)。GDT(全局描述符表)定义了内存段的属性,是保护模式的基石。Linux 0.11的初始GDT非常简单,只包含几个条目:空描述符、内核代码段、内核数据段。
  3. 打开A20地址线:这是一个历史遗留问题。早期8086/8088 CPU只有20根地址线,可寻址1MB。为了兼容,第21根地址线(A20)默认被关闭。在保护模式下,我们需要访问超过1MB的内存,必须打开它。方法是通过向键盘控制器8042发送命令。
    call empty_8042 mov al,#0xD1 out #0x64,al call empty_8042 mov al,#0xDF out #0x60,al call empty_8042
  4. 设置保护模式标志:将控制寄存器CR0的第0位(PE位)设置为1。
    mov ax,#0x0001 lmsw ax ! 加载机器状态字(MSW),实际上就是设置CR0的低16位
    执行lmsw指令后,CPU正式进入保护模式。但此时CS的值还是实模式下的段选择子(0x9020左移4位是基地址),我们需要一个段间跳转来让CPU加载新的段描述符到CS。
  5. 跳转到保护模式下的代码
    jmpi 0,8 ! 跳转到段选择子8,偏移0处
    这里的8是段选择子。在GDT中,索引为1的条目(因为索引从0开始,8 = 1*8 + 0,其中RPL=0)对应的是内核代码段描述符。这个跳转指令会让CPU用选择子8去GDT中查找对应的描述符,加载到CS寄存器,并跳转到描述符定义的基地址(在Linux 0.11的初始GDT中,内核代码段基地址是0)加上偏移0处,也就是物理地址0x0000而那里,正是我们刚刚移动过去的system模块的头部,也就是head.s的起点。

6. head.s:保护模式下的初始化

head.s是用AT&T语法编写的汇编代码,运行在32位保护模式下。它主要完成以下工作:

  1. 重新设置GDT和IDT:setup中设置的GDT/IDT是临时的。head.s会设置更完整的GDT,并初始化IDT(虽然中断处理函数暂时都是空的)。
  2. 设置页目录和页表,开启分页机制:这是现代操作系统虚拟内存的核心。head.s会在物理内存中创建页目录表和4个页表,将线性地址0-16MB映射到相同的物理地址0-16MB(恒等映射)。然后设置CR3寄存器指向页目录,并设置CR0的PG位开启分页。
  3. 设置栈指针:为内核代码的执行设置一个栈。
  4. 调用C语言主函数:在完成所有底层初始化后,head.s使用call main指令跳转到内核的C语言入口函数main()(在init/main.c中)。至此,汇编语言的舞台彻底落幕,操作系统内核的C代码时代正式开始。

7. 实验操作、调试与问题排查

理解了原理,动手实验才能巩固知识。以下是在HITOSLAB实验一中可能遇到的操作和问题。

7.1 编译与运行步骤

假设你已有一个配置好的Linux 0.11实验环境(例如赵炯博士的《Linux内核完全注释》附带环境):

  1. 解压与进入目录tar zxf linux-0.11.tar.gz && cd linux-0.11
  2. 编译:在源码根目录执行make。这个命令会:
    • 编译bootsect.ssetup.s为二进制文件。
    • 编译head.s和所有内核C代码,链接成system模块。
    • 使用tools/build.c工具将bootsectsetupsystem依次写入Image文件,并确保bootsect正好512字节且末尾两个字节是启动标志0xAA55
  3. 运行:确保Bochs配置文件bochsrc中的floppya指向正确的Image文件路径,然后运行bochs -f bochsrc。如果一切正常,Bochs窗口会显示“Loading system ...”,然后出现一堆内核初始化信息。

7.2 使用Bochs进行调试

Bochs的强大之处在于其调试功能。你可以在配置文件中启用调试器,或者运行时按Ctrl+C中断。

  • 查看寄存器:在Bochs调试命令行输入rreg
  • 查看内存x /nuf addr。例如,x /40bx 0x7c00查看0x7c00处开始的40个字节,以十六进制显示。
  • 反汇编u start endu /num。例如,u 0x7c00从0x7c00开始反汇编。
  • 单步执行sstepnnext(跳过子程序调用)。
  • 设置断点vb seg:off(虚拟地址断点)或lb addr(线性地址断点)。例如,vb 0x7c00在bootsect开始处断点。
  • 连续执行c

一个经典的调试流程

  1. bochsrc中设置magic_break: enabled=1
  2. bootsect.sstart:标签后添加一行汇编指令:xchg %bx, %bx。在Bochs中,这条指令是“魔术断点”,执行到此会自动进入调试器。
  3. 重新编译运行,Bochs会在最开始暂停,此时你可以单步跟踪每一条指令,观察寄存器和内存的变化,直观地理解引导过程。

7.3 常见问题与解决方案实录

在实验过程中,你几乎一定会遇到以下问题:

问题现象可能原因排查思路与解决方案
make编译失败,提示as86ld86找不到缺少古老的8086汇编器/链接器安装bin86包:sudo apt-get install bin86。或者使用实验环境提供的已编译好的工具链。
makebuild工具编译/执行出错build.c与现代GCC不兼容检查build.c源码,通常需要将void main()改为int main(),并在文件开头添加#include <unistd.h>。网上有大量已修复的版本。
Bochs启动后黑屏,无任何输出1.Image文件未正确生成。
2. Bochs配置文件中磁盘镜像路径错误。
3. 模拟的磁盘类型不对(应为1.44MB软盘)。
1. 检查make输出是否有错误,确认Image文件大小合理(>100KB)。
2. 使用绝对路径指定floppya
3. 确保配置中是1_44=Image
屏幕显示“Loading system ...”后卡死或乱码1.system模块编译有问题。
2.read_it子程序加载内核时磁盘读取错误。
3. 内存移动(到0x0000)出错。
1. 使用Bochs调试器,在read_it和移动内存的代码处设断点,检查读取的扇区数、目标地址是否正确。
2. 检查tools/build.c,看它是否正确计算了system模块的大小并写入Image
进入保护模式后立即崩溃(Triple fault)1. GDT设置错误(基地址、界限、属性)。
2. 进入保护模式后的跳转指令(jmpi 0,8)中的选择子错误。
3. A20地址线未成功开启。
1. 在setup.s的lgdt gdt_48后暂停,使用Bochs命令info gdt检查GDT内容是否正确。
2. 单步执行到lmsw ax之后,检查CR0的PE位是否为1。
3. 检查A20开启代码,确保empty_8042子程序能正确等待键盘控制器空闲。
修改bootsect.s中的提示信息后,编译运行无变化修改后未重新编译,或未正确生成Image1. 执行make clean然后make彻底重新编译。
2. 确认修改的字符串长度未超过原字符串,否则需要调整打印代码(mov cx,#长度)。

实操心得:调试汇编的思维:调试引导程序不同于调试高级语言。你需要密切关注:1) **段寄存器(CS, DS, ES, SS)指令指针(IP/EIP)**的变化;2) **标志寄存器(EFLAGS)**的状态,特别是进位CF、零标志ZF;3)关键内存区域(如0x7c00, 0x90000, 0x90200, 0x10000)的内容。在Bochs中,善用x命令查看内存,用u命令反汇编当前指令,是定位问题的关键。

8. 实验的深度扩展思考

完成基础实验后,你可以尝试以下扩展,这将极大深化你的理解:

  1. 修改启动信息:这是最简单的扩展。找到bootsect.smsg1处的字符串“Loading system ...”,将其改为你自己的信息,如“Hello, OS World!”。注意字符串长度和mov cx,#24这行代码中的长度值需要匹配。
  2. 研究tools/build.c:这个工具是连接编译和启动的桥梁。它决定了bootsectsetupsystemImage文件中的布局。阅读其源码,理解它如何计算system模块的大小并将其写入bootsect.s中的一个变量(在编译时替换),这对于理解整个构建流程至关重要。
  3. 尝试从硬盘启动:Linux 0.11默认配置为从软盘启动。研究如何修改代码和Bochs配置,使其从一个硬盘镜像(如c.img)启动。这涉及到引导扇区代码识别硬盘分区表(MBR结构),并加载活动分区引导记录等更复杂的过程。
  4. 探究更现代的实现:对比现代GRUB等引导加载器,理解它们如何支持多重引导规范、文件系统识别、图形界面等复杂功能。你会发现,其核心原理——加载自身、读取磁盘、切换保护模式——与这个简单的bootsect.s一脉相承。

通过这个实验,你收获的不仅仅是一段汇编代码的分析能力,更是对计算机从加电到操作系统接管整个硬件的完整链路的深刻洞察。这种从“裸机”角度理解系统的能力,是区分普通应用开发者和资深系统开发者的重要标志。当你下次看到“Loading system...”时,你脑海中浮现的将是CPU寄存器、内存地址、磁盘扇区与机器指令交织而成的壮丽图景。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/13 15:50:09

C++快速排序从入门到工业级优化:原理、实现与性能调优

1. 项目概述&#xff1a;为什么是快速排序&#xff1f; 如果你写过C&#xff0c;尤其是刷过LeetCode或者准备过面试&#xff0c;那“快速排序”这四个字对你来说绝对不陌生。它几乎是算法世界里出场率最高的明星之一&#xff0c;也是面试官检验你基本功的经典考题。但很多人对它…

作者头像 李华
网站建设 2026/8/13 15:49:08

深入解析Dubbo SPI机制:从Java标准SPI到微服务扩展实战

1. 项目概述&#xff1a;为什么我们要深挖Dubbo的SPI&#xff1f; 如果你用过Dubbo&#xff0c;或者对Java微服务框架有所了解&#xff0c;那么“SPI”这个词你一定不陌生。它就像隐藏在Dubbo这座宏伟宫殿里的一扇魔法门&#xff0c;表面上看起来平平无奇&#xff0c;但一旦你掌…

作者头像 李华
网站建设 2026/8/13 15:48:48

终极指南:如何快速掌握Freeplane思维导图工具的高效使用技巧

终极指南&#xff1a;如何快速掌握Freeplane思维导图工具的高效使用技巧 【免费下载链接】freeplane Application for Mind Mapping, Knowledge Management, Project Management. Develop, organize and communicate your ideas and knowledge in the most effective way. 项…

作者头像 李华
网站建设 2026/8/13 15:48:43

MySQL数据库物理备份与恢复实战:XtraBackup与二进制日志全解析

1. 项目概述&#xff1a;当数据库遭遇“物理毁灭”时&#xff0c;我们如何力挽狂澜&#xff1f; 在数据库运维的日常里&#xff0c;最让人脊背发凉的场景&#xff0c;莫过于服务器硬盘突然“暴毙”。这不是指简单的逻辑删除或误操作&#xff0c;而是实实在在的物理介质故障——…

作者头像 李华
网站建设 2026/8/13 15:44:54

GIS-gdal-java.lang.NoSuchMethodError

背景概述 这段时间使用gdal的时候&#xff0c;出现了一个找不到方法的的问题&#xff0c;具体报错内容如下&#xff1a; java.lang.NoSuchMethodError: int org.gdal.gdal.Dataset.FlushCache()at it.geosolutions.imageio.gdalframework.GDALImageWriter.write(GDALImageWrite…

作者头像 李华