本系列参考: 学习开发一个RISC-V上的操作系统 - 汪辰 - 2021春 整理而来,主要作为xv6操作系统学习的一个前置基础。
RVOS是本课程基于RISC-V搭建的简易操作系统名称。
课程代码和环境搭建教程参考github仓库: https://github.com/plctlab/riscv-operating-system-mooc/blob/main/howto-run-with-ubuntu1804_zh.md
前置知识:
可执行文件中各个段在虚拟内存中的地址,在链接阶段确定,然后程序装载阶段,就按照各个段在链接阶段设置好的虚拟地址进行装载。
此部分内容详细可参考<<程序员的自我修养—装载,链接和库>>一书
链接器一般都提供多种控制整个链接过程的方法,以用来产生用户所须要的文件。
一般链接器有如下三种方法:
由于各个链接器平台的链接控制过程各不相同,我们只能侧重一个平台来介绍。ld链接器的链接脚本功能非常强大,我们接下来以ld作为主要介绍对象。
ld 在用户没有指定链接脚本的时候会使用默认链接脚本。我们可以使用下面的命令行来查看ld默认的链接脚本:
ld -verbose
默认的ld链接脚本存放在/usr/lib/ldscripts/下,不同的机器平台、输出文件格式都有相应的链接脚本。
ld会根据命令行要求使用相应的链接脚本文件来控制链接过程,当我们使用ld来链接生成一个可执行文件的时候,它就会使用elf_i386.x作为链接控制脚本;
当我们使用ld来生成一个共享目标文件的时候,它就会使用elf_i386.xs作为链接控制脚本。
当然,为了更加精确地控制链接过程,我们可以自己写一个脚本,然后指定该脚本为链接控制脚本。比如可以使用-T参数:
ld –T link.script
什么情况下需要使用链接脚本?
绝大部分情况下,我们使用链接器提供的默认链接规则对目标文件进行链接。这在一般情况下是没有问题的,但对于一些特殊要求的程序,比如:
在编译普通的应用程序时,可以使用默认的链接器脚本,但是对于内核程序来说,它本身也是一个.elf文件,这个.elf文件该怎么组织,各个段放到内存中什么地方,这个由于和底层硬件强相关,所以需要我们自己编写相关的链接器脚本:
os.elf: ${OBJS}
${CC} ${CFLAGS} -Ttext=0x80000000 -o os.elf $^
${OBJCOPY} -O binary os.elf os.bin
.代表当前所处的内存地址
链接器会把定义符号放入符号表中,符号表中的符号是我们可以在程序中访问到的。
链接器语法详细内容可以参考GUN文档,或者程序员自我修养–装载,链接与库的4.5节。
参考课程02节的os.ld链接器脚本文件
如何在代码中获取在链接器脚本中定义的相关符号值呢?
参考课程02节mem.s文件
注意:
在c程序中获取链接器脚本中定义的符号,有两种方式:
SECTIONS
{
.text :
{
*(.text)
}
.data :
{
*(.data)
}
.bss :
{
*(.bss)
}
/* 定义一个名为 _custom_symbol 的符号,并将其赋值为 42 */
PROVIDE(_custom_symbol = 42);
}
#include <stdio.h>
extern int _custom_symbol;
int main() {
printf("The value of _custom_symbol is: %d\n", _custom_symbol);
return 0;
}
SECTIONS
{
/* ...其他部分... */
/* 定义一个名为 _asm_var 的符号,并将其赋值为 100 */
PROVIDE(_asm_var = 100);
}
.section .data
.global asm_var
asm_var:
.word _asm_var
#include <stdio.h>
extern int asm_var;
int main() {
printf("The value of asm_var is: %d\n", asm_var);
return 0;
}
将汇编文件作为绑定的中间转换层有以下几个好处:
总之,通过将汇编文件作为绑定的中间转换层,可以提供更大的灵活性、细粒度的控制能力,提高代码的可读性和可维护性,以及更好地支持跨平台开发。这对于一些特定的需求和项目来说是非常有益的。
数据结构设计:
此处采用数组方式来管理内存。
此部分代码基于课程02小节的page.c文件展开讲解
/*
* Following global vars are defined in mem.S
*/
extern uint32_t TEXT_START;
extern uint32_t TEXT_END;
extern uint32_t DATA_START;
extern uint32_t DATA_END;
extern uint32_t RODATA_START;
extern uint32_t RODATA_END;
extern uint32_t BSS_START;
extern uint32_t BSS_END;
extern uint32_t HEAP_START;
extern uint32_t HEAP_SIZE;
/*
* _alloc_start points to the actual start address of heap pool
* _alloc_end points to the actual end address of heap pool
* _num_pages holds the actual max number of pages we can allocate.
*/
static uint32_t _alloc_start = 0;
static uint32_t _alloc_end = 0;
static uint32_t _num_pages = 0;
对于数据结构的选择,我们这里选取数组结构:
由于物理内存被划分为一块块固定大小的内存,所以我们可以通过附加索引信息记录某个页是否已经分配出去,并且索引记录的下标和对应的物理页下标进行映射,映射公式为:
并且我们使用Page结构体来作为索引记录,用于表示某个物理页是否已经分配出去,并且由于用户通常一次性申请好几个连续物理页,释放的时候传入分配内存起始地址,我们需要回收先前分配给该用户的多个连续物理页,因此还需要一个记号标记当前物理页是否为某次连续分配中的最后一个物理页:
/*
* Page Descriptor
* flags:
* - bit 0: flag if this page is taken(allocated)
* - bit 1: flag if this page is the last page of the memory block allocated
*/
struct Page {
uint8_t flags;
};
内存管理模块初始化:
void page_init()
{
/*
* We reserved 8 Page (8 x 4096) to hold the Page structures.
* It should be enough to manage at most 128 MB (8 x 4096 x 4096)
*/
//_num_pages是实例用户可用的物理页数量
_num_pages = (HEAP_SIZE / PAGE_SIZE) - 8;
printf("HEAP_START = %x, HEAP_SIZE = %x, num of pages = %d\n", HEAP_START, HEAP_SIZE, _num_pages);
struct Page *page = (struct Page *)HEAP_START;
//初始化索引记录---每条索引记录对应一个用户可用物理页面
for (int i = 0; i < _num_pages; i++) {
_clear(page);
page++;
}
//物理页对齐4KB---将给定的地址按页面边界(4KB)对齐,确保地址位于所在页面的起始位置
_alloc_start = _align_page(HEAP_START + 8 * PAGE_SIZE);
//堆内存最大范围
_alloc_end = _alloc_start + (PAGE_SIZE * _num_pages);
printf("TEXT: 0x%x -> 0x%x\n", TEXT_START, TEXT_END);
printf("RODATA: 0x%x -> 0x%x\n", RODATA_START, RODATA_END);
printf("DATA: 0x%x -> 0x%x\n", DATA_START, DATA_END);
printf("BSS: 0x%x -> 0x%x\n", BSS_START, BSS_END);
printf("HEAP: 0x%x -> 0x%x\n", _alloc_start, _alloc_end);
}
//初始化过程就是将标志位清空
static inline void _clear(struct Page *page){
page->flags = 0;
}
注意: 此处出现的printf函数是在02小节中编写的printf.c文件中出现的,而非c语言提供的库函数,最终输出底层还是借助的上一节中编写uart.c代码,借助串口输出到连接设备的屏幕上。
连续分配多个物理页面:
/*
* Allocate a memory block which is composed of contiguous physical pages
* - npages: the number of PAGE_SIZE pages to allocate
*/
void *page_alloc(int npages)
{
/* Note we are searching the page descriptor bitmaps. */
int found = 0;
//遍历索引数组
struct Page *page_i = (struct Page *)HEAP_START;
//_num_pages表示堆内存页面总数(用户可用堆内存--上面page_init函数中初始化过了)
for (int i = 0; i <= (_num_pages - npages); i++) {
//判断当前页面是否空闲
if (_is_free(page_i)) {
found = 1;
/*
* meet a free page, continue to check if following
* (npages - 1) pages are also unallocated.
*/
// 检查接下来的npages-1个物理页面是否同样空闲
struct Page *page_j = page_i + 1;
for (int j = i + 1; j < (i + npages); j++) {
//只要有一个物理页面不空闲,说明这块连续内存空间大小不满足我们的要求
if (!_is_free(page_j)) {
//重新设置found=0
found = 0;
break;
}
page_j++;
}
/*
* get a memory block which is good enough for us,
* take housekeeping, then return the actual start
* address of the first page of this memory block
*/
//找到了满足要求的连续内存空间
if (found) {
//设置好相关物理页面对应的索引记录标志位为占用状态
struct Page *page_k = page_i;
for (int k = i; k < (i + npages); k++) {
_set_flag(page_k, PAGE_TAKEN);
page_k++;
}
//设置连续分配的页面中最后一个页面的flags标志位第1位为1,表示为当前分配中的最后一个物理页面
page_k--;
_set_flag(page_k, PAGE_LAST);
//返回分配内存的起始地址
return (void *)(_alloc_start + i * PAGE_SIZE);
}
}
page_i++;
}
return NULL;
}
判断页面是否空闲和设置索引记录标记的函数如下:
static inline int _is_free(struct Page *page)
{
if (page->flags & PAGE_TAKEN) {
return 0;
} else {
return 1;
}
}
static inline void _set_flag(struct Page *page, uint8_t flags)
{
page->flags |= flags;
}
释放内存:
/*
* Free the memory block
* - p: start address of the memory block
*/
void page_free(void *p)
{
/*
* Assert (TBD) if p is invalid
*/
//内存地址不合法或者超出的堆内存最大限制,直接返回
if (!p || (uint32_t)p >= _alloc_end) {
return;
}
/* get the first page descriptor of this memory block */
struct Page *page = (struct Page *)HEAP_START;
//定位对应的索引记录下标
page += ((uint32_t)p - _alloc_start)/ PAGE_SIZE;
/* loop and clear all the page descriptors of the memory block */
//将对应page被占用的标记清空,同时如果是连续分配的最后一个页面,清空其PAGE_LAST标记
while (!_is_free(page)) {
if (_is_last(page)) {
_clear(page);
break;
} else {
_clear(page);
page++;;
}
}
}
清空PAGE_LAST标志的函数如下:
static inline int _is_last(struct Page *page)
{
if (page->flags & PAGE_LAST) {
return 1;
} else {
return 0;
}
}
#include "os.h"
/*
* Following functions SHOULD be called ONLY ONE time here,
* so just declared here ONCE and NOT included in file os.h.
*/
extern void uart_init(void);
extern void page_init(void);
void start_kernel(void)
{
uart_init();
uart_puts("Hello, RVOS!\n");
page_init();
//页面分配测试
page_test();
while (1) {}; // stop here!
}
void page_test()
{
void *p = page_alloc(2);
printf("p = 0x%x\n", p);
//page_free(p);
void *p2 = page_alloc(7);
printf("p2 = 0x%x\n", p2);
page_free(p2);
void *p3 = page_alloc(4);
printf("p3 = 0x%x\n", p3);
}
输出:
可尝试基于课程02节已有的Page.c扩展出类似C语言中提供的malloc和free函数。