01_实验一_操作系统的启动start

实验一 操作系统的启动

从源代码到可运行的操作系统(前置知识)

API 与 SDK

以 C 语言编写的操作系统为背景进行介绍,EOS 是由 C 语言编写的

操作系统和应用程序之间一个重要的纽带就是应用程序接口(简称 API)。操作系统通过开放 API 为应 > 用程序提供服务,应用程序通过使用这些 API 实现其功能 。在操作系统或应用程序运行时,API 可能只是

一个简单的调用和被调用的关系。但是在编写操作系统的源代码时,必须要解决如何才能开放 API 的问题;

在编写应用程序的源代码时,又必须要解决如何才能使用 API 的问题。

SDK 是 Software Development Kit 的缩写,翻译成中文就是**"软件开发工具包"** 。操作系统通过向开发者提供 SDK 来开放其 API, 开发者在为操作系统编写应用程序时,通过使用 SDK 来调用 API。所以,如果要为操作系统开发应用程序,就需要首先获得操作系统的 SDK。

SDK: 一般采用文件的形式并结合特定的编程语言向开发者提供操作系统的 API。有些 SDK 还会提供相关的文档、编程范例和工具软件等。SDK 为了向开发者提供操作系统的 API,往往会包含头文件、导入库文件和动态链接库文件

  • 头文件

以特定编程语言(C、C++等)编写的文本文件,通常使用.H 做为后缀名。头文件的主要作用是导出操作系统使用的一些数据类型(例如操作系统中使用的结构体类型)和 API 函数的声明。

  • 导入库文件

是根据操作系统需要导出的 API 函数而生成的特定格式的二进制文件。

导入库文件在 Linux 中的后缀名是.A,在 Windows 中的后缀名是.LIB。导入库文件的主要作用是告诉应用程序的可执行文件,其调用的 API 函数在操作系统中的地址。导入库文件一般会被放在 SDK 中的 Lib(Library)

  • 动态链接库文件

包含了操作系统导出的 API 函数的可执行代码的二进制文件。例如 Windows 导出的 API 函数主要保存在 kernel32.dll、user32.dll 和 gdi32.dll 三个文件中。动态链接库文件在 Linux 中的后缀名是.SO,在 Windows 中的后缀名是.DLL。动态链接库文件的格式一般与可执行文件是相同的,只是不能直接执行。应用程序的可执行文件在执行时必须依赖这些动态链接库文件,因为其调用的系统 API 函数的可执行代码都保存在这些文件中。动态链接库文件一般会被放在 SDK 中的 Bin(Binary)文件夹中。Windows 提供的 SDK 中之所以没有包含动态链接库文件,是因为在 Windows 的系统目录中已经存在这些文件了。

EOS 操作系统内核从源代码变为可以在虚拟机上运行的过程

  • 在 IDE 环境将 EOS 操作系统内核包含的源代码文件生成为二进制文件时,会将 boot.asm 文件生成为 boot.bin 文件
    (软盘引导扇区程序),将 loader.asm 文件生成为 loader.bin 文件(加载程序),将其它的源代码文件生
    成为 kernel.dll 文件和 libkernel.a 文件。

  • 其中 kernel.dll 文件是 EOS 操作系统的内核,EOS 操作系统的 API 函数就是从此文件导出的,所以在生成此文件的同时还要生成配套的导入库文件 libkernel.a。

  • 当 EOS 应用程序的可执行文件与导入库文件 libkernel.a 链接后,就可以在执行时调用 kernel.dll 文件导出的 API 函数了。

  • IDE 环境成功生成 EOS 的二进制文件后,会自动生成 EOS SDK。IDE 环境会首先新建一个 SDK 文件夹,然后将 eos.h(导出 API 函数的声明)、eosdef.h(导出数据类型的定义)和 error.h(导出错误码)三个头文件复制到 SDK 文件夹中的 INC 文件夹中。

  • 将生成的四个二进制文件都复制到 BIN 文件夹中(EOS SDK 为了简单,将导入库文件也放入了 BIN 文件夹,而没有使用 LIB 文件夹)。

  • 疑问:

    EOS 操作系统的 API 函数不是从 kernel.dll 文件导出的吗?为什么还要将 boot.bin 和 loader.bin 放入 SDK 中呢?在后面的介绍中读者会得到这个问题的答案。

  • 在 IDE 环境启动执行 EOS 操作系统时,会将 boot.bin、loader.bin 和 kernel.dll 三个二进制文件写入软盘镜像文件中,然后让虚拟机来执行软盘中的 EOS 操作系统。

  • 这三个二进制文件是 EOS 操作系统运行所必需的,之后的学习中会知道学会他们的作用。

  • EOS 内核源代码可以生成两种不同版本的二进制文件:DEBUG 版本(有调试信息)和 RELEASE 版本(无调试信息)。IDE 环境会将 DEBUG 版本的二进制文件复制到 SDK/BIN/DEBUG 文件夹中;将 RELEASE 版本的二进制文件复制到 SDK/BIN/RELEASE 文件夹中。相应的,EOS 应用程序也会有 DEBUG 版本和 RELEASE 版本的可执行文件,并且只能与对应版本的 EOS 内核二进制文件一起使用。

