在[[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. .net程序员学习java篇一(搭建SSM)

    一.安装IDE 相比于.net环境的一气呵成,java可能麻烦一点,这里记录下来,避免萌新踩坑 1.1安装JDK,这里不要玩什么花哨,老老实实选个大众版(Oracle JDK1.8x),设置环境变量, ...

  2. 【题解】A18747.眼红的同学

    题目链接:眼红的同学 题干信息很简单,看到数据量之后就不简单了.在数据量小的时候可以使用双层循环暴力的方法来求答案.显然对于这道题而言O(n^2)是完全过不去的. 前置知识: 使用树状数组求逆序对 会 ...

  3. MFC之多字节和宽字节的总结

    ANSI字符集  所支持的就是多字节的也叫窄字节,类型来说就对应char类型.Unicode字符集 也叫宽字符集 所支持的就是宽字符集,从类型上来说就是 wchar_t类型.gb2312是中国的编码, ...

  4. 【C# 序列化】System.Text.Json.Nodes ---Json数据交换格式 对应C#类

    请先阅读 JSON数据交换格式 Json数据交换格式 对应C#类 System.Text.Json.Nodes:.NET 6 依微软的计划,System.Text.Json 应取代Newtonsoft ...

  5. 23ai中的True Cache到底能做啥?

    最近,Oracle的产品管理总监在Oracle数据库内幕中介绍了True Cache. 原文链接如下: https://blogs.oracle.com/database/post/introduci ...

  6. 在Rainbond中一键部署高可用 EMQX 集群

    本文描述如何通过云原生应用管理平台 Rainbond 一键安装高可用 EMQX 集群.这种方式适合不太了解 Kubernetes.容器化等复杂技术的用户使用,降低了在 Kubernetes 中部署 E ...

  7. c#WinFrom自定义图表仪表控件-频谱

    这是为客户定制的一个频谱图表控件,先看下成品效果,gif较大,略等片刻 开发步骤分析: 1.界面有多个间距不等的线分割的区域,每个区域的值范围不同,我们就需要把每个区域定义出来,方便我们操作的时候来计 ...

  8. 易盾逆向分析-知乎login

    声明 本文章中所有内容仅供学习交流,抓包内容.敏感网址.数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,若有侵权,请联系我立即删除! 目标网站 aHR0cHM6 ...

  9. Zenlayer如何将万台设备监控从Zabbix迁移到Flashcat

    作为全球首家以超连接为核心的云服务商,Zenlayer 致力于将云计算.内容服务和边缘技术融合,为客户提供全面的解决方案.通过构建可靠的网络架构和高效的数据传输,Zenlayer 帮助客户实现更快速. ...

  10. Java8中LocalDateTime与时间戳timestamp的互相转换及ChronoUnit工具类

    Java8中LocalDateTime与时间戳timestamp的互相转换及ChronoUnit工具类import java.time.*;import java.time.format.DateTi ...