在[[08 内核第一条指令|上一节]]我们使用了编写entry.asm函数中编写了内核的第一条指令,但是我们使用的汇编.这里注意我们仍然是嵌入了这段asm代码到我们的rust代码之中,然后进行编译.但是即使连使用fn main都不被允许,因此我们如果希望使用rust来编写内核代码,因此我们最好为内核提供函数调用.

开发方式

  1. 使用asm编写汇编代码进行初始化,然后将控制权交给rust编写的内核入口.
  2. 使用rust进行主要的内核开发工作

asm内核初始化

asm内核初始化是分为两个阶段进行的:

  1. 设置栈空间
  2. 在内核内使能函数调用
  3. 直接调用使用rust的内核入口参数

本节知识清单

官方的知识清单非常的不错:

  1. 我们需要关注知识清单的内容是我们需要关注的内容
  2. 知识清单的问题方式往往不是直接照本宣科一个问题对应一个答案的,必须消化知识之后才能回答这样的问题
  3. 可以看到rCore其实包含:os,RISC-V,rust,asm,计算机组成原理的内容,我们只是学习到和这个项目沾边的内容是不够的,还需要继续精进才能应对貌似"没来头"的面试问题.
  • 如何使得函数返回时能够跳转到调用该函数的下一条指令,即使该函数在代码中的多个位置被调用?
  • 对于一个函数而言,保证它调用某个子函数之前,以及该子函数返回到它之后(某些)通用寄存器的值保持不变有何意义?
  • 调用者函数和被调用者函数如何合作保证调用子函数前后寄存器内容保持不变?调用者保存和被调用者保存寄存器的保存与恢复各自由谁负责?它们暂时被保存在什么位置?它们于何时被保存和恢复(如函数的开场白/退场白)?
  • 在 RISC-V 架构上,调用者保存和被调用者保存寄存器如何划分的?
  • sp 和 ra 是调用者还是被调用者保存寄存器,为什么这样约定?
  • 如何使用寄存器传递函数调用的参数和返回值?如果寄存器数量不够用了,如何传递函数调用的参数?

函数调用与栈

  1. 如果考虑目前的函数调用是按照一个列表来执行的,这个列表保存着当前的指令的物理地址.

    1. 把所有需要执行的指令保存在一个顺序列表里,但是思考一下,如果每一个指令都是确定的,那分支结构和循环结构怎么实现呢,那怎么实时和物理世界交互呢?
    2. 因此需要用一个控制器决策下一条指令的地址,然后再执行那一条指令.
    3. 那么再进一步,更加复杂的函数调用更需要一个控制器的决策
      1. 其它的控制流只需要跳转到一个固定的位置
      2. 函数的调用需要跳转到一个在运行时决定的位置
  2. 官方文档中聊了指令怎么解决函数调用问题的,和在多层函数调用时的局限性,这里需要好好看看原文
  3. 得出结论
    1. 在调用子函数之前,我们需要在物理内存中的一个区域 保存 (Save) 函数调用上下文中的寄存器;而在函数执行完毕后,我们会从内存中同样的区域读取并 恢复 (Restore) 函数调用上下文中的寄存器。
    2. 实际上,这一工作是由子函数的调用者和被调用者(也就是子函数自身)合作完成。函数调用上下文中的寄存器被分为如下两类:
      1. 被调用者保存(Callee-Saved) 寄存器 :被调用的函数可能会覆盖这些寄存器,需要被调用的函数来保存的寄存器,即由被调用的函数来保证在调用前后,这些寄存器保持不变;
      2. 调用者保存(Caller-Saved) 寄存器 :被调用的函数可能会覆盖这些寄存器,需要发起调用的函数来保存的寄存器,即由发起调用的函数来保证在调用前后,这些寄存器保持不变。

调用规范

关于 调用者保存寄存器被调用者保存寄存器 , 两者的刚刚被提到的时候,我一度以为这是两种实现模式,类似于冯诺依曼和哈佛构型的不同.实际上真的是有划分的.

调用规范就是规定这三点:

  1. 函数的输入参数和返回值如何传递;
  2. 函数调用上下文中调用者/被调用者保存寄存器的划分;
  3. 其他的在函数调用流程中对于寄存器的使用方法。

关于 内存中的一块区域 实际上指的就是 栈区 .

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的里边可以设置stackheap空间大小,实际上就是和这里相关的,要融会贯通好.

重新观察linker.ld

