从零手写操作系统之RVOS内存管理模块简单实现-02

  • 内存管理分类
  • 内存映射表(Memory Map)
  • Linker Script 链接脚本
    • 语法
    • 基于符号定义获取程序运行时内存分布
  • 基于 Page 实现动态内存分配
    • 代码讲解
    • 调试
  • 扩展

本系列参考: 学习开发一个RISC-V上的操作系统 - 汪辰 - 2021春 整理而来,主要作为xv6操作系统学习的一个前置基础。

RVOS是本课程基于RISC-V搭建的简易操作系统名称。

课程代码和环境搭建教程参考github仓库: https://github.com/plctlab/riscv-operating-system-mooc/blob/main/howto-run-with-ubuntu1804_zh.md

前置知识:

  • RVOS环境搭建-01

内存管理分类

  • 自动管理内存 - 栈 (Stack)
  • 静态内存 - 全局变量/静态变量
  • 动态管理内存 - 堆(heap)

内存映射表(Memory Map)


可执行文件中各个段在虚拟内存中的地址,在链接阶段确定,然后程序装载阶段,就按照各个段在链接阶段设置好的虚拟地址进行装载。

此部分内容详细可参考<<程序员的自我修养—装载,链接和库>>一书


Linker Script 链接脚本

链接器一般都提供多种控制整个链接过程的方法,以用来产生用户所须要的文件。

一般链接器有如下三种方法:

  • 使用命令行来给链接器指定参数,ld的-o、-e参数就属于这类。
  • 将链接指令存放在目标文件里面,编译器经常会通过这种方法向链接器传递指令。方法也比较常见,只是我们平时很少关注,比如VISUAL C++编译器会把链接参数放在PE目标文件的.drectve段以用来传递参数。
  • 使用链接控制脚本,使用链接控制脚本方法就是本节要介绍的,也是最为灵活、最为强大的链接控制方法。

由于各个链接器平台的链接控制过程各不相同,我们只能侧重一个平台来介绍。ld链接器的链接脚本功能非常强大,我们接下来以ld作为主要介绍对象。

ld 在用户没有指定链接脚本的时候会使用默认链接脚本。我们可以使用下面的命令行来查看ld默认的链接脚本:

ld -verbose

默认的ld链接脚本存放在/usr/lib/ldscripts/下,不同的机器平台、输出文件格式都有相应的链接脚本。

  • 比如Intel IA32下的普通可执行ELF文件链接脚本文件为elf_i386.x;
  • IA32下共享库的链接脚本文件为elf_i386.xs等。

ld会根据命令行要求使用相应的链接脚本文件来控制链接过程,当我们使用ld来链接生成一个可执行文件的时候,它就会使用elf_i386.x作为链接控制脚本;

当我们使用ld来生成一个共享目标文件的时候,它就会使用elf_i386.xs作为链接控制脚本。

当然,为了更加精确地控制链接过程,我们可以自己写一个脚本,然后指定该脚本为链接控制脚本。比如可以使用-T参数:

ld –T link.script

什么情况下需要使用链接脚本?

绝大部分情况下,我们使用链接器提供的默认链接规则对目标文件进行链接。这在一般情况下是没有问题的,但对于一些特殊要求的程序,比如:

  • 操作系统内核、BIOS(Basic Input Output System)或一些在没有操作系统的情况下运行的程序(如引导程序Boot Loader或者嵌入式系统的程序,或者有一些脱离操作系统的硬盘分区软件PQMagic等),以及另外的一些须要特殊的链接过程的程序,如一些内核驱动程序等,它们往往受限于一些特殊的条件,如须要指定输出文件的各个段虚拟地址、段的名称、段存放的顺序等,因为这些特殊的环境,特别是某些硬件条件的限制,往往对程序的各个段的地址有着特殊的要求。

在编译普通的应用程序时,可以使用默认的链接器脚本,但是对于内核程序来说,它本身也是一个.elf文件,这个.elf文件该怎么组织,各个段放到内存中什么地方,这个由于和底层硬件强相关,所以需要我们自己编写相关的链接器脚本:

  • 在之前的环境准备小节中,我们makefile文件中编写的ld链接命令中只通过-Ttext=0x80000000命令指明了代码段的在内存中的起始地址
os.elf: ${OBJS}${CC} ${CFLAGS} -Ttext=0x80000000 -o os.elf $^${OBJCOPY} -O binary os.elf os.bin
  • 但是在本节中我们将会使用链接器脚本文件os.ld来描述整个链接过程

语法





.代表当前所处的内存地址

