从 0 开始写一个操作系统

作者:解琛
时间:2020 年 8 月 30 日

  • 从 0 开始写一个操作系统
  • 三、Bootloader 的实现
    • 3.1 项目整体框架
      • 3.1.1 bootloader
      • 3.1.2 ucore 操作系统
        • 3.1.2.1 系统初始化
        • 3.1.2.2 内存管理
        • 3.1.2.3 外设驱动
        • 3.1.2.4 中断处理
        • 3.1.2.5 内核调试
      • 3.1.3 公共库
      • 3.1.4 工具
    • 3.2 bootloader 进入保护模式
      • 3.2.1 清理环境
      • 3.2.2 使能 A20
      • 3.2.3 初始化 GDT 表
      • 3.2.4 进入保护模式
      • 3.2.5 更新 cs 的基地址
      • 3.2.6 创建堆栈
      • 3.2.7 进入 bootmain
    • 3.3 bootloader 加载 ELF 格式的 OS
      • 3.3.1 readsect
      • 3.3.2 readseg
      • 3.3.3 bootmain
    • 3.4 内核初始化
    • 3.5 生成主引导扇区
    • 3.6 编译运行

三、Bootloader 的实现

安装开发环境。

sudo apt install build-essential git qemu-system-x86 vim-gnome gdb make diffutils exuberant-ctags tmux openssh-server cscope meld

3.1 项目整体框架

项目完整的代码见:lab1_result

.
├── boot
│   ├── asm.h
│   ├── bootasm.S
│   └── bootmain.c
├── kern
│   ├── debug
│   │   ├── assert.h
│   │   ├── kdebug.c
│   │   ├── kdebug.h
│   │   ├── kmonitor.c
│   │   ├── kmonitor.h
│   │   ├── panic.c
│   │   └── stab.h
│   ├── driver
│   │   ├── clock.c
│   │   ├── clock.h
│   │   ├── console.c
│   │   ├── console.h
│   │   ├── intr.c
│   │   ├── intr.h
│   │   ├── kbdreg.h
│   │   ├── picirq.c
│   │   └── picirq.h
│   ├── init
│   │   └── init.c
│   ├── libs
│   │   ├── readline.c
│   │   └── stdio.c
│   ├── mm
│   │   ├── memlayout.h
│   │   ├── mmu.h
│   │   ├── pmm.c
│   │   └── pmm.h
│   └── trap
│       ├── trap.c
│       ├── trapentry.S
│       ├── trap.h
│       └── vectors.S
├── libs
│   ├── defs.h
│   ├── elf.h
│   ├── error.h
│   ├── printfmt.c
│   ├── stdarg.h
│   ├── stdio.h
│   ├── string.c
│   ├── string.h
│   └── x86.h
├── Makefile
└── tools├── function.mk├── gdbinit├── grade.sh├── kernel.ld├── lab1init├── moninit├── sign.c└── vector.c10 directories, 48 files

3.1.1 bootloader

  • boot/bootasm.S

    • 定义并实现了 bootloader 最先执行的函数 start;
    • 此函数进行了一定的初始化;
    • 完成了从实模式到保护模式的转换;
    • 调用 bootmain.c 中的 bootmain 函数;
  • boot/bootmain.c
    • 定义并实现了 bootmain 函数实现了通过屏幕、串口和并口显示字符串;
    • bootmain 函数加载 ucore 操作系统到内存,然后跳转到 ucore 的入口处执行;
  • boot/asm.h
    • 是 bootasm.S 汇编文件所需要的头文件,主要是一些与 X86 保护模式的段访问方式相关的宏定义。

3.1.2 ucore 操作系统

3.1.2.1 系统初始化

  • kern/init/init.c:ucore 操作系统的初始化启动代码。

3.1.2.2 内存管理

  • kern/mm/memlayout.h:ucore 操作系统有关段管理(段描述符编号、段号等)的一些宏定义;
  • kern/mm/mmu.h:ucore 操作系统有关 X86 MMU 等硬件相关的定义;
    • 包括 EFLAGS 寄存器中各位的含义;
    • 应用、系统段类型;
    • 中断门描述符定义;
    • 段描述符定义;
    • 任务状态段定义;
    • NULL 段声明的宏 SEG_NULL;
    • 特定段声明的宏 SEG;
    • 设置中断门描述符的宏 SETGATE;
  • kern/mm/pmm.[ch]:设定了 ucore 操作系统在段机制中要用到的全局变量;
    • 任务状态段 ts;
    • 全局描述符表 gdt[];
    • 加载全局描述符表寄存器的函数 lgdt;
    • 临时的内核栈 stack0;
    • 对全局描述符表和任务状态段的初始化函数 gdt_init。

