【xv6操作系统】xv6 启动过程分析

一、调试用到的汇编代码

为了方便,  Makefile 会创建.asm 文件,可以通过它来定位究竟是哪个指令导致了 bug。

可以看到,  kernel 从 80000000 地址处开始执行,第二列为相应指令(如 auipc) 的 16 进制表示(如 00009117)。

二、  过程流程图

对 xv6 的启动过程绘制了流程图:

三、详细过程分析

·不带 gdb 运行 xv6

$ make qemu

这里会编译文件,然后调用 QEMU。

这里本质上是通过 C 语言来模拟仿真 RISC-V 处理器(一块直接连接硬件设备的主 板)。

1.开始调试

调试这一过程需要两个窗口:执行窗口和调试窗口。

·在执行窗口中输入# make CPUS=1 qemu -gdb

为了方便调试,我们把 CPU 设置为 1,而不是默认的 4(qemu 模拟的 riscv 是 4 核 的) 。 在单核或者单线程场景下,单个断点就可以停止整个程序的运行。

·在调试窗口中输入# gdb-multiarch

2._entry

 risc-v 计算机上电时,它自身初始化,并运行一个引导加载器(存储在 ROM

中)。引导加载器装载 xv6 的内核到内存的 0x8000000 开始的存储空间中(kernel.ld 文 件)。之所以将内核放在 0x80000000 而不是 0x0,是因为地址范围 0x0:0x80000000 包   I/O 设备。

然后在 machine mode 下,  CPU 从 kernel/entry.s _entry(kernel/entry.s:6) 处 开始执行 xv6 。risc-v 启动时 paging 硬件是禁用的:虚拟地址直接映射到物理地址,  无内存隔离性。

_entry 处的指令为 CPU 设置了栈,这样 xv6 就可以运行 C 代码。

· 输入(gdb) layout split,从这个视图可以看出 gdb 要执行的下一条指令是什么。 ·在_entry 处下断点:(gdb) b _entry

·查看 0x80000000 处的反汇编代码:(gdb) x/6i 0x80000000

·然后继续执行:(gdb) c,发现线程 1 运行到_entry 处停了下来

kernel/entry.S 的源码:

# qemu -kernel loads the kernel at 0x80000000

# and causes each CPU to jump there.

# kernel.ld causes the following code to

# be placed at 0x80000000.

.section .text

.global _entry

_entry:

# set up a stack for C.

# stack0 is declared in start.c,

# with a 4096-byte stack per CPU.

# sp = stack0 + (hartid * 4096)(每个 CPU 对应的栈起始地址)

la sp, stack0(把 stack0 的地址读到 sp 寄存器中)

li a0, 1024*4(把 4096 这个立即数读到 a0 寄存器中)

csrr a1, mhartid(把当前 CPU  ID 读到 a1 寄存器中)

addi a1, a1, 1

mul a0, a0, a1

add sp, sp, a0

# jump to start() in start.c

call start(如果 start 函数返回(一般不会出现)那么进入死循环)

spin:

j spin

3._entry -> start() -> main()

_entry 调用 start(),start()调用 kernel main.c,xv6 进入 supervisor mode。

为了进入 supervisor mode,risc-v 提供指令 mret 。This instruction is most often

used to return from a previous call from supervisor mode to machine mode. start isn’t returning from such a call, and instead sets things up as if there had been one: it sets

the previous privilege mode to supervisor in the register mstatus, it sets the return address to main by writing mains address into the register mepc, disables virtual   address translation in supervisor mode by writing 0 into the page-table register

satp, and delegates all interrupts and exceptions to supervisor mode.

kernel/start.c 的源码 :

#include "types.h"

#include "param.h"

#include "memlayout.h"

#include "riscv.h"

#include "defs.h"

void main();

void timerinit();

// entry.S needs one stack per CPU.

__attribute__ ((aligned (16))) char stack0[4096 * NCPU];stack0 要求 16bit 对齐)

// a scratch area per CPU for machine-mode timer interrupts.

uint64 timer_scratch[NCPU][5];(定义了共享变量, 即每个 CPU 的暂存区用于 machine -mode 定时 器中断,它是和 timer 驱动之间传递数据用的)

