操作系统os.elf会被放在内存的0x80000000位置,这是一个很粗粒度的说法,因为os.elf有数据段、代码段等各种段,每个段放在内存的哪个位置?这个问题在之前是由riscv64-unknown-elf-ld来完成的,即它将各个.o文件的代码段和数据段等合并在.elf文件的代码段和数据段,并且指定了.elf文件的代码段和数据段应该放在virt虚拟机器的哪个物理位置。
在之前链接的过程中,通过-Ttext=0x80000000来将os.elf代码段放在指定的物理内存部分,通过qemu启动时的-bios none来跳过bios,直接执行0x80000000地址的代码,其余段的分配均有riscv64-unknown-elf-ld来完成。
但现在我们想自己管理内存,即决定每个.o文件的各段如何合并到.elf的各段中,以及.elf的各段应该在物理内存的哪个位置,这可以编写os.ld文件并且在链接时通过-T os.ld来指定,以此达到管理内存的效果。
os.ld的编写决定了各个段的映射,当os.elf编译完成后,数据段和代码段等大小都已经固定,但是堆,是在程序运行时动态分配的,故需要我们自己额外处理,类似于C语言中的malloc和free函数。
为了便于调试,还需要实现printf函数。字符串可以放入缓冲区直接打印,但变量的打印需要识别百分号,然后将变量的值放入缓冲区,等全部识别完成后,就可以借助uart来实现打印缓冲区的效果了。
ld会通过linker script脚本来决定如何将.o文件中的各个段合并到.elf文件的各个段,并且会将.elf文件的各个段映射到真实的物理内存地址上。
#include "c_code/include/platform.h"
OUTPUT_ARCH("riscv")
ENTRY(_start)
MEMORY
{
ram (wxa!ri) : ORIGIN = 0x80000000, LENGTH = LENGTH_RAM
}
SECTIONS
{
.text : {
PROVIDE(_text_start = .);
*(.text .text.*)
PROVIDE(_text_end = .);
} > ram
.rodata : {
PROVIDE(_rodata_start = .);
*(.rodata .rodata.*)
PROVIDE(_rodata_end = .);
} > ram
.data : {
. = ALIGN(4096);
PROVIDE(_data_start = .);
*(.sdata .sdata.*)
*(.data .data.*)
PROVIDE(_data_end = .);
} > ram
.bss : {
PROVIDE(_bss_start = .);
*(.sbss .sbss.*)
*(.bss .bss.*)
*(COMMON)
PROVIDE(_bss_end = .);
} > ram
PROVIDE(_memory_start = ORIGIN(ram));
PROVIDE(_memory_end = ORIGIN(ram) + LENGTH(ram));
PROVIDE(_heap_start = _bss_end);
PROVIDE(_heap_size = _memory_end - _heap_start);
}
关于linker script文件的一些解释:
void page_init()
{
ptr_t _heap_start_aligned = _align_ptr(HEAP_START);
uint32_t num_reserved_pages = LENGTH_RAM / (PAGE_SIZE * PAGE_SIZE);
_alloc_start = _heap_start_aligned + num_reserved_pages * PAGE_SIZE;
_num_pages = (HEAP_SIZE - (_heap_start_aligned - HEAP_START)) / PAGE_SIZE - num_reserved_pages;
_alloc_end = _alloc_start + _num_pages * PAGE_SIZE;
printf("LENGTH_RAM = %d\n", LENGTH_RAM);
printf("HEAP_START = %p (aligned to %p), HEAP_SIZE = 0x%lx, \n"
"num of reserved pages = %d, num of pages to be allocated for heap = %d\n",
HEAP_START, _heap_start_aligned, HEAP_SIZE, num_reserved_pages, _num_pages);
struct Page_index *pi = (struct Page_index *)HEAP_START;
for (int i = 0; i < _num_pages; ++i)
{
_clear(pi);
pi++;
}
printf("HEAP : %p -> %p\n", _alloc_start, _alloc_end);
printf("BSS : %p -> %p\n", BSS_START, BSS_END);
printf("DATA : %p -> %p\n", DATA_START, DATA_END);
printf("RODATA : %p -> %p\n", RODATA_START, RODATA_END);
printf("TEXT : %p -> %p\n", TEXT_START, TEXT_END);
}
一些说明:
void *page_alloc(int npages)
{
int found = 0;
struct Page_index *pi = (struct Page_index *)HEAP_START;
for (int i = 0; i <= _num_pages - npages; i++)
{
if (_is_free(pi))
{
found++;
struct Page_index *pi_j = pi + 1;
for (int j = 0; j < npages - 1; j++)
{
if (_is_free(pi_j))
pi_j++;
else
{
found = 0;
break;
}
}
if (found)
{
for (struct Page_index *page = pi; page < pi_j; page++)
_set_flag(page, PAGE_TAKEN);
_set_flag(pi_j, PAGE_LAST);
return (void *)(_alloc_start + i * PAGE_SIZE);
}
}
pi++;
}
return NULL;
}
一些说明:
void page_free(void *p)
{
ptr_t page = (ptr_t)p;
struct Page_index *pi = (struct Page_index *)(HEAP_START + (page - _alloc_start) / PAGE_SIZE);
while (1)
{
_clear(pi);
pi++;
if (_is_last(pi))
return;
}
}
一些说明:
为啥要自己写printf,不能直接使用标准库里面的?在回答这个问题之前,先来解释一下为什么我们平常写的C语言程序,经过编译之后,可以直接执行printf等语句。
编译器经过cc1和as将C语言文件变为.o目标文件,然后通过collect2将该目标文件和libc.so文件进行链接,而后可以执行printf等函数。
但是在该实验中,链接过程只会把自己编写的C和S文件进行编译后链接,并不会链接libc.so文件,故而如果在C中直接用printf,则会报错。如果手动将libc.so链接进自己写的代码,也可以,但是这会导致操作系统代码量骤增,因为其包含了很多不需要的函数。
这里采用自己实现简易版printf,即只利用编译器支持的类型和操作来实现printf,像va_list、size_t、va_arg等,都是编译器内置的一些类型,故而可以直接使用,不需要链接其他动态库。
os.h文件额外include了stddef和stdarg两个头文件,这两个文件内容均是宏定义#define和类型定义typedef,故而不存在链接其他so库的情况。
决定编译好的elf中的各个段放在真实物理内存的哪个位置,堆空间的分配和释放需要自己实现,其余部分是固定不变的,组织方式采用数组方式,另外printf也需要自己实现简易版。