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

news2024/11/16 1:58:06

一、调试用到的汇编代码

为了方便,  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.coloradmin.cn/o/1502440.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++ 打印输出十六进制数 指定占位符前面填充0

C 打印十六进制数据&#xff0c;指定数据长度&#xff0c;前面不够时&#xff0c;补充0. 代码如下&#xff1a; #include <iostream> #include <iomanip> #include <cmath>using namespace std;int main() {unsigned int id 0xc01;unsigned int testCaseId…

解决虚拟机静态网址设置后还是变动的的问题

源头就是我的虚拟机静态网址设置好了以后但是网址还是会变动 这是我虚拟机的配置 这是出现的问题 然后我去把多余的ens33的文件都删了 然后还不行 后来按照这个图片进行了下 然后接解决了

string 底层模拟实现常用接口

目录 前言 什么是string? 为什么要学习使用string&#xff1f;string的优势&#xff1f; 因此&#xff0c;string类的成员变量也如图顺序表一样&#xff0c;如下图所示&#xff1a; 构造函数 拷贝构造 析构函数 size() 、capacity&#xff08;&#xff09; operato…

C语言数据结构之二叉堆

愿你千山暮雪 海棠依旧 不为岁月惊扰平添忧愁 &#x1f3a5;前期回顾-二叉树 &#x1f525;数据结构专栏 期待小伙伴们的支持与关注&#xff01;&#xff01;&#xff01; 目录 前期回顾 二叉堆的概念及结构 二叉堆的创建 顺序表的结构声明 顺序表的创建与销毁 二叉堆的插入 …

selenium常用操作汇总

本文总结使用selenium进行web/UI自动化时&#xff0c;会用到的一些常用操作。 定位元素 driver.find_element_by_xpath()#1、绝对路径 2、元素属性 3、层级和属性结合 4、使用逻辑运算符 driver.find_element_by_id()#根据id定位&#xff0c;HTML规定id属性在HTML文档中必须是唯…

Mysql -- 约束

注意:约束是作用于表中字段上的,可以在创建表/修改表的时候添加约束. -- ------------------------------------------------------------------- 约束演示 ---------------------------------------------- create table user(id int primary key auto_increment comment 主键…

CorelDRAW Standard2024适合业余爱好者和家庭企业的图形设计软件

CorelDRAW Standard 2024是一款功能强大的矢量图形设计软件&#xff0c;专为图形爱好者、家庭用户、微型企业和学生们设计。该软件在Windows平台上运行&#xff0c;并提供了智能对象、布局、插图和模板等功能&#xff0c;帮助用户快速创建高质量的设计作品。 CorelDRAW Standa…

seq2seq翻译实战-Pytorch复现

&#x1f368; 本文为[&#x1f517;365天深度学习训练营学习记录博客 &#x1f366; 参考文章&#xff1a;365天深度学习训练营 &#x1f356; 原作者&#xff1a;[K同学啊 | 接辅导、项目定制]\n&#x1f680; 文章来源&#xff1a;[K同学的学习圈子](https://www.yuque.com/…

ssm+vue的农业信息管理系统(有报告)。Javaee项目,ssm vue前后端分离项目。

演示视频&#xff1a; ssmvue的农业信息管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm vue前后端分离项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&…

备考银行科技岗刷题笔记(持续更新版)

银行考试计算机部分复习 IEEE 802.11的帧格式 1.1 IEEE 802.11是什么&#xff1f; 802.11是国际电工电子工程学会&#xff08;IEEE&#xff09;为无线局域网络制定的标准。目前在802.11的基础上开发出了802.11a、802.11b、802.11g、802.11n、802.11ac。并且为了保证802.11更…

npm install没有创建node_modules文件夹

问题记录 live-server 使用时 报错&#xff1a;live-server : 无法将“live-server”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。 npm install 安装 但是 这时npm install没有创建node_modules文件夹&#xff0c;只生成package-lock.json文件 方法一&#xff1a; 手…

NineData与OceanBase完成产品兼容认证,共筑企业级数据库新生态

近日&#xff0c;云原生智能数据管理平台 NineData 和北京奥星贝斯科技有限公司的 OceanBase 数据库完成产品兼容互认证。经过严格的联合测试&#xff0c;双方软件完全相互兼容、功能完善、整体运行稳定且性能表现优异。 此次 NineData 与 OceanBase 完成产品兼容认证&#xf…

软考70-上午题-【面向对象技术2-UML】-UML中的图1

一、图的定义 图是一组元素的图形表示&#xff0c;大多数情况下把图画成顶点、弧的联通图。 顶点&#xff1a;代表事物&#xff1b; 弧&#xff1a;代表关系。 可以从不同的角度画图&#xff0c;UML提供了13种图&#xff1a;&#xff08;只看9种&#xff09; 类图&#xff…

学习c语言:顺序表

一、顺序表的概念和结构 1.1 线性表 线性表&#xff08; linearlist &#xff09;是n个具有相同特性的数据元素的有限序列。线性表是⼀种在实际中⼴泛使⽤的数据结构&#xff0c;常⻅的线性表&#xff1a;顺序表、链表、栈、队列、字符串... 线性表在逻辑上是线性结构&#x…

【网站项目】096实验室开放管理系统

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

15-单片机烧录FreeTOS操作系统后,程序的执行流程

任务创建 1、在系统上电后&#xff0c;第一个执行的是启动文件由汇编语言编写的复位函数 通过复位函数来初始化系统的时钟&#xff0c;然后再执行__main,初始化系统的堆和栈&#xff0c;然后跳转到main函数 2、在main函数中可以直接进行任务创建操作 因为在FreeRTOS中会自动…

c++ primer plus 第十五章笔记 友元,异常和其他

友元类&#xff1a; 两个类不存在继承和包含的关系&#xff0c;但是我想通过一个类的成员函数来修改另一个类的私有成员和保护成员的时候&#xff0c;可以使用友元类。 class A {private:int num;//私有成员//...public: //...friend class B;//声明一个友元类 }class…

SpringBootWeb(接收请求数据,返回响应结果,分层解耦,Spring的IOCDI)【详解】

目录 一、接收请求数据 1. 接收表单参数 1.原始方式【了解】 2.SpringBoot方式 3.参数名不一致RequestParam 2.实体参数 1.简单实体对象 2.复杂实体对象 3.数组集合参数 4.日期参数 3. JSON参数 1.Postman发送JSON数据 2.服务端接收JSON数据 4. 路径参数(rest风格…

httprunner结合pytest的关键字

1. 通用关键字 可参考官方文档&#xff1a; Write Testcase - HttpRunner V3.x Docs 2. 特别关键字 2.1. 步骤step前置 2.1.1. setup_hook 关键源码 def setup_hook(self, hook: Text, assign_var_name: Text None) -> "RunRequest":if assign_var_name:sel…

【Python】新手入门:全局变量和局部变量的概念、区别以及用法

【Python】新手入门&#xff1a;全局变量和局部变量的概念、区别以及用法 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&#x1f448;…