自己动手写操作系统(一)

本系列文章将一步步实现一个简单的操作系统。实验环境是在Linux系统下通过Bochs虚拟机运行我们自己写的操作系统。

一、实验环境搭建

1. Ubuntu的安装,Windows用户可以选择在虚拟机中安装Ubuntu,具体安装教程可自行搜索。

2. Bochs虚拟机的安装

在学习编写操作系统的过程中,我们需要一个虚拟机来模拟出一个虚拟的计算机硬件环境,比如cpu、内存、硬盘等,并且能够运行并且调试我们写的代码。Bochs很好的提供了以上所有功能,在它面前我们就像上帝一样随时可以让时间停止,”钻“到计算机内部,查看这个虚拟电脑的一切信息,这正是开发操作系统所需要的。

Ubuntu下我们可以直接运行以下命令以源码方式安装Bochs(注意我们之所以选择以源码方式安装,是因为通过apt install安装的Bochs是没有调试功能的)

$ sudo apt update
$ sudo apt install build-essential libx11-dev xorg-dev libgtk2.0-dev
$ wget https://sourceforge.net/projects/bochs/files/bochs/2.7/bochs-2.7.tar.gz
$ tar zxvf bochs-2.7.tar.gz
$ cd bochs-2.7/
$ ./configure --enable-debugger --enable-disasm --enable-debugger-gui
$ make 
$ sudo make install

./configure后面的参数便是打开调试功能的开关

到此实验环境搭建完毕。

二、简单的引导扇区汇编代码

先简单讲一下计算机的启动流程,详细启动过程可参考我的另一篇博客 操作系统启动过程

按下开机键后,计算机首先会运行BIOS中的代码,BIOS在进行硬件检查和初始化后,会按照设置好的启动顺序(我们在使用U盘安装系统时,经常要进入BIOS设置这个启动顺序),依次寻找启动设备(比如硬盘、U盘等)。然后将第一个可用的启动设备的第一个扇区载入内存0x7c00处,并把执行权限交给它。

启动设备的第一个扇区我们称之为引导扇区(MBR),共512个字节,必须以数值0x55及0xaa结尾。包括三部分内容:引导加载程序(Boot Loader)(前446个字节,如GRUB等)、磁盘分区表(DPT,Disk Partition Table)、分区有效性标志(55AA)。其中的引导加载程序负责加载启动硬盘分区中的操作系统。

在BIOS向引导程序移交执行权之前,BIOS会对处理器进行初始化,这其中就包括处理器的代码段寄存器CS和指令指针寄存器IP。当BIOS跳转至引导程序时,CS和IP的值分别为0x0000和0x7c00。此时的处理器处于实模式下,物理地址必须经过CS寄存器和IP寄存器转换才能得到。转换公式为:物理地址=CS<<4+IP,也就是物理地址0x7c00处。

BIOS由Bochs虚拟机提供,我们接下来写的就是这个512字节的引导扇区(MBR)的汇编代码。目前它并不用加载操作系统,我们只让它在屏幕上打印出经典的“hello world”即可。

首先看一下汇编代码:

	org 0x07c00
 	mov ax,cs
	mov ds,ax
	mov es,ax
	mov ax,Message
	mov bp,ax
	mov cx, 13
	mov ax,0x1301
	mov bx,0x0002
	mov dh,0
    mov dl,0
	int 0x10
	jmp $
Message: 
	db "Hello, world!"
    times 510-($-$$) db 0
    dw  0xaa55

代码和数据是按汇编程序的编写顺序依次连续存放到内存的,即上面的程序在0x7c00处开始存放的是org 0x07c00的机器指令,在512字节最后放的是0xaa55数据。

BIOS程序在把引导程序加载到内存时,同时还创建了中断系统,在物理内存的前1KB空间初始化中断向量表,在物理内存最后256KB物理地址空间内保存中断处理程序。cpu运行完BIOS后,物理内存的布局如下:

本程序就是调用0x10号中断,在屏幕上打印字符串。在调用0x10号中断处理程序往显示器的屏幕上打印字符串时,所有的参数都是通过cpu中的寄存器传递的,各参数的含义如下:

  • 寄存器ah:0x13表示向屏幕打印字符串;
  • 寄存器al:指定光标和字符的属性
    0,表示字符的属性值保存在寄存器bl中,光标停留在字符串的首字符
    1,表示字符的属性值保存在寄存器bl中,光标停留在字符串的尾字符
    2,表示字符的属性值紧跟在字符之后,光标停留在字符串的首字符
    3,表示字符的属性值紧跟在字符之后,光标停留在字符串的尾字符
  • 寄存器bl:若寄存器al的值为0或者1时,保存字符的属性值。如图所示,字符属性由一个字节大小的数据表示。
  • 寄存器[es:bp]:保存字符串的首字符在数据段中的逻辑地址
  • 寄存器cx:保存字符串的长度
  • 寄存器dh,dl:字符串在屏幕上的起始坐标,其中寄存器dh为行号,寄存器dl为列号。显示器的屏幕只能显示25行字符,并且每行只能显示80个字符,因此dh的取指范围为0~24,dl的取指范围为0~79。

代码解析:

第1行,告诉编译器程序加载到内存的0x7c00处。

第2~4行,统一数据段寄存器DS和附加段寄存器ES的值和代码段寄存器CS一致,即不论数据段还是代码段,段起始地址都是0x7c00。

第5~6行,将字符串"Hello, world!"的首地址传递给寄存器bp。注意任何不被方括号[ ]括起来的标签或变量名都被认为是地址,访问标签或变量中的内容必须使用[ ]

第7行,将字符串的长度传递给寄存器cx

第8~11行,字符串属性设置

第12行,调用0x10号中断处理程序,在屏幕上显示字符串

第13行,使cpu进入死循环
因为cpu会不停的根据寄存器[cs:ip]中的逻辑地址转换后的物理地址,从物理内存读取机器指令,然后对其解析、执行。其中,当运行完一条机器指令后,cpu自动将该机器指令的下一条机器指令的偏移地址赋值给ip。当cpu运行完可执行文件中的最后一个机器指令后,若不采取任何措施,则cpu会将下面的数据看做机器指令,进行取指、解析、执行。因此,需要一条让cpu进入死循环的机器指令作为可执行文件的最后一条机器指令。

$表示当前行被汇编后的地址,$$表示程序被汇编后的开始地址,也就是0x7c00.

第15、17行,分别以字节和字的形式存放的数据

第16行,表示将0这个字节重复510-($-$$)遍,也就是在剩下的空间不停填充0,直到第510个字节为止。这样加上结束标志0xaa55占用的两个字节,恰好是512个字节。

在运行代码之前我们需要将其转换成计算机能读懂的机器指令形式,这就需要编译器。我们编译c代码使用GCC,编译汇编程序使用nasm编译器。

把上面的代码保存成boot.asm,然后使用nasm编译一下,生成二进制可执行文件boot.bin

$ nasm boot.asm -o boot.bin

三、虚拟硬盘的制作

下面我们将制作一个虚拟硬盘并将已经生成的可执行文件boot.bin放到虚拟硬盘的第一个磁盘块(引导扇区MBR)中。Bochs虚拟机将使用这块“硬盘”引导启动。

首先选择合适的地方创建一个工程目录

$ mkdir projectest
$ cd projectest

将可执行文件boot.bin拷贝到该工程目录中

然后在本层目录中创建一个大小为1MB的硬盘镜像文件b.img的命令如下:

$ dd if=/dev/zero of=b.img bs=512 count=2048

dd是文件拷贝命令,其中:

  • if=/dev/zero:表示拷贝的源文件的路径,"/dev/zero"是一个特殊的文件,可以提供n个0(n的值等于bs和count参数的积)
  • of=b.img:表示拷贝的目标文件路径。若不存在,则创建该文件
  • bs=512:表示块大小,单位为B
  • count=2048:表示拷贝的文件的块的数量。

由bs和count参数可知,硬盘镜像文件的大小为:2048*512B=1MB,硬盘镜像文件制作好后,将可执行文件boot.bin拷贝到硬盘镜像文件b.img(硬盘)的引导扇区的命令如下:

$ dd if=boot.bin of=b.img bs=512 seek=0 conv=notrunc