EOS 应用程序从源代码变为可以在虚拟机上运行的过程

  • 在编写 EOS 应用程序的源代码之前,必须首先获得 EOS SDK 文件夹。然后,在 EOS 应用程序的头文件 eosapp.h 中包含 SDK/INC 文件夹中的三个头文件。图中用点划线表示。
  • 实际上,eosapp.h 只需要包含 eos.h 文件就即可,因为在 eos.h 文件中已经包含了 eosdef.h 和 error.h 文件。
  • Eosapp.c 源文件和各个头文件的包含关系可以参见下图。
  • 在 IDE 环境将 EOS 应用程序包含的源代码文件生成为二进制文件时,链接器会将由 eosapp.c 文件和 C 运行时库源代码文件生成的目标文件与 SDK/BIN 文件夹中的导入库文件 libkernel.a 一同链接,生成 EOS 应用程序的可执行文件 eosapp.exe。
  • 之前提出的为什么要将 boot.bin 和 loader.bin 也放入 SDK ?原因如下:
  • 在 IDE 环境运行 EOS 应用程序时,会将 SDK/BIN 文件夹中的 boot.bin、loader.bin 和 kernel.dll 写入软盘镜像,这样 EOS 操作系统才能够启动运行。同时会将 EOS 应用程序的可执行文件 eosapp.exe 和一个内容为"eosapp.exe"的文本文件 autorun.txt 写入软盘镜像,这样在 EOS 操作系统成功启动后,就会自动运 autorun.txt 文件中记录的应用程序可执行文件 eosapp.exe。

EOS 编程基础

EOS 内核源代码的结构

在"项目管理器"窗口中可以看到如图所示的结构。

各个结构的文件夹各自的说明如下表格:

文件夹或文件 说明
api/ 文件夹包含了内存管理器的源代码文件。此模块公开的信息在 inc/mm.h 头文件中声明,供其他模块使用。
bochs/ 包含了内核对象管理器的源代码文件。此模块公开信息在 inc/ob.h 头文件中声明,供其他模块使用
boot/ 只包含了两个汇编文件 boot.asm 和 loader.asm。这两个文件生成的二进制文件 boot.bin 和 loader.bin 会被写入软盘文件
inc/ 此文件包含三个公共头文件 eos.h eosdef.h error.h,会被复制到 SDK 文件夹的 INC 子文件夹中。文件还包括了各个模块用于公开其信息的头文件,通过包含这些头文件,各个模块间可以相互提供服务。
io/ 包括了 IO 管理器以及各种设备驱动程序的源代码文件。此模块公开的信息在 inc/io.h 头文件中声明,供其他模块使用。
ke/ 包含了 EOS内核管理功能和系统进程的源代码文件。内核管理功能包括了内核初始化、中断管理、时钟管理和系统进程等。其中 start.c 文件包含了内核的入口函数 KiSystemStartup 的源代码,sysproc.c 文件包含了系统进程的源代码。此模块的公开信息在 inc/ke.h 头文件中声明,供其他模块使用。
mm/ 包含了内存管理器的源代码文件。此模块公开的信息在 inc/mm.h 头文件中声明,供其他模块使用。
ob/ 包含了内核对象管理器的源代码文件。此模块公开的信息在 inc/ob.h 头文件中声明,供其他模块使用
ps/ 包含了进程、线程管理器的源代码文件。此模块的公开信息在 inc/ps.h 头文件中声明,供其他模块使用。
rtl/ 此文件包含了内核运行时库的源代码文件。内核运行时库为内核各个模块提供了一些常用的函数和数据结构算法,例如 C 运行时库中的字符串函数在源文件 crt.c 中实现,链表数据结构算法在源文件 list.c 中实现。此模块公开的信息在 inc/rtl.h 头文件中声明,供其他模块使用。
Floppy.img 软盘镜像文件。由 EOS 内核源代码文件生成的二进制文件会被写入此软盘镜像文件中。然后虚拟机就可以执行软盘中的 EOS 操作系统。直接双击此文件,可以使用 Floppy Image Editor 工具打开此文件,并编辑软盘中的内容。
License.txt 《EOS 核心源代码协议》

另外,有些文件夹中还包含了名称为"i386"的子文件夹,这些子文件夹中包含了和本模块相关的 X86 硬件平台特有功能的源代码。

预定义的 C 数据类型

待学习 ing。

EOS 的启动过程

虽然操作系统可以从多种存储设备启动 ,例如硬盘、软盘、光盘和闪存等。但是无论操作系统从哪种设备启动,其基本原理都是一样的:BIOS 程序首先将存储设备的引导记录(Boot Record)载入内存 ,并执行引导记录中的引导程序;然后,引导程序会将存储设备中的操作系统内核载入内存 ,并进入内核的入口点开始执行 ;最后**操作系统内核完成系统的初始化,**并允许用户与操作系统进行交互。

BIOS 程序的执行过程

BIOS(Basic Input/Output System)是基本输入输出系统 的简称。BIOS 能为电脑提供最低级、最直接的硬件控制与支持,是联系最底层的硬件系统和软件系统的桥梁。 为了在关机后使 BIOS 不会丢失,早期的 BIOS 存储在 ROM 中 ,并且其大小不会超过 64KB;而目前的 BIOS 大多有 1MB 到 2MB ,所以会被存储在**闪存(Flash Memory)**中。

BIOS 用到的 CPU、软盘、硬盘、显卡、内存等系统硬件配置信息,会和计算机的实时时钟信息一起存放在一块可读写的 CMOS 存储器中。当关机后,系统会通过一块后备电池向 CMOS 供电 ,以确保其中的信息不会丢失

BIOS 作用:

  • 自检及初始化

    CPU 加电后会首先执行 BIOS 程序,其中 POST(Power-On Self-Test)加电自检程序是执行的第一个例行程序,主要是对 CPU、内存等硬件设备进行检测和初始化。

  • 设定中断

    BIOS 中断调用即 BIOS 中断服务程序 ,是计算机系统软、硬件之间的一个可编程接口 。开机时,BIOS 会通知 CPU 各种硬件设备的中断号,并提供中断服务程序。软件可以通过调用 BIOS 中断对软盘驱动器、键盘及显示器等外围设备进行管理。

  • 将 CPU 移交操作系统

    BIOS 会根据在 CMOS 中保存的配置信息来判断使用哪种设备 启动操作系统,并将 CPU 移交给操作系统使用。例如,如果是从软盘启动操作系统,BIOS 会在完成自检及初始化后,将软盘的引导扇区加载到物理内存的 0x7C00 处 ,然后让CPU 执行软盘引导扇区中的引导程序(从 0x7C00 处执行),然后启动操作系统。
    CPU 在加电瞬间,其各个寄存器会自动初始化为默认值,其中CS 和 IP 寄存器的默认值指向了 BIOS 程序的第一条指令 ,从而使 CPU 开始执行 BIOS 程序。BIOS 程序在执行一些必要的开机自检和初始化后,会将自己复制到从 0xA0000 开始的物理内存中并继续执行。 然后,BIOS 开始搜寻可引导的存储设备 。如果找到,则将存储设备中的引导扇区读入物理内存 0x7C00 处 ,并跳转到 0x7C00 继续执行,从而将 CPU 交给引导扇区中的 Boot 程序。

  • 在 CPU 中**,CS 的全拼为"Code Segment"** ,翻译为"代码段寄存器",对应于内存中的存放代码的内存区域,用来存放内存代码段区域的入口地址(段基址)。