// assembly code in kernelvec.S for machine -mode timer interrupt.

extern void timervec();(声明了 timer 中断处理函数,在接下来的 timer 初始化函数中被用到)

// entry.S jumps here in machine mode on stack0.

void

start()

{

// set M Previous Privilege mode to Supervisor, for mret.

unsigned long x = r_mstatus();

x &= ~MSTATUS_MPP_MASK;

x |= MSTATUS_MPP_S;

w_mstatus(x);

// set M Exception Program Counter to main, for mret.

// requires gcc -mcmodel=medany

w_mepc((uint64)main);(设置了汇编指令 mret  PC 指针跳转的函数,也就是 main 函数)

// disable paging for now.

w_satp(0);(这行代码暂时关闭了分页功能, 即直接使用物理地址)

// delegate all interrupts and exceptions to supervisor mode.

w_medeleg(0xffff);

w_mideleg(0xffff);

w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);

// configure Physical Memory Protection to give supervisor mode

// access to all of physical memory.

w_pmpaddr0(0x3fffffffffffffull);

w_pmpcfg0(0xf);

// ask for clock interrupts.

timerinit();clock 的初始化)

// keep each CPU's hartid in itstp register, for cpuid().

(将 CPU  ID 值保存在寄存器 tp 中)

int id = r_mhartid();

w_tp(id);

// switch to supervisor mode and jump to main().

asm volatile("mret");

}

// set up to receive timer interrupts in machine mode,

// which arrive at timervec in kernelvec.S,

// which turns them into software interrupts for

// devintr() in trap.c.

clock 时钟驱动的初始化函数)

void

timerinit()

{

// each CPU has a separate source of timer interrupts.

int id = r_mhartid();(读出 CPU  ID

// ask the CLINT for a timer interrupt.

(设置中断时间间隔)

int interval = 1000000; // cycles; about 1/10th second in qemu.

*(uint64*)CLINT_MTIMECMP(id) = *(uint64*)CLINT_MTIME + interval;

// prepare information in scratch[] for timervec.

// scratch[0..2] : space for timervec to save registers.

// scratch[3] : address of CLINT MTIMECMP register.

// scratch[4] : desired interval (in cycles) between timer interrupts.

(利用刚才在文件开头声明的 timer_scratch 变量, 把刚才的 CPU  ID 和设置的中断间隔设置到 scratch 寄存器中, 以供 clock 驱动使用)

uint64 *scratch = &timer_scratch[id][0];

scratch[3] = CLINT_MTIMECMP(id);

scratch[4] = interval;

w_mscratch((uint64)scratch);

// set the machine-mode trap handler.

w_mtvec((uint64)timervec);

// enable machine-mode interrupts.

w_mstatus(r_mstatus() | MSTATUS_MIE);

// enable machine-mode timer interrupts.

w_mie(r_mie() | MIE_MTIE);

}

start 函数调用 mret,跳转到 main 函数。

kernel/main.c 的源码 :

#include "types.h"

#include "param.h"

#include "memlayout.h"

#include "riscv.h"

#include "defs.h"

volatile static int started = 0;

// start() jumps here in supervisor mode on all CPUs.

void

main()

{

if(cpuid() == 0){(判断当前的 CPU  ID 是否为主 CPU 。如果是主 CPU ,则执行一系列的初始化 操作。)

consoleinit();(控制台初始化)

printfinit();(打印模块初始化)

printf("\n");

printf("xv6 kernel is booting\n");

printf("\n");

kinit();         // physical page allocator(页表分配器)

kvminit();       // create kernel page table

kvminithart();   // turn on paging(打开分页机制)

procinit();      // process table

trapinit();      // trap vectors

trapinithart();  // install kernel trap vector

plicinit();      // set up interrupt controller

plicinithart();  // ask PLIC(中断控制器 Platform Level Interrupt Controller for device interrupts binit();         // buffer cache

iinit();         // inode table(磁盘节点的初始化)

fileinit();      // file table

virtio_disk_init(); // emulated hard disk

userinit();      // first user process(创建第一个用户进程,第一个进程执行一个小程序 user/initcode.S ,该程序通过调用 exec 系统调用重新进入内核)

__sync_synchronize();gcc 提供的原子操作,保证内存访问的操作都是原子操作) started = 1;(设置初始化完成的标志)

} else {(如果不是主 CPU ,首先循环等待主 CPU 初始化完成)

while(started == 0)

;

__sync_synchronize();

printf("hart %d starting\n", cpuid());

kvminithart();

trapinithart();

plicinithart(); }

// turn on paging

// install kernel trap vector

// ask PLIC for device interrupts

scheduler();

}

