操作系统学习与实践1: 构建HelloWold,展示OS启动过程

本文介绍了从头开始使用C和x86汇编语言开发操作系统的教程,涵盖了OS基础知识、启动过程、局部性原理、分层存储器、以及在MSYS2环境下构建开发环境和编写实践代码的过程,包括编译、调试和使用QEMU模拟器。
摘要由CSDN通过智能技术生成

一、概述

         本系列的目的是使用 C 和 x86 汇编语言从头开始学习操作系统 (OS) 开发,典型特点是:“增量”学习。

   实践参考:

            http://www.brokenthorn.com/Resources/OSDevIndex.html

             https://cplusplus.com/

          注:在开始对系统进行编程时,您会发现根本没有任何东西可以帮助您。上电时,系统也以 16 位实模式运行,32 位编译器不支持该模式。这是第一件重要的事情:如果你想创建一个16位实模式操作系统,你必须使用16位C编译器。但是,如果您决定创建 32 位操作系统,则必须使用 32 位 C 编译器。 16 位 C 代码与 32 位 C 代码不兼容。

二、OS基础知识

        1. 整体上操作系统向下管理硬件,向上为用户/应用提供服务,总体上功能分为:

                (1)CPU管理:主要是程序的执行,如中断、进程、调度算法等。

                (2)内存管理:主要对ROM、RAM等的管理,包括分配内存、释放内存、内存保护。

                (3)设备管理:即IO设备,包括FLASH, 硬盘、USB、鼠标、键盘、显示器等读写。

                 (4)文件系统:主要是对数据存储管理,包括读写操作。

                 (5)UI界面: 包括shell命令行和图形化操作界面。

                 (6)信息安全: 授权访问管理,如用户、角色权限访问管理。

        2. 操作系统启动过程

        操作系统的启动过程是一个复杂且关键的步骤,它涉及到硬件、固件(BIOS/UEFI)、引导程序(Bootloader)以及操作系统本身的多个阶段。以下是一个简化的概述,描述了从计算机开机到操作系统完全加载的典型过程:

       (1)上电与自检(Power-On Self-Test, POST):当计算机接通电源后,首先进行的是上电自检(POST)。这是由计算机的固件(BIOS或UEFI)执行的一系列检查,用于确保硬件设备(如CPU、内存、硬盘、外设等)正常工作。如果POST检测到任何问题,计算机可能会发出声音或显示错误信息。

      (2)固件初始化:固件(BIOS或UEFI)初始化后,会寻找可启动的设备。这通常是通过在硬盘、SSD、光盘驱动器、USB设备等中查找引导扇区来完成的。固件会按照预设的启动顺序检查这些设备。

      (3)引导加载程序(Bootloader):一旦固件找到了可启动的设备,它会读取该设备的第一个扇区(引导扇区),这个扇区通常包含一个小型的引导加载程序。这个程序的任务是加载更复杂的引导管理程序或直接加载操作系统。

       (4) 加载操作系统内核:引导加载程序接下来会加载操作系统的内核到内存中。这通常涉及到从硬盘或其他存储介质读取内核文件,并将其复制到内存中适当的位置。

       (5)内核初始化:内核被加载到内存后,控制权被交给内核。内核初始化各种驱动程序,设置中断处理程序,初始化系统服务和进程调度器等。这一步骤是操作系统启动过程中最关键的部分。        

        (6)系统服务和守护进程:内核初始化完成后,它会启动系统服务和守护进程,如网络服务、日志系统等。

          (7)用户界面:操作系统会加载用户界面。对于图形界面(如Windows、macOS或Linux的桌面环境),这涉及到启动显示管理器(如X Window System)和登录界面。对于命令行界面,用户将直接看到一个命令提示符或终端。

           (8) 登录过程:用户在登录界面输入凭据后,操作系统会验证用户身份,并根据用户配置加载个人设置和启动应用程序。

       3. 局部性原理

        局部性通常有两种形式:时间局部性(temporal locality):时间局部性指的是:被引用过一次的存储器位置在未来会被多次引用(通常在循环中)。空间局部性(spatial locality)如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。

       4. 分层存储器

           存储器分为三类:CPU寄存器、SRAM、DRAM和磁盘存储,其中:

        (1)CPU寄存器:是CPU内部组成单元,由NOT,AND, OR,NOR等门电路组成的一个一个具备读写功能的触发器,直接参与逻辑运算、算术运算,速度最快,1个时钟周期,也最昂贵。

        (2)一/二级高速缓存: SRAM,由行和列组成的存储阵列(由存储单元阵列、地址译码器、读写控制逻辑、输入/输出缓冲器和电源组成),SRAM存储单元使用锁存器保持数据,不需要像DRAM那样定期刷新。访问速度在2~4个时钟周期,价格上比寄存器便宜。

        (3)  动态存储(DRAM): 与SRAM相似采用由行和列组成的存储阵列组成,二者区别主要在于存储单元实现不同,DRAM采用的是由一个晶体管和一个电容器组成,晶体管作为开关控制电容器的充电状态,电容器存储的是电荷,代表数据的0和1状态,所以必须不断周期刷新才能保存数据;SRAM采用的是由六个晶体管组成,形成一个双稳态的翻转锁存器。这个翻转锁存器可以稳定地存储一位数据(0或1),直到被明确地改变。DRAM结构简单,价格低,但由于每次访问前均需要刷新,因此速度较SRAM慢。

        (4)磁盘: 与上述不同,磁盘属于块设备(每块大小512字节),顺序访问按照扇区读写,不能随机访问。

   扇区组成:

          