3.1.2.3 外设驱动

  • kern/driver/intr.[ch]:实现了通过设置 CPU 的 eflags 来屏蔽和使能中断的函数;
  • kern/driver/picirq.[ch]:实现了对中断控制器 8259A 的初始化和使能操作;
  • kern/driver/clock.[ch]:实现了对时钟控制器 8253 的初始化操作;
  • kern/driver/console.[ch]:实现了对串口和键盘的中断方式的处理操作。

3.1.2.4 中断处理

  • kern/trap/vectors.S:包括 256 个中断服务例程的入口地址和第一步初步处理实现;

    • 此文件是由 tools/vector.c 在编译 ucore 期间动态生成的;
  • kern/trap/trapentry.S
    • 紧接着第一步初步处理后,进一步完成第二步初步处理;
    • 并且有恢复中断上下文的处理,即中断处理完毕后的返回准备工作;
  • kern/trap/trap.[ch]:紧接着第二步初步处理后,继续完成具体的各种中断处理操作。

3.1.2.5 内核调试

  • kern/debug/kdebug.[ch]:提供源码和二进制对应关系的查询功能,用于显示调用栈关系;
  • kern/debug/kmonitor.[ch]:实现提供动态分析命令的 kernel monitor,便于在 ucore 出现 bug 或问题后,能够进入 kernel monitor 中,查看当前调用关系;
  • kern/debug/panic.c | assert.h:提供了 panic 函数和 assert 宏,便于在发现错误后,调用 kernel monitor;
    • 可以充分利用 assert 宏和 panic 函数,提高查找错误的效率。

3.1.3 公共库

  • libs/defs.h:包含一些无符号整型的缩写定义;
  • libs/x86.h:一些用 GNU C 嵌入式汇编实现的 C 函数(由于使用了 inline 关键字,所以可以理解为宏)。

3.1.4 工具

  • Makefile 和 function.mk:指导 make 完成整个软件项目的编译,清除等工作;
  • sign.c:一个 C 语言小程序,是辅助工具,用于生成一个符合规范的硬盘主引导扇区;
  • tools/vector.c:生成vectors.S,此文件包含了中断向量处理的统一实现。

3.2 bootloader 进入保护模式

完整的代码见 boot/bootasm.S。

定义基本的段信息和标志位。

#include <asm.h>.set PROT_MODE_CSEG,        0x08                    # kernel code segment selector
.set PROT_MODE_DSEG,        0x10                    # kernel data segment selector
.set CR0_PE_ON,             0x01                    # protected mode enable flag

机器上电,从 %cs=0 $pc=0x7c00,进入后:

# start address should be 0:7c00, in real mode, the beginning address of the running bootloader
.globl start
start:

3.2.1 清理环境

首先,清理环境,包括将 flag 置 0 和将段寄存器置 0。

.code16                                             # Assemble for 16-bit modecli                                             # Disable interruptscld                                             # String operations increment# Set up the important data segment registers (DS, ES, SS).xorw %ax, %ax                                   # Segment number zeromovw %ax, %ds                                   # -> Data Segmentmovw %ax, %es                                   # -> Extra Segmentmovw %ax, %ss                                   # -> Stack Segment

3.2.2 使能 A20

开启 A20:通过将键盘控制器上的 A20 线置于高电位,全部 32 条地址线可用,可以访问 4G 的内存空间。

    #  Enable A20:#  For backwards compatibility with the earliest PCs, physical#  address line 20 is tied low, so that addresses higher than#  1MB wrap around to zero by default. This code undoes this.
seta20.1:inb $0x64, %al                                  # Wait for not busy(8042 input buffer empty).testb $0x2, %aljnz seta20.1movb $0xd1, %al                                 # 0xd1 -> port 0x64,发送写8042输出端口的指令;outb %al, $0x64                                 # 0xd1 means: write data to 8042's P2 portseta20.2:inb $0x64, %al                                  # Wait for not busy(8042 input buffer empty).testb $0x2, %aljnz seta20.2movb $0xdf, %al                                 # 0xdf -> port 0x60,打开 A20;outb %al, $0x60                                 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1

3.2.3 初始化 GDT 表

一个简单的 GDT 表和其描述符已经静态储存在引导区中,载入即可。

    # Switch from real to protected mode, using a bootstrap GDT# and segment translation that makes virtual addresses# identical to physical addresses, so that the# effective memory map does not change during the switch.lgdt gdtdesc

在文件的最后定义 gdtdesc。

