从零开始的操作系统生活02-对Hello World的补充

1、开始

github

1.1、环境配置

1
sudo apt-get install g++ binutils libc6-dev-i386

1.2、对linker.ld的补充

linker脚本是用来控制link过程的文件,文件中包含内容为linker的处理命令,主要用于描述输入文件到输出文件时各个内容的分步以及内存映射等

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)
}
}

1.2.1、首地址设置

首先是.SECTION{}中第一句*. = 0x0100000;*

这句话的意思是设置我们所要输出的文件的字段的起始地址,比如我们将地址设置为0x100000,则后面的.text段就从该地址开始放置,紧接着是.data段和.bss段,同样我们也可以使用如下方法:

1
2
3
4
5
6
7
8
. = 0x010000;
.text :
{
}
. = 0x080000;
.data :
{
}

将不同的段放置在不同的区域

1.2.2、data段的设置

接下来是.data段中的内容,ELF文件中对于.data段的描述是:已初始化的全局和静态C变量

在调用KernelMain方法之前,我们还需要的一个步骤就是保证初始化必要的全局变量,这些全局变量存放在**.init_array**段中。

为了完成以上操作,我们还需要在Kernel.cpp中添加初始化函数callConstructors(),该函数获取.init_array段中的指令

1
2
3
4
5
6
7
8
9
typedef void (*constructor)();
extern "C" constructor start_ctors;
extern "C" constructor end_ctors;

extern "C" void callConstructors(){
for(constructor* i = &start_ctors; i != &end_ctors; i++){
(*i)();
}
}

这里面其实我也不是很清楚内部的运行机制,暂时码住,以后遇到答案之后再回来做出解释

补充一:kernel.cpp中两个函数start_ctors与end_stors获取到linker.ld中的两个位置,然后执行linker.ld两个位置之间的命令(代码)

还记得我们前面说过,在loader.s中需要暴漏KernelMain的地址让链接器知道吗?同理,我们也需要暴漏callConstructors的地址,也就是在loader.s中添加如下代码:

1
2
3
4
.section .text
.extern kernelMain
.extern callConstructors
.global loader

第三行代码为我们刚刚所添加的内容

最后我们来看.data段中的内容,第一句与最后一句分别为start_ctors和end_ctors,这两部分是链接器指定的两个具体地址,然后将初始化指令,也就是.data段中的数据放置在这两段地址之间,也就是说链接器指定了一段空间用于存放.data段的数据,也就是**.init_array*,前面提到过,KEEP的作用是即使为引用符号,我们也要保留这部分*,另一种解释方法就是KEEP函数可以保证数据不被系统释放,属于对全局变量的保护,也可能是出于对程序生命周期的考虑。

SORT_BY_INIT_PRIORITY函数会对**.init_array**中的数据以升序的方式进行排序,然后再放置到输出文件中

1.2.3、/DISCARD/

放置在**/DISCARD/**段中的内容不会被链接器输出出去

1.3、printf函数

还记得上一节中我们写的kernel.cpp文件吗,里面调用了printf函数进行打印,但是我们并没有引入头文件,结果可想而知,肯定无法编译过去,报错内容如下:identifier “printf” is undefined

printf函数本身就是调用操作系统的函数然后进行打印,所以我们不可以直接引入头文件进行编译,必须自己实现,实现的方法如下:

1
2
3
4
5
6
void printf(const char* str){
unsigned short *VideoMemory = (unsigned short*)0xb8000;
for(int i=0;str[i];i++){
VideoMemory[i] = (VideoMemory[i] & 0xFF00) | str[i];
}
}

0xb8000是操作系统向屏幕写入数据的第一个地址,我们只需要将数据一次写入相应的地址,最后就能够打印出来

根据0xb8000中的介绍,操作系统使用两个字节存储一个字符,第一个字节存储字符内容,第二个字节存储字符的颜色,我们这里不关心颜色,所以只需要将每一个低位字节置零之后将要写入的字符“与”进去即可。

同时我们也需要注意大端和小端的问题!!!

也就是VideoMemory[i] = (VideoMemory[i] & 0xFF00) | str[i];

1.4、运行结果

直接运行make mykernel.bin,可以得到如下的文件序列:

运行结果

或者也可以分步运行:

1
2
3
make kernel.o
make loader.o
make mykernel.bin

2、后记

在看视频链接1进行学习以及在网上查阅资料时发现了一处不太一样的地方,我们使用objdump -d kernel.o进行反汇编时可能会发现KernelMain等函数编译后的方法名称有可能变成**_KernelMain**,这就需要对loader.s以及linker.ld进行修改,修改的方法就是把反汇编的名称替换原来的名称,具体做法可以参照视频链接1,但是我并没有遇到这个问题


从零开始的操作系统生活02-对Hello World的补充
http://example.com/2022/09/20/wyoos002/
作者
Anhongzhan
发布于
2022年9月20日
许可协议