第32章 汇编语言 - 实践项目:小型操作系统内核(一)

32.2 开发环境搭建

开发一个操作系统内核需要适当的工具和环境。这部分将详细介绍如何选择合适的工具链,设置仿真器或虚拟机,以及创建构建和测试的自动化流程。

32.2.1 工具链的选择

要开始编写操作系统的代码,首先需要选择一套合适的工具链。对于汇编语言和C语言混合编程的小型操作系统内核来说,以下工具是必不可少的:

  • NASM (Netwide Assembler): 是一种用于x86架构的开源汇编器,支持多种输出格式,非常适合用来编写引导加载程序和其他低级代码。
  • GCC (GNU Compiler Collection): 提供了对C/C++等高级语言的支持,可以编译用户态的应用程序或内核模块。
  • LD (GNU Linker): 用于链接多个目标文件,生成最终的可执行文件或库。
  • Binutils: 包含了一系列二进制工具,如objcopy、objdump等,用于处理目标文件和二进制文件。
  • Make: 项目构建工具,用于管理源文件的编译过程。
  • QEMU: 快速模拟器,可用于模拟多种硬件平台,并且可以直接从磁盘映像启动,非常适合测试自定义的操作系统。
  • GRUBSyslinux: 引导加载程序,可以用来加载多阶段的引导程序和内核映像。
32.2.2 设置仿真器或虚拟机

为了安全地测试你的操作系统代码,推荐使用仿真器或虚拟机。这里我们将介绍如何设置QEMU作为我们的主要仿真工具:

  1. 安装QEMU : 根据你的主机操作系统,使用包管理器安装QEMU。例如,在Debian/Ubuntu上可以使用sudo apt-get install qemu-system-x86来安装。

  2. 创建硬盘映像 : 使用qemu-img命令创建一个虚拟硬盘映像,该映像将用作你操作系统的"硬盘"。

    shell 复制代码
    qemu-img create -f raw myos.img 10M
  3. 配置QEMU启动参数: 编写一个脚本来简化启动QEMU的过程。你可以指定引导映像、内存大小、显示模式等参数。例如:

    shell 复制代码
    qemu-system-i386 -drive format=raw,file=myos.img -m 128M -vga std -monitor stdio
  4. 使用虚拟机(可选): 如果你需要更接近真实硬件的测试环境,可以选择设置一个虚拟机(如VirtualBox或VMware),并安装一个基础的操作系统作为开发平台。

32.2.3 构建和测试流程的自动化脚本

为了提高开发效率,应该建立一套自动化的构建和测试流程。这可以通过编写Shell脚本或使用构建工具如Make来实现:

  1. 编写Makefile : Makefile是一种描述编译规则的文件,它告诉make命令如何编译和链接程序。为你的项目创建一个Makefile,以便可以简单地通过make命令来编译整个项目。

    makefile 复制代码
    # 示例Makefile内容
    all: boot.bin kernel.bin
    
    boot.bin: boot.asm
    	nasm -f bin $< -o $@
    
    kernel.bin: kernel.o
    	ld -T linker.ld -o $@ $^
    
    kernel.o: kernel.c
    	gcc -c -ffreestanding -o $@ $<
    
    clean:
    	rm -f *.bin *.o
  2. 编写测试脚本: 创建一个简单的Shell脚本来调用QEMU运行你的操作系统。这个脚本还可以包含额外的功能,比如复制最新的二进制文件到虚拟硬盘映像中。

    shell 复制代码
    #!/bin/sh
    # 测试脚本示例
    make
    cp boot.bin myos.img
    qemu-system-i386 -drive format=raw,file=myos.img -m 128M -vga std -monitor stdio
  3. 集成持续集成(CI): 如果你有版本控制系统(如Git),可以考虑设置CI服务(如GitHub Actions, Travis CI等)。每当推送新的更改时,CI服务器会自动拉取最新代码、运行构建和测试脚本,并报告结果。

通过以上步骤,你将拥有一个稳定且高效的开发环境,使得你可以专注于核心操作系统的开发工作。

32.3 理解硬件和BIOS

在深入编写操作系统内核之前,理解计算机的启动过程以及BIOS(Basic Input/Output System)或UEFI(Unified Extensible Firmware Interface)的工作原理是非常重要的。这部分内容将帮助你了解如何与硬件交互,并为开发引导加载程序和内核打下坚实的基础。

32.3.1 计算机启动过程