大小为 512 字节)就会 被 BIOS 程序加载到物理内存的 0x7C00 处。此时的物理内存如图 3-2 所示。其中,常规内存(640K)与上位内存(384K)组成了在实模式下 CPU 能够访问的 1M 地址空间。并且此时只有两个区域的空白物理内存可供正在运行的 Boot 程序使用,即"用户可用(1)"和"用户可用(2)"。

Boot 程序的执行过程

Loader 程序的执行过程

内核的初始化过程

实验正式开始

准备过程

  • 在"项目管理器"窗口中打开 boot/boot.asm 和 boot/loader.asm 两个汇编文件。boot.asm 是软盘引导扇区程序的源文件,loader.asm 是加载程序的源文件。简单阅读一下这两个文件中的 NASM 汇编代码和注释。
  • 生成项目。
  • 生成完成后,使用 Windows 资源管理器打开 项目文件夹中的 Debug 文件夹。找到由 boot.asm 文件生成的软盘引导扇区程序 boot.bin 文件 ,该文件的大小一定为 512 字节 (与软盘引导扇区的大小一致)。找到由 loader.asm 生成的加载程序 loader.bin 文件 ,记录下此文件的大小 1566 字节,在下面的实验过程中会用到。找到由其它源文件生成的 EOS 操作系统内核文件 kernel.dll。

调试 EOS 操作系统的启动过程

使用 Boch Debug 作为远程目标机

  • 在"项目管理器"窗口中,右键点击项目节点,在弹出的快捷菜单中选择"属性"。
  • 在弹出的"属性页"对话框右侧的属性列表中找到"远程目标机"属性,将此属性值修改为"Bochs Debug"(此时按 F1 可以获得关于此属性的帮助)。
  • 点击"确定"按钮关闭"属性页"对话框。接下来就可以使用 Bochs 虚拟机提供的调试功能单步调试 BIOS 程序和 EOS 操作系统的软盘引导扇区程序了。

调试 BIOS 程序

  • 启动调试,此时会弹出两个 Bochs 窗口。标题为"Bochs for windows - Display"的窗口相当于计算机的显示器 ,用于显示操作系统的输出 。标题为"Bochs for windows - Console"的窗口是 Bochs 的控制台 ,用来输入调试命令,输出各种调试信息。
  • 启动调试后,Bochs 会在 CPU 要执行的第一条指令(即 BIOS 的第一条指令)处中断。 此时,Display 窗口还没有显示任何内容,Console 窗口会显示将要执行的 BIOS 第一条指令的相关信息,并等待用户输入调试命令
  • 行首的[0xfffffff0]表示此条指令所在的物理地址。
  • f000:fff0 表示此条指令所在的逻辑地址(段地址:偏移地址),如果将其转换为物理地址,则得到的物理地址与行首显示的物理地址是一致的。
  • jmp far f000:e05b 是此条指令的反汇编代码。
  • 行尾的 ea5be000f0 是此条指令的十六进制字节码,可以看出此条指令长度为 5 个字节。