主引导记录(MBR,Main Boot Record)是位于磁盘最前边的一段引导(Loader)代码。MBR扇区位于物理硬盘的0柱面,0磁头,1扇区,也就是整个硬盘的第一个扇区(偏移量为0),共占512个字节(即一个扇区),每个物理硬盘只有一个MBR扇区。

MBR扇区由三部分构成:第一部分是446字节的引导代码,也就是上面提到的MBR;第二部分是DPT(Disk Partition Table,硬盘分区表),包含4个表项,每个表项16字节,共占用64字节;第三部分是2个字节的结束标志,0x55AA。

        (5)FLASH盘

        Flash存储器,也称为闪存,是一种广泛使用的非易失性存储技术,能够在断电情况下保存数据,进一步分为两类:一是NAND flash,以块的方式可以代替硬盘,即当前的SSD盘(寿命在10年);而是NOR flash,可以像DRAM那样随机访问。

        (6)ROM

        ROM主要由地址译码器、存储体、读出线及读出放大器等部分组成。整体一次性写入,后续只能读。

        

三、研发环境

        在msys2环境中安装gcc、nasm、make、qemu。

      1.  msys2默认的软件安装包工具是pacman,pacman的使用方法如下:

pacman -Syy  让本地的包数据库和远程的软件仓库同步
pacman -Syu 也可以使用一句命令同时进行同步软件库并更新系统到最新状态

安装或者升级单个软件包,或者一列软件包(包含依赖包),使用如下命令:
pacman -S package_name1 package_name2

卸载软件包
删除单个软件包,保留其全部已经安装的依赖关系
pacman -R package_name
删除指定软件包,及其所有没有被其他已安装软件包使用的依赖关系:
pacman -Rs package_name
指定根目录
pacman -s XXX -r /mnt

    2. 安装nasm

        运行命令 pacman -S nasm

$ pacman -S nasm
resolving dependencies...
looking for conflicting packages...

Packages (1) nasm-2.16.01-1

Total Download Size:   0.32 MiB
Total Installed Size:  2.57 MiB

:: Proceed with installation? [Y/n] y

  3. 安装qemu,并设置环境变量

  运行命令 pacman -S mingw-w64-x86_64-qemu

   使用nano文本编辑器, 修改~/.bashrc, 添加:

  export PATH="$PATH:/mingw64/bin"


lazybird@lazybird-home UCRT64 ~
$ nano ~/.bashrc  #在最后添加export PATH="$PATH:/mingw64/bin"

lazybird@lazybird-home UCRT64 ~
$ source ~/.bashrc

启动qemu试验:qemu-system-i386

  3. 安装micro 代替nano,因为micro支持分屏

        (1)启动micro: micro 1.txt;

        (2)Ctrl+E: 切换到micro的命令(注:Ctrl+B是切换到bash命令);

        (3)在micro命令下使用:vsplit或hsplit切分窗口;

        (4)Ctrl+W:在不同窗口之间切换。

              

四、实践代码

为了简单起见,直接用汇编语言来实现,编译器选用nasm,批处理编译命令采用make工具。

        1.  所需要的程序设计背景知识:

   (1)扇区的大小在历史上通常是512字节,但现代硬盘可能使用更大尺寸的扇区,如4KB(4096字节)。存储设备的操作通常以扇区为单位进行。当计算机需要读取或写入数据时,它会指定一个扇区号,然后设备会定位到该扇区并执行读取或写入操作。在硬盘或其他可启动的存储设备上,第一个扇区(通常是第一个扇区)通常被用作引导扇区,它包含了启动计算机所需的代码和数据。

        (2)引导扇区(EBR)会以0x55AA作为结尾标志。这个标志告诉BIOS或其他启动固件,引导扇区已经结束,且引导代码是有效的。

        (3)x86内存结构

        16位地址线可访问的内存为2^16=64K字节,而早期的内存可以做到1M字节(2^20),如何利用这16位地址来表示1M的内存空间呢,提出了段地址访问方式,也就是说用两个数据来表示一个地址:

                      物理地址=段基址:段内偏移

       将整个1M内存分为16个段(Segment),每个段的大小最大为2^16=64k, 找一个物理地址时,先找到所在的段,然后左移4位,然后再加上段内的偏移地址:

                  物理地址 = (段基址 << 4) + 偏移量。

  段寄存器 (例如 CS, DS, ES)
+------------------+
| 选择子 (段号)     |  <-- 这通常会指向GDT中的一个段描述符
+------------------+
          |
          |  <-- 段描述符中包含段的基址和限长等信息
          V
