文章目录
- 1. 工具预备
- 1.1 Ubuntu涉及到的编译工具
- 1.2 VScode涉及到的插件
- 1.3 virtual Box创建一个新的空虚拟机
- 2. 文件目录
- 3. 操作系统的引导 `loader.s`
- 3.1 涉及到的知识点有
- 3.2 程序执行的内存分配
- 3.3 汇编指令引导开机
- 4. C++编写的内核代码 kernel.cpp
- 4.1 定义打印函数printf
- 4.2 定义显示的主函数
- 4.3 kernel文件
- 5. Makefile 工程管理
- 6. linker
- 7. 激动人心的时刻到了
很久以前, 就想尝试实现一个OS, 所以在很久以前有对30天完成一个操作系统进行学习, 但是半途而废了; 最近由于工作签了, 正好空出这段时间来深入学习一下OS和编译器, 毕竟每个程序员都有一个梦想(实现一个自己的OS), 正好我未来的工作也是编译器方面, 因此我给自己定了两个小目标, 毕业之前把myos弄出来, 然后在把mycompiler弄出来, 至少将来去公司面对一堆清北大佬应该不至于只是端茶送水;
本科阶段一直玩的是单片机(51单片机, STM32, arduino, 树莓派, nanopi, cortexA9的arm开发板), 虽然看上去玩过很多, 但其实无非就是上层应用, 并不知道其底层的原理或者架构, 比如操作系统的几个核心知识: 内存管理, 中断, 定时器, 字符设备和块设备, IO, 网络, 端口; 正好借着这样的机会, 把整个体系结构梳理一遍.
我已经尽可能的把每一句都加了注释, 尽可能的详细乃至于墨迹, 所以希望大家不喜勿喷, 同时我已经将整个项目添加到了我的gitee
, 大家可以直接下载进行make mykernel.iso
编译, 生成可以通过虚拟机open的mykernel.iso
文件. 点击这里跳转到我的gitee
以下对应的是myos1_helloWorld/
, 先搭建一个操作系统的引导和框架, 成功引导后打印一个hello world
, 后面在往里面造轮子吧.
1. 工具预备
- 操作系统: win10及子系统wsl的Ubuntu18.04
- 编译器: 子系统Ubuntu(用于编译
.s
汇编,.ld
link,.cpp
c++ ) - 编辑器: VScode (用于连接子系统上的Ubuntu里面的编译环境, 以及编写代码)
- 虚拟机: Virtual (用于测试写好的操作系统)
1.1 Ubuntu涉及到的编译工具
as -version # 查看汇编编译器的版本
# GNU assembler (GNU Binutils for Ubuntu) 2.30
g++ --version # 查看C++编译器的版本
# g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
ld --version # 查看链接器的版本
# GNU ld (GNU Binutils for Ubuntu) 2.30
# 上面三个默认包含
grub-mkrescue --version # grub引导工具版本
# grub-mkrescue (GRUB) 2.02-2ubuntu8.23
# 安装grub引导工具
sudo apt install grub-pc
sudo apt install grub-efi-amd64
sudo apt install mtools
sudo apt install xorriso
xorriso --version
# xorriso 1.4.8 : RockRidge filesystem manipulator, libburnia project.
1.2 VScode涉及到的插件
以及C++
和WSL
1.3 virtual Box创建一个新的空虚拟机
创建新的虚拟机
→选择类型Other(Other/unknown)
→分配内存64M
→不添加虚拟磁盘→在存储设置选择虚拟盘中选择最后生成的.ios
文件→启动就会显示hello world
了
2. 文件目录
(mypy) zjq@myos1_helloWorld$ tree
.
├── Makefile # 编译指令汇总, 这里能学习到如何编写Makefile管理一个项目
├── kernel.cpp # 机器的内核程序, 这里可以了解到用户态和内核态的知识点
├── linker.ld # 链接脚本, 将所有的文件都链接到一个
├── loader.s # 汇编指令进行引导
└── readme.md
3. 操作系统的引导 loader.s
操作系统引导探究
首先明确一点, 电脑上电后, 首先是启动BIOS, 也就是一个固定代码, 检测当前机器上的硬盘(磁盘),
BIOS将磁盘的第一个扇区(磁盘最开始的512字节)载入内存,放在0x0000:0x7c00处(见图三),如果这个扇区的最后两个字节是“55 AA”,那么这就是一个引导扇区,这个磁盘也就是一块可引导盘。通常这个大小为512B的程序就称为引导程序(boot)。如果最后两个字节不是“55 AA”,那么BIOS就检查下一个磁盘驱动器。
通过上面的表述我们可以总结出如下三点引导程序所具有的特点:
- 它的大小是512B,不能多一字节也不能少一字节,因为BIOS只读512B到内存中去。
- 它的结尾两字节必须是“55 AA”,这是引导扇区的标志。
- 它总是放在磁盘的第一个扇区上(0磁头,0磁道,1扇区),因为BIOS只读第一个扇区。
3.1 涉及到的知识点有
%
指的是引用esp
堆栈栈顶的指针kernel_stack
内核栈, 每个需要执行的函数, 都需要先使用kernel_stack创建空间, 然后在将函数的参数入栈, 最后在call 函数进行执行_system_constructors
轮训函数, 初始化所有的构造函数cli
; 将IF置0,屏蔽掉“可屏蔽中断”,当可屏蔽中断到来时CPU不响应,继续执行原指令hlt
; 本指令是处理器“暂停”指令。jmp _stop
; 命令跳转指令.section .text
代码段.section .bss
未初始化的全局变量/初始化为0的内存段.section .multboot
规范编译内核, 才可以被GRUB引导
3.2 程序执行的内存分配
在C++中, 虚拟内存分为代码段,数据段, BSS段, 堆区, 文件映射区, 栈区六个部分
- 代码段: 包括只读存储区(字符串常量)和文本区(程序的机器代码), 只读
- 数据段: 存储程序中已初始化的全局变量和静态变量; 属于静态内存分配
- BSS段: 存储未初始化或初始化为0的全局变量和静态变量(局部+全局); 属于静态分配, 程序结束后静态变量资源由系统自动释放。
- 堆区: 调用 new/malloc 函数时在堆区动态分配内存,同时需要调用 delete/free 来手动释放申请的内存。频繁的malloc free造成内存空间不连续, 产生碎片, 因此堆比栈效率低
- 映射区:存储动态链接库以及调用 mmap 函数进行的文件映射
- 栈区: 存储函数的返回地址,返回值, 参数, 局部变量; 编译器自动释放,
3.3 汇编指令引导开机
存成 loader.s
/* ; 对于BootLoader来讲, 他不知道什么是kernel, 他只按照设定位置开始运行程序, 所以我们需要将kernel程序写入到指定的位置 0x1badb002 没有原因, 太爷爷们的规定
注意: .开头不会被翻译成机器指令, 而是给汇编器一种特殊知识, 称之为汇编指示,或者委操作 */
.set MAGIC, 0x1badb002 /*GRUB魔术块*/
.set FLAGS, (1<<0 | 1<<1) /*;GRUB标志块*/
.set CHECKSUM, -(MAGIC + FLAGS) /*;校验块*/
/* ; Boot程序按照Mutileboot 规范来编译内核,才可以被GRUB引导 */
.section .multboot
.long MAGIC
.long FLAGS
.long CHECKSUM
/* */
.section .text /* 代码段 */
/* 引用外部函数, 调用时候可以遍历所有文件找到该函数
这里之所以需要增加一个_kernel的"_" 是因为在ld时找不到函数所在, 这是因为kernel.cpp文件在经过编译之后
已经变成了call 87 <_kernelMain+0x9>, 所以这里需要使用_kernelMain来引入
查看命令 objdump -d kernel.o
*/
.extern _kernelMain
.extern _system_constructors /* 引用外部函数, 调用时候可以遍历所有文件找到该函数 */
.global loader /* .global 表示全局可见 */
/* AT&T 和 Intel对寄存器使用不一样, Intel不加符号, 而At&T使用%
下面先把两个寄存器数据(eax, ebx)压栈, 然后调用函数 kernelMain, 并且将两个参数传递给这个函数
*/
loader:
mov $kernel_stack, %esp
call _system_constructors
push %eax
push %ebx
call _kernelMain /* 这里就是引导执行这个函数, 这个函数在kernel.cpp里面定义 */
/*
cli ; 将IF置0,屏蔽掉“可屏蔽中断”,当可屏蔽中断到来时CPU不响应,继续执行原指令
hlt ; 本指令是处理器“暂停”指令。
jmp _stop ; 命令跳转指令
*/
_stop:
cli
hlt
jmp _stop
/* ; 未初始化变量端 */
.section .bss
/* 这个段开辟空间是2M */
.space 2*1024*1024
/* */
kernel_stack:
4. C++编写的内核代码 kernel.cpp
4.1 定义打印函数printf
由于我们之前都是使用的C语言库的函数printf, 但是这里没有库能调用, 只能字节往屏幕上写;
这里的知识点: 屏幕相当于一个存储器, 存储字符, 所以我们只需要往这个存储器里面保存我们想让它展示的字符即可; 而显示的存储器地址是 0xb8000
, 这是固定的, 因此要写什么, 直接往这里面写就行了
4.2 定义显示的主函数
由于这里使用的C语言编写的主函数, 所以需要加上extern "C"
指定, 从而解决一些麻烦
4.3 kernel文件
文件命名为 kernel.cpp
// 这里给显存地址(0xb8000)写数据即可
void printf(char* str){
static unsigned short* VideoMemory=(unsigned short*)0xb8000;
for(int i=0; str[i]!='\0';++i){
VideoMemory[i]=(VideoMemory[i] & 0xFF00)|str[i];
}
}
//操作系统构造函数委托方法
typedef void(*constructor)();
//全局定义构造委托
constructor start_ctors;
//全局定义析构委托
constructor end_ctors;
//轮询函数,并且执行
extern "C" void system_constructors(){
for(constructor* i= &start_ctors; i != &end_ctors; i++){
(*i)();
}
}
// warning: ISO C++11 does not allow conversion from string literal to 'char *' [-Wwritable-strings]
// 这是由于下面定义方法是C, 使用extern "C" 表示是C语言
// void kernelMain(void * multiboot_structure, unsigned int magicnumber){
extern "C" void kernelMain(void* multiboot_structure, unsigned int magicnumber){
printf((char*)"Hello World");
while(1);
}
5. Makefile 工程管理
知识点和关键点:
- 必须命名为
Makefile
- 无论是
Cpuls
还是As
都是32位
, 需要指定
Makefile起始内容不多, 主要是我写的注释☺
命名为 Makefile
# 依赖的公共模块
# GCCPARAMS = -m32 -W -fno-use-cxa-atexit -nostdlib -fno-builtin -fno-builtin -fno-rtti -fno-exceptions -fno-leading-underscore
GPPPARAMS = -m32 -Iinclude -fno-use-cxa-atexit -fleading-underscore -fno-exceptions -fno-builtin -nostdlib -fno-rtti -fno-pie
ASPARAMS = --32
LDPARAMS = -melf_i386 -no-pie
objects = loader.o kernel.o
# $@ 取所有输出文件
# $< 取第一个依赖
# 使用命令 make kernel.o 就会运行对应的 `clang++ -c -o kernel.o kernel.cpp`
# 使用命令 make loader.o 就会运行对应的 `as -o loader.o loader.s`
# g++ 使用-m32指定生成32位文件
# as 使用--32指定生成32位文件
%.o: %.cpp
g++ ${GPPPARAMS} -o $@ -c $<
%.o: %.s
as ${ASPARAMS} -o $@ $<
# 这里先执行make clean, 然后在执行make mykernel.bin
all: clean mykernel.bin
echo "build successed"
# ld -T的意思是 运行普通ld脚本
# 这里执行 make mykernel.bin会生成mykernel.bin
mykernel.bin: linker.ld ${objects}
ld ${LDPARAMS} -T $< -o $@ ${objects}
install: mykernel.bin
sudo cp $< /boot/mykernel.bin
# 执行make clean 将全部的中间文件进行删除
clean:
rm -rf *.o *.out *.bin *.iso iso
# 制作启动工具 执行make mykernel.iso
mykernel.iso : mykernel.bin
mkdir -p iso/boot/grub
cp $< iso/boot/
echo 'set timeout=8\n\
set default=0\n\
menuentry "my os" {\n\
multiboot /boot/mykernel.bin\n\
boot\n\
}' > iso/boot/grub/grub.cfg
grub-mkrescue --output=$@ iso
rm -rf iso
6. linker
linker的作用是: link *.obj
文件,产生*.exe
可执行程序
加载loader.s
生成的.o
文件, 然后再将kernel.cpp
生成的函数都添加到里面
linker.ld
/* 入口参数 */
ENTRY(loader)
OUTPUT_FORMAT(elf32-i386)
OUTPUT_ARCH(i386:i386)
SECTIONS {
. = 0x0100000;
.text :{
*(.muiltboot)
*(.text*)
*(.rodata)
}
.data : {
/* 将构造放到start到end意思就是把所有的对象都构造一遍 */
/* 至于为何使用 "_" 这是因为通过对.o文件反编译发现, 里面call 的是 _start_ctors */
_start_ctors = .;
/* 这部分不要被垃圾回收 */
KEEP(*(.init_array ));
/* init_array 构造函数初始化*/
KEEP(*(SORT_BY_INIT_PRIORITY( .init_array.* )));
_end_ctors = .;
*(.data)
}
.bss : {
*(.bss)
}
/DISCARD/ : {
*(.fini_array*)
*(.comment)
}
}
7. 激动人心的时刻到了
通过上面四个文件, 执行make mykernel.iso
会生成 mykernel.iso
然后通过virtualBox创建的新的虚拟机, 选择上面生成的 mykernel.iso
就可以打开运行了
至此, 一个完成的最基础的os框架已经搭建出来了, 接下来就是往这个os里面添加各种内存管理, 中断, 计时器, IO, 存储等功能了 !
不要停下前进的脚步, 加油!