查看 CPU 在没有执行指令之前主要寄存器和内存中的数据:

  1. 在 Console 窗口中输入调试命令 sreg 后按回车,显示当前 CPU 中各个段寄存器的值,如图 10-2。其中 CS 寄存器信息行中的"s=0xf000"表示 CS 寄存器的值为 0xf000。
  2. 输入调试命令 r 后按回车,显示当前 CPU 中各个通用寄存器的值,如图 10-3。其中"rip: 0x00000000:0000fff0"表示 IP 寄存器的值为 0xfff0。
  3. 输入调试命令 xp /1024b 0x0000 ,查看开始的 1024 个字节的物理内存。在 Console 中输出的这 1K 物理内存的值都为 0,说明 BIOS 中断向量表还没有被加载到此处。
  4. 输入调试命令 xp /512b 0x7c00 ,查看软盘引导扇区应该被加载到的内存位置。输出的内存值都为 0,说明软盘引导扇区还没有被加载到此处。
  5. 通过以上的调试步骤可以验证 BIOS 第一条指令所在逻辑地址中的段地址和 CS 寄存器 值是一致的,偏移地址和 IP 寄存器的值是一致的。由于此时还没有执行任何指令,内存也还没有被使用,所以其中的值都为 0。
  • "段地址" 通常是指在计算机存储器中的一个段(segment)的地址。在 x86 架构的计算机中,内存地址通常由两个部分组成:段地址和偏移地址。 段地址指的是存储器中的段的起始地址,而偏移地址则是相对于段起始地址的偏移量。

调试软盘引导扇区程序

BIOS 在执行完自检和初始化工作后,会将软盘引导扇区(512 字节)加载到物理地址 0x7c00-0x7dff 位置,并从 0x7c00 处的指令开始执行引导程序,所以接下来练习从 0x7c00 处调试软盘引导扇区程序:

以上这个是软盘引导扇区被加载后的物理内存的布局。

  1. 输入调试命令 vb 0x0000:0x7c00,这样就在逻辑地址 0x0000:0x7c00(相当于物理地址 0x7c00)处添加了一个断点。
  2. 输入调试命令 c 继续执行,在 0x7c00 处的断点中断。中断后会在 Console 窗口中输出下一个要执行的指令,即软盘引导扇区程序的第一条指令,如下(0) [0x00007c00] 0000:7c00 (unk. ctxt): jmp .+0x006d (0x00007c6f) ; eb6d
  1. 输入调试命令 sreg 验证 CS 寄存器(0x0000)的值。

如下图 CS 寄存器的数值为 0x0000;

  1. 输入调试命令 r 验证 IP 寄存器(0x7c00)的值。

如下图 IP 寄存器的数值为 0x7c00

  1. 由于 BIOS 程序此时已经执行完毕,输入调试命令 xp /1024b 0x0000 验证此时 BIOS 中断向量表已经被载入
  1. 输入调试命令 xp /512b 0x7c00 显示软盘引导扇区程序的所有字节码。观察此块内存最开始的两个字节分别为 0xeb 和 0x6d,这和引导程序第一条指令的字节码(eb6d)是相同的。此块内存最后的两个字节分别为 0x55 和 0xaa,表示引导扇区是激活的,可以用来引导操作系统,这两个字节是 boot.asm 中最后一行语句。
  1. 输入调试命令 xp /512b 0x0600 验证图 3-2 中第一个用户可用区域是空白的。
  1. 输入调试命令 xp /512b 0x7e00 验证图 3-2 中第二个用户可用区域是空白的。
  1. 输入调试命令 xp /512b 0xa0000 验证图 3-2 中上位内存已经被系统占用。

下面是 Boch for Windows - Display 的界面图

boot.lst 列表文件,帮助开发者调试 boot.asm 文件中的汇编代码步骤