其中:

  • seek=0:表示把可执行文件boot.bin拷贝到硬盘镜像文件b.img的引导扇区(扇区号为0)。
  • conv=notrunc:表示不改变目标文件的大小,若没有该选项,则硬盘镜像文件b.img的大小会由1MB变为可执行文件boot.bin的大小512B。

这样一个写入了引导程序的“硬盘”就制作好了。

四、Bochs的使用

1. 启动Bochs

"硬盘”制作好后,要想启动bochs还需要一个配置文件——bochsrc.bxrc。为什么需要配置文件呢?因为你需要告诉bochs你希望的虚拟机是什么样的,比如,内存多大,使用哪个硬盘启动等等。在下载bochs的源码包中有一个.bochsrc,就是官方提供的配置文件示例,我们可以根据这个更改。

下面是本实验用到的bochs配置文件代码

romimage: file=/usr/local/share/bochs/BIOS-bochs-latest 
vgaromimage: file=/usr/local/share/bochs/VGABIOS-lgpl-latest 
ata0-master: type-disk, path="b.img"
megs: 16
cpu: count=1
boot: disk

其中:

  • romimage:指定bochs运行过程中使用的ROM-BIOS的路径。
  • vgaromimage:指定bochs运行过程中使用的VGA的ROM-BIOS的路径。
  • ata0-master:指定硬盘镜像文件b.img的路径。
  • megs:指定物理内存的大小,单位为MB。
  • cpu:指定cpu的个数,1个。
  • boot:指定启动方式,从硬盘启动。

将上面的代码保存为bochsrc.bxrc,也存到工程目录下。

现在一切准备就绪,启动bochs的命令如下:

$ bochs -q -f bochsrc.bxrc

其中:

  • -q: 跳过bochs启动后的配置界面。
  • -f : bochsrc.bxrc:指定配置文件的路径。
  • 如果不指定路径,那么Bochs将按照如下顺序在当前目录中寻找配置文件:
  1. .bochsrc

  2. bochsrc

  3. bochsrc.txt

  4. bochsrc.bxrc (windows only)

  5. /home/.bochsrc (Unix only)

  6. /etc/bochsrc(Unix only)

运行bochs后会在终端出现bochs调试命令行,等待我们输入调试命令,这里输入c继续执行
在这里插入图片描述
可以看到在虚拟机中我们的引导程序已成功运行,在屏幕上打印出了hello world。
在这里插入图片描述

2. Bochs调试

