YSOS 实验记录(报告
和学长聊天机缘巧合被拉来玩玩YSOS这个操作系统的实验课,由于我不是正式选这门课的学生,所以也不用写实验报告,于是决定写博文来记录下来我的试验记录~
内容顺序可能不会严格按照实验的顺序,会在部分位置插入我想了解以及去了解了的知识。
0x00
0x00的实验就是使用rust写几个小函数熟悉rust编程,我没有严格按照实验要求来写,做了一些自以为是的优化(
函数编写
*错误处理
pub enum OsErr {
FileNotFound,
Unknown,
PermissionDenied,
ErrorCode(i32),
}
impl OsErr {
pub fn as_str(&self) -> &'static str{
match self{
OsErr::FileNotFound => "File not found!",
OsErr::PermissionDenied => "You don't have enough permission!",
OsErr::Unknown => "Unknown error!",
OsErr::ErrorCode(code) => {
if *code == -99 {"Invalid argument!"}
else {
"Specific error"
}
},
}
}
pub fn errono(&self) -> i32{
match self {
OsErr::FileNotFound => -2,
OsErr::PermissionDenied => -3,
OsErr::Unknown => -1,
OsErr::ErrorCode(code) => *code,
}
}
pub fn is_not_found(&self) -> bool{
*self == OsErr::FileNotFound
}
}
impl fmt::Display for OsErr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f,"Os Error: {}",self.as_str())
}
}
pub type OSResult<T> = Result<T,OsErr>;创建一个函数 count_down(seconds: u64)
该函数接收一个 u64 类型的参数,表示倒计时的秒数。
函数应该每秒输出剩余的秒数,直到倒计时结束,然后输出 Countdown finished!。
fn count_down(seconds: u64) {
for i in (0..=seconds).rev(){
println!("{} seconds remaining...", i);
if i == 0{
println!("Countdown finished!");
}
}
}创建一个函数 read_and_print(file_path: &str)
该函数接收一个字符串参数,表示文件的路径。
函数应该尝试读取并输出文件的内容。如果文件不存在,函数应该使用 expect 方法主动 panic,并输出 File not found!。
尝试使用io::Result<()>作为返回值,并使用?将错误向上传递。
fn read_and_print(file_path: &str) -> OSResult<()> {
use std::fs;
use std::io::ErrorKind;
let file_content = fs::read_to_string(file_path).map_err(|e| {
match e.kind(){
ErrorKind::NotFound => OsErr::FileNotFound,
_ => OsErr::Unknown,
}
})?;
println!("The content of {file_path} is {file_content}");
Ok(())
}创建一个函数 file_size(file_path: &str) -> Result<u64, &str>
该函数接收一个字符串参数,表示文件的路径,并返回一个 Result。
函数应该尝试打开文件,并在 Result 中返回文件大小。如果文件不存在,函数应该返回一个包含 File not found! 字符串的 Err。
尝试将std::io::Result转换为std::Result,你可能需要map_err等函数。
fn file_size(file_path: &str) -> OSResult<u64> {
use std::fs;
use std::io::ErrorKind;
let file_len=fs::metadata(file_path).map(|meta| meta.len() )
.map_err(|e| {
match e.kind(){
ErrorKind::NotFound => OsErr::FileNotFound,
_ => OsErr::Unknown,
}
})?;
Ok(file_len)
}这里我用自定义错误类型替代了题目要求的文件不存在时返回的错误。
实现一个进行字节数转换的函数,并格式化输出:
实现函数
humanized_size(size: u64) -> (f64, &'static str)将字节数转换为人类可读的大小和单位使用 1024 进制,并使用二进制前缀(B, KiB, MiB, GiB)作为单位
补全格式化代码,使得你的实现能够通过如下测试:
#[test] fn test_humanized_size() { let byte_size = 1554056; let (size, unit) = humanized_size(byte_size); assert_eq!("Size : 1.4821 MiB", format!(/* FIXME */)); }
fn byte_to(size:u64) -> (f64,&'static str){
let units=["B","KiB","MiB","GiB","TiB"];
let mut unit_idx=0;
let mut temp_size=size;
while temp_size >= 1024 && unit_idx < units.len() -1 {
temp_size >>= 10;
unit_idx +=1;
}
let size_f=size as f64 / (1_u64 << (unit_idx*10)) as f64;
(size_f,units[unit_idx])
}这里我本来是考虑用给出的字节数/n个1024去达到目的,但是又担心在循环中的性能损耗,在与G老师的一些交流后,想起来了还有位运算这种神奇运算,wow!
$2^{10}=1024$,所以除以1024等于位运算右移10位,这不比做除法快?
在大多数 CPU 架构中,DIV(除法)指令需要几十个时钟周期,而SHR(右移)只需要 1 个时钟周期。
但是这时候又出现了一个问题,位运算只适用于整数,那么小数应该怎么办?
经过查询,发现可以使用 定点数 的方法:先放大、再位移、最后整数取模得到原先的小数。
#### 在 main 函数中,按照如下顺序调用上述函数:
- 首先调用
count_down(5)函数进行倒计时 - 然后调用
read_and_print("/etc/hosts")函数尝试读取并输出文件内容 - 最后使用
std::io获取几个用户输入的路径,并调用file_size函数尝试获取文件大小,并处理可能的错误。
fn main() -> OSResult<()> {
count_down(5);
println!("\n");
read_and_print("/etc/hosts")?;
println!("Pls input some paths (input exit() to exit)");
loop {
println!("The path of the file is :");
io::stdout().flush().map_err(|_| OsErr::Unknown)?;
let mut input_path = String::new();
io::stdin().read_line(&mut input_path)
.map_err(|_| OsErr::Unknown)?;
let path=input_path.trim();
if path=="exit()" {
println!("Exiting……");
break;
}
if path.is_empty(){
continue;
}
match file_size(path) {
Ok(size) => println!("The size of {} is {}",path,size),
Err(e) => println!("Error! {}",e)
}
}
Ok(())
}自行搜索学习如何利用现有的 crate 在终端中输出彩色的文字
为了不引入其他库,所以我们只能使用ANSI 转义序列实现。
3/4位颜色:
初始的规格只有8种颜色,只给了它们的名字。SGR参数30-37选择前景色,40-47选择背景色。
实现为更明亮的颜色而不是不同的字体,从而提供了8种额外的前景色,但通常情况下并不能用于背景色,虽然有时候反显(SGR代码7)可以允许这样。
| 名称 | 前景色代码 | 背景色代码 | VGA[nb 2] | CMD[nb 3] | Terminal.app) | PuTTY | mIRC | xterm | X[nb 4] | Ubuntu[nb 5] |
|---|---|---|---|---|---|---|---|---|---|---|
| 黑 | 30 | 40 | 0,0,0 | 1,1,1 | ||||||
| 红 | 31 | 41 | 170,0,0 | 128,0,0 | 194,54,33 | 187,0,0 | 127,0,0 | 205,0,0 | 255,0,0 | 222,56,43 |
| 绿 | 32 | 42 | 0,170,0 | 0,128,0 | 37,188,36 | 0,187,0 | 0,147,0 | 0,205,0 | 0,255,0 | 57,181,74 |
| 黄 | 33 | 43 | 170,85,0[nb 6] | 128,128,0 | 173,173,39 | 187,187,0 | 252,127,0 | 205,205,0 | 255,255,0 | 255,199,6 |
| 蓝 | 34 | 44 | 0,0,170 | 0,0,128 | 73,46,225 | 0,0,187 | 0,0,127 | 0,0,238[15] | 0,0,255 | 0,111,184 |
| 品红 | 35 | 45 | 170,0,170 | 128,0,128 | 211,56,211 | 187,0,187 | 156,0,156 | 205,0,205 | 255,0,255 | 118,38,113 |
| 青 | 36 | 46 | 0,170,170 | 0,128,128 | 51,187,200 | 0,187,187 | 0,147,147 | 0,205,205 | 0,255,255 | 44,181,233 |
| 白 | 37 | 47 | 170,170,170 | 192,192,192 | 203,204,205 | 187,187,187 | 210,210,210 | 229,229,229 | 255,255,255 | 204,204,204 |
| 亮黑(灰) | 90 | 100 | 85,85,85 | 128,128,128 | 129,131,131 | 85,85,85 | 127,127,127 | 127,127,127 | 128,128,128 | |
| 亮红 | 91 | 101 | 255,85,85 | 255,0,0 | 252,57,31 | 255,85,85 | 255,0,0 | 255,0,0 | 255,0,0 | |
| 亮绿 | 92 | 102 | 85,255,85 | 0,255,0 | 49,231,34 | 85,255,85 | 0,252,0 | 0,255,0 | 144,238,144 | 0,255,0 |
| 亮黄 | 93 | 103 | 255,255,85 | 255,255,0 | 234,236,35 | 255,255,85 | 255,255,0 | 255,255,0 | 255,255,224 | 255,255,0 |
| 亮蓝 | 94 | 104 | 85,85,255 | 0,0,255 | 88,51,255 | 85,85,255 | 0,0,252 | 92,92,255[16] | 173,216,230 | 0,0,255 |
| 亮品红 | 95 | 105 | 255,85,255 | 255,0,255 | 249,53,248 | 255,85,255 | 255,0,255 | 255,0,255 | 255,0,255 | |
| 亮青 | 96 | 106 | 85,255,255 | 0,255,255 | 20,240,240 | 85,255,255 | 0,255,255 | 0,255,255 | 224,255,255 | 0,255,255 |
| 亮白 | 97 | 107 | 255,255,255 | 255,255,255 | 233,235,235 | 255,255,255 | 255,255,255 | 255,255,255 | 255,255,255 |
使用示例:
const COLOR_RED: &str = "\x1b[31m";
const COLOR_GREEN: &str = "\x1b[32m";
const COLOR_YELLOW: &str = "\x1b[33m";
const COLOR_BLUE: &str = "\x1b[34m";
const COLOR_RESET: &str = "\x1b[0m";
println!("{}This is RED text{}", COLOR_RED, COLOR_RESET);实现一个元组结构体 UniqueId(u16)
使得每次调用 UniqueId::new() 时总会得到一个新的不重复的 UniqueId。 - 你可以在函数体中定义 static 变量来存储一些全局状态 - 你可以尝试使用 std::sync::atomic::AtomicU16 来确保多线程下的正确性(无需进行验证,相关原理将在 Lab 5 介绍,此处不做要求) - 使得你的实现能够通过如下测试:
#[test]
fn test_unique_id() {
let id1 = UniqueId::new();
let id2 = UniqueId::new();
assert_ne!(id1, id2);
}struct UniqueID(u16);
impl UniqueID {
pub fn new() -> Self{
static COUNTER:AtomicU16 =AtomicU16::new(0);
let id = COUNTER.fetch_add(1, Ordering::SeqCst);
UniqueID(id)
}
}
YSOS 启动
crates/boot/src/main.rs
#![no_std]
#![no_main]
#[macro_use]
extern crate log;
extern crate alloc;
use core::arch::asm;
use uefi::{Status,entry};
#[entry]
fn efi_main() -> Status{
uefi::helpers::init().expect("Failed to initialize utilities");
log::set_max_level(log::LevelFilter::Info);
let std_num = 25302031;
loop{
info!("Hello World! \n from UEFI bootloader @ {}",std_num);
for _ in 0..0x10000000{
unsafe{
asm!("nop");
}
}
}
}补充阅读
BIOS——UEFI与Legacy
什么是BIOS?
BIOS(英文:Basic Input/Output System),即基本输入输出系统,是在通电启动阶段执行硬件初始化,以及为操作系统提供运行时服务的固件。
现在,其作用是初始化和测试硬件组件,以及从存储设备中加载启动程序(GRUB等?),在由启动程序加载操作系统;当加载操作系统后,BIOS通过系统管理模式为操作系统提供硬件抽象。
系统管理模式(System Management mode)(以下简称SMM)是Intel在80386SL(一款x86系列CPU)之后引入x86体系结构的一种CPU的执行模式。
系统管理模式只能通过系统管理中断(System Management Interrupt, SMI)进入,并只能通过执行RSM指令退出。SMM模式对操作系统透明,换句话说,操作系统根本不知道系统何时进入SMM模式,也无法感知SMM模式曾经执行过。为了实现SMM,Intel在其CPU上新增了一个引脚SMI# Pin,当这个引脚上为高电平的时候,CPU会进入该模式。在SMM模式下一切被都屏蔽,包括所有的中断。SMM模式下的执行的程序被称作SMM处理程序,所有的SMM处理程序只能在称作系统管理内存(System Management RAM,SMRAM)的空间内运行。可以通过设置SMBASE的寄存器来设置SMRAM的空间。SMM处理程序只能由系统固件(如BIOS或UEFI)实现。(From Wikipedia)
SMM的作用
During platform initialization the PC's firmware (BIOS, UEFI) has complete control of the system and can perform whatever configuration operations are required to prepare the system for an OS to take over. Once an OS is running it expects that it has complete control of the system. If the firmware developer would like the firmware to continue to have some control over the system, SMM is used.
SMM is intended to be completely transparent to the OS. When the system enters SMM, the firmware preserves the state of the CPU in a region of RAM designated as "SMRAM". During SMM the firmware performs low-level management operations like changing fan speeds, checking thermal zones, adjusting the CPU speed, etc. Before leaving SMM, the firmware restores the state of the CPU from SMRAM. From the perspective of the OS, those low-level management operations are happening atomically and automatically in the background.
(From OsDev)
SMM is designed so that OS developers do not need to be aware of it, other than understanding its relationship with ACPI.(SMM 的设计使操作系统开发者无需了解它,只需理解它与 ACPI 的关系即可。)
由于在 SMM 中执行的代码在正常情况下无法作系统检测到,SMM 成为高级恶意软件的有吸引力的载体。SMM 的 CPU 实现和 BIOS 固件实现可能被检测、滥用或干扰,是重大的安全问题。待办事项——包含一些隐形事物的链接和一些 SMM 黑客论文。
: CTF可能会出这方面的题?
硬件抽象层(英语:Hardware Abstraction Layer,缩写HAL)是软件层的例行程序包,用于模拟特定系统平台的细节使程序可以直接访问硬件的资源。将硬件方面的不同抽离操作系统的核心,核心模式的代码就不必因为硬件的不同而需要修改。因此硬件抽象层可加大软件的移植性。
之所以有硬件抽象(Hardware abstraction)这个概念,是由于数字电脑具体的硬件操作相当繁杂,因此将具体的硬件操作抽象化简,避免由于直接以具体的机器代码撰写程序,而在将程序移植到不同硬件时,需要重写整个程序。其概念与目的,类似于数据结构中的抽象数据类型(Abstract data type),皆为保护程序免受变化的冲击。
前述的现象可借由语言现象获得一些启示,当人在记忆时,会抽象地记忆,而非具体地将具体的消息记下,在记忆时,并不会记忆文章具体的长相,而是抽象的内容。如果不是如此,当需要以另一种语言重现该篇文章时,仍然需要将其抽象化,再将其转译为另一语言的写法。在记忆谈话时,也类似于此。(From Wikipedia)
我很想在这里继续深入了解BIOS的相关知识,但是难度和深度好像有一些超乎我的预期了,所以喜提一只鸽子。
什么是UEFI
统一可扩展固件接口(英语:Unified Extensible Firmware Interface,缩写UEFI)是一种个人电脑系统规格,用来定义操作系统与系统固件之间的软件界面,作为BIOS的替代方案[2]。可扩展固件接口负责加电自检(POST)、联系操作系统以及提供连接操作系统与硬件的界面。
UEFI的前身是Intel在1998年开始开发的Intel Boot Initiative,后来被重命名为可扩展固件接口(Extensible Firmware Interface,缩写EFI)。
:怎么什么都是Intel推进的,但是Intel最后好像什么都没吃上(
UEFI和BIOS的区别:
二者显著的区别就是UEFI是用模块化,C语言风格的参数堆栈传递方式,动态链接的形式构建的系统,较BIOS而言更易于实现,容错和纠错特性更强,缩短了系统研发的时间。它可以执行于x86-64、IA32、ARM等架构上(在个人电脑上通常是x86-64平台),突破传统16位代码的寻址能力,达到处理器的最大寻址。它利用加载UEFI驱动程序的形式,识别及操作硬件,不同于BIOS利用挂载实模式中断的方式增加硬件功能。后者必须将一段类似于驱动程序的16位代码(如RAID卡的Option ROM)放置在固定的0x000C0000至0x000DFFFF之间存储区中,运行这段代码的初始化部分,它将挂载实模式下约定的中断向量向其他程序提供服务。
UEFI将引导数据存储在 .efi 文件中,而不是固件中。你经常会在新款的主板中找到 UEFI 启动模式。UEFI 启动模式包含一个特殊的 EFI 分区,用于存储 .efi 文件并用于引导过程和引导加载程序。
与操作系统的关系:
只是硬件和预启动软件间的接口规范
UEFI环境下不提供中断的机制,也就是说每个UEFI驱动程序必须用轮询(polling)的方式来检查硬件状态,并且需要以解释的方式运行,较操作系统下的机械码驱动效率更低;
UEFI系统不提供复杂的缓存器保护功能,它只具备简单的缓存器管理机制,具体来说就是指运行在x64或x86处理器的长模式或保护模式下,以最大寻址能力为限把缓存器分为一个平坦的段(Segment),所有的程序都有权限访问任何一段位置,并不提供真实的保护服务。
当UEFI所有组件加载完毕时,便会启动操作系统的启动程序,如果UEFI固件内置UEFI Shell,也可以启动UEFI Shell命令提示。UEFI应用程序(UEFI Application)和UEFI驱动程序(UEFI driver)是PE格式的.efi文件,可用C语言编写。
组成:
- Pre-EFI初始化模块(PEI)
- UEFI驱动程序执行环境(DXE)
- UEFI驱动程序(UEFI driver)
- 兼容性支持模块(CSM)
- UEFI高层应用(UEFI Application)
- GUID磁盘分区表
- 系统管理模式(SMM)
:一样,后面在回来学UEFI部分的内容吧(
Makefile 文件
Makefile由若干条规则(Rule)构成,每一条规则指出一个目标文件(Target),若干依赖文件(prerequisites),以及生成目标文件的命令。
#规则 目标文件: 依赖文件1 依赖文件2
m: a b
cat a b >m
#要生产m,依赖a和b ,使用cat命令合并了a与b,并写入到m在Makefile的规则中,命令必须以Tab开头,不能是空格。
make执行时,默认执行第一条规则,所以,我们把规则x.txt放到前面。
x: m c
cat m c > x
m: a b
cat a b > mmake默认执行第一条规则,也就是创建x,但是由于x依赖的文件m不存在(另一个依赖c已存在),故需要先执行规则m创建出m文件,再执行规则x。执行完成后,当前目录下生成了两个文件m和x。
把默认执行的规则放第一条,其他规则的顺序是无关紧要的,因为
make执行时自动判断依赖。make使用文件的创建和修改时间来判断是否应该更新一个目标文件。
m 和 x 都是make自动生成的文件,可以安全的删除
clean:
rm -f m
rm -f xclean没有依赖文件,所以必须用make clean运行。
但是如果我们有一个叫做
clean的文件,那么clean规则就不会执行了如果我们希望
make把clean不要视为文件,可以添加一个标识:.PHONY: clean
.PHONY: clean
clean:
rm -f m
rm -f x
一个规则可以有多条命令,如:
cd:
pwd
cd ..
pwd输出:
$ make cd
pwd
/home/ubuntu/makefile-tutorial/v1
cd ..
pwd
/home/ubuntu/makefile-tutorial/v1发现cd ..命令执行后,并未改变当前目录,两次输出的pwd是一样的,这是因为make针对每条命令,都会创建一个独立的Shell环境,类似cd ..这样的命令,并不会影响当前目录。
解决办法是把多条命令以;分隔,写到一行:
cd_ok:
pwd; cd ..; pwd;再执行cd_ok目标就得到了预期结果:
$ make cd_ok
pwd; cd ..; pwd
/home/ubuntu/makefile-tutorial/v1
/home/ubuntu/makefile-tutorial可以使用\把一行语句拆成多行,便于浏览:
cd_ok:
pwd; \
cd ..; \
pwd另一种执行多条命令的语法是用&&,它的好处是当某条命令失败时,后续命令不会继续执行:
cd_ok:
cd .. && pwd如果我们不想打印某一条命令,可以在命令前加上@,表示不打印命令
no_output:
@echo 'not display'
echo 'will display'有些时候,我们想忽略错误,继续执行后续命令,可以在需要忽略错误的命令前加上-:
ignore_error:
-rm zzz.txt #对于执行可能出错,但不影响逻辑的命令,可以用-忽略。
echo 'ok'变量定义用变量名 = 值或者变量名 := 值,通常变量名全大写,?=若变量未定义,則替它指定新的值,否則用原有的值。
+=
CFLAGS = -Wall -g
CFLAGS += -O2
# 此時 CFLAGS 的值就變成 -Wall -g -O2引用变量用$(变量名)
自动变量:$@表示目标文件,$^表示所有依赖文件,$(wildcard *.c)为通配符,$<表示规则中第一个依赖文件。
因此,我们可以这么写:
world.out: hello.o main.o
cc -o $@ $^在没有歧义时可以写$@,也可以写$(@),有歧义时必须用括号,例如$(@D)。
# 模式匹配规则:当make需要目标 xyz.o 时,自动生成一条 xyz.o: xyz.c 规则:
%.o: %.c
@echo 'compiling $<...'
cc -c -o $@ $<解读实验仓库中的makefile
# 变量命名
OVMF := assets/OVMF.fd
ESP := esp
BUILD_ARGS :=
QEMU_ARGS := -m 64M
QEMU_OUTPUT := -nographic
MODE ?= release
CUR_PATH := $(shell pwd)
DBG_INFO := false
ifeq (${MODE}, release)
BUILD_ARGS := --release
endif
# 伪目标声明
.PHONY: build run debug clean launch intdbg \
target/x86_64-unknown-uefi/$(MODE)/ysos_boot.efi
run: build launch
# 启动qemu
launch:
@qemu-system-x86_64 \
-bios ${OVMF} \
-net none \
$(QEMU_ARGS) \
$(QEMU_OUTPUT) \
-drive format=raw,file=fat:${ESP} \
-snapshot
# 中断调试模式,用于排查故障、三重故障
intdbg:
@qemu-system-x86_64 \
-bios ${OVMF} \
-net none \
$(QEMU_ARGS) \
$(QEMU_OUTPUT) \
-drive format=raw,file=fat:${ESP} \
-snapshot \
-no-reboot -d int,cpu_reset
# GDB调试
debug:
@qemu-system-x86_64 \
-bios ${OVMF} \
-net none \
$(QEMU_ARGS) \
$(QEMU_OUTPUT) \
-drive format=raw,file=fat:${ESP} \
-snapshot \
-s -S
clean:
@cargo clean
build: $(ESP)
$(ESP): $(ESP)/EFI/BOOT/BOOTX64.EFI
$(ESP)/EFI/BOOT/BOOTX64.EFI: target/x86_64-unknown-uefi/$(MODE)/ysos_boot.efi
@mkdir -p $(@D) # 提取目标文件所在的目录部分 $@指目标文件,D指目录
cp $< $@
target/x86_64-unknown-uefi/$(MODE)/ysos_boot.efi: crates/boot
cd crates/boot && cargo build $(BUILD_ARGS)
0x01
实验任务
在
crates/kernel目录下运行cargo build --release,之后找到编译产物,并使用readelf命令查看其基本信息,回答以下问题:
- 请查看编译产物的架构相关信息,与配置文件中的描述是否一致?
- 找出内核的入口点,它是被如何控制的?结合源码、链接、加载的过程,谈谈你的理解。
- 请找出编译产物的 segments 的数量,并且用表格的形式说明每一个 segments 的权限、是否对齐等信息。
直接运行 cargo build --release发现cargo不能成功运行,报错
error: `.json` target specs require -Zjson-target-spec原因是项目使用了自定义JSON target,而构建命令默认不放行json target,需要手动加上-Zjson-target-spec,最后使用cargo build -Zjson-target-spec --release构建。
$ readelf ysos_kernel -h
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0xffffff00000010d0
Start of program headers: 64 (bytes into file)
Start of section headers: 34960 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 7
Size of section headers: 64 (bytes)
Number of section headers: 12
Section header string table index: 10$ file ysos_kernel
ysos_kernel: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped编译产物架构为X86-64,与配置文件中"arch": "x86_64"描述相符。
从main.rs开始分析,从调用crate/boot/src/lib.rs的boot::entry_point宏开始
#[macro_export]
macro_rules! entry_point {
($path:path) => {
#[unsafe(export_name = "_start")]
pub extern "C" fn __impl_start(boot_info: &'static $crate::BootInfo) -> ! {
// validate the signature of the program entry point
let f: fn(&'static $crate::BootInfo) -> ! = $path;
f(boot_info)
}
};
}
把kernel_main导出为_start(kernel.ld中指出的入口ENTRY(_start)),然后从此开始。
接着对boot_info调用引入的ysos_kernel的init方法,随后循环输出消息Hello World from YatSenOS v2!,然后在0到0x10000000内一直保持CPU空转。
segment指ELF文件中给加载器看的装在单元,是操作系统真正会按他映射到内存的区域。
对 bootloader 来说,关键的是 LOAD segment,因为它们决定“从文件哪个偏移读数据,放到哪个虚拟地址,用什么权限映射,内存里占多大”。$ readelf ysos_kernel -lW
Elf file type is EXEC (Executable file)
Entry point 0xffffff00000010d0
There are 7 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x001000 0xffffff0000000000 0xffffff0000000000 0x0005a4 0x0005a4 R 0x1000
LOAD 0x002000 0xffffff0000001000 0xffffff0000001000 0x001ee5 0x001ee5 R E 0x1000
LOAD 0x004000 0xffffff0000003000 0xffffff0000003000 0x001020 0x001020 RW 0x1000
LOAD 0x006000 0xffffff0000005000 0xffffff0000005000 0x000000 0x000020 RW 0x1000
GNU_RELRO 0x005000 0xffffff0000004000 0xffffff0000004000 0x000020 0x000020 R 0x1
GNU_EH_FRAME 0x00157c 0xffffff000000057c 0xffffff000000057c 0x00000c 0x00000c R 0x4
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0
Section to Segment mapping:
Segment Sections...
00 .rodata .eh_frame_hdr .eh_frame
01 .text
02 .data .got
03 .bss
04 .got
05 .eh_frame_hdr
06| # | Type | VirtAddr | Offset | FileSiz | Flg | Align | 对齐 | 内容 |
|---|---|---|---|---|---|---|---|---|
| 0 | LOAD | 0xffffff0000000000 | 0x001000 | 0x0005a4 | R | 0x1000 | 是 | .rodata .eh_frame_hdr .eh_frame |
| 1 | LOAD | 0xffffff0000001000 | 0x002000 | 0x001ee5 | R E | 0x1000 | 是 | .text |
| 2 | LOAD | 0xffffff0000003000 | 0x004000 | 0x001020 | RW | 0x1000 | 是 | .data .got |
| 3 | LOAD | 0xffffff0000005000 | 0x006000 | 0x000000 | RW | 0x1000 | 是 | .bss |
| 4 | GNU_RELRO | 0xffffff0000004000 | 0x005000 | 0x000020 | R | 0x1 | 地址本身页对齐 | .got |
| 5 | GNU_EH_FRAME | 0xffffff000000057c | 0x00157c | 0x00000c | R | 0x4 | 否 | .eh_frame_hdr |
| 6 | GNU_STACK | 0xffffff0000000000 | 0x000000 | 0x000000 | RW | 0 | / | 栈权限声明,不映射文件内容 |
补全代码
// FIXME: root page table is readonly, disable write protect (Cr0) 处,我一先没看到文档中给出的示例写法,询问AI后给出了
let cr0_flags_bk= Cr0::read();
unsafe{
Cr0::write(cr0_flags_bk & !Cr0Flags::WRITE_PROTECT);
} 的写法,使用了位与运算,可读性相对较低。
于是后续改回了文档示例的写法
let cr0_flags_bk= Cr0::read();
unsafe{
Cr0::update(|f| f.remove(Cr0Flags::WRITE_PROTECT));
} 后续的FIXME可以根据lib.rs中的信息修补
// FIXME: map physical memory to specific virtual address offset
elf::map_physical_memory(config.physical_memory_offset, max_phys_addr, &mut page_table, &mut allocator);
// FIXME: load and map the kernel elf file
elf::load_elf(&elf, config.physical_memory_offset, &mut page_table, &mut allocator)
.expect("Failed to load kernel");
// FIXME: map kernel stack
elf::map_range(config.kernel_stack_address, config.kernel_stack_size, &mut page_table, &mut allocator)
.expect("Failed to map kernel");
// FIXME: recover write protect (Cr0)
unsafe {
Cr0::update(|f| f.insert(Cr0Flags::WRITE_PROTECT));
}lib.rs中load_segment的代码修补
分别对可写和 不可执行权限进行映射
为什么是不可执行,而不是可执行?
早期x86中没有单独的可执行权限位,CPU默认可读即可执行。后续为了防止缓冲溢出攻击才引入了
NX也即不可执行位。再再再记一遍位运算,每每每次都会忘记
位或|(OR) ,有1则1,全0才0。 例:0001 | 0010 = 0011 用于增加某个权限而不破坏原有的权限。位与&(AND),全1才1,有0则0。 例:0011 & 0010 = 0010 用于检查某个变量是否包含某个权限。若上例中0011为存在+可写,用其与0010做位与运算得到的1处便是有的权限;0处便是没有的权限。按位取反~(Rust整数中用!) 0 1互换。 例:~(0010) = 1101 常和位与合用,撤销某个操作。如:0011为存在+可写,要清除可写0010,使用0011 & ~(0010)=0011 1101 = 0001,便只剩下存在权限,去除了可写权限。
移位(包括:左移<<,右移>>把二进制位整体向左或向右移动指定的位数,在数学上等价于乘(除)$2^n$。 例:a= 1<< 0; (a=0001);b=1<<1 (b=0010)对于4KB的页 c = 0x12345678; d = c >> 12 则d=0x12345
// FIXME: handle page table flags with segment flags
let seg_flags=segment.flags();
if seg_flags.is_write(){
page_table_flags |= PageTableFlags::WRITABLE;
}
if !seg_flags.is_execute(){
page_table_flags |= PageTableFlags::NO_EXECUTE;
}调试内核
先按教程装
gef!!!然后写
.gdbinit文件
file esp/KERNEL.ELF
gef-remote localhost 1234
tmux-setup
b ysos_kernel::init- 使用
make build DBG_INFO=true或python ysos.py build -p debug编译内核,确保编译时开启了调试信息。 - 使用
make debug启动 QEMU 并进入调试模式 - 在另一个终端中,使用
gdb -q命令进入 GDB 调试环境。 - 使用
c命令继续执行,你将会看到 QEMU 窗口中的输出,同时 GDB 将会在断点处停下。


分别用x/10i $pc 和info symbol $pc查看断点处的汇编和符号。
其中x代表examine(检查),i是instruction(指令) $pc是当前的程序计数器/指令指针寄存器RIP。

ysos_kernel::init + 8 in section .text用vmmap和readelf检查加载情况
前者在gdb下运行,输出

后者直接在终端下运行
lbyxiaolizi@XiaoliLaptop:~/YSOS/lab$ readelf -l esp/KERNEL.ELF
Elf file type is EXEC (Executable file)
Entry point 0xffffff00000010d0
There are 7 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000001000 0xffffff0000000000 0xffffff0000000000
0x00000000000005a4 0x00000000000005a4 R 0x1000
LOAD 0x0000000000002000 0xffffff0000001000 0xffffff0000001000
0x0000000000001ee5 0x0000000000001ee5 R E 0x1000
LOAD 0x0000000000004000 0xffffff0000003000 0xffffff0000003000
0x0000000000001020 0x0000000000001020 RW 0x1000
LOAD 0x0000000000006000 0xffffff0000005000 0xffffff0000005000
0x0000000000000000 0x0000000000000020 RW 0x1000
GNU_RELRO 0x0000000000005000 0xffffff0000004000 0xffffff0000004000
0x0000000000000020 0x0000000000000020 R 0x1
GNU_EH_FRAME 0x000000000000157c 0xffffff000000057c 0xffffff000000057c
0x000000000000000c 0x000000000000000c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x0
Section to Segment mapping:
Segment Sections...
00 .rodata .eh_frame_hdr .eh_frame
01 .text
02 .data .got
03 .bss
04 .got
05 .eh_frame_hdr
06问答:
set_entry函数做了什么?为什么它的实现是 unsafe 的(下附实现)#[cfg(feature = "boot")] static mut ENTRY: usize = 0; #[inline(always)] #[cfg(feature = "boot")] pub fn set_entry(entry: usize) { unsafe { ENTRY = entry; } }答:
set_entry函数从解析的ELF文件头中提取内核入口地址。他的实现将
entry传给了static mut ENTRY,对全局可变静态变量的读取和修改都是unsafe的,为了防止数据竞争
jump_to_entry函数做了什么?要传递给内核的参数位于哪里?查询call指令的行为和 x86_64 架构的调用约定,借助调试器进行说明。#[cfg(feature = "boot")] pub fn jump_to_entry(bootinfo: *const BootInfo, stacktop: u64) -> ! { unsafe { assert!(ENTRY != 0, "ENTRY is not set"); core::arch::asm!("mov rsp, {}; call {}", in(reg) stacktop, in(reg) ENTRY, in("rdi") bootinfo); } unreachable!() }答:
jump_to_entry函数通过汇编语言把CPU的栈指针寄存器修改为stacktop,跳转到前面set_entry保存的内核入口。
in("rdi") bootinfo把bootinfo的指针传进rdi寄存器里in(reg) stacktop把stacktop挑一个随便的空闲通用寄存器扔进去in(reg) ENTRY同样挑一个寄存器把ENTRY的地址扔进去- 最后执行汇编
mov rsp, {}; call {}要传递给内核的参数
entry_point!宏做了什么?内核为什么需要使用它声明自己的入口点?答:
entry_point!真是不好找,在本实验中,他出现在./crates/kernel/src/main.rs:11:boot::entry_point!(kernel_main);,查询其相关资料,可以在Writing an OS in rust中找到其是bootloadercrates提供的宏#[macro_export] macro_rules! entry_point { ($path:path) => { #[export_name = "_start"] pub extern "C" fn __impl_start(boot_info: &'static $crate::bootinfo::BootInfo) -> ! { // 1. 验证用户提供的入口函数的签名是否严格匹配 let f: fn(&'static $crate::bootinfo::BootInfo) -> ! = $path; // 2. 调用用户的内核入口函数 f(boot_info) } }; }他首先定义了导出名称为
_start,告诉BL入口,然后用C中的调用约定,函数的第一个参数必须从rdi中获取,在检测内核入口函数的类型是否正确。
- 如何为内核提供直接访问物理内存的能力?你知道几种方式?代码中所采用的是哪一种?
答:随着内存大小的增长,诞生出了虚拟内存技术,他把物理内存的地址抽象成了一段独立的内存区域,在访问真实的物理内存之前需要先进行内存转换。其中转换前的叫做
虚拟地址,转换后的叫做物理地址。(显而易见的,我们可以因此对一段物理内存通过虚拟内存实现内存复用,并且由此会产生更多的安全问题。由此诞生了内存分段->内存分片 和 内存分页->页表->多级页表 技术。
1. 恒等映射:虚拟地址=物理地址 1. 线性偏移映射:虚拟地址=物理地址+固定偏移量(Offset),偏移量通常需要打到把所有物理内存搬到虚拟地址的高半区 1. 动态映射:内核按需临时映射特地的物理页,用完即丢。代码中用了第二种线性偏移映射,用固定的Offset进行偏移。
详见:https://os.phil-opp.com/zh-CN/paging-introduction/
- 为什么 ELF 文件中不描述栈的相关内容?栈是如何被初始化的?它可以被任意放置吗?
答:ELF文件描述的是静态的程序布局,而栈是动态的运行时资源,其大小和位置不需要被硬编码进ELF中。
栈的初始化和放置:
- BL从配置文件中读到内核地址和大小
elf::load_elf(&elf, config.physical_memory_offset, &mut page_table, &mut allocator) .expect("Failed to load kernel");
- 随后申请物理页并配置页表,把内存映射为RW
elf::map_range(config.kernel_stack_address, config.kernel_stack_size, &mut page_table, &mut allocator) .expect("Failed to map kernel");
- 最后,计算栈顶
stacktop,通过跳转汇编修改CPU的rsp寄存器能否随意放置? 理论上可以放在随便的空闲地方,但是为了安全,现代OS会把其放在内核的高半区,并且可以在栈底设置一个未映射的Guard Page防止栈溢出损坏其他数据。
- 请解释指令
layout asm的功能。倘若想找到当前运行内核所对应的 Rust 源码,应该使用什么 GDB 指令?答:
layout asm是GDB的TUI命令,将终端窗口的上半部分显示当前pc(程序计数器)附近的机器指令汇编代码。查找Rust源码应该使用
layout src,可以直接使用list在命令行中打印当前执行点附近的源码。
UART 与日志输出
实验中自旋锁与互斥锁的内容在rs圣经中有详细讲解
UART 16550有8个寄存器,它们共享一组端口地址,通过偏移量区分,其中一些寄存器在DLAB(线路控制寄存器的第7位)开启或关闭时有不同含义。
- Data Register/Division Latch Low(+0)
DLAB=0时为数据口;DLAB=1时为波特率低位。
- Interrupt Enable Register/Divisor Latch High(+1) IER
控制哪些事件能打断CPU的工作
| 位 (Bit) | 名称 | 描述 |
|---|---|---|
| Bit 0 | ERBFI | 接收数据中断。设为 1 时,每收到一个字节都会触发中断。 |
| Bit 1 | ETBEI | 发送持有寄存器空中断。设为 1 时,只要能发数据了就触发中断。 |
| Bit 2 | ELSI | 接收状态中断。当发生错误(如奇偶校验错)时触发。 |
| Bit 3 | EDSSI | Modem 状态中断。线路状态变化(如断开)时触发。 |
| Bit 0-7 | DLM | 仅当 DLAB=1 时: 存储波特率除数的高 8 位。 |
- FIFO Control Register (+2) FCR(只写)
控制硬件内部的16字节缓冲区,优化高频数据传输。
| 位 (Bit) | 名称 | 描述 |
|---|---|---|
| Bit 0 | Enable | 是否启用 FIFO。不启用时,一次只能处理 1 字节。 |
| Bit 1 | Clear R | 设为 1 会清空接收 FIFO。 |
| Bit 2 | Clear T | 设为 1 会清空发送 FIFO。 |
| Bit 6-7 | Trigger | 触发阈值。00: 1字节, 01: 4字节, 10: 8字节, 11: 14字节。 |
- Line Control Register (+3) LCR
串口通信的协议说明书,两端必须配置一致才能正常通信
| 位 (Bit) | 名称 | 描述 |
|---|---|---|
| Bit 0-1 | Word Len | 数据长度。00: 5位, 01: 6位, 10: 7位, 11: 8位(常用)。 |
| Bit 2 | Stop Bit | 停止位。0: 1位, 1: 1.5位或2位。 |
| Bit 3-5 | Parity | 奇偶校验。控制是否校验以及如何校验(奇、偶、固定位等)。 |
| Bit 6 | Break | 开启后,发送端会发送一个长间隔信号。 |
| Bit 7 | DLAB | 核心开关。1:访问波特率寄存器;0:访问数据/中断寄存器。 |
- Modem Control Register (+4) MCR
控制与外部设备的握手信号和芯片模式
| 位 (Bit) | 名称 | 描述 |
|---|---|---|
| Bit 0 | DTR | Data Terminal Ready。设为 1 时,通知对方设备:本设备已准备好。 |
| Bit 1 | RTS | Request To Send。设为 1 时,请求发送数据。 |
| Bit 2 | OUT1 | 辅助输出 1。在普通 PC 上通常不接,但在某些机器上用于控制硬件。 |
| Bit 3 | OUT2 | 辅助输出 2。非常关键,在 PC 架构中,这一位必须设为 1 才能允许串口触发硬件中断(IRQ)。 |
| Bit 4 | Loop | 自检模式(环回模式)。设为 1 时,串口发送的数据会直接被自己接收,用于测试。 |
- Line Status Register (+5) LSR
内核代码中send,receive函数最常查询的寄存器,用来判断缓冲区数据状态。
| 位 (Bit) | 名称 | 描述 |
|---|---|---|
| Bit 0 | DR | Data Ready。为 1 表示接收缓冲区有数据,可以读取。 |
| Bit 1 | OE | Overrun Error。没及时读取,新数据把旧数据顶掉了。 |
| Bit 5 | THRE | Transmitter Empty。发送缓冲区空,可以写下一个字符了。 |
| Bit 7 | Err | FIFO 数据错误标识。 |
- Modem Status Register (+6) MSR
显示当前的线路的物理状态,在Modem控制或流控中使用。
| 位 (Bit) | 名称 | 描述 |
|---|---|---|
| Bit 0 | DCTS | 清除发送(CTS)状态改变。 |
| Bit 4 | CTS | 当前 CTS 信号线的电平状态。 |
| Bit 5 | DSR | 数据设备准备就绪信号。 |
- Scratch Register (+7)
硬件完全不使用的临时存储位
| 位 (Bit) | 名称 | 描述 |
|---|---|---|
| Bit 0-7 | Scratch | 你可以写任何东西进去,再读出来。如果读写一致,说明该端口确实存在 UART 芯片。 |
在阅读完了上述信息后,就可以着手补全代码了(虽然我顺序好像是反着来的
使用**更好的写法**,常量泛型,对照实验任务文档中的c语言代码来实现
use core::fmt;
use x86_64::instructions::port::Port;
/// A port-mapped UART 16550 serial interface.
pub struct SerialPort<const BASE_ADDR: u16>{
}
impl<const BASE_ADDR: u16> SerialPort<BASE_ADDR> {
pub const fn new() -> Self {
Self { }
}
/// Initializes the serial port.
pub fn init(&self) -> Result<(), &'static str>{
// FIXME: Initialize the serial port
let mut data=Port::<u8>::new(BASE_ADDR);
let mut ier= Port::<u8>::new(BASE_ADDR+1);
let mut lcr=Port::<u8>::new(BASE_ADDR+3);
let mut fcr= Port::<u8>::new(BASE_ADDR+2);
let mut mcr=Port::<u8>::new(BASE_ADDR+4);
unsafe{
ier.write(0x00); // +1 禁用所有中断
lcr.write(0x80); // +3 启用DLAB
data.write(0x83); // +0 波特率除数低位
ier.write(0x00); // +1 波特率除数高位
lcr.write(0x03); //+3 禁用DLAB,设置8位数据,无校验,1位停止位
fcr.write(0xC7); // +2 启用FIFO,清空,设置14字节触发阈值
mcr.write(0x0B); //+4 0000 1011 设置RTS/DSR,启用辅助输出OUT2(用于触发中断)
mcr.write(0x1E); //+4 0001 1110 自检,设置为环回模式
data.write(0xAE); //+0 发送测试字节0xAE
if data.read()!=0xAE{
return Err("Serial hardware faulty or not found.");
}
mcr.write(0x0F); //禁用环回,启用所有控制位
}
Ok(())
}
/// Sends a byte on the serial port.
pub fn send(&mut self, data: u8) {
// FIXME: Send a byte on the serial port
let mut lsr=Port::<u8>::new(BASE_ADDR+5);
unsafe {
while (lsr.read() & 0x20)==0 {}
Port::<u8>::new(BASE_ADDR).write(data);
}
}
/// Receives a byte on the serial port no wait.
pub fn receive(&mut self) -> Option<u8> {
// FIXME: Receive a byte on the serial port no wait
let mut lsr=Port::<u8>::new(BASE_ADDR+5);
unsafe {
if (lsr.read()&1)==0 {
None
}else {
Some(Port::<u8>::new(BASE_ADDR).read())
}
}
}
}
impl<const BASE_ADDR: u16> fmt::Write for SerialPort<BASE_ADDR> {
fn write_str(&mut self, s: &str) -> fmt::Result {
for byte in s.bytes() {
self.send(byte);
}
Ok(())
}
}
Better logger
use log::{Level, Metadata, Record};
pub fn init() {
static LOGGER: Logger = Logger;
log::set_logger(&LOGGER)
.map(|()|{
log::set_max_level(log::LevelFilter::Info);
})
.expect("Faild to initialize logger");
// FIXME: Configure the logger
info!("Logger Initialized.");
}
struct Logger;
impl log::Log for Logger {
fn enabled(&self, _metadata: &Metadata) -> bool {
_metadata.level() <= log::max_level()
}
fn log(&self, record: &Record) {
// FIXME: Implement the logger with serial output
if self.enabled(record.metadata()){
let level = record.level();
let target = record.target();
let file = record.file_static().unwrap_or("unknown file");
let line = record.line().unwrap_or(0);
let args = record.args();
let color_code= match level {
Level::Error => "\x1b[31m", //red
Level::Warn => "\x1b[33m", //yellow
Level::Info => "\x1b[32m", //green
Level::Debug => "\x1b[36m", //blue
Level::Trace => "\x1b[90m", //grey
};
let reset_code = "\x1b[0m";
println!(
"{}[{:5}]{} [{} @ {}:{}]: {}",
color_code,
level,
reset_code, // 及时重置颜色,只给级别上色
target,
file,
line,
args
);
}
}
fn flush(&self) {}
}
思考题
- 在根目录的
Cargo.toml中,指定了依赖中boot包为default-features = false,而它会被内核引用,禁用默认 feature 是为了避免什么问题?请结合crates/boot的Cargo.toml谈谈你的理解。
答:禁用默认feature是为了防止把BL专用的环境配置污染给内核,导致命名空间冲突(好像不应该用这个词
- 在
crates/boot/src/main.rs中参考相关代码,聊聊max_phys_addr是如何计算的,为什么要这么做?
let max_phys_addr = mmap
.entries()
.map(|m| m.phys_start + m.page_count * 0x1000)
.max()
.unwrap()
.max(0x1_0000_0000); // include IOAPIC MMIO area答:用闭包获取内存中的m,用起始地址+分页数*页大小计算最大内存地址。
为了给内核建立物理内存的直接映射提供范围界限
最后.max(0x1_0000_0000)是十六进制的4GB边界。在x86架构中,CPU和各种外部硬件通信是将硬件的寄存器直接映射到物理内存的地址空间中的(MMIO Memory-Mapped I/O),通常被强制安放在物理地址的4GB边界下方,因此必须映射到4GB边界,防止内核启动后配置某些硬件的寄存器是找不到页表映射而触发的Page Fault。
- 串口驱动是在进入内核后启用的,那么在进入内核之前,显示的内容是如何输出的?
答:内核启动前显示的内容是由UEFI固件输出的。
- 在 QEMU 中,我们通过指定
-nographic参数来禁用图形界面,这样 QEMU 会默认将串口输出重定向到主机的标准输出。
- 假如我们将
Makefile中取消该选项,QEMU 的输出窗口会发生什么变化?请观察指令make run QEMU_OUTPUT=的输出,结合截图分析对应现象。- 在移除
-nographic的情况下,如何依然将串口重定向到主机的标准输入输出?请尝试自行构造命令行参数,并查阅 QEMU 的文档,进行实验。- 如果你使用
ysos.py来启动 qemu,可以尝试修改-o选项来实现上述功能。
答:
- 去掉
-nographic参数后就会弹出一个GUI窗口 - 需要显示传递
-serial stdio参数给QEMU,以使得QEMU把com1重定向到stdio
结合本实验中 ELF 映射、页表与串口初始化的实现,讨论:
- 你会如何划分“最小可启动路径”和“可调试增强路径”?
- 若只能保留 3 个关键日志点,你会放在哪些阶段,为什么?
答:
- 最小可启动路径:只包含加载ELF、页表映射、栈与控制权移交
可调试增强路径:多包含权限控制、串口和日志系统、Panic Handler和一些一致性判断
只能保留三个关键日志点:
- BL移交控制权
- 内核串口初始化
- Panic Handler入口
若内核映射时误把某段权限设置为可写+可执行,你预期这会对系统安全性和调试复杂度带来什么影响?
答:
系统安全性:破坏W^X 原则(Write XOR Execute),即一段内存要么只写、要么只执行。
如果即可写又可执行,那么内核中存在的缓冲区溢出漏洞可以被很容易的利用。
调试复杂度:如果可执行程序没有W权限,那么会触发Page Fault异常,如果是W+X那么会默认写入成功,等其他程序执行到时才会发现。
假设引导阶段必须在“更快启动”和“更多一致性检查”之间二选一,你会如何取舍并给出依据?
更多一致性检查。调试时发现错误修复的耗时和难度都远小于实际业务中发现错误时修复。
相关推荐
- 暂无相关推荐,看看别的吧。
0 评论