NASM 汇编器在将 boot.asm 生成为 boot.bin 的同时,会生成一个 boot.lst 列表文件,帮助开发者调试 boot.asm 文件中的汇编代码。按照下面的步骤查看 boot.lst 文件:

  1. 选择"视图"菜单中的"项目管理器",打开项目管理器窗口。

  2. 在"项目管理器"窗口中,右键点击"boot"文件夹中的 boot.asm 文件。

  3. 在弹出的快捷菜单中选择"打开生成的列表文件",在源代码编辑器中就会打开文件 boot.lst。

  4. 将 boot.lst 文件和 boot.asm 文件对比可以发现,此文件包含了 boot.asm 文件中所有的汇编代码,同时在代码的左侧又添加了更多的信息。

  5. 在 boot.lst 中查找到软盘引导扇区程序第一条指令所在的行(第 73 行)

    73 00000000 EB6D

    jmp short Start

    此行包含的信息有:

    73 是行号。
    00000000 是此条指令相对于程序开始位置的偏移(第一条指令应该为 0)。
    EB6D 是此条指令的字节码,和之前记录下来的指令字节码是一致的。
    

    软盘引导扇区程序的主要任务就是将软盘中的 loader.bin 文件加载到物理内存的 0x1000 处,然后跳转到 loader 程序的第一条指令(物理地址 0x1000 处的指令)继续执行 loader 程序。按照下面的步骤调试此过程:

    • 在 boot.lst 文件中查找到加载完毕 loader.bin 文件后要跳转到 loader 程序中执行的指令(第 278 行)

    根据此指令相对于程序开始(0x7C00)的偏移 (0x0181)可以得到此指令的逻辑地址为 0x0000:7D81。

    • 输入调试命令 vb 0x0000:0x7d81 添加一个断点。(这个断点是相加计算出来的)
    • 输入调试命令 c 继续执行,到断点处中断。在 Console 窗口中显示

    此条指令会跳转到物理内存 0x1000 处(即 Loader 程序的第一条指令)继续执行。

    • 按照打开 boot.lst 文件的方法打开 loader.lst 文件,并在此文件中查找到 loader 程序的第一条指令(第 33 行)
    • 输入调试命令 xp /8b 0x1000 查看内存 0x1000 处的数据,验证此块内存的前三个字节和 loader.lst 文件中的第一条指令的字节码是相同的。说明 loader 程序已经加载到从地址 0x1000 开始的内存中了。
    • 根据之前记录的 loader.bin 文件的大小,自己设计一个**查看内存的调试命令,查看内存中 loader 程序结束位置的字节码,**并与 loader.lst 文件中最后指令的字节码比较。

    loader.bin 的文件大小为 1566 字节(0h061e)。0x061e + 0x1000 = 0x161e

    所以命令为 xp /1566b 0x1000结果如下:

比较后是一样的。

至此调试软盘引导扇区程序结束。

调试加载程序

Loader 程序的主要任务是将操作系统内核文件(kernel.dll 文件)加载到内存中,然后让 CPU 进入保护模式并且启用分页机制,最后进入操作系统内核开始执行(调用 kernel.dll 的入口点函数)。

调试过程如下:

  • 在 loader.lst 文件中查找到准备进入 EOS 操作系统内核执行的指令(第 755 行)
  • 计算此条指令物理地址的过程要复杂一些。首先,其偏移地址 0x14f 是相对于节 (节 SECTION 是 NASM 汇编中的概念)开始的。由于在 boot.asm 程序中只有一个节,所以之前计算的结果都是正确的, 但是在 loader.asm 程序中有两个节,并且此条指令是在第二个节(32 位的保护模式代码)。下面引用的代码是 loader.lst 文件中第一个节的最后一条指令(第 593 行)

因为第一个节中最后一条指令的偏移为 0x03c0,并且此条指令长度为 3 个字节(字节码为 C20600),所以在进入第二个节之前的物理地址为 0x13c3(0x1000+0x03c0+0x3),由于第二个节是 32 位汇编程序,因此需要补一个字节确保四字节对齐,所以第二个节的起始物理地址是 0x13c4,从而可以计算出进入内核执行的指令所在的物理地址为 0x1513(0x13c4+0x14f),如下图所示。

  • 使用添加物理地址断点的调试命令 pb 0x1513 添加一个断点。
  • 输入调试命令 c 继续执行,在刚刚添加的断点处中断。在 Console 窗口中显示要执行的下一条指令(注意,此时指令中访问的地址均为逻辑地址):

