[rCore学习笔记 09]为内核支持函数调用
在[[08 内核第一条指令|上一节]]我们使用了编写entry.asm函数中编写了内核的第一条指令,但是我们使用的汇编.这里注意我们仍然是嵌入了这段asm代码到我们的rust代码之中,然后进行编译.但是即使连使用fn main都不被允许,因此我们如果希望使用rust来编写内核代码,因此我们最好为内核提供函数调用.
开发方式
- 使用
asm编写汇编代码进行初始化,然后将控制权交给rust编写的内核入口. - 使用
rust进行主要的内核开发工作
asm内核初始化
asm内核初始化是分为两个阶段进行的:
- 设置栈空间
- 在内核内使能函数调用
- 直接调用使用
rust的内核入口参数
本节知识清单
官方的知识清单非常的不错:
- 我们需要关注知识清单的内容是我们需要关注的内容
- 知识清单的问题方式往往不是直接照本宣科一个问题对应一个答案的,必须消化知识之后才能回答这样的问题
- 可以看到
rCore其实包含:os,RISC-V,rust,asm,计算机组成原理的内容,我们只是学习到和这个项目沾边的内容是不够的,还需要继续精进才能应对貌似"没来头"的面试问题.
- 如何使得函数返回时能够跳转到调用该函数的下一条指令,即使该函数在代码中的多个位置被调用?
- 对于一个函数而言,保证它调用某个子函数之前,以及该子函数返回到它之后(某些)通用寄存器的值保持不变有何意义?
- 调用者函数和被调用者函数如何合作保证调用子函数前后寄存器内容保持不变?调用者保存和被调用者保存寄存器的保存与恢复各自由谁负责?它们暂时被保存在什么位置?它们于何时被保存和恢复(如函数的开场白/退场白)?
- 在 RISC-V 架构上,调用者保存和被调用者保存寄存器如何划分的?
- sp 和 ra 是调用者还是被调用者保存寄存器,为什么这样约定?
- 如何使用寄存器传递函数调用的参数和返回值?如果寄存器数量不够用了,如何传递函数调用的参数?
函数调用与栈
- 如果考虑目前的函数调用是按照一个列表来执行的,这个列表保存着当前的指令的物理地址.
- 把所有需要执行的指令保存在一个顺序列表里,但是思考一下,如果每一个指令都是确定的,那分支结构和循环结构怎么实现呢,那怎么实时和物理世界交互呢?
- 因此需要用一个控制器决策下一条指令的地址,然后再执行那一条指令.
- 那么再进一步,更加复杂的函数调用更需要一个控制器的决策
- 其它的控制流只需要跳转到一个固定的位置
- 函数的调用需要跳转到一个在运行时决定的位置
- 官方文档中聊了指令怎么解决函数调用问题的,和在多层函数调用时的局限性,这里需要好好看看原文
- 得出结论
- 在调用子函数之前,我们需要在物理内存中的一个区域 保存 (Save) 函数调用上下文中的寄存器;而在函数执行完毕后,我们会从内存中同样的区域读取并 恢复 (Restore) 函数调用上下文中的寄存器。
- 实际上,这一工作是由子函数的调用者和被调用者(也就是子函数自身)合作完成。函数调用上下文中的寄存器被分为如下两类:
- 被调用者保存(Callee-Saved) 寄存器 :被调用的函数可能会覆盖这些寄存器,需要被调用的函数来保存的寄存器,即由被调用的函数来保证在调用前后,这些寄存器保持不变;
- 调用者保存(Caller-Saved) 寄存器 :被调用的函数可能会覆盖这些寄存器,需要发起调用的函数来保存的寄存器,即由发起调用的函数来保证在调用前后,这些寄存器保持不变。
调用规范
关于 调用者保存寄存器 和 被调用者保存寄存器 , 两者的刚刚被提到的时候,我一度以为这是两种实现模式,类似于冯诺依曼和哈佛构型的不同.实际上真的是有划分的.
调用规范就是规定这三点:
- 函数的输入参数和返回值如何传递;
- 函数调用上下文中调用者/被调用者保存寄存器的划分;
- 其他的在函数调用流程中对于寄存器的使用方法。
关于 内存中的一块区域 实际上指的就是 栈区 .
sp 寄存器常用来保存 栈指针 (Stack Pointer),它指向内存中栈顶地址.
原文中这里比较难以理解,我这样理解他,在函数调用的过程中需要保存不止一个寄存器的内容,那么两次函数调用之间实际上不是差了一个地址,而是占用了栈里的一块空间,因此叫栈帧.
在一个函数中,作为起始的开场代码负责分配一块新的栈空间,即将 sp 的值减小相应的字节数即可,于是物理地址区间 新旧[新sp,旧sp) 对应的物理内存的一部分便可以被这个函数用来进行函数调用上下文的保存/恢复,这块物理内存被称为这个函数的 栈帧 (Stack Frame)。