当计算机开机时,它会经历一系列步骤来初始化硬件并加载操作系统:

  1. 加电自检 (POST, Power-On Self Test):

    • BIOS或UEFI首先执行POST以检查硬件是否正常工作。
    • 它测试内存、CPU、键盘等基本组件,并显示错误代码(如果有的话)。
  2. 初始化硬件:

    • 设置系统时钟、配置外围设备(如硬盘控制器)、初始化显卡等。
  3. 查找并加载引导加载程序:

    • 根据预设顺序查找可启动介质(如硬盘、光盘、USB驱动器)。
    • 一旦找到有效的引导扇区(对于BIOS),或者EFI系统分区中的引导文件(对于UEFI),就会加载到内存中。
  4. 传递控制给引导加载程序:

    • 引导加载程序接管CPU控制,并负责加载操作系统内核到内存中。
    • 对于传统的BIOS系统,引导加载程序通常位于硬盘的第一个扇区(MBR,主引导记录);对于UEFI系统,则是从EFI系统分区加载特定的引导应用程序。
  5. 启动操作系统:

    • 最终,引导加载程序将控制权交给操作系统内核,开始操作系统的初始化过程。
32.3.2 BIOS和UEFI简介
  • BIOS (Basic Input/Output System):

    • 是一种固件程序,嵌入在主板上的ROM芯片中。
    • 提供了最基本的硬件抽象层,允许操作系统与底层硬件进行通信。
    • 使用中断服务程序来处理输入输出请求,例如从键盘读取字符或向屏幕写入文本。
    • 支持实模式下的16位代码执行,限制了可用内存空间和性能。
  • UEFI (Unified Extensible Firmware Interface):

    • 是BIOS的现代替代品,提供了更强大的功能集。
    • 可以运行32位甚至64位代码,支持更大的地址空间和更多样的硬件接口。
    • 具有图形化用户界面和网络连接能力,使得安装操作系统更加灵活。
    • UEFI使用一个叫做EFI系统分区的地方来存储引导加载程序和其他相关文件。
    • 提供了一套标准化的服务API,方便操作系统开发者编写兼容的引导加载程序。
32.3.3 中断向量表和中断服务程序
  • 中断向量表 (IVT, Interrupt Vector Table):

    • 在实模式下,中断向量表是一个由256个条目组成的数组,每个条目包含一个中断服务程序的地址。
    • IVT通常位于内存的最低端(0x0000:0x0000 到 0x0000:0x03FF),每个条目占用4个字节(2字节段地址+2字节偏移地址)。
    • 中断号作为索引访问相应的中断服务程序。
  • 中断服务程序 (ISR, Interrupt Service Routine):

    • 当发生硬件中断或软件触发的中断时,CPU会暂停当前任务,保存状态信息,并跳转到对应的ISR去处理事件。
    • ISR完成必要的处理后,通过IRET指令返回原来被中断的任务。
    • 开发者可以在自己的操作系统中重新定义某些ISR,以实现定制化的硬件响应逻辑。
    • 对于保护模式,中断描述符表(IDT)取代了IVT,提供更安全和灵活的中断管理机制。

理解和掌握这些概念是构建操作系统内核的关键。接下来,你可以利用这些知识来设计和实现你的引导加载程序,确保它能够正确地与BIOS或UEFI交互,并为后续的操作系统初始化做好准备。

32.4 创建引导加载程序

引导加载程序是操作系统启动过程中至关重要的组件。它负责初始化硬件环境,并将操作系统的内核加载到内存中,以便开始执行。我们将逐步介绍如何创建一个简单的引导加载程序。

32.4.1 引导扇区结构

引导扇区(Boot Sector)是磁盘上的第一个512字节的扇区。在传统的PC架构中,BIOS会在计算机启动时自动读取这个扇区并将其加载到内存地址0x7C00处开始执行。引导扇区必须以特定的结束签名0xAA55结尾,这是BIOS用来识别有效引导扇区的关键标志。

  • 大小:512字节
  • 结束签名 :最后两个字节为0xAA55
  • 执行位置 :从内存地址0x7C00开始执行
32.4.2 编写第一个汇编代码

下面是一个非常基础的引导加载程序的例子,使用NASM汇编语言编写:

assembly 复制代码
; boot.asm - 简单的引导加载程序

bits 16                 ; 使用16位实模式
org 0x7C00              ; 假设BIOS会将我们加载到这个地址

start:
    cli                 ; 关闭中断,防止意外发生
    xor ax, ax          ; 清除AX寄存器
    mov ds, ax          ; 设置数据段寄存器为0
    mov es, ax          ; 设置额外段寄存器为0
    mov ss, ax          ; 设置堆栈段寄存器为0
    mov sp, 0x7C00      ; 设置堆栈指针为引导扇区的开始处
    sti                 ; 恢复中断

    ; 打印消息
    mov si, hello_msg   ; 将字符串的地址放入SI寄存器
    call print_string   ; 调用打印函数