(此时的 Display 窗口)

由于这里使用了函数指针的概念,所以,根据反汇编指令可以确定内核入口点函数的地址就保存在逻辑地址 ds:0x8000117 处的四个字节中。

  • 使用查看虚拟内存的调试命令 x /1wx ds:0x80001117 查看内存中保存的 32 位函数入口地址,在 Console 窗口中会输出类似下面的内容:

记录下此块内存中保存的函数地址 0x80017de0,后面的实验会验证内核入口点函数的地址与此地址是一致的。

  • 选择"调试"菜单中的"停止调试"菜单项,停止调试。

调试内核

前面使用 Bochs 虚拟机提供的调试功能了解了 EOS 操作系统的引导和加载过程,接下来可以继续调试 EOS 操作系统的内核,验证从加载程序进入内核入口点函数的过程。步骤如下:

  • 在"项目管理器"窗口中,右键点击项目节点,在弹出的快捷菜单中选择"属性"。
  • 在弹出的"属性页"对话框右侧的属性列表中找到"远程目标机"属性,将此属性值修改为"Bochs GDB stub"。
  • 点击"确定"按钮关闭"属性页"对话框。

  • 在"项目管理器"窗口中打开 ke 文件夹中的 start.c 文件,此文件中只定义了一个函数,就是操作系统内核的入口点函数 KiSystemStartup。

  • 在 KiSystemStartup 函数中的代码行(第 52 行)KiInitializePic();

    添加一个断点。

  • 按 F5 启动调试,会在刚刚添加的断点处中断。

  • 在 start.c 源代码文件中的 KiSystemStartup 函数名上点击鼠标右键,在弹出的快捷菜单中选择"添加监视",KiSystemStartup 函数就被添加到了"监视"窗口中。在"监视"窗口中可以看到此函数地址为

与之前记录的在内存 ds:x80001117 处保存的函数入口地址相同,说明的确是由 Loader 程序进入了操作系统内核。

  • 按 F5 继续执行 EOS 操作系统内核,在 Display 窗口中显示 EOS 操作系统已经启动,并且控制台程序已经开始运行了。

前后的状态截图如下:

  • 删除断点,停止调试。

EOS 启动后的状态和行为

已经完整学习了 EOS 操作系统的启动过程。当 EOS 启动完毕进入内核后,会进一步完成一系列的内核初始化工作,直到 EOS 操作系统可以接收用户输入的命令为止。因为内核初始化阶段涉及到的知识较多,不太适合刚刚接触操作系统原理的我深入学习,所以,可以在全面掌握了操作系统概念后再回过头来仔细研究 EOS 内核的初始化过程,这对于深入理解操作系统原理也是有很大帮助的。但是,还是需要在这里了解一下 EOS 在完成内核初始化后,处于什么样的状态,具有什么样的行为,这对于顺利完成后续的实验是很重要的。

实验步骤如下:

  • 在 ke/sysproc.c 文件的第 372 行,也就是"ver"命令函数中添加一个断点。
  • 启动调试。
  • 待 EOS 启动完成后,在控制台中输入命令"ver"后按回车,会在刚刚添加的断点处中断。
  • 刷新进程线程窗口,会得到如图所示的内容。

表格中的数据进行详细的记录和说明:

在进程列表中只有一个 ID 为 1 的系统进程,其优先级为 24,包含有 6 个线程,其中 ID 为 2 的线程是该进程的主线程,系统进程是没有映像名称的,或者可以认为"kernel.dll"就是系统进程的镜像名称。