+------------------+       +-----------------+
| 段描述符 (在GDT中) |       | 段基址          |
+------------------+       +-----------------+
| 基址: 0x0000      |       | 限长: 0xFFFF      |
| 访问权限: 读/写   |       | ...              |
+------------------+       +-----------------+
          |
          |  <-- 实际的物理地址是通过组合段基址和偏移量来计算的
          V
+------------------+
| 段内偏移量 (Offset)  |  <-- 这是相对于段基址的偏移
+------------------+
          |
          |  <-- 物理地址 = (段基址 << 4) + 偏移量
          V
+------------------+
| 物理地址 = 0x0000 + Offset  |  <-- 这是最终访问的物理地址
+------------------+

x86使用了CS (代码段)、DS(数据段)、SS(堆栈段)、ES,FS,GS(Extra Segment)四种类型来存储段基址。

    内存地址访问: segment: [ base + index * scale + displacement]

        对于16位x86: segment可以是CS,DS,SS,DS,ES,FS,GS

                                 base: BP/BX

                                 index: SI/DI

                                  scale:1

                                  displacement: 常量数值

  示例:

   

        (4) 中断0x10调用打印功能

              

x86使用SI/DI寄存器存储索引:

        2. 编写源码

              (1)首先设定BIOS加载引导加载程序的地址:org 0x7C00;

     (2)在0x7C00地址设置第一条指令为跳转指令,跳转到main函数入口:jmp main

     (3)在main函数中首先设置数据段和堆栈,然后调用puts子程序打印出"Hello world!";

               (4)puts::子程序,用于打印字符串到屏幕上。它通过BIOS中断0x10实现打 印功能;

                (5)hlt:这条指令让CPU进入等待状态,直到下一个外部中断。

      

org 0x7C00  ;这条指令设置了程序的起始地址为0x7C00。这是传统的引导扇区的最大偏移量,也是BIOS加载引导加载程序的地址。 
bits 16 ; 这条指令指定了汇编器应该使用16位的汇编语法。


%define ENDL 0x0D, 0x0A ;这条指令定义了一个宏,ENDL,它代表回车换行符,用于在字符串输出后添加换行。


start: ;这是程序的入口点。
    jmp main


;
; Prints a string to the screen
; Params:
;   - ds:si points to string
;
puts:
    ; save registers we will modify
    push si
    push ax
    push bx

.loop:
    lodsb               ; loads next character in al
    or al, al           ; verify if next character is null?
    jz .done

    mov ah, 0x0E        ; call bios interrupt
    mov bh, 0           ; set page number to 0
    int 0x10

    jmp .loop

.done:
    pop bx
    pop ax
    pop si    
    ret
    

main:
    ; setup data segments
    mov ax, 0           ; can't set ds/es directly
    mov ds, ax
    mov es, ax
    
    ; setup stack
    mov ss, ax
    mov sp, 0x7C00      ; stack grows downwards from where we are loaded in memory

    ; print hello world message
    mov si, msg_hello
    call puts

    hlt

.halt:
    jmp .halt



msg_hello: db 'Hello world!', ENDL, 0


times 510-($-$$) db 0 ; 计算出从当前位置到引导扇区末尾还需要多少字节的空间,然后用0(空字节)来填充这些空间,确保生成的二进制文件大小正好是512字节,满足引导扇区的大小要求。这是制作启动盘时常见的做法,以确保引导扇区的完整性和正确性。- $:这个符号代表当前地址。当你在编写代码时,每写入一个指令或者定义一个标签,$的值就会更新为当前的程序计数器(PC)值,即下一条将要执行或定义的指令的地址。- $$:这个符号代表当前段的基地址。在段式内存管理中,每个段(如代码段、数据段等)都有一个基地址。$$就是用来引用当前段的基地址的。- `times`:重复指定的次数- `db 0`:`db`是"define byte"的缩写,意味着定义一个字节。`0`是字节的值
dw 0AA55h;这是引导扇区的结束标志,0xAA55是一个特定的值,用于标识引导扇区的有效性。

 3. 编译程序

   使用makefile以实现自动化:

    

ASM=nasm

SRC_DIR=src
BUILD_DIR=build

$(BUILD_DIR)/main_floppy.img: $(BUILD_DIR)/main.bin
	cp $(BUILD_DIR)/main.bin $(BUILD_DIR)/main_floppy.img
	truncate -s 1440k $(BUILD_DIR)/main_floppy.img
	
$(BUILD_DIR)/main.bin: $(SRC_DIR)/main.asm
	mkdir -p $(BUILD_DIR)
	$(ASM) $(SRC_DIR)/main.asm -f bin -o $(BUILD_DIR)/main.bin

     目录结构如下:           

└─buidos
    └─part1
        │  Makefile
        │
        ├─build
        │      main.bin
        │      main_floppy.img
        │
        └─src
                main.asm

五、运行调试

    bash: 

      $ qemu-system-i386.exe main_floppy.img

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值