保留fp信息
仔细阅读官方文档可以发现,fp是父亲栈帧的结束地址 fp ,是一个被调用者保存寄存器,栈上多个 fp 信息实际上保存了一条完整的函数调用链,通过适当的方式我们可以实现对函数调用关系的跟踪。
那么要保留其信息需要修改编译选项,这时候要修改.cargo/config,重点关注"-Cforce-frame-pointers=yes".
(其实在之前的章节这句话都被添加上了)
// .cargo/config
[build]
target = "riscv64gc-unknown-none-elf"
[target.riscv64gc-unknown-none-elf]
rustflags = [
"-Clink-args=-Tsrc/linker.ld", "-Cforce-frame-pointers=yes"
]
分配并使用启动栈
修改entry.asm文件
# os/src/entry.asm
.section .text.entry
.globl _start
_start:
la sp, boot_stack_top
call rust_main
.section .bss.stack
.globl boot_stack_lower_bound
boot_stack_lower_bound:
.space 4096 * 16
.globl boot_stack_top
boot_stack_top:
在第 11 行在内核的内存布局中预留了一块大小为 4096 * 16 字节也就是 64KiB 的空间用作接下来要运行的程序的栈空间。
在 RISC-V 架构上,栈是从高地址向低地址增长
这里插一句,之前玩过STM32Cube IDE的里边可以设置stack和heap空间大小,实际上就是和这里相关的,要融会贯通好.
重新观察linker.ld
这段官方文档说的很清楚了,要仔细观察boot_stack_lower_bound和boot_stack_top两个label.
还有一点非常需要注意的,第6行写明了,调用了rust部分的内核入口函数,从此就把控制权交给了rust.
编写rust代码
内核入口函数
内核入口函数rust_main,#[no_mangle] 以避免编译器对它的名字进行混淆.
// os/src/main.rs
#[no_mangle]
pub fn rust_main() -> ! {
loop {}
}
detail
在Rust编程语言中,#[no_mangle]是一个属性(attribute),用于控制编译器对函数名称的处理方式。当我们不在函数前面加上这个属性时,Rust编译器会采用名称修饰(name mangling)技术,对函数名称进行修改,以支持诸如泛型、命名空间、重载等功能。这样的名称在链接时对于C语言或其他不使用相同名称修饰规则的语言来说是不可见的。
然而,当需要与C语言或其他语言编写的代码进行互操作,或者需要从外部直接调用Rust编写的函数时,就需要保证Rust函数的名称在编译后不被改变。这时就可以使用#[no_mangle]属性来避免名称修饰,确保函数名在编译后的二进制文件中保持原样,便于外部代码识别和调用。
清空.bss段
.bss段的数据初始应该是0,因此需要清零.
这时候我们有了rust的内核入口,因此可以用rust编写一个清理函数了.
main.rs编写为:
// os/src/main.rs
#![no_std]
#![no_main]
mod sbi;
mod lang_items;
use core::arch::global_asm;
global_asm!(include_str!("entry.asm"));
#[no_mangle]
pub fn rust_main() -> ! {
clear_bss();
loop {}
}
fn clear_bss() {
extern "C" {
fn sbss();
fn ebss();
}
(sbss as usize..ebss as usize).for_each(|a| {
unsafe { (a as *mut u8).write_volatile(0) }
});
}
detail
函数签名:
fn clear_bss() {
定义了一个名为
clear_bss的函数,没有参数也没有返回值。外部链接:
extern "C" {
fn sbss();
fn ebss();
}
这部分声明了两个外部函数
sbss和ebss,它们没有实际的实现,但预计在链接阶段会由链接器解析到具体的地址。extern "C"表示这两个函数遵循C调用约定,这是为了与C语言编写的代码或链接脚本兼容,确保名字不会被Rust的名称修饰规则修改。
这里注意官方文档:
extern “C” 可以引用一个外部的 C 函数接口(这意味着调用它的时候要遵从目标平台的 C 语言调用规范)。但我们这里只是引用位置标志并将其转成 usize 获取它的地址。由此可以知道.bss段两端的地址。清零BSS段:
(sbss as usize..ebss as usize).for_each(|a| {
unsafe { (a as *mut u8).write_volatile(0) }
});
这里使用了范围表达式
(sbss as usize..ebss as usize)来创建一个从sbss地址到ebss地址的迭代器,这两个地址通常标志着BSS段的起始和结束。for_each遍历这个地址范围内的每一个地址,并对每个地址执行闭包中的操作。unsafe块:由于直接操作内存地址是不安全的,这部分代码需要用unsafe关键字包裹。这是告诉Rust编译器这里包含的手动内存管理操作需要程序员自己保证安全性。(a as *mut u8):将当前地址a转换为指向u8类型的可变指针。.write_volatile(0):通过write_volatile方法将该地址的内存设置为0。volatile关键字在此用于告诉编译器不对这块内存进行优化,确保每次写入操作都实际执行到硬件层面,这对于与硬件交互或多线程环境中的共享内存尤其重要。
[rCore学习笔记 09]为内核支持函数调用的更多相关文章
- C++ GUI Qt4学习笔记09
C++ GUI Qt4学习笔记09 qtc++ 本章介绍Qt中的拖放 拖放是一个应用程序内或者多个应用程序之间传递信息的一种直观的现代操作方式.除了剪贴板提供支持外,通常它还提供数据移动和复制的功 ...
- 机器学习实战(Machine Learning in Action)学习笔记————09.利用PCA简化数据
机器学习实战(Machine Learning in Action)学习笔记————09.利用PCA简化数据 关键字:PCA.主成分分析.降维作者:米仓山下时间:2018-11-15机器学习实战(Ma ...
- Linux内核分析第六周学习笔记——分析Linux内核创建一个新进程的过程
Linux内核分析第六周学习笔记--分析Linux内核创建一个新进程的过程 zl + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/U ...
- Nodejs学习笔记(四)——支持Mongodb
前言:回顾前面零零碎碎写的三篇挂着Nodejs学习笔记的文章,着实有点名不副实,当然,这篇可能还是要继续走着离主线越走越远的路子,从简短的介绍什么是Nodejs,到如何寻找一个可以调试的Nodejs ...
- springmvc学习笔记---面向移动端支持REST API
前言: springmvc对注解的支持非常灵活和飘逸, 也得web编程少了以往很大一坨配置项. 另一方面移动互联网的到来, 使得REST API变得流行, 甚至成为主流. 因此我们来关注下spring ...
- CSS学习笔记09 简单理解BFC
引子 在讲BFC之前,先来看看一个例子 <!DOCTYPE html> <html lang="en"> <head> <meta cha ...
- [原创]java WEB学习笔记09:ServletResponse & HttpServletResponse
本博客为原创:综合 尚硅谷(http://www.atguigu.com)的系统教程(深表感谢)和 网络上的现有资源(博客,文档,图书等),资源的出处我会标明 本博客的目的:①总结自己的学习过程,相当 ...
- 学习笔记之Linux内核编译过程
准备工作 物理主机:win8(32位) 虚拟机工具:VirtualBox_4.3.16_Win32 虚拟主机:xubuntu-12.04.4 安装virtualBox功能增强包 设置好虚拟机与主机的共 ...
- Linux内核学习笔记2——Linux内核源码结构
一 内核组成部分 内核是一个操作系统的核心,主要由五个部分组成:进程调度,内存管理,虚拟文件系统,网络结构,进程间通信. 1.进程调度(SCHED) 控制进程对CPU的访问.当需要选择下一个进程运行时 ...
- 【Spring学习笔记-3】国际化支持
[Spring]国际化支持 一.总体结构: 两个国际化资源中的内容: 二.程序 2.1 配置Spring上下文 beans.xml文件 <?xml version="1.0" ...
随机推荐
- 超详细--redis在Linux环境搭建主从复制
引言Redis是一个高性能的缓存中间件,一个Redis服务器可以支撑很多的并发请求.但是在一些超高的并发场景下,虽然Redis读写速度很快,但也会产生读写压力过大,服务器负载过高的情况.为了分担读写的 ...
- gitlab docker 自动部署报错 /bin/bash: line 118: docker: command not found
原因找不到docker,我们需要绑一下docker 列出所有gitlab-runner配置文件 find / | grep config.toml [root@izwz99pke7zxkpm7l51t ...
- 图像处理技术OpencvSharp入门
目录 第一部分 初识Opencv 1.C# 下Opencv库 2.安装OpenCvSharp 第二部分 OpencvSharp入门 1.加载图像文件 2.显示图像 第三部分 基础应用 1.颜色转换 2 ...
- 【VMware vSphere】使用vSphere Lifecycle Manager(vLCM)管理独立主机和集群的生命周期。
vSphere Lifecycle Manager(vLCM)是 vSphere 7 中引入的一项新功能,它提供了一种集中式.自动化和简单性的方式来管理和升级 vSphere 基础架构组件(如vCen ...
- 数据结构之栈(Java,C语言的实现)以及相关习题巩固
目录 栈 概念以及代码实现 例题 232. 用栈实现队列 1614. 括号的最大嵌套深度 234. 回文链表 1614. 括号的最大嵌套深度 LCR 123. 图书整理 I 206. 反转链表 402 ...
- 箭头函数中的this指向
// 箭头函数中的this指向 // 如果是箭头函数,this指向是,父级程序的,this的指向 // 如果父级程序是一个函数,函数也是有t ...
- CSP-S2019 江西 题解
为什么有 \(5\) 道题? [CSP-S2019 江西] 和积和 简单化一下式子: \[(n + 1) \times \sum A_i \times B_i - (\sum A_i) \times ...
- Vue第三方库与插件实战手册
title: Vue第三方库与插件实战手册 date: 2024/6/8 updated: 2024/6/8 excerpt: 这篇文章介绍了如何在Vue框架中实现数据的高效验证与处理,以及如何集成E ...
- ABC332
D 我们可以把矩阵 \(\text{A}\) 看成 \({p,q}\). \(p\) 指现在一行最开始在哪里,\(q\) 指现在这一列最开始在哪里. 于是我们枚举 \(p\) 和 \(q\) 所有可能 ...
- ARC169
A 我们定义 \(dp_{dep}\) 为第 \(dep\) 层会对上一层产生多少的影响. 如果有一层的影响大于 \(0\),在足够次计算后那么肯定是正号.如果小于零那就一定是负号. 由于越久影响到的 ...