从零开始的操作系统生活01-Hello World

0、目标

使用cpp开发一个操作系统,具体能走到哪一步看毅力,加油!

1、参考内容

视频链接1

视频链接2

github

2、开始

操作系统是一组主管并控制计算机操作、运用和运行硬件、软件资源和提供公共服务来组织用户交互的相互关联的系统软件程序

首先观察我们简单的操作系统的目录

index

kernel.cpp是我们操作系统的运行程序,loader.s负责把我们的操作系统加载到硬件中运行,linker.ld负责将kernel与loader链接到一起,生成机器能够“读懂”的结构,Makefile负责进行编译。

2.1、kernel.cpp

首先我们来编写最简单的操作系统:

1
2
3
4
extern "C" void kernelMain(void* multiboot_structure, unsigned int magicnumber){
printf("hello world!");
while(1);
}

我们先来看程序的主体部分

1
2
3
4
void kernelMain(){
printf("hello world!");
while(1);
}

程序主体就是要打印一句hello world,然后无限循环,操作系统本身就是无限循环,这里我们留下两个坑

*extern “C”以及void* multiboot_structure, unsigned int magicnumber*稍后会做出解释

2.2、GRUB multiboot引导规范

操作系统的启动流程如下:

操作系统启动流程

参考链接

图中第三步GRUB引导存在各种各样的规范,我们所使用的称为multiboot引导规范,之所以存在各种规范,原因就是方便进行开发,比如我们下面的loader.s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.set MAGIC, 0x1badb002
.set FLAGS, (1<<0 | 1<<1)
.set CHECKSUM, -(MAGIC + FLAGS)

.section .multiboot
.long MAGIC
.long FLAGS
.long CHECKSUM
.section .text
.extern kernelMain
.global loader

loader:
mov $kernel_stack, %esp
push %eax
push %ebx
call kernelMain

_stop:
cli
hlt
jmp _stop

.section .bss
.space 2*1024*1024
kernel_stack:

multiboot的头的前三部分最为重要:

  • magic:magic域固定为十六进制的0x1BADB002,这样方便BIOS的识别
  • flags:指出操作系统影响需要引导程序提供或支持的特性
  • checksum:32位无符号值,保证前三个域的值相加位0,即magic+flags+checksum=0

下面的内容即可得到解释:

1
2
3
4
5
6
7
8
.set MAGIC, 0x1badb002
.set FLAGS, (1<<0 | 1<<1)
.set CHECKSUM, -(MAGIC + FLAGS)

.section .multboot
.long MAGIC
.long FLAGS
.long CHECKSUM

在引导multiboot规范头后,我们需要放出程序的入口点,即我们去哪执行kernel.cpp

这里插入一个汇编知识,汇编程序中以**.**开头的名称并不是指令的助记符,不会被翻译称机器指令,而是给汇编器一些特殊只是,成为汇编指示或伪操作。

.section把代码划分为若干个段,成簇被操作系统加载执行时,每个段被加载到不同的地址,操作系统对不同的段赋予不同的读写执行权限

ELFis a format for storing programs or fragments of programs on disk, created as a result of compiling and linking

我们使用.section设置的各个段最终就存储在ELF文件中,下图介绍了ELF文件各个段所存储的内容极其权限

ELF

观察我们的loader

1
2
3
4
5
6
7
8
9
10
11
.section .text
.extern kernelMain
.global loader

loader:
mov $kernel_stack, %esp
push %eax
push %ebx
call kernelMain

kernel_stack:

首先定义.text段(已编译程序的机器代码),然后再text段中引入KernalMain编译后的内容

.global伪指令暴漏loader,以便让链接器linker能够找到loader地址

再接下来,loader中的内容,%esp是栈指针,在我们的视频链接2的第一课提到,cpp文件并不会自动设置栈指针,需要我们手动设置,文件第11行有一个kernel_stack,我们将该位置设置为栈顶

接下来就要填我们前面提过的坑了,首先是void multiboot_structure, unsigned int magicnumber*。按照规定,我们需要向kernel.cpp中传入两个参数,分别是Bootloader的地址和magic,这两个参数分别存在eax和ebx中,使用push指令将参数进行传递,其中一个坑填完。

接下来是另一个坑,我们在执行call kernelMain时会出现一个找不到函数的error,原因是cpp编译成汇编指令时会对函数名进行改变,所以我们访问不到,解决办法就是在cpp文件中添加extern “C”

最后**_stop中,cli指令将IF置0,屏蔽掉“可屏蔽中断”,hlt指令是处理器的暂停指令,jmp**是跳转指令

1
2
3
4
5
6
7
_stop:
cli
hlt
jmp _stop

.section .bss
.space 2*1024*1024

理论上我们的程序无法执行到_stop,因为call指令执行之后应当进入无线循环,如果执行到stop说明程序出现了问题

程序最后设置BSS段空间为2M,用于防止未初始化的全局和静态C变量

2.3、linker.ld

首先放出linker.ld的内容,然后再尽力去分析每个部分的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
ENTRY(loader)
OUTPUT_FORMAT(elf32-i386)
OUTPUT_ARCH(i386:i386)

