寒假OS笔记第一天
本次寒假的目标:自制一个toy kernal
今天准备完成的事情:
找到自制OS所需要的资料
开始第一行代码
寻找资料
在知乎的这个专题和这个专题找到了很多很有帮助的回答:
https://www.zhihu.com/question/25628124
https://www.zhihu.com/question/22463820
从中总结出来的是:
- 一个人写一个Demo级别的OS是绝对可行的
- 《30天自制操作系统》这本书只适合beginner,不适合计算机专业的学生
- 看英文材料是必经之路,逃不掉的
- …
从这些地方收集到的资料有:
-
Bran’s Kernal Development Tutorial
一个bootloader是GRUB的操作系统编写教程
https://link.zhihu.com/?target=http%3A//www.osdever.net/bkerndev/Docs/gettingstarted.htm -
Xv6
一个简单的Unix-like的教学操作系统
https://link.zhihu.com/?target=https%3A//pdos.csail.mit.edu/6.828/2011/xv6.html -
hurlex
一个github上的简短的操作系统教程
http://wiki.0xffffff.org/ -
linux 0.11内核详解/剖析
一本书,年代久远,怀疑内容会不会过时 -
BrokenThorn Entertainment
http://www.brokenthorn.com/Resources/OSDevIndex.html
稍微看了一些目录,感觉还不错 -
JamesM’s kernel development tutorials
https://link.zhihu.com/?target=http%3A//www.jamesmolloy.co.uk/tutorial_html/
现在和以前不一样了,以前什么资料都没有,一清二白,能自己写出内核的那都是真正的大佬。现在的资料比以前多多了,但是筛选资料的难度也增大了。
我准备这些都参考参考。先看一些简单的入个门,然后再深入了解。
此外,我手上目前还有的参考书有一本操作系统概念。
规划
今天先看个有中文的,构造一下概念,然后再看英语的教程。
有中文的教程目前看来只有hurlex了
http://wiki.0xffffff.org/
正式笔记
第一个参考的文档是hurlex
我发现它也是本科生写的。
而且它是基于JamesM’s kernel development tutorials写的。
先看看这个文档的架构
首先是对计算机启动过程等等的概述,其次是按部就班的各个步骤,看起来逻辑很清晰的样子,最远到了内存线程的创建和切换,估计JamesM’s kernel development tutorials最远也只到这附近
每一篇文章的篇幅都并不是很长(对我来说)
算是一个比较简单的能让我建立一个概念的教程吧
Hurlex教程学习——开发环境
这个hurlex是建立在x86架构CPU的保护模式下的。
问题1:什么是CPU的保护模式
CPU有至少两个模式:实模式,保护模式。CPU刚刚启动的时候就是以实模式启动的,是一个速度很快的8086,等到操作系统接管后,它进入实模式,有了更多的功能
实模式和保护模式最大的不同在与地址转换方式
它的开发环境、工具、语言为:
- linux系统
- C语言
- gcc编译器
- GUN make构建工具
- qemu虚拟机
第一步,下载qemu虚拟机
我用的是ubuntu 20.4 LTS
使用如下指令进行安装:
$ sudo apt install qemu
但是之后我这里并不是和教程一样,在/usr/bin
目录下面有一个qemu-system-i386
的文件
另外,使用locate qemu
也找不到我任何下载过了的蛛丝马迹
淦
深思熟虑之下决定使用sudo apt purge qemu
先卸载了,然后用bochs
bochs是另外一款虚拟机
其实还可以使用vmware,但是我嫌它太笨重了。
使用sudo apt install bochs
来进行下载
bochs从来没用过,我估计得花点时间学一学
第二步,开发过程中使用的脚本文件
这个教程直接给出了一个makefile
但是作为一个成熟的开发者,我才不会用现成的makefile,我当然得自己写一个
所以先跳过这一段
bochs的配置文件
经过十五分钟的学习后,我大概懂了bochs配置文件怎么写
# 能使用的内存大小,单位为mb
megs: 32
# ROM文件,这个文件在/usr/share/bochs里面能找到,但是bochs自己提供了BXSHARE这个环境变量
romimage: file="$BXSHARE/BIOS-bochs-latest"
vgaromimage: file="$BXSHARE/VGABIOS-lgpl-latest"
# 软盘。因为我们的目的是生成系统镜像,然后使用bochs运行,这个系统镜像就相当于软盘。有一个软盘就用floppya,两个就是 floppyb,以此类推
floppya: 1_44=floppy.img, status=inserted
# 设置启动设备
boot: floppy
# 鼠标是否启用
mouse: enabled=0
# 启用键盘映射,现在的版本的bochs使用keyboard而不是keyboard_mapping
keyboard: keymap="$BXSHARE/keymaps/x11-pc-us.map"
# CPU配置
clock: sync=realtime
使用man bochsrc
能获得详细的使用文档
在用户的家目录下创建.bochsrc
,内容就是上面的这些内容
使用bochs
试运行
提示错误:
00000000000p[ ] >>PANIC<< dlopen failed for module 'x' (libbx_x.so): file not found
使用apt-file search libbx_x.so
,发现少下载了bochs-x
$ apt-file search libbx_x.so
bochs-x: /usr/lib/bochs/plugins/libbx_x.so
bochs-x: /usr/lib/bochs/plugins/libbx_x.so.0
bochs-x: /usr/lib/bochs/plugins/libbx_x.so.0.0.0
赶紧用sudo apt install bochs-x
下一个
再次运行,出现一个小黑框。因为我们的软盘floppy.img
根本不存在,所以是小黑框
命令行输入q
就能关掉
恰个午饭再说,上午的工作就这些吧(因为我睡到十一点才起床所以啥都没干)
三,计算机启动过程、GRUB、multiboot
计算机启动过程,这篇教程给出了一篇博客:http://www.ruanyifeng.com/blog/2013/02/booting.html
此处我做一些简单的总结
首先,计算机必须先启动起来才能正常运行程序,但是不运行程序计算机又不能启动,因此,计算机的启动是一个很矛盾的过程,必须先将一小段程序装进内存起引导程序的作用。
计算机的启动过程分为:
- BIOS
- 主引导
- 硬盘启动
- 操作系统
- BIOS
BIOS的意思是Basic Input and output system
开机程序被刷进ROM芯片,计算机开机后的第一件事情就是读取它。
BIOS会干这几件事情:
- 硬件自检,检查计算机硬件是否能满足运行的基本条件
- 启动顺序,自检完成后,BIOS需要知道下一阶段的启动程序放在哪个地方
- 主引导
知道下个阶段的启动程序放在哪个地方之后,BIOS将控制权交给启动程序。
启动程序放在某个存储设备中,计算机会先读取第一个扇区(512字节),称之为主引导记录。
若主引导记录的最后两个字节是0x55、0xAA表明设备可以被启动
主引导程序的主要作用是告诉计算机去硬盘的哪个位置寻找操作系统
它的前446个字节主要是调用操作系统的机器码
第446个字节到510个字节是分区表(partition table),将硬盘分为若干个区
最后两字节就是0x55、0xAA
硬盘分为很多区,每个区可以装不同的操作系统,主引导记录需要知道将控制权转交给哪个区
分区表的长度只有64个字节,分为四份,每份16个字节,因此一个硬盘最多只能分4个一级分区
16个字节的结构:
第1个字节:若为0x00 说明该分区为需要转交控制权的分区。四个分区只有一个能是0x00
第2~4字节:第一个扇区的物理信息
第5个字节:主分区类型
第6~8个字节:主分区最后一个扇区的物理位置(注:我怀疑这是32字节程序的资料)
第9~12个字节:主分区第一个扇区的逻辑地址
第13~16字节:主分区的扇区总数(小于2的32次方)
- 磁盘启动
主引导程序确定硬盘分区后,将控制权转交给硬盘
这里有三种情况
- 卷引导。即是普通的情况
- 扩展分区与逻辑分区。处理的是4个主分区不够用的情况。扩展分区里面最多只能有一个分区能被定义为扩展分区。扩展分区里面又分为多个区,称之为逻辑分区。基本上这种情况极其少见
- 启动管理器。操作系统安装在扩展分区的时候就使用这种方法启动。它将运行启动管理器,由用户决定启动哪个操作系统。目前linux中最流行的就是GRUB了
- 操作系统。控制权转交给操作系统后,内核先被载入内存。linux第一个运行的程序为
/sbin/init/
,它的pid号为1,其他进程都是它的后代。init程序加载系统的各个模块,直到执行用户登录程序为止。
GRUB是一个多重操作系统启动管理器。使用ubuntu的我每次开机的时候都会看到那个界面。
很明确了,我们要学习的是32位的操作系统,起码我们能接触到的资料目前为止都是32位。总有一天我要写出64位的操作系统。
首先,32位程序中CPU寻址的范围是4GB,指向内存。但是除了内存外,计算机还有很多其他的设备也有存储空间。
因此,我们需要给它们分配不同的访问地址进行区分。
所谓端口统一编址,就是把所有的外设一股脑地写进4GB的地址空间里面。我们对某一个地址进行访问事实上是对某个外设进行访问。
所谓端口独立编址,就是指对外设进行独立编制。
接下来,教程里面介绍的这些东西我在《深入解析linux 0.11》这本书里面看过。但是忘得一干二净了。
晚上补回来。
总的来说,按下电源键后CPU会进入初始化,初始化完成后进入取指令、执行、取指令的循环。CPU执行的第一条指令的地址是0xFFFFFFF0,它指向外设即BIOS,BIOS执行自检后在外设表中逐个检查第一个扇区,一旦检查到第一个扇区的最后两个字节为0x55、0xAA,则将这个扇区加载到内存里面,然后将控制权转移到这个程序里面,这个扇区再寻找操作系统放的位置,再把控制权转交给操作系统。
我们的任务就从设置这个扇区的数据开始。
这个扇区会加载一个bootloader程序,它负责将软硬件的环境设置到一个合适的状态然后加载操作系统内核并且移交执行权限。
不过,这篇教程并不打算自己写bootloader程序,因为像我这样子的初学者还是太young了。因此这篇教程打算直接就使用GRUB作为启动程序。
GRUB提供multiboot规范,它描述如何构造一个能被GRUB识别并且加载的操作系统内核。
由于我查看的教程没有对multiboot的描述,我只好自己百度。我搜到的内容为:
https://www.cnblogs.com/qq952693358/p/6713215.html
一些很具体的内容随用随搜,没有必要记住,所以不出现在笔记里面。
multiboot描述了引导程序引导操作系统时的接口。
符合multiboot规范的OS映像总是包含一个magic multiboot头,以便避免理解各种形式的可执行文件。
一个OS映像是一个某种操作系统使用的标准格式的32位可执行文件。它还必须额外包含一个multiboot头。这个multiboot头必须包含在OS映像的前8192字内,并且是32位对齐的。
里面一些比较重要的(我认为的)东西:
magic,必须等于0x1BADB002
flags,指出OS需要引导程序提供或者支持的特性。
反正我就看了个大概,因为我知道我肯定记不住。
Hurlex教程学习——Hello OS
我的第一个能独立在裸机上跑的程序是hello world
首先再复习下计算机开机的流程
BIOS启动,进行硬件自检,在设备的第一个扇区搜索引导程序
引导程序完成操作系统的设置,并将操作系统引导到内存
要写一个能在裸机上运行的程序,首先要把它存在某个设备上,再想办法将它加载到内存上面。
这个教程使用的设备是虚拟软盘。确实很容易制作。
这个教程使用的bootloader是GRUB,因此我们需要遵循GRUB的multiboot规范。并且在做程序前,我们需要把GRUB下载到软盘上面,也就是所谓的引导盘。
然后,再编写代码和编译脚本。
此后,还要配置BOCHS,以便它能运行这个程序。
因此,流程为:
- 制作引导盘
- 编译代码
- 运行程序
制作引导盘
制作引导盘是我花的时间最多的地方。
涉及到的指令有:
dd
,用来制作软盘losetup
,loop setup,将某个文件关联成设备mount
,挂载设备umount
,卸下设备
首先使用dd bs=512 count=2880 if=/dev/zero of=floppy.img
来制作一张1.5MB软盘
bs的意思是每个扇区的大小为512字节
conut的意思是有2880个扇区
if:input file,of:outputfile
/dev/zero
我迷迷糊糊知道是什么东西,大概从它那里复制来的东西都是空白的吧
这样子就能制作一张空白的虚拟软盘
接下来要做的是往这张虚拟软盘里面安装GRUB引导程序
我使用的GRUB引导程序版本为0.96
注意,现在大部分的GRUB引导程序的版本都是二点几的,和以前的版本完全不一样
而且,划重点,以前的GRUB引导程序只有几十K,至多几百K,而GRUB2动不动就几万K的,一张1.5MB的软盘完全装不下。
并且,GRUB2没有用户交互界面,也就是它取消了grub
命令
因此,想要制作启动盘,要么在虚拟机上面搞一个,要么自己安装一个远古版本的GRUB
但是,现在的ubunt 20.04 LTS貌似已经不支持下载远古时期的GRUB了,即使有一个grub-legcy-ec2可以下载,也不提供grub
命令
因此,到这里下载一个远古时期的GRUB
ftp://alpha.gnu.org/pub/gnu/grub
最好下载一个已经编译好的
解压之后里面有一个stage1
和stage2
首先挂载我们做好的软盘
$ sudo losetup /dev/loop7 floppy.img
$ sudo mount /dev/loop /mnt/floopy
首先把floopy和loop7
关联起来
什么是loop设备,建议百度
要怎么知道哪个loop是可以用的呢?使用sudo fdisk -l
查看,没有找到就说明这个loop设备没有和别的存储设备关联起来,就可以用
接下来挂载软盘
注意,/mnt/floopy
是mkdir
手动创建的
现在我们已经挂载完成了,所有对/mnt/floopy
的操作都会写入软盘
首先在里面建立文件目录,然后把刚刚的stage1
、stage2
文件转移进去
$ sudo mkdir -p /mnt/floopy/boot/grub
$ sudo cp stage1 /mnt/floopy/boot/grub
$ sudo cp stage2 /mnt/floopy/boot/grub
接下来,需要写一份grub的初始化文件,称之为grub.conf
$ sudo vim /mnt/floppy/boot/grub/grub.conf
文件内容如下:
title YOUR OS TITLE
root (fd0)
kernel YOUR OS ROUTE
例如我的配置就是
title ymwm os 0.0.0
root (fd0)
kernel /ymwm_kernel
然后使用grub
命令进入grub
的交互模式
输入以下指令
grub> device (fd0) /dev/loop7
grub> root(fd0)
grub> setup(fd0)
grub> quit
一般来说都能成功,如果成功不了,看看是不是用root身份启动的
做好软盘之后,我们就该开始写代码了
首先,这些代码都是抄的
; 魔数
MBOOT_HEADER_MAGIC equ 0x1BADB002
; 按页对齐
MBOOT_PAGE_ALIGN equ 1 << 0
; 内存空间的信息包含在multiboot中
MBOOT_MEM_INFO equ 1 << 1
;定义multiboot标记
MBOOT_HEADER_FLAGS equ MBOOT_PAGE_ALIGN | MBOOT_MEM_INFO
MBOOT_CHECKSUM equ -(MBOOT_HEADER_MAGIC+MBOOT_HEADER_FLAGS)
[BITS 32]
section .text ;代码段开始
dd MBOOT_HEADER_MAGIC
dd MBOOT_HEADER_FLAGS
dd MBOOT_CHECKSUM
[GLOBAL start] ;入口
[GLOBAL glb_mboot_ptr] ;声明struct multiboot*变量
[EXTERN kern_entry] ; 声明内核C代码入口
start:
cli ; 关闭内核保护模式的中断
mov esp, STACK_TOP ; 内核栈地址
mov ebp, 0 ; 栈指针
and esp, 0FFFFFFF0H ; 栈地址对齐
mov [glb_mboot_ptr], ebx ; 将ebx存入
call kern_entry ; 调用内核入口函数
stop:
hlt ; 停机指令
jmp stop ; 结束
section .bss
stack:
resb 32768
glb_mboot_ptr:
resb 4
STACK_TOP equ $-stack-1
它用的是NASM汇编,但是我对这个的了解还不如对澳大利亚大白鼠的了解多
但是我大概加上注释以后能看的懂它在干什么
首先是定义multiboot头
然后是定义入口和全局变量、声明内核代码的入口
接下来一段含义我并不懂
esp栈顶指针,ebp存放基址,ebx在内存寻址的时候存放基址
它们为什么是这些值,说实话我不懂
然后是一堆数据的定义
这个就是引导程序了,它的大小很小
接下来是入口函数
我们上面声明的入口函数叫kernel_entry
。那么我们就定义一个kernel_entry
函数
// entry.c
int kernel_entry()
{
return 0;
}
然后是makefile和链接脚本
因为我makefile没学好,虽然事实上手动编译我也会,但是还是先抄一下教程的代码
#!Makefile
C_SOURCES = $(shell find . -name "*.c")
C_OBJECTS = $(patsubst %.c, %.o, $(C_SOURCES))
S_SOURCES = $(shell find . -name "*.s")
S_OBJECTS = $(patsubst %.s, %.o, $(S_SOURCES))
CC = gcc
LD = ld
ASM = nasm
C_FLAGS = -c -Wall -m32 -ggdb -gstabs+ -nostdinc -fno-pic -fno-builtin -fno-stack-protector -I include
LD_FLAGS = -T scripts/kernel.ld -m elf_i386 -nostdlib
ASM_FLAGS = -f elf -g -F stabs
all: $(S_OBJECTS) $(C_OBJECTS) link update_image
.c.o:
@echo 编译代码文件 $< ...
$(CC) $(C_FLAGS) $< -o $@
.s.o:
@echo 编译汇编文件 $< ...
$(ASM) $(ASM_FLAGS) $<
link:
@echo 链接内核文件...
$(LD) $(LD_FLAGS) $(S_OBJECTS) $(C_OBJECTS) -o ymwm_kernel
.PHONY:clean
clean:
$(RM) $(S_OBJECTS) $(C_OBJECTS) ymwm_kernel
.PHONY:update_image
update_image:
sudo mount floppy.img /mnt/kernel
sudo cp ymwm_kernel /mnt/kernel/ymwm_kernel
sleep 1
sudo umount /mnt/kernel
.PHONY:mount_image
mount_image:
sudo mount floppy.img /mnt/kernel
.PHONY:umount_image
umount_image:
sudo umount /mnt/kernel
.PHONY:qemu
qemu:
qemu -fda floppy.img -boot a
.PHONY:bochs
bochs:
bochs
.PHONY:debug
debug:
qemu -S -s -fda floppy.img -boot a &
sleep 1
cgdb -x tools/gdbinit
等我以后学会了一定要自己写
/*
* kernel.ld -- 针对 kernel 格式所写的链接脚本
*/
ENTRY(start)
SECTIONS
{
/* 段起始位置 */
. = 0x100000;
.text :
{
*(.text)
. = ALIGN(4096);
}
.data :
{
*(.data)
*(.rodata)
. = ALIGN(4096);
}
.bss :
{
*(.bss)
. = ALIGN(4096);
}
.stab :
{
*(.stab)
. = ALIGN(4096);
}
.stabstr :
{
*(.stabstr)
. = ALIGN(4096);
}
/DISCARD/ : { *(.comment) *(.eh_frame) }
}
里面有一些参数,它的含义是不使用C语言的标准库和GCC的内建库,因为我们的工作是从0开始的,不能使用那些基于特定系统的库。知道有那么些参数就够了。
接下来还需要搞一些以后会经常使用的数据结构
#ifndef INCLUDE_TYPES_H_
#define INCLUDE_TYPES_H_
#ifndef NULL
#define NULL 0
#endif
#ifndef TRUE
#define TRUE 1
#define FALSE 0
#endif
typedef unsigned int uint32_t;
typedef int int32_t;
typedef unsigned short uint16_t;
typedef short int16_t;
typedef unsigned char uint8_t;
typedef char int8_t;
#endif // INCLUDE_TYPES_H_
现在我的文件的目录结构是:
.
├── bochsrc
├── boot
│ └── boot.s
├── floppy.img
├── include
│ └── types.h
├── init
│ └── entry.c
├── Makefile
└── scripts
└── kernel.ld
4 directories, 7 files
这个操作很炫,用tree
命令实现的
按下make
,就会自动生成一个内核,并且将这个内核加载到软盘里面
因为我们的内核毕竟现在什么都没做,为了让它做点什么,把入口函数改成:
#include "types.h"
int kern_entry()
{
uint8_t *input = (uint8_t *)0xB8000;
uint8_t color = (0 << 4) | (15 & 0x0F);
*input ++ = 'H'; *input ++ = color;
*input ++ = 'e'; *input ++ = color;
*input ++ = 'l'; *input ++ = color;
*input ++ = 'l'; *input ++ = color;
*input ++ = 'o'; *input ++ = color;
*input ++ = ','; *input ++ = color;
*input ++ = ' '; *input ++ = color;
*input ++ = 'W'; *input ++ = color;
*input ++ = 'o'; *input ++ = color;
*input ++ = 'r'; *input ++ = color;
*input ++ = 'l'; *input ++ = color;
*input ++ = 'd'; *input ++ = color;
*input ++ = '!'; *input ++ = color;
return 0;
}
这样子它就会输出白色的helloworld
实不相瞒,代码我抄的,鬼知道啥意思
然后按下make进行编译
它会提示你少一个nasm
命令
无妨,sudo apt install nasm
来一个
然后按下make进行编译
编译成功
因为之前已经配置好bochs了,直接在当前目录输入bochs就可以了
刚刚进去会是一个小黑框什么都没有,需要在命令行输入c
才会开始运行
运行后并不是直接开机而是进入GRUB引导界面
因为这个“内核”是我们自己写的,所以我们很清楚它的位置在/ymwm_kernel
grub> kernel /ymwm_kernel
grub> boot
先加载系统镜像,再boot
注意左上角会出现一个白色的hello world
任务完成
一日总结
实际上我写下这段话已经是第二天了
昨天搞GRUB和BOCHS花了我一下午加一个晚上的时间
主要还是因为我对linux不熟悉
而且那些教程基本上都没有说清楚它们使用的软件环境和软件版本
我花了一个晚上才意识到我使用的GRUB是不行的
等到全部就绪的时候已经是凌晨了
总结一下今天,准确说是昨天,我做了哪些事情:
以下,学习
代表我认真看了并且掌握了,了解
说明我认真看了但是到此为止
- 查找了很多资料教程,并且明确了(暂时可以那么说)自己的学习路线
- 学习了计算机的启动过程
- 学习了GRUB的操作(低版本)
- 学习了BOCHS的配置(借助
man bochs
) - 了解了multiboot规范
- 学习了linux下如何制作软盘、如何挂载软盘
- 完成了一个可以输出hello world的小东西,虽然对原理不甚知之
明天,或者说是今天预备完成的事情有:
- 认真学习multiboot规范
- 认真学习makefile的写法
- 认真学习NASM的语法
- 有时间的花重新学习编译过程
- 完成“字符模式下的显卡驱动”