main()中,第 16 -18 行会输出”\n ” ”xv6 kernel is booting\n”   “\n 

·输入(gdb) u 18 使程序运行到第 18 行,并查看执行窗口。

4. main() -> kvminit ()

输入(gdb) n,继续运行;

运行到 kvminithart()时,在执行终端按下“CTRL-a”释放后按“c”,回到 qemu,输入  info mem” 查看当前页表信息(底层页表的信息,不是原始页表!)

此时系统还未启动分页机制

5. main() -> kvminithart()

执行 kvminithart() 页表始址寄存器 satp 指向内核页表

以 16 进制显示页表始址寄存器 satp 的值:(gdb) p/x $satp,里面存放的是内核页表的 块号

查看内核页表信息:

6.mian() -> userinit()

main(kernel/main.c)初始化设备和子系统后,通过调用 userinit(kernel/proc:212)创 建第一个进程。

进入函数(gdb) s

第 230 行:新创建的进程在内核态第一次被调度

第 239 行:1#进程用户态返回地址(用户程序计数器)

执行到 247 行,完成第一个用户进程 initcode 的建立,显示它的 pid、状态和进程页表 首地址。

此时 1#进程还未被调度,系统处于内核态。

页表始址寄存器的内容,即内核页表块号未发生变化。

7.main() -> scheduler()

(gdb) n ,userinit( )结束,返回到 main()。

随后执行到 scheduler(),(gdb) s 进入函数。

(gdb) u 455,选中 1#进程,查看进程 pid 和进程状态

6. scheduler() ->swtch() ->forkret()->usertrapret() ->userret()

进入 userret()汇编函数后,   (gdb) b *0x3ffffff10e

sret 指令使权限由内核态降至用户态

9.userret() -> initcode.S

第一个进程执行一个汇编(risc-v)小程序,  initcode.S(user/initcode.s:1) (通过调 用 exec system call 重入 kernel)。exec 用新程序替换当前进程的内存和寄存器。   一旦 内核完成 exec ,它返回到用户空间。

在虚拟地址 0x0 处设置断点,执行到 user/initcode.S 的入口,查看 satp 寄存器的 值。

打印 1#进程 initcode 的页表

10. initcode.S -> init()