SECTIONS
{
. = 0x0100000;

.text :
{
*(.multiboot)
*(.text*)
*(.rodata)
}

.data :
{
start_ctors = .;
KEEP(*( .init_array ));
KEEP(*(SORT_BY_INIT_PRIORITY( .init_array.* )));
end_ctors = .;

*(.data)
}

.bss :
{
*(.bss)
}

/DISCARD/ :
{
*(.fini_array*)
*(.comment)
}
}

关于ld的参考内容,我找到了一下几个网站,后面讲解的内容也大概出自这里:

OSDEV

Binutils

Using ld

首先来看前面三行

1
2
3
ENTRY(loader)
OUTPUT_FORMAT(elf32-i386)
OUTPUT_ARCH(i386:i386)

The linker command language includes a command specifically for defining the first executable instruction in an output file (its entry point). Its argument is a symbol name: ENTRY(symbol)

这句话翻译一下就是链接器的语言包括一句这样的指令:定义输出文件中要执行的第一行代码

参数的形式就是ENTRY(symbol)

所以**ENTRY(loader)**就是告诉程序需要从loader处开始执行,我们在loader.s中有一句.global loader,暴漏了loader的位置,从而ld能够获取这个位置

OUTPUT_FORMAT(elf32-i386)以及OUTPUT_ARCH(i386:i386)就是指输出文件的格式以及输出文件所支持的平台

OSDEV介绍linker的features中有这样一句

Supports most known output formats (ELF, Win32/PE, COFF, a.out etc).

进一步找到Linker Scripts关键词可以找到OUTPUT_FORMAT可以填的内容

Format Description
binary A flat binary with no formatting at all.
elf32-i386 32-bit ELF format for the i386 architecture.
elf64-x86-64 64-bit ELF format for the x86-64 architecture.
pe-i386 32-bit PE format for the i386 architecture.

接下来是后面的.SECTIONS部分,Section Defintions中提到

The most frequently used statement in the SECTIONS command is the section definition, which specifies the properties of an output section: its location, alignment, contents, fill pattern, and target memory region

Section的格式一般为:

1
2
3
4
5
SECTIONS { ...
secname : {
contents
}
... }

secname is the name of the output section

contents is a specification of what goes there

secname需要包含在要输出文件各种中,比如(a.out中包括.text段,.data段或者.bss段,ELF文件包含的段前面提到过)

我们的SECTIONS中就包含.text,.bss以及.data段

1
2
3
4
5
6
.text : 
{
*(.multiboot)
*(.text*)
*(.rodata)
}

上面的代码。.text是我们要输出的段(section),{}里面的内容是输入的内容,.multiboot表示成簇的multiboot段,*(.multiboot)表示所有文件的multiboot段,我们将{}内的内容作为输入,输出为.text段,同理.bss段

.data段的内容留到下一节一起解释,其中KEEP的作用是即使为引用符号,我们也要保留这部分

2.4、Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GPPPARAMS = -m32 -fno-use-cxa-atexit -nostdlib -fno-builtin -fno-rtti -fno-exceptions -fno-leading-underscore
ASPARAMS = --32
LDPARAMS = -melf_i386

objects = loader.o kernel.o

%.o : %.cpp
g++ ${GPPPARAMS} -o $@ -c $<

%.o : %.s
as ${ASPARAMS} -o $@ $<

mykernel.bin: linker.ld ${objects}
ld ${LDPARAMS} -T $< -o $@ ${objects}

install: mykernel.bin
sudo cp $< /boot/mykernel.bin

首先是GPPPARAMS,可以堪称g++ paramters,以下是各个参数代表的意思

1
2
3
4
5
6
7
8
-m32                    # 指定编译为32位应用程序
-fno-use-cxa-atexit # 禁用C析构函数__cxa_atexit
-fleading-underscore # 编译时在C语言符号前加上下划线
-fno-exceptions # 禁用异常机制
-fno-builtin # 不使用C语言的内建函数
-nostdlib # 不链接系统标准启动文件和标准库文件
-fno-rtti # 禁用运行时类型信息
-fno-pie # 禁用PIE模式

ASPARAMS = –32表示使用32位模式编译

LDPARAMS = -melf_i386表示 -m -elf_i386保证调用时将需要的参数加上,然后调用真正的参数

后面的属于Makefile的常用内容,这里不做详细解释

1
2
3
4
$@:	表示目标文件
$^: 表示所有的依赖文件
$<: 表示第一个依赖文件
$?: 表示比目标还要鑫的以来文件列表

3、后记

视频链接后面还有编写grub.cfg等操作,由于我们不使用这种方式打开自己的OS,所以这里暂时不展示该流程

本节留下了一个linker.ld的坑,下一节应该会填补完整

3.1、乱七八糟的知识

The entry Point中提到,Entry is only one of several ways of choosing the entry point.

其他的方法包括:

  • the `-e’ entry command-line option;
  • the ENTRY(symbol) command in a linker control script;
  • the value of the symbol start, if present;
  • the address of the first byte of the .text section, if present;
  • The address 0.

其中第三条。the value of the symbol start, if present,在《Linux内核完全剖析》中有这样一段,

entry_start

此处用到了entry start

这也算小小的学以致用了把。


从零开始的操作系统生活01-Hello World
http://example.com/2022/09/19/wyoos001/
作者
Anhongzhan
发布于
2022年9月19日
许可协议