在线程列表中有 6 个线程,它们都是系统线程。其中优先级为 0 的是空闲线程,当没有优先级大于 0 的线程占用处理器时,空闲线程就会在处理器上运行并处于运行状态(Running),否则就处于就绪状态(Ready),随时准备继续在处理器上运行。ID 为 17 的线程是控制台派遣线程,用于将键盘事件派遣到活动的控制台线程,所以在没有键盘事件发生的时间里,该线程总是处于阻塞状态(Waiting),等待键盘事件的到来。余下的四个线程都是控制台线程,分别对应于四个控制台,由于它们执行的是同一个控制台线程函数(ke/sysproc.c 文件中的 KiShellThread 函数),所以它们的起始地址都是相同的。控制台线程只有在执行控制台命令的时候才会处于运行状态,其它时间它们都在等待控制台派遣线程为它们分配键盘事件,会处于阻塞状态。由于本次是在控制台 1 中执行的 ver 命令,所以控制台 1 对应的 ID 号为 18 的线程会处于运行状态(使用绿色底色,并且由当前线程指针 PspCurrentThread 指向),而其它的三个控制台线程都处于阻塞状态。

转换控制台之后,重新运行,发现另一个线程处于运行态。

验证当没有任何程序或者命令运行时候,空闲线程 ID = 2 就会处于运行状态。

删除所有断点后,在 ke/sysproc.c 文件的第 143 行添加一个断点。读者会注意到这是在一个死循环中添加了一个断点,没错,当没有其它线程运行时,空闲线程总是会不停的执行这个死循环,直到有中断发生,或者有更高优先级的线程抢占了处理器。

也可以删除全部断点后,调试 输入 pt命令查看 EOS 启动后的进程和线程的信息。

如下图所示:

在 FloppyImageEditor 中加入 Hello.exe 之后选择"文件"菜单中的"保存"后关闭 FloppyImageEditor。

明哪个是应用程序的进程,它和系统进程有什么区别,哪个是应用程序的主线程,它和系统线程有什么区别。

为 EOS 内核添加 help 命令

操作系统通常都会提供一个 help 命令,用户启动操作系统后,可以使用这个命令查看系统提供的命令和功能介绍,从而学习操作系统的基本使用方法。
要求:

修改 ke/sysproc.c 文件,在其中实现 help 命令,其功能是列出系统提供的其它命令和功能介绍。

在这之前内核中是没有 help 命令的。

实验步骤:

  • 在 ke/sysproc.c 文件中,添加 help 命令的函数的声明
c 复制代码
//help命令函数的声明。
PRIVATE
VOID
ConsoleCmdHelp(
	IN HANDLE StdHandle
	);
//
  • 在 ke/sysproc.c 文件中,修改 KiShellThread 函数,在该函数中添加 help 控制台命令。
c 复制代码
 else if (0 == stricmp(Line,"help")){
			ConsoleCmdVersionNumber(StdHandle);
			continue;
		}
  • 在 ke/sysproc.c 文件中,添加 ConsoleCmdHelp 函数的实现。
c 复制代码
/*++

功能描述:
	查看系统提供的命令和功能介绍

参数:


返回值:
	无。

--*/
PRIVATE VOID ConsoleCmdHelp(
IN HANDLE StdHandle
)
{
fprintf(StdHandle, "ver:show EOS version.\n");
//
// TODO
//
}

思考与练习

为什么 EOS 操作系统从软盘启动时要使用 boot.bin 和 loader.bin 两个程序?使用一个可以吗?它们各自的主要功能是什么?如果将 loader.bin 的功能移动到 boot.bin 文件中,则 boot.bin 文件的大小是否仍然能保持小于 512 字节?

软盘引导扇区加载完毕后内存中有两个用户可用的区域,为什么软盘引导扇区程序选择将 loader.bin 加载到第一个可用区域的 0x1000 处呢?这样做有什么好处?这样做会对 loader.bin 文件的大小有哪些限制。

练习使用 Bochs 单步调试 BIOS 程序、软盘引导扇区程序和 loader 程序,加深对操作系统启动过程的理解。

EOS 空闲线程在其死循环中不停的执行 i++,从效率和节能的角度来说,这种方式都是不可取的。请读者尝试使用内联汇编将 i++替换为停机指令"HLT"