init(user/init.c:15)创建一个新控制台设备文件(如果需要 然后作为文件描述符 0 、1 、2 打开它。然后在 console 启动一个 shell,系统启动。

继续执行,  exec 加载 user/init,使用  1#进程的 PCB — proc[1]建立 init 进程映像, 释放 initcode 的页表和内存。

(gdb) c ,执行到 1#进程 init 的入口 0x0,查看此时寄存器 satp 的值。

11.init() -> fork()

清空所有断点后,在 init 进程中 fork 处设置断点,进入内核态,执行 fork 系统调用: 创建 2#进程。

查看 init 进程 pid,子进程 pid,init 的根页表始址,子进程的根页表始址。

12.init() -> sh()

在*0xc6 处设置断点(查看 init.asm 文件,发现 c6 处为 exec(  sh , argv);语句执行地 ),接着执行到 init 的 34 行(fork 后回到用户态,   sh.c 还未执行)

查看 2#进程根页表的块号:

13. sh()

(gdb) c ,执行 exec(  “sh , argv),启动 shell。系统启动完成!

至此,操作系统就有了 init 进程(pid=1)和 shell 进程(pid=2)两个进程,操作系统也 就这样启动了。  init 进程是 shell 进程的守护进程,当 shell 进程挂掉后,  init 进程会重  新 fork 、exec 出一个新的 shell 进程。

14.启动成功,显示信息

系统启动后,按下 Ctrl-p 显示系统中用户进程基本信息

第一列为进程 pid,第二列为进程状态,第三列为进程名称

四、参考文献及链接:

[1]xv6 系统启动代码分析(MIT 6.S081 FALL 2020)_#define entry_start 0x80000000-CSDN博客

[2] Russ Cox, Frans Kaashoek, Robert Morris, xv6: A simple, Unix -like teaching operating system, 2020.

[3]mit6.s081 - xv6启动过程-CSDN博客

[4]6.S081 Lab00 xv6启动过程(从代码出发,了解操作系统启动过程)_cpus=1-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/272530.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

缩放算法优化步骤详解

添加链接描述 背景 假设数据存放在在unsigned char* m_pData 里面,宽和高分别是:m_nDataWidth m_nDataHeight 给定缩放比例:fXZoom fYZoom,返回缩放后的unsigned char* dataZoom 这里采用最简单的缩放算法即: 根据比…

猫头虎分享已解决Bug || 系统监控故障:MonitoringServiceDown, MetricsCollectionError

博主猫头虎的技术世界 🌟 欢迎来到猫头虎的博客 — 探索技术的无限可能! 专栏链接: 🔗 精选专栏: 《面试题大全》 — 面试准备的宝典!《IDEA开发秘籍》 — 提升你的IDEA技能!《100天精通鸿蒙》 …

如何在一个pycharm项目中创建jupyter notebook文件,并切换到conda环境中

1、第一步可以直接在pycharm项目中创建jupyter notebook文件 2、假若想要切换成pytorch环境做实验例子,会发现报这个错误 Jupyter server process exited with code 1 C:\Users\12430\.conda\envs\pytorch3.11\python.exe: No module named jupyter在这里&#xff…

Python快速入门系列-2(Python的安装与环境设置)

第二章:Python的安装与环境设置 2.1 Python的下载与安装2.1.1 访问Python官网2.1.2 安装Python对于Windows用户对于macOS用户对于Linux用户 2.2 集成开发环境(IDE)的选择与设置2.2.1 PyCharm2.2.2 Visual Studio Code2.2.3 Jupyter Notebook2…

jvm堆概述

《java虚拟机规范》中对java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。 一个JVM实例只存在一个堆内存(就是new 出来一个对象),java内存管理的核心区域 java堆区在jvm启动的时候就被创建,空间大小确定。是jvm管理的最大一…

力扣--滑动窗口438.找到字符串中所有字母异位词

思路分析: 使用两个数组snum和pnum分别记录字符串s和p中各字符出现的次数。遍历字符串p,统计其中各字符的出现次数,存储在pnum数组中。初始化snum数组,统计s的前m-1个字符的出现次数。从第m个字符开始遍历s,通过滑动窗…

STM32_3-1点亮LED灯与蜂鸣器发声

STM32之GPIO GPIO在输出模式时可以控制端口输出高低电平,用以驱动Led蜂鸣器等外设,以及模拟通信协议输出时序等。 输入模式时可以读取端口的高低电平或电压,用于读取按键输入,外接模块电平信号输入,ADC电压采集灯 GP…

C# WinForm AndtUI第三方库 Table控件使用记录

环境搭建 1.在NuGet中搜索AndtUI并下载至C# .NetFramework WinForm项目。 2.添加Table控件至窗体。 使用方法集合 1.单元格点击事件 获取被点击记录特定列内容 private void dgv_CellClick(object sender, MouseEventArgs args, object record, int rowIndex, int columnIn…

一篇搞懂什么是LRU缓存|一篇搞懂LRU缓存的实现|LRUCache详解和实现

LRUCache 文章目录 LRUCache前言项目代码仓库什么时候会用到缓存(Cache)缓存满了,怎么办?什么是LRUCacheLRUCache的实现LRUCache对应的OJ题实现LRUCache对应的STL风格实现 前言 这里分享我的一些博客专栏,都是干货满满的。 手撕数据结构专栏…

解决ts报错:类型“entry”上不存在属性“$AppTools”

uniapp ts 项目,已经将AppTools挂在了vue的原型上,但是在vue页面使用时报错,如图: 解决: 在项目根目录下的tsconfig.json文件添加如下配置: "include": ["src/**/*"],这样报错就消失…

ChatGPT数据分析应用——热力图分析

ChatGPT数据分析应用——热力图分析 ​ 热力图分析既可以算作一种可视化方法,也可以算作一种分析方法,主要用于直观地展示数据的分布情况。接下来我们让ChatGPT解释这个方法的概念并提供相应的案例。发送如下内容给ChatGPT。 ​ ChatGPT收到上述内容后&…

云计算科学与工程实践指南--章节引言收集

云计算科学与工程实践指南–章节引言收集 //本文收集 【云计算科学与工程实践指南】 书中每一章节的引言。 我已厌倦了在一本书中阅读云的定义。难道你不失望吗?你正在阅读一个很好的故事,突然间作者必须停下来介绍云。谁在乎云是什么? 通…

opengl 学习(二)-----你好,三角形

你好&#xff0c;三角形 分类demo效果解析 分类 opengl c demo #include "glad/glad.h" #include "glfw3.h" #include <iostream> #include <cmath> #include <vector>using namespace std;/** * 在学习此节之前&#xff0c;建议将这…

MIT6.828操作系统工程实验学习笔记(二)

前言 这篇文章是接上文的内容&#xff0c;依然是对Lab1的记录 如何启动保护模式 要启动保护模式&#xff0c;需要完成以下三个步骤&#xff1a; 在内存中加载GDT&#xff0c;设置GDTR设置CR0寄存器的PE&#xff08;Protected Enable&#xff09;位&#xff0c;启用保护模式…

【深度学习笔记】6_9 深度循环神经网络deep-rnn

注&#xff1a;本文为《动手学深度学习》开源内容&#xff0c;部分标注了个人理解&#xff0c;仅为个人学习记录&#xff0c;无抄袭搬运意图 6.9 深度循环神经网络 本章到目前为止介绍的循环神经网络只有一个单向的隐藏层&#xff0c;在深度学习应用里&#xff0c;我们通常会用…

使用API有效率地管理Dynadot域名,进行DNS域名解析

关于Dynadot Dynadot是通过ICANN认证的域名注册商&#xff0c;自2002年成立以来&#xff0c;服务于全球108个国家和地区的客户&#xff0c;为数以万计的客户提供简洁&#xff0c;优惠&#xff0c;安全的域名注册以及管理服务。 Dynadot平台操作教程索引&#xff08;包括域名邮…

算法设计与分析(超详解!) 第一节 算法概述

1.算法的定义 算法的非形式化定义&#xff1a;算法是规则的有限集合&#xff0c;是为解决特定问题而规定的一系列操作。 可以理解为&#xff1a;算法&#xff08;algorithm&#xff09;是指在解决问题时&#xff0c;按照某种机械的步骤一定可以得到问题的结果&#xff08;有的…

图形库实战丨C语言扫雷小游戏(超2w字,附图片素材)

目录 效果展示 游玩链接&#xff08;无需安装图形库及VS&#xff09; 开发环境及准备 1.VS2022版本 2.图形库 游戏初始化 1.头文件 2.创建窗口 3.主函数框架 开始界面函数 1.初始化 1-1.设置背景颜色及字体 1-2.处理背景音乐及图片素材 1-3.处理背景图位置 2.选…

【软件安装教程】Anaconda

【软件安装教程】Anaconda 系统: Windows11 64位版本: Anaconda3-2024.02-1官方地址: Anaconda网盘地址: 百度网盘 下载 点击此连接 Anaconda 进入官网下载最新版 点击此连接 百度网盘 进入网盘下载 Anaconda3-2024.02-1 安装 双击下载好的文件 点击 Next 点击 I Agree …

求根节点到叶节点数字之和

题目 题目链接 . - 力扣&#xff08;LeetCode&#xff09; 题目描述 代码实现 class Solution { public:int sumNumbers(TreeNode* root) {return _sumNumbers(root, 0);}int _sumNumbers(TreeNode* root, int preSum){preSum preSum * 10 root->val;if(root->left…