链接器会把定义符号放入符号表中,符号表中的符号是我们可以在程序中访问到的。

链接器语法详细内容可以参考GUN文档,或者程序员自我修养–装载,链接与库的4.5节。


基于符号定义获取程序运行时内存分布

参考课程02节的os.ld链接器脚本文件

如何在代码中获取在链接器脚本中定义的相关符号值呢?

参考课程02节mem.s文件

注意:

  • 在C代码中直接获取链接器脚本中定义的符号是有一定的限制的。C语言是一种静态编译语言,在编译时会将源代码转换为机器码,并生成可执行文件。链接器脚本用于指导链接器如何组织可执行文件的各个部分,包括代码段、数据段、符号表等。
  • 在C代码中,无法直接引用链接器脚本中定义的符号的值,因为C编译器并不了解链接器脚本的细节。C编译器只能根据给定的C代码进行编译,将代码转换为机器码,并生成符号表。符号表中包含了在C代码中定义的全局变量、函数等符号及其对应的地址。
  • 要在C代码中获取链接器脚本中定义的符号的值,一种常见的做法是通过在C代码中声明外部变量,并使用链接器脚本中定义的符号来初始化这些外部变量。这样,链接器在链接阶段会将外部变量与链接器脚本中定义的符号关联起来,并将符号的值赋给外部变量。然后,C代码就可以通过访问这些外部变量来获取链接器脚本中定义的符号的值。
  • 总之,C代码无法直接获取链接器脚本中定义的符号的值,但可以通过声明外部变量并与符号关联来间接获取。这种间接的方式使得C代码能够与链接器脚本进行交互,并共享符号的值。

在c程序中获取链接器脚本中定义的符号,有两种方式:

  • 链接器脚本中使用PROVIDER定义符号,并在c语言中通过extern声明外部变量进行绑定
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;
}
  • 通过汇编定义一个全局变量绑定到链接器脚本中的符号,c程序中定义extern变量和汇编文件中定义的全局变量相绑定
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;
}

将汇编文件作为绑定的中间转换层有以下几个好处:

  1. 灵活性:使用汇编文件可以更加灵活地控制符号的定义和绑定。你可以直接在汇编文件中定义符号,并将其与链接器脚本中的符号绑定,而不依赖于C语言的语法和限制。这使得你可以更精确地控制符号的位置、大小和属性。

  2. 细粒度控制:汇编语言提供了更细粒度的控制能力。你可以直接使用汇编指令来定义变量、设置符号的初始值,以及指定变量的大小和对齐方式。这使得你可以更好地适应特定的需求,如嵌入式系统的内存布局和对齐要求。

  3. 可读性:使用汇编文件作为绑定的中间转换层可以提高代码的可读性和可维护性。通过将符号的定义和绑定从链接器脚本和C代码中分离出来,可以更清晰地表达代码的意图,并使得代码更易于理解和修改。

  4. 跨平台支持:使用汇编文件作为中间转换层可以更好地支持跨平台开发。汇编语言是与硬件平台相关的,通过直接编写汇编代码,可以更好地适应不同的硬件架构和操作系统环境。这使得你的代码更具可移植性和可扩展性。

总之,通过将汇编文件作为绑定的中间转换层,可以提供更大的灵活性、细粒度的控制能力,提高代码的可读性和可维护性,以及更好地支持跨平台开发。这对于一些特定的需求和项目来说是非常有益的。


基于 Page 实现动态内存分配


数据结构设计:

此处采用数组方式来管理内存。


代码讲解

此部分代码基于课程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;

对于数据结构的选择,我们这里选取数组结构:

由于物理内存被划分为一块块固定大小的内存,所以我们可以通过附加索引信息记录某个页是否已经分配出去,并且索引记录的下标和对应的物理页下标进行映射,映射公式为:

  • 物理页地址=alloc_start + 索引下标 * PAGE_SIZE

并且我们使用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;
};
  • 利用flags标记的第0位表示物理页是否分配
  • 利用flags标记的第1位表示是否为某次分配中的最后一个物理页

内存管理模块初始化:

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;
}
  • 保留堆内存前面8个物理页用于存放索引记录信息
  • 初始化相关索引信息
  • 堆内存分配起始地址页面对齐

注意: 此处出现的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=0found = 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函数。