常用调试命令作用
b使用物理地址打断点
vb使用逻辑地址打断点
blist查看所有断点信息
n单步执行(遇到函数跳过)
s单步执行(遇到函数进入函数内部)
c继续执行
r查看所有通用寄存器的值
(eax、ebx、ecx、edx、esp、ebp、esi、edi、eip、eflags)
sreg查看所有段寄存器的值
u /5打印CPU接下来将执行的5条指令
xp查看物理内存中指定物理地址的内容
xp /2bx 物理地址:打印两个字节,以十六进制格式输出。
xp /13c 物理地址:打印13个字节,以ASCII码对应的字符显示
watch 变量名运行时,若某一行代码修改了变量,则中断,并打印修改前后的值。
q退出调试继续执行
  • 12
    点赞
  • 75
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
本书在详细分析操作系统原理的基础上,用丰富的实例代码,一步一步地指导读者用C语言和汇编语言编出一个具备操作系统基本功能的操作系统框架。本书不同于其他的理论型书籍,而是提供给读者一个动手实践的路线图。书中讲解了大量在开发操作系统中需注意的细节问题,这些细节不仅能使读者更深刻地认识操作系统的核心原理,而且使整个开发过程少走弯路。全书共分7章。 本书适合各类程序员、程序开发爱好者阅读,也可作为高等院校操作系统课程的实践参考书。 折叠 作品目录 第1章 马上动手一个最小的"操作系统"1 1.1 准备工作1 1.2 10分钟完成的操作系统1 1.3 Boot Sector3 1.4 代码解释3 1.5 水面下的冰山5 1.6 回顾6 第2章 搭建你的工作环境7 2.1 虚拟计算机(Virtual PC)7 2.1.1 Virtual PC初体验8 2.1.2 创建你的第一个Virtual PC9 2.1.3 虚拟软盘研究12 2.1.4 虚拟软盘实战14 2.2 编译器(NASM & GCC)18 2.3 安装虚拟Linux19 2.4 在虚拟Linux上访问Windows文件夹26 2.5 安装虚拟PCDOS26 2.6 其他要素29 2.7 Bochs29 2.7.1 Bochs vs. Virtual PC vs. VMware30 2.7.2 Bochs的使用方法31 2.7.3 用Bochs进行调试33 2.7.4 在Linux上开发34 2.8 总结与回顾36 第3章 保护模式(Protect Mode)37 3.1 认识保护模式37 3.1.1 GDT(Global Descriptor Table) 42 3.1.2 实模式到保护模式,不一般的jmp45 3.1.3 描述符属性47 3.2 保护模式进阶50 3.2.1 海阔凭鱼跃50 3.2.2 LDT(Local Descriptor Table)58 3.2.3 特权级62 3.3 页式存储82 3.3.1 分页机制概述83 3.3.2 编代码启动分页机制84 3.3.3 PDE和PTE85 3.3.4 cr388 3.3.5 回头看代码88 3.3.6 克勤克俭用内存90 3.3.7 进一步体会分页机制100 3.4 中断和异常107 3.4.1 中断和异常机制109 3.4.2 外部中断111 3.4.3 编程操作8259A113 3.4.4 建立IDT116 3.4.5 实现一个中断117 3.4.6 时钟中断试验119 3.4.7 几点额外说明121 3.5 保护模式下的I/O122 3.5.1 IOPL122 3.5.2 I/O许可位图(I/O Permission Bitmap)123 3.6 保护模式小结123 第4章 让操作系统走进保护模式125 4.1 突破512字节的限制125 4.1.1 FAT12126 4.1.2 DOS可以识别的引导盘131 4.1.3 一个最简单的Loader132 4.1.4 加载Loader入内存133 4.1.5 向Loader交出控制权142 4.1.6 整理boot.asm142 4.2 保护模式下的"操作系统"144 第5章 内核雏形146 5.1 用NASM在Linux下Hello World146 5.2 再进一步,汇编和C同步使用148 5.3 ELF(Executable and Linkable Format)150 5.4 从Loader到内核155 5.4.1 用Loader加载ELF155 5.4.2 跳入保护模式161 5.4.3 重新放置内核170 5.4.4 向内核交出控制权175 5.4.5 操作系统的调试方法176 5.5 扩充内核184 5.5.1 切换堆栈和GDT184 5.5.2 整理我们的文件夹191 5.5.3 Makefile191 5.5.4 添加中断处理200 5.5.5 两点说明218 5.6 小结219 第6章 进程221 6.1 迟到的进程221 6.2 概述222 6.2.1 进程介绍222 6.2.2 未雨绸缪--形成进程的必要考虑222 6.2.3 参考的代码224 6.3 最简单的进程224 6.3.1 简单进程的关键技术预测225 6.3.2 第一步--ring0→ring1227 6.3.3 第二步--丰富中断处理程序243 6.3.4 进程体设计技巧254 6.4 多进程256 6.4.1 添加一个进程体256 6.4.2 相关的变量和宏257 6.4.3 进程表初始化代码扩充258 6.4.4 LDT260 6.4.5 修改中断处理程序261 6.4.6 添加一个任务的步骤总结263 6.4.7 号外:Minix的中断处理265 6.4.8 代码回顾与整理269 6.5 系统调用280 6.5.1 实现一个简单的系统调用280 6.5.2 get_ticks的应用286 6.6 进程调度292 6.6.1 避免对称--进程的节奏感292 6.6.2 优先级调度总结300 第7章 输入/输出系统302 7.1 键盘302 7.1.1 从中断开始--键盘初体验302 7.1.2 AT、PS/2键盘304 7.1.3 键盘敲击的过程304 7.1.4 解析扫描码309 7.2 显示器325 7.2.1 初识TTY325 7.2.2 基本概念326 7.2.3 寄存器328 7.3 TTY任务332 7.3.1 TTY任务框架的搭建334 7.3.2 多控制台340 7.3.3 完善键盘处理346 7.3.4 TTY任务总结354 7.4 区分任务和用户进程354 7.5 printf357 7.5.1 为进程指定TTY357 7.5.2 printf()的实现358 7.5.3 系统调用write()361 7.5.4 使用printf()363 后记366

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值