这段官方文档说的很清楚了,要仔细观察boot_stack_lower_boundboot_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

  1. 函数签名:

    fn clear_bss() {

    定义了一个名为clear_bss的函数,没有参数也没有返回值。

  2. 外部链接:

    extern "C" {
    fn sbss();
    fn ebss();
    }

    这部分声明了两个外部函数sbssebss,它们没有实际的实现,但预计在链接阶段会由链接器解析到具体的地址。extern "C"表示这两个函数遵循C调用约定,这是为了与C语言编写的代码或链接脚本兼容,确保名字不会被Rust的名称修饰规则修改。

    这里注意官方文档:

    extern “C” 可以引用一个外部的 C 函数接口(这意味着调用它的时候要遵从目标平台的 C 语言调用规范)。但我们这里只是引用位置标志并将其转成 usize 获取它的地址。由此可以知道 .bss 段两端的地址。

  3. 清零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]为内核支持函数调用的更多相关文章

  1. C++ GUI Qt4学习笔记09

    C++ GUI Qt4学习笔记09   qtc++ 本章介绍Qt中的拖放 拖放是一个应用程序内或者多个应用程序之间传递信息的一种直观的现代操作方式.除了剪贴板提供支持外,通常它还提供数据移动和复制的功 ...

  2. 机器学习实战(Machine Learning in Action)学习笔记————09.利用PCA简化数据

    机器学习实战(Machine Learning in Action)学习笔记————09.利用PCA简化数据 关键字:PCA.主成分分析.降维作者:米仓山下时间:2018-11-15机器学习实战(Ma ...

  3. Linux内核分析第六周学习笔记——分析Linux内核创建一个新进程的过程

    Linux内核分析第六周学习笔记--分析Linux内核创建一个新进程的过程 zl + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/U ...

  4. Nodejs学习笔记(四)——支持Mongodb

    前言:回顾前面零零碎碎写的三篇挂着Nodejs学习笔记的文章,着实有点名不副实,当然,这篇可能还是要继续走着离主线越走越远的路子,从简短的介绍什么是Nodejs,到如何寻找一个可以调试的Nodejs ...

  5. springmvc学习笔记---面向移动端支持REST API

    前言: springmvc对注解的支持非常灵活和飘逸, 也得web编程少了以往很大一坨配置项. 另一方面移动互联网的到来, 使得REST API变得流行, 甚至成为主流. 因此我们来关注下spring ...

  6. CSS学习笔记09 简单理解BFC

    引子 在讲BFC之前,先来看看一个例子 <!DOCTYPE html> <html lang="en"> <head> <meta cha ...

  7. [原创]java WEB学习笔记09:ServletResponse & HttpServletResponse

    本博客为原创:综合 尚硅谷(http://www.atguigu.com)的系统教程(深表感谢)和 网络上的现有资源(博客,文档,图书等),资源的出处我会标明 本博客的目的:①总结自己的学习过程,相当 ...

  8. 学习笔记之Linux内核编译过程

    准备工作 物理主机:win8(32位) 虚拟机工具:VirtualBox_4.3.16_Win32 虚拟主机:xubuntu-12.04.4 安装virtualBox功能增强包 设置好虚拟机与主机的共 ...

  9. Linux内核学习笔记2——Linux内核源码结构

    一 内核组成部分 内核是一个操作系统的核心,主要由五个部分组成:进程调度,内存管理,虚拟文件系统,网络结构,进程间通信. 1.进程调度(SCHED) 控制进程对CPU的访问.当需要选择下一个进程运行时 ...

  10. 【Spring学习笔记-3】国际化支持

    [Spring]国际化支持 一.总体结构: 两个国际化资源中的内容: 二.程序 2.1  配置Spring上下文 beans.xml文件 <?xml version="1.0" ...

随机推荐

  1. [C#] 禁用控制台关闭按钮

    禁用控制台关闭按钮 internal class Program { [DllImport("user32.dll", EntryPoint = "FindWindow& ...

  2. 在mobaxten上使用scala报错

    查看报错信息 [ERROR] Failed to construct terminal; falling back to unsupported java.io.IOException: Cannot ...

  3. html2canvas + jspdf导出pdf,文字重叠,样式不显示或者文字不显示

    先在html引入cdn <script src="https://html2canvas.hertzen.com/dist/html2canvas.js"></s ...

  4. 无法删除此对象,因为未在 ObjectStateManager 中找到它。

    无法删除此对象,因为未在 ObjectStateManager 中找到它. 不能直接删除实体类, 用Service提供的: void Delete(long[] ids); void Delete(l ...

  5. 西数 WD SATA SSD 固态 蓝盘 复制和剪切速度慢

    现象:速度只有4,5M,活动时间100%.用AS SSD 测试速度正常. 问题:冷数据掉速.冷数据门. 解决方法:用DiskFresh,刷新下. 刷新时间,要看你存储数据的多少.我的1T 蓝盘,用了3 ...

  6. [DP] DP优化总结

    写在前面 $ DP $,是每个信息学竞赛选手所必会的算法,而 $ DP $ 中状态的转移又显得尤为关键.本文主要从状态的设计和转移入手,利用各种方法对朴素 $ DP $ 的时间复杂度和空间复杂度进行优 ...

  7. TensorFLow手写字识别深度学习网络分析详解

    Tensorflow和MNIST简介 TensorFlow 是一个采用数据流图,用于数值计算的开源软件库.它是一个不严格的"神经网络"库,可以利用它提供的模块搭建大多数类型的神经网 ...

  8. ACPI Table 与 Device Tree

    背景 在分析Linux内核驱动的时候,有时候会看到一些acpi字样的接口. 之前一直没搞明白ACPI是什么,现在知道了. Reference : https://www.cnblogs.com/jun ...

  9. Oracle 三种分页方法

    Oracle的三层分页指的是在进行分页查询时,使用三种不同的方式来实现分页效果,分别是使用ROWNUM.使用OFFSET和FETCH.使用ROW_NUMBER() OVER() 1.使用ROWNUM ...

  10. C# Newtonsoft增删改查(本地存储)(简单便捷)(拿来即用)

    调用方法: LocalSetupHelper.SetData(Sss.维护, "密码", "123456"); //保存 var c=LocalSetupHel ...