# Bootstrap GDT
.p2align 2                                          # force 4 byte alignment
gdt:SEG_NULLASM                                     # null segSEG_ASM(STA_X|STA_R, 0x0, 0xffffffff)           # code seg for bootloader and kernelSEG_ASM(STA_W, 0x0, 0xffffffff)                 # data seg for bootloader and kernelgdtdesc:.word 0x17                                      # sizeof(gdt) - 1.long gdt                                       # address gdt

3.2.4 进入保护模式

通过将 cr0 寄存器 PE 位置 1 便开启了保护模式。

    movl %cr0, %eaxorl $CR0_PE_ON, %eaxmovl %eax, %cr0

3.2.5 更新 cs 的基地址

通过长跳转更新 cs 的基地址。

    # Jump to next instruction, but in 32-bit code segment.# Switches processor into 32-bit mode.ljmp $PROT_MODE_CSEG, $protcseg.code32
protcseg:

3.2.6 创建堆栈

设置段寄存器,并建立堆栈。

    # Set up the protected-mode data segment registersmovw $PROT_MODE_DSEG, %ax                       # Our data segment selectormovw %ax, %ds                                   # -> DS: Data Segmentmovw %ax, %es                                   # -> ES: Extra Segmentmovw %ax, %fs                                   # -> FSmovw %ax, %gs                                   # -> GSmovw %ax, %ss                                   # -> SS: Stack Segment# Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)movl $0x0, %ebpmovl $start, %esp

3.2.7 进入 bootmain

转到保护模式完成,进入 boot 主方法。

    call bootmain

3.3 bootloader 加载 ELF 格式的 OS

完整的代码见 boot/bootmain.C。

3.3.1 readsect

首先看 readsect 函数,readsect 从设备的第 secno 扇区读取数据到 dst 位置。

#include <defs.h>
#include <x86.h>
#include <elf.h>unsigned init SECTSIZE = 512;/* waitdisk - wait for disk ready */
static void
waitdisk(void) {while ((inb(0x1F7) & 0xC0) != 0x40)/* do nothing */;
}/* readsect - read a single sector at @secno into @dst */
static void
readsect(void *dst, uint32_t secno) {// wait for disk to be readywaitdisk();outb(0x1F2, 1);                         // count = 1,设置扇区的数目为 1;outb(0x1F3, secno & 0xFF);outb(0x1F4, (secno >> 8) & 0xFF);outb(0x1F5, (secno >> 16) & 0xFF);outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);outb(0x1F7, 0x20);                      // cmd 0x20 - read sectors;// wait for disk to be readywaitdisk();// read a sectorinsl(0x1F0, dst, SECTSIZE / 4);         // 读取到 dst 位置;// 除数 4,因为这里以 DW 为单位;
}

3.3.2 readseg

readseg 简单包装了 readsect,可以从设备读取任意长度的内容。

/* ** readseg - read @count bytes at @offset from kernel into virtual address @va,* might copy more than asked.* */
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {uintptr_t end_va = va + count;// round down to sector boundaryva -= offset % SECTSIZE;// translate from bytes to sectors; kernel starts at sector 1uint32_t secno = (offset / SECTSIZE) + 1;// 加1因为0扇区被引导占用;// ELF文件从1扇区开始;// If this is too slow, we could read lots of sectors at a time.// We'd write more to memory than asked, but it doesn't matter --// we load in increasing order.for (; va < end_va; va += SECTSIZE, secno ++) {readsect((void *)va, secno);}
}

3.3.3 bootmain

struct elfhdr * ELFHDR = ((struct elfhdr *)0x10000);/* bootmain - the entry of bootloader */
void
bootmain(void) {// read the 1st page off disk// 首先读取ELF的头部;readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);// is this a valid ELF?// 通过储存在头部的幻数判断是否是合法的 ELF 文件;if (ELFHDR->e_magic != ELF_MAGIC) {goto bad;}struct proghdr *ph, *eph;// load each program segment (ignores ph flags)// ELF 头部有描述 ELF 文件应加载到内存什么位置的描述表;// 先将描述表的头地址存在 ph;ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);eph = ph + ELFHDR->e_phnum;// 按照描述表将ELF文件中数据载入内存;for (; ph < eph; ph ++) {readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);}// ELF 文件 0x1000 位置后面的 0xd1ec 比特被载入内存 0x00100000;// ELF 文件 0xf000 位置后面的 0x1d20 比特被载入内存 0x0010e000;// 根据 ELF 头部储存的入口信息,找到内核的入口;// call the entry point from the ELF header// note: does not return((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();bad:outw(0x8A00, 0x8A00);outw(0x8A00, 0x8E00);/* do nothing */while (1);
}