从零手写操作系统之RVOS内存管理模块简单实现-02相关推荐

  1. 利用图文和代码深度解析操作系统OS的内存管理实现原理机制和算法

    利用图文和代码深度解析操作系统OS的内存管理实现原理机制和算法. 内存作为计算机系统的组成部分,跟开发人员的日常开发活动有着密切的联系,我们平时遇到的Segment Fault.OutOfMemory ...

  2. Linux内核学习--内存管理模块

    Linux内核学习--内存管理模块 首先,Linux内核主要由五个部分组成,他们分别是:进程调度模块.内存管理模块.文件系统模块.进程间通信模块和网络接口模块. 本部分所讲的内存是内存管理模块,其主要 ...

  3. MTK:内存管理机制简单分析

    MTK内存管理机制简单分析 1:内存: 内存,在手机里面,是个较为紧缺的资源,特别是在功能机上面.经常在功能机上面产生的内存不足,申请失败的地方比比皆是, 更是屡见不鲜,经常会为了节省内存,会进行代码 ...

  4. spark内存管理模块

    Spark 作为一个基于内存的分布式计算引擎,其内存管理模块在整个系统中扮演着非常重要的角色.理解 Spark 内存管理的基本原理,有助于更好地开发 Spark 应用程序和进行性能调优.本文旨在梳理出 ...

  5. chrome/chromium 上的内存管理模块-allocator介绍

    本文介绍chromium在不同平台上 malloc/new 是如何封装调用的. 从代码中很容易发现,chromium的基础代码并不是仅仅使用"malloc"来分配内存 例如:    ...

  6. 操作系统原理之内存管理(第四章第一部分)

    内存管理的⽬标:实现内存分配和回收,提高内存空间的利用率和内存的访问速度 一.存储器的层次结构 寄存器:在CPU内部有一组CPU寄存器,寄存器是cpu直接访问和处理的数据,是一个临时放数据的空间. 高 ...

  7. 【JavaEE】简单了解操作系统、进程内存管理

    目录 前言: 一.操作系统: 操作系统的定位: 应用程序: 系统调用: 操作系统内核: 驱动程序: 硬件设备: 二.进程: 什么是进程? 进程的描述与组织: 描述: 组织: PCB中的 特征(属性) ...

  8. 操作系统探秘之内存管理

    进程运行的基本原理 编译 编译器将代码编译成计算机识别的二进制指令. 链接 库和目标二进制模块结合链接成一个可装入模块. 装入 将可装入模块装入内存. 编译 编程语言的不同所以需要用户完成编译的动作. ...

  9. 操作系统-课堂笔记-内存管理(南航)

    文章目录 内存管理 回顾 内存管理的作用是什么? 如何分配物理内存 物理内存分配方案 1.连续分配存储管理(可应用于嵌入式设备) 1.1单一连续分配 1.2固定分区分配 1.3可变分区分配 连续分配存 ...

最新文章

  1. 电影情感分析 NLP实战
  2. 程序员硬核劝告:现在还不是出门的时候
  3. mate 7 可以安装linux,centos7安装mate
  4. 搜索引擎蜘蛛为什么对网站不爬行呢?
  5. Android网络编程使用HttpClient访问web站点
  6. c++ windows获得当前工作目录文件_使用命令行修改当前工作目录
  7. 黑魔法(method-swizzling)解决第三方库引发的问题
  8. Android面试总结经
  9. 余宏德:Sun所有的核心技术都是开放的
  10. 线程的基本状态 java 1615477073
  11. 献给1975-1985年出生的人们!!!!
  12. 运用div css和java_如何将css应用于div模式
  13. 数据结构与算法 哈希表的特点
  14. 开氏温度与摄氏度换算_8789 单位换算小技巧
  15. 大学毕业后拉开差距的真正原因
  16. 计算机网络中属于通信子网,计算机网络通常被划分为通信子网和资源子网,通信子网提供信息传输服务,资源子网提供共享资源。...
  17. 锐道发布Dorado Dorado7标准件 -1.0.24 beta版
  18. c语言汇编混合编译不了,IAR汇编与C语言混合编程的问题(内附源程序)
  19. linux 使用c语言如何获取网关地址
  20. 英伟达RTX 2060发布:《战地5》光追超60帧,349美元(转载自IT之家)

热门文章

  1. first blogs C语言
  2. C++高级编程(第3版)_学习记录
  3. UE4 3D指南针功能实现
  4. 得到c++程序Process ID [getpid()], 调高CPU优先级 [renice]
  5. java combox_关于combox的onvaluechanged方法
  6. 获取linux时间戳
  7. MySQL数据库部署详细流程,手把手教你如何搭建
  8. in作为介词的用法_三分钟弄懂“at”,“in”,“on”三个时间介词的基本用法!...
  9. 解决树莓派3 基于ubuntu mate 16 的WIFI连接
  10. 4000多页合集的计算机、网络、算法知识总结