myos1 大学生利用C++构建一个完整的操作系统打印helloworld

很久以前, 就想尝试实现一个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 汇编,.ldlink, .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就检查下一个磁盘驱动器。

通过上面的表述我们可以总结出如下三点引导程序所具有的特点:

  1. 它的大小是512B,不能多一字节也不能少一字节,因为BIOS只读512B到内存中去。
  2. 它的结尾两字节必须是“55 AA”,这是引导扇区的标志。
  3. 它总是放在磁盘的第一个扇区上(0磁头,0磁道,1扇区),因为BIOS只读第一个扇区。

3.1 涉及到的知识点有

3.2 程序执行的内存分配

请添加图片描述

在C++中, 虚拟内存分为代码段,数据段, BSS段, 堆区, 文件映射区, 栈区六个部分

  1. 代码段: 包括只读存储区(字符串常量)和文本区(程序的机器代码), 只读
  2. 数据段: 存储程序中已初始化的全局变量和静态变量; 属于静态内存分配
  3. BSS段: 存储未初始化或初始化为0的全局变量和静态变量(局部+全局); 属于静态分配, 程序结束后静态变量资源由系统自动释放。
  4. 堆区: 调用 new/malloc 函数时在堆区动态分配内存,同时需要调用 delete/free 来手动释放申请的内存。频繁的malloc free造成内存空间不连续, 产生碎片, 因此堆比栈效率低
  5. 映射区:存储动态链接库以及调用 mmap 函数进行的文件映射
  6. 栈区: 存储函数的返回地址,返回值, 参数, 局部变量; 编译器自动释放,

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]!='';++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 工程管理

知识点和关键点:

  1. 必须命名为Makefile
  2. 无论是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=8n
	set default=0n
	menuentry "my os" {n
		multiboot /boot/mykernel.binn
		bootn
	}' > 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, 存储等功能了 !

不要停下前进的脚步, 加油!

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>