3.4 内核初始化

完整的代码见 kern/init/init.c。

#include <defs.h>void kern_init(void) __attribute__((noreturn));void kern_init(void) {while(1);
}

3.5 生成主引导扇区

完整的代码见 tools/sign.c。

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>int
main(int argc, char *argv[]) {struct stat st;if (argc != 3) {fprintf(stderr, "Usage: <input filename> <output filename>\n");return -1;}if (stat(argv[1], &st) != 0) {fprintf(stderr, "Error opening file '%s': %s\n", argv[1], strerror(errno));return -1;}printf("'%s' size: %lld bytes\n", argv[1], (long long)st.st_size);if (st.st_size > 1022) {fprintf(stderr, "%lld >> 510!!\n", (long long)st.st_size);return -1;}char buf[1024];memset(buf, 0, sizeof(buf));FILE *ifp = fopen(argv[1], "rb");int size = fread(buf, 1, st.st_size, ifp);if (size != st.st_size) {fprintf(stderr, "read '%s' error, size is %d.\n", argv[1], size);return -1;}fclose(ifp);buf[1022] = 0x55;buf[1023] = 0xAA;FILE *ofp = fopen(argv[2], "wb+");size = fwrite(buf, 1, 1024, ofp);if (size != 1024) {fprintf(stderr, "write '%s' error, size is %d.\n", argv[2], size);return -1;}fclose(ofp);printf("build 1024 bytes boot sector: '%s' success!\n", argv[2]);return 0;
}

3.6 编译运行

当前案例的工程目录分布如下。

jerome@jerome:~/6.本地实验室/4.os/1.动手写一个操作系统/0.helloWorld$ tree
.
├── boot
│   ├── asm.h
│   ├── bootasm.S
│   └── bootmain.c
├── kern
│   └── init
│       └── init.c
├── libs
│   ├── defs.h
│   ├── elf.h
│   └── x86.h
├── Makefile
└── tools├── function.mk├── kernel.ld└── sign.c5 directories, 12 files

需要把 3 个库文件 和 3 个工具文件 加入到工程中。

moocos-> make
+ cc kern/init/init.c
+ ld bin/kernel
+ cc boot/bootasm.S
+ cc boot/bootmain.c
+ cc tools/sign.c
+ ld bin/bootblock
'obj/bootblock.out' size: 484 bytes
build 512 bytes boot sector: 'bin/bootblock' success!
10000+0 records in
10000+0 records out
5120000 bytes (5.1 MB) copied, 0.0592984 s, 86.3 MB/s
1+0 records in
1+0 records out
512 bytes (512 B) copied, 0.000865678 s, 591 kB/s
11+1 records in
11+1 records out
6138 bytes (6.1 kB) copied, 0.00371367 s, 1.7 MB/s
[~/Desktop/0.helloWorld]
moocos-> ls -l bin/
total 5028
-rw-rw-r-- 1 moocos moocos     512  8月 31 00:33 bootblock
-rwxrwxr-x 1 moocos moocos    6138  8月 31 00:33 kernel
-rwxrwxr-x 1 moocos moocos   16356  8月 31 00:33 sign
-rw-rw-r-- 1 moocos moocos 5120000  8月 31 00:33 ucore.img
[~/Desktop/0.helloWorld]

通过执行如下命令可在硬件虚拟环境 qemu 中运行 bootloader 或 OS。

make qemu

这里需要放到虚拟环境中去运行,因为是 32 位的操作系统,虚拟环境的搭建参考我之前写的文章:[从 0 开始写一个操作系统] 二、系统软件启动过程。

可以看到,bootloader 已经运行成功了。