hang:
    jmp hang            ; 死循环,阻止CPU继续执行

; 打印字符串函数
print_string:
    lodsb               ; 从SI中加载一个字节到AL
    or al, al           ; 测试AL是否为零
    jz done             ; 如果是,返回
    mov ah, 0x0E        ; BIOS teletype输出功能
    int 0x10            ; 调用BIOS视频服务
    jmp print_string    ; 继续下一个字符
done:
    ret                 ; 返回调用者

hello_msg db 'Hello, OS World!', 0

times 510-($-$$) db 0   ; 填充剩余的空间直到510个字节
dw 0xAA55               ; 引导签名
32.4.3 加载更多代码到内存

为了使操作系统能够做更多的事情,通常需要加载额外的代码到内存中。这可以通过以下步骤实现:

  1. 确定加载位置:选择一个不会与现有代码冲突的内存地址。
  2. 读取磁盘:利用BIOS中断INT 13h来读取磁盘上的其他扇区。例如,可以读取紧接着引导扇区之后的扇区。
  3. 跳转到新代码:一旦所有必要的扇区都被加载到内存中,引导加载程序应该设置好段寄存器并跳转到新的代码位置开始执行。

这里是一个简单的例子,用于读取第二个扇区(假设它是内核的第一部分):

assembly 复制代码
read_kernel:
    mov bx, 0x8000      ; 内核加载地址
    mov dh, 1           ; 读取的扇区数
    mov dl, [bootdev]   ; 使用之前保存的启动设备号
    call disk_load      ; 调用磁盘读取例程
    jmp 0x0000:0x8000   ; 跳转到内核代码

disk_load:
    ; 实现读取磁盘的逻辑,通常涉及INT 13h中断
    ; ...
    ret
32.4.4 测试引导加载程序

测试引导加载程序的最佳方式是使用仿真器如QEMU,因为它允许你在不改变实际硬件的情况下快速迭代和调试。

  1. 编译引导代码:使用NASM将汇编代码编译成原始二进制格式。

    shell 复制代码
    nasm -f bin boot.asm -o boot.bin
  2. 创建虚拟硬盘映像:如果你打算模拟从硬盘启动,可以创建一个虚拟硬盘映像文件,并将引导代码复制进去。

    shell 复制代码
    dd if=/dev/zero of=myos.img bs=512 count=2880 # 创建一个1.44MB的软盘映像
    dd if=boot.bin of=myos.img conv=notrunc       # 将引导代码写入映像的第一个扇区
  3. 使用QEMU测试:启动QEMU并指定你的映像文件作为启动介质。

    shell 复制代码
    qemu-system-i386 -drive format=raw,file=myos.img

通过以上步骤,你应该能够构建一个基本的引导加载程序,并验证它的正确性。随着项目的进展,你可以逐渐增加更多功能,比如支持更多的硬件、更复杂的内存管理以及最终切换到保护模式等。

相关推荐
.Vcoistnt3 分钟前
Codeforces Round 976 (Div. 2) and Divide By Zero 9.0(A-E)
数据结构·c++·算法·贪心算法·动态规划·图论
诸神缄默不语14 分钟前
里氏替换原则(Liskov Substitution Principle,LSP):面向对象设计的基本原则
开发语言·里氏替换原则
pursuit_csdn21 分钟前
LeetCode 916. Word Subsets
算法·leetcode·word
TU.路25 分钟前
leetcode 24. 两两交换链表中的节点
算法·leetcode·链表
0xCC说逆向31 分钟前
Windows图形界面(GUI)-QT-C/C++ - QT控件创建管理初始化
c语言·开发语言·c++·windows·qt·mfc·sdk
蒜蓉大猩猩34 分钟前
Node.js --- 详解MongoDB与Mongoose
数据库·后端·mongodb·node.js
qingy_20461 小时前
【算法】图解排序算法之归并排序、快速排序、堆排序
java·数据结构·算法
张声录11 小时前
【ETCD】【源码阅读】深入探索 ETCD 源码:了解 `Range` 操作的底层实现
java·数据库·etcd
ptc学习者1 小时前
用sql 基线 替换执行计划
java·开发语言·ffmpeg
weixin_438197381 小时前
mysql存储过程创建与删除(参数输入输出)
数据库·sql·mysql