首页 > 编程语言 >myos1 大学生利用C++构建一个完整的操作系统打印helloworld

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

时间:2022-11-24 11:01:38浏览次数:64  
标签:kernel 函数 helloworld loader myos1 iso C++ fno mykernel


文章目录

  • ​​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. 激动人心的时刻到了​​

​本文最终的代码​

​点击这里跳转到我的gitee​

很久以前, 就想尝试实现一个OS, 所以在很久以前有对30天完成一个操作系统进行学习, 但是半途而废了; 最近由于工作签了, 正好空出这段时间来深入学习一下OS和编译器, 毕竟每个程序员都有一个梦想(实现一个自己的OS), 正好我未来的工作也是编译器方面, 因此我给自己定了两个小目标, 毕业之前把myos弄出来, 然后在把mycompiler弄出来, 至少将来去公司面对一堆清北大佬应该不至于只是端茶送水;

本科阶段一直玩的是单片机(51单片机, STM32, arduino, 树莓派, nanopi, cortexA9的arm开发板), 虽然看上去玩过很多, 但其实无非就是上层应用, 并不知道其底层的原理或者架构, 比如操作系统的几个核心知识: 内存管理, 中断, 定时器, 字符设备和块设备, IO, 网络, 端口; 正好借着这样的机会, 把整个体系结构梳理一遍.

我已经尽可能的把每一句都加了注释, 尽可能的详细乃至于墨迹, 所以希望大家不喜勿喷, 同时我已经将整个项目添加到了我的​​gitee​​​, 大家可以直接下载进行​​make mykernel.iso​​​编译, 生成可以通过虚拟机open的​​mykernel.iso​​文件.

以下对应的是​​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涉及到的插件

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


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


以及​​C++​​​和​​WSL​

1.3 virtual Box创建一个新的空虚拟机

​创建新的虚拟机​​​→选择类型​​Other(Other/unknown)​​​→分配内存​​64M​​​→不添加虚拟磁盘→在存储设置选择虚拟盘中选择最后生成的​​.ios​​​文件→启动就会显示​​hello world​​了

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

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 涉及到的知识点有

  • ​%​​ 指的是引用
  • ​esp​​ 堆栈栈顶的指针
  • ​​kernel_stack​​
  • ​_system_constructors​​ 轮训函数, 初始化所有的构造函数
  • ​cli​​ ; 将IF置0,屏蔽掉“可屏蔽中断”,当可屏蔽中断到来时CPU不响应,继续执行原指令
  • ​hlt​​ ; 本指令是处理器“暂停”指令。
  • ​jmp _stop​​ ; 命令跳转指令
  • ​.section .text​​ 代码段
  • ​.section .bss​​ 未初始化的全局变量/初始化为0的内存段
  • ​.section .multboot​​ 规范编译内核, 才可以被GRUB引导

3.2 程序执行的内存分配

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

在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​​, 这是固定的, 因此要写什么, 直接往这里面写就行了

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

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 工程管理

知识点和关键点:

  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=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​​可执行程序

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

加载​​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​​就可以打开运行了

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

至此, 一个完成的最基础的os框架已经搭建出来了, 接下来就是往这个os里面添加各种内存管理, 中断, 计时器, IO, 存储等功能了 !

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


标签:kernel,函数,helloworld,loader,myos1,iso,C++,fno,mykernel
From: https://blog.51cto.com/u_15888063/5882668

相关文章

  • 【iOS-Cocos2d游戏开发之十二】浅析使用C++/C/OC进行iOS游戏混编出现“failed with ex
    ​​​李华明Himi ​​​​原创   大家都知道Xcode中支持C、C++、Object-C3种语言的混编,在上一节Box2d中介绍过cocos2d封装的box2d是c++源码实现的,那么如果想让编译器......
  • c++ 代码模板
    #include<bits/stdc++.h>usingnamespacestd;typedefunsignedlonglongull;typedeflongdoubledoubleL;typedeflonglongll;#define_SILENCE_CXX20_CISO646......
  • c++类的二进制组件复用
    本文内容是对EssntialCOM一书中第一章内容的总结,该章内容很好的阐述了c++类的二进制复用所需要面对的一些问题及解决方案。我在使用c++开发模块中,使用了以下几种分发方......
  • windows--cmake与c++的使用教程(14)
    1概述本文基于前文环境本节目标:target_include_directories用法2作用target_include_directories的作用,用于给固定目标指定头文件搜索路径。moderncmake之......
  • C++零基础入门学习路线图
    C++入门学习路线图分为三阶段:C++基础入门、C++核心编程、C++提高编程。以下学习路线图参考B站黑马程序员《匠心精作C++从0到1入门编程》C++基础入门 1C++初识 ......
  • C++全栈开发学习路线图
    C语言基础与提高 C语言基础 指针、内存管理 变量、条件、字符串、数组、函数、结构体 C语言提高 多级指针的使用 接口的封......
  • 数据结构(二):括号匹配(C++,栈)
    好家伙,写题,题目代码在最后 来吧,  1.栈 栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一......
  • C++ 嵌入式实时操作系统调试心得
    1、如果设置了全局vector变量,然后在程序中一直pushback,如果是系统内存较小,运行一段时间后可能会崩溃;2、如果使用C语言编程采用动态内存,一定要在变量生存周期结束时对内存......
  • C++ --- 标准库std::max/std::min和window头文件中宏max/min冲突
    转载:https://blog.twofei.com/668/在包含了Windows.h的C++源代码中使用std::min/std::max会出现错误。intmain(){intx=std::max(0,1);inty=std......
  • C++ 判断闰年简单代码
    闰年闰年分为普通闰年和世纪闰年1582年以来的置闰规则:普通闰年:公历年份是4的倍数,且不是100的倍数的,为闰年(如2004年、2020年等就是闰年)。世纪闰年:公历年份是整百数的......