[从 0 开始写一个操作系统] 三、Bootloader 的实现相关推荐

  1. [从 0 开始写一个操作系统] 一、准备知识

    从 0 开始写一个操作系统 作者:解琛 时间:2020 年 8 月 29 日 从 0 开始写一个操作系统 一.准备知识 1.1 实现方案 1.2 gcc 1.2.1 AT&T 汇编基本语法 1 ...

  2. 从0开始写一个操作系统

    前文 之前很早就写过关于这方面的文章,但过于简陋,于是趁着有点时间再次整理了下,并以代码的角度进行讲解一下.[由于水平有限,如有错误请批评指正] 正文 0 本文写的一个简单OS所实现的目标 系统内核对 ...

  3. 从0写一个操作系统 | 01-环境搭建

    前言 前不久朋友推荐了一本书,叫做<操作系统真象还原>,看了一部分后觉得还蛮有趣的,想试着着手写一个操作系统,一边学习一边写. 本专栏会慢慢更新实现的过程,由于本人只能在工作之余钻研这本书 ...

  4. 我是如何学习写一个操作系统(完结):总结和系列索引

    前言 从一开始的引导程序到现在的文件系统已经有十篇了,算是自己对操作系统的学习的一个笔记,原本是想结合自己之前写的玩具操作系统FragileOS,但是之后就转到了结合Linux 0.11的代码去写这个 ...

  5. 写一个操作系统有多难?自制 os 极简教程

    不知道正在阅读本文的你,是否是因为想自己动手写一个操作系统.我觉得可能每个程序员都有个操作系统梦,或许是想亲自动手写出来一个,或许是想彻底吃透操作系统的知识.不论是为了满足程序员们自带的成就感,还是为 ...

  6. 自己动手写一个简单的bootloader

    自己动手写一个简单的bootloader 15年10月31日19:44:27 (一) start.S 写这一段代码前,先要清楚bootloader开始的时候都做什么了.无非就是硬件的初始化,我们想要写 ...

  7. 自己动手写一个操作系统——MBR(1)

    文章目录 前言 MBR 1) 512 字节镜像 2) 0x55 和 0xAA qemu 运行 参考 前言 上篇<自己动手写一个操作系统--我们能做什么,我们需要做什么>我们介绍到 BIOS ...

  8. 从0开始写一个小程序

    项目简介 从0开始写一个小程序,本来想写一个新闻类的程序,后来发现调用的聚合数据api每天只能访问100次,就换成豆瓣的了,直接用豆瓣的接口又访问不了,在网上查了一下,要把豆瓣的地址换成"h ...

  9. 自己动手写一个操作系统——MBR(2)

    前言 上篇文章<自己动手写一个操作系统--MBR(1)>,我们使用 dd 生成了一个 512 字节的镜像,并用 vim 将其最后两个字节修改成了 55 AA,以此来完成了 MBR 的构建. ...

  10. 详细介绍如何从0开始写一个数据通信,将数据从单片机发送到上位机(或者虚拟示波器)进行数据或图像显示,以及常见问题或注意事项解答,本文主要以匿名上位机为例,适合新手和小白

      本文主要内容:详细介绍如何从0开始写一个数据通信,将数据从单片机发送到上位机(或者虚拟示波器)进行数据或图像显示,帮助我们调节一些参数,比如电机PID的调节.波形融合等,以及在我们写通信协议的时候 ...

最新文章

  1. 用VS2005打开方案出现“此安装不支持该项目类型”
  2. 负载均衡技术中的真集群和伪集群
  3. Django model层 mysql_Django模型层(models.py)之模型创建
  4. OpenCV Laplacian算子
  5. 物理机Windows系统下使用SSH连接虚拟机Ubuntu
  6. 中国大学MOOC 计算机组成原理第4章 测试(上)
  7. -wl是不是c语言的标识符,C语言基础知识考试
  8. 工作区、暂存区、版本库、远程仓库
  9. 云+X案例展 | 民生类:肯耐珂萨入围腾讯SaaS加速器首期成员名单
  10. 数据库设计三大范式应用实例剖析(讲得比较清楚)
  11. NLPCC 2021 Final Call For Papers
  12. 算法学习笔记——数据结构:树状数组BIT
  13. 字符串匹配和KMP模式匹配(没太学懂,暂时不写)
  14. 分段模糊隶属度的自定义python函数(线性插值原理)
  15. Papers with Code一个查找论文和对应代码的神器
  16. 解决图片按顺序加载问题
  17. python:calendar 日历相关库
  18. nuxt中客户端呈现的虚拟DOM树与服务器呈现的内容不匹配
  19. 安超云生态|安超云与长城超云完成兼容互认证 携手打造协同生态
  20. centos6.6 极点五笔安装

热门文章

  1. java日期计算天数_用Java计算两个日期之间的天数
  2. xp信息服务器iis5.0,XP中安装iis5.0/IIS6.0的详细操作方法步骤(图文教程)
  3. 蓝凌OA SSRF+JNDI远程命令执行
  4. 5g8大学计算机基础东南大学,东南大学新学期迎来5G新网红 省内首家高校5G概念示范厅来了...
  5. Dell PowerEdge R750 Intel DAOS 顺利通过“HighPerf Ready 1.0”测试
  6. 【系统化学习】CSDN算法技能树测评
  7. SAS,软件使用中reg报错/gplot错误的解决方法。
  8. 卷积神经网络结构相关
  9. [Azure - VM] 解决办法:无法通过SSH连接VM,解决错误:This service allows sftp connections only.
  10. ios13 微信提示音插件_ios13怎么改微信提示音