2024年8月

IDL根据Landsat QA波段去云处理【代码】

​ landsat QA波段(质量评估波段)是Landsat卫星影像数据中的一个特殊波段,他在Landsat5-9的每个产品中都存在。虽然我们常用的Landsat影像数据有B1-B7波段,但QA波段并不是其中之一。它可以反映出云、云阴影、雪等类别的像素,常常应用在影像处理中对云像素去除。

​ 最近有在写landsat像素去云处理,查了网上许多QA波段值解释说明,发现都是基于
二进制
的,但IDL不同于GEE的算法,没有>>这种的按位运算符,只能先转成二进制,再自己写算法处理。算法写好后,为了发博客就去查了官网,又发现官网更新的QA波段值解释说明已经更新到了
十进制
,于是又写了一下根据十进制的去云处理(真的大哭)。

方法一:根据QA给定的二进制值解释进行处理

​ 上面的图片列出了QA波段的每一位所代表的含义,该含义为二进制存储的信息。

​ QA波段的存储方式为十进制,所以转换为二进制值进行判断,下图为某一像素二进制值说明。该像素为云的可能性很大。

代码思路:

  1. 读取图像,将十进制的数据转换为二进制格式
  2. 云像素识别,并标记,例如(只去除云像素和云阴影像素),为了方便,只使用了bit为3和4的两个为参考,并未加入置信值(confidence)
  3. 创建掩膜,对原图像进行掩膜
PRO LANDSAT_MASK_CLOUD
  COMPILE_OPT IDL2
  e = ENVI()
  
  raster = e.OpenRaster('F:\gbytemp\LC09_L2SP_127031_20220509_20220511_02_T1\LC09_L2SP_127031_20220509_20220511_02_T1_MTL.xml')
  qaPixelRaster = e.OpenRaster('F:\gbytemp\LC09_L2SP_127031_20220509_20220511_02_T1\LC09_L2SP_127031_20220509_20220511_02_T1_QA_PIXEL.TIF')
  data = qaPixelRaster.GetData()
  dimensions = SIZE(data, /DIMENSIONS)
  dataBit = data.toBits()
;  QA Bit    Description                values
;     0      Fill
;     1      Dilated Cloud              1
;     2      Cirrus                     1
;     3      Cloud                      1
;     4      Cloud Shadow               1
;     5      Snow                       1
;     8-9    Cloud Confidence           01Low 10Reserved 11 High
;     10-11  Cloud Shadow Confidence    01Low 10Reserved 11 High
;     12-14  Snow/Ice Confidence        01Low 10Reserved 11 High
;     14-15  Cirrus Confidence          01Low 10Reserved 11 High

  stop
  mask = MAKE_ARRAY(dimensions, VALUE=1, /INTEGER)
  FOR N = 0, dimensions[0]-1 DO BEGIN
    FOR M = 0, dimensions[1]-1 DO BEGIN
;      本文只用到bit 3(云)、bit 5(云阴影)进行去云操作
;      其中3和4表示二进制的位置,从右往左数(0开始)所以3和4的索引位置为-4和-5
      IF dataBit[-4, N, M] EQ 1 OR dataBit[-5, N, M] EQ 1 THEN BEGIN
        mask[N, M] = 0
      ENDIF
    ENDFOR
  ENDFOR
  

  file = e.GetTemporaryFilename()
  maskRaster = ENVIRaster(mask, URI=file)
  maskRaster.Save
  maskedRaster = ENVIMaskRaster(raster[0], maskRaster)

  e.Data.Add, maskedRaster
  view=e.GetView()
  layer=view.CreateLayer(maskedRaster)
  stop
END


去云结果对比图:

方法二:根据QA给定的十进制值解释进行处理

​ 十进制值解释含义如下:

代码思路:

  1. 读取图像
  2. 云像素识别,并标记,例如(只去除云像素和云阴影像素),为了方便,只使用了高置信值云22280、和高置信值云阴影23888为参考,
  3. 创建掩膜,对原图像进行掩膜
PRO LANDSAT_MASK_CLOUD
  COMPILE_OPT IDL2
  e = ENVI()
  
  raster = e.OpenRaster('F:\gbytemp\LC09_L2SP_127031_20220509_20220511_02_T1\LC09_L2SP_127031_20220509_20220511_02_T1_MTL.xml')
  qaPixelRaster = e.OpenRaster('F:\gbytemp\LC09_L2SP_127031_20220509_20220511_02_T1\LC09_L2SP_127031_20220509_20220511_02_T1_QA_PIXEL.TIF')
  data = qaPixelRaster.GetData()
  dimensions = SIZE(data, /DIMENSIONS)

  stop
  mask = MAKE_ARRAY(dimensions, VALUE=1, /INTEGER)
  FOR N = 0, dimensions[0]-1 DO BEGIN
    FOR M = 0, dimensions[1]-1 DO BEGIN
      IF data[N, M] EQ 55052 OR data[N, M] EQ 23888 THEN BEGIN
        mask[N, M] = 0
      ENDIF
    ENDFOR
  ENDFOR

  file = e.GetTemporaryFilename()
  maskRaster = ENVIRaster(mask, URI=file)
  maskRaster.Save
  maskedRaster = ENVIMaskRaster(raster[0], maskRaster)

  e.Data.Add, maskedRaster
  view=e.GetView()
  layer=view.CreateLayer(maskedRaster)
  stop
END


去云结果对比图:

在AIGC类的APP中,实现那种一个字一个字、一行一行地打印出文字的效果,可以通过多种方法来实现。下面是一些实现方法,使用Swift和OC来举例说明。

OC版

1. 基于定时器的逐字打印效果

可以使用
NSTimer
来逐字逐行地显示文字。

#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, strong) UITextView *textView;
@property (nonatomic, strong) NSString *content;
@property (nonatomic, assign) NSInteger currentIndex;
@property (nonatomic, strong) NSTimer *timer;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.textView = [[UITextView alloc] initWithFrame:self.view.bounds];
    self.textView.font = [UIFont systemFontOfSize:18];
    self.textView.editable = NO;
    self.textView.scrollEnabled = YES;
    [self.view addSubview:self.textView];

    self.content = @"这是需要逐字逐行打印的文字内容。\n让我们来实现它。";
    self.currentIndex = 0;

    [self startPrinting];
}

- (void)startPrinting {
    self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(printNextCharacter) userInfo:nil repeats:YES];
}

- (void)printNextCharacter {
    if (self.currentIndex >= self.content.length) {
        [self.timer invalidate];
        self.timer = nil;
        return;
    }

    NSRange range = NSMakeRange(self.currentIndex, 1);
    NSString *nextCharacter = [self.content substringWithRange:range];
    self.textView.text = [self.textView.text stringByAppendingString:nextCharacter];
    
    self.currentIndex += 1;
}

@end

2. 使用CADisplayLink来实现高精度逐字打印

CADisplayLink
可以在屏幕刷新时调用指定的方法,相较于
NSTimer
,其精度和性能更高。

#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, strong) UITextView *textView;
@property (nonatomic, strong) NSString *content;
@property (nonatomic, assign) NSInteger currentIndex;
@property (nonatomic, strong) CADisplayLink *displayLink;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.textView = [[UITextView alloc] initWithFrame:self.view.bounds];
    self.textView.font = [UIFont systemFontOfSize:18];
    self.textView.editable = NO;
    self.textView.scrollEnabled = YES;
    [self.view addSubview:self.textView];
    
    self.content = @"这是需要逐字逐行打印的文字内容。\n让我们来实现它。";
    self.currentIndex = 0;

    [self startPrinting];
}

- (void)startPrinting {
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(printNextCharacter)];
    self.displayLink.preferredFramesPerSecond = 10; // 控制打印速度
    [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}

- (void)printNextCharacter {
    if (self.currentIndex >= self.content.length) {
        [self.displayLink invalidate];
        self.displayLink = nil;
        return;
    }

    NSRange range = NSMakeRange(self.currentIndex, 1);
    NSString *nextCharacter = [self.content substringWithRange:range];
    self.textView.text = [self.textView.text stringByAppendingString:nextCharacter];
    
    self.currentIndex += 1;
}

@end

3. CATextLayer + Animation

还可以使用
CATextLayer
和动画来实现更为复杂和流畅的逐字逐行打印效果。

#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>

@interface ViewController ()

@property (nonatomic, strong) CATextLayer *textLayer;
@property (nonatomic, strong) NSString *content;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.textLayer = [CATextLayer layer];
    self.textLayer.frame = self.view.bounds;
    self.textLayer.fontSize = 18;
    self.textLayer.alignmentMode = kCAAlignmentLeft;
    self.textLayer.contentsScale = [UIScreen mainScreen].scale;
    self.textLayer.wrapped = YES;
    [self.view.layer addSublayer:self.textLayer];

    self.content = @"这是需要逐字逐行打印的文字内容。\n让我们来实现它。";

    [self startPrinting];
}

- (void)startPrinting {
    self.textLayer.string = @"";
    
    for (NSInteger index = 0; index < self.content.length; index++) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(index * 0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSString *nextCharacter = [self.content substringWithRange:NSMakeRange(index, 1)];
            self.textLayer.string = [self.textLayer.string stringByAppendingString:nextCharacter];
        });
    }
}

@end

Swift版

1. 基于定时器的逐字打印效果

可以使用
Timer
来逐字逐行地显示文字。

import UIKit

class ViewController: UIViewController {
    private let textView = UITextView()
    private let content = "这是需要逐字逐行打印的文字内容。\n让我们来实现它。"
    private var currentIndex = 0
    private var timer: Timer?

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(textView)
        textView.frame = view.bounds
        textView.font = UIFont.systemFont(ofSize: 18)
        textView.isEditable = false
        textView.isScrollEnabled = true
        startPrinting()
    }

    private func startPrinting() {
        timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(printNextCharacter), userInfo: nil, repeats: true)
    }

    @objc private func printNextCharacter() {
        guard currentIndex < content.count else {
            timer?.invalidate()
            timer = nil
            return
        }
        
        let nextIndex = content.index(content.startIndex, offsetBy: currentIndex)
        textView.text.append(content[nextIndex])
        currentIndex += 1
    }
}

2. 使用CADisplayLink来实现高精度逐字打印

CADisplayLink
可以在屏幕刷新时调用指定的方法,相较于
Timer
,其精度和性能更高。

import UIKit

class ViewController: UIViewController {
    private let textView = UITextView()
    private let content = "这是需要逐字逐行打印的文字内容。\n让我们来实现它。"
    private var currentIndex = 0
    private var displayLink: CADisplayLink?

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(textView)
        textView.frame = view.bounds
        textView.font = UIFont.systemFont(ofSize: 18)
        textView.isEditable = false
        textView.isScrollEnabled = true
        startPrinting()
    }

    private func startPrinting() {
        displayLink = CADisplayLink(target: self, selector: #selector(printNextCharacter))
        displayLink?.preferredFramesPerSecond = 10  // 控制打印速度
        displayLink?.add(to: .main, forMode: .default)
    }

    @objc private func printNextCharacter() {
        guard currentIndex < content.count else {
            displayLink?.invalidate()
            displayLink = nil
            return
        }
        
        let nextIndex = content.index(content.startIndex, offsetBy: currentIndex)
        textView.text.append(content[nextIndex])
        currentIndex += 1
    }
}

3. CATextLayer + Animation

还可以使用
CATextLayer
和动画来实现更为复杂和流畅的逐字逐行打印效果。

import UIKit

class ViewController: UIViewController {
    private let textLayer = CATextLayer()
    private let content = "这是需要逐字逐行打印的文字内容。\n让我们来实现它。"
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        textLayer.frame = view.bounds
        textLayer.fontSize = 18
        textLayer.alignmentMode = .left
        textLayer.contentsScale = UIScreen.main.scale
        textLayer.isWrapped = true
        view.layer.addSublayer(textLayer)
        
        startPrinting()
    }
    
    private func startPrinting() {
        textLayer.string = ""
        for (index, character) in content.enumerated() {
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.1) {
                self.textLayer.string = "\(self.textLayer.string ?? "")\(character)"
            }
        }
    }
}

写在前面

本随笔是非常菜的菜鸡写的。如有问题请及时提出。

可以联系:1160712160@qq.com

GitHhub:
https://github.com/WindDevil
(目前啥也没有

编程题

实现一个裸机应用程序A,能打印调用栈

首先在这里卡了我很久的是调用栈保存在哪里,回想到上一部分画的图,其实就在
.bss
段后边.

这里注意
sp
寄存器是栈指针寄存器,
fp
寄存器是帧指针寄存器,是不一样的.

这里重提一下
os/.cargo/config
的内容,
"-Cforce-frame-pointers=yes"
代表保存栈指针:

# os/.cargo/config
[build]
target = "riscv64gc-unknown-none-elf"

[target.riscv64gc-unknown-none-elf]
 rustflags = [
     "-Clink-arg=-Tsrc/linker-qemu.ld", "-Cforce-frame-pointers=yes"
 ]

此时可以通过
fp
的值(保证它不在trap过程中在用户栈或者内核栈中).

这时候可以创建
os/src/stack_trace.rs
:

use core::{arch::asm, ptr};

pub unsafe fn print_stack_trace() -> () {
    let mut fp: *const usize;
    asm!("mv {}, fp", out(reg) fp);

    println!("== Begin stack trace ==");
    while fp != ptr::null() {
        let saved_ra = *fp.sub(1);
        let saved_fp = *fp.sub(2);

        println!("0x{:016x}, fp = 0x{:016x}", saved_ra, saved_fp);

        fp = saved_fp as *const usize;
    }
    println!("== End stack trace ==");
}


main.rs
里把它加入
main.rs
作为一个子模块:

mod stack_trace;

编辑
lang_items.rs
,在
panic
函数里加入
unsafe { print_stack_trace(); }
.

注意添加上依赖
use crate::stack_trace::print_stack_trace;


AppManager
的函数
load_app
的所有APP运行完毕的部分加上
panic!("Shutdown machine!");
,代替原来的正常关机.

impl AppManager {
    pub fn print_app_info(&self) {
        println!("[kernel] num_app = {}", self.num_app);
        for i in 0..self.num_app {
            println!(
                "[kernel] app_{} [{:#x}, {:#x})",
                i,
                self.app_start[i],
                self.app_start[i + 1]
            );
        }
    }

    unsafe fn load_app(&self, app_id: usize) {
        if app_id >= self.num_app {
            println!("All applications completed!");
            panic!("Shutdown machine!");
            //shutdown(false);
        }
        println!("[kernel] Loading app_{}", app_id);
        // clear app area
        core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, APP_SIZE_LIMIT).fill(0);
        let app_src = core::slice::from_raw_parts(
            self.app_start[app_id] as *const u8,
            self.app_start[app_id + 1] - self.app_start[app_id],
        );
        let app_dst = core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, app_src.len());
        app_dst.copy_from_slice(app_src);
        // Memory fence about fetching the instruction memory
        // It is guaranteed that a subsequent instruction fetch must
        // observes all previous writes to the instruction memory.
        // Therefore, fence.i must be executed after we have loaded
        // the code of the next app into the instruction memory.
        // See also: riscv non-priv spec chapter 3, 'Zifencei' extension.
        asm!("fence.i");
    }

    pub fn get_current_app(&self) -> usize {
        self.current_app
    }

    pub fn move_to_next_app(&mut self) {
        self.current_app += 1;
    }
}

然后直接在
os
目录下
make run
.如果报错了

error: unused import: `crate::sbi::shutdown`
  --> src/batch.rs:3:5
   |
3  | use crate::sbi::shutdown;
   |     ^^^^^^^^^^^^^^^^^^^^
   |

直接注释掉
use crate::sbi::shutdown;
即可.

这样就可以得到结果:

[rustsbi] RustSBI version 0.3.1, adapting to RISC-V SBI v1.0.0
.______       __    __      _______.___________.  _______..______   __
|   _  \     |  |  |  |    /       |           | /       ||   _  \ |  |
|  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  |
|      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  |
|  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  |
| _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__|
[rustsbi] Implementation     : RustSBI-QEMU Version 0.2.0-alpha.2
[rustsbi] Platform Name      : riscv-virtio,qemu
[rustsbi] Platform SMP       : 1
[rustsbi] Platform Memory    : 0x80000000..0x88000000
[rustsbi] Boot HART          : 0
[rustsbi] Device Tree Region : 0x87000000..0x87000f02
[rustsbi] Firmware Address   : 0x80000000
[rustsbi] Supervisor Address : 0x80200000
[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
[rustsbi] pmp02: 0x80000000..0x80200000 (---)
[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
[rustsbi] pmp04: 0x88000000..0x00000000 (-wr)
[kernel] Hello, world!
[kernel] num_app = 5
[kernel] app_0 [0x8020a038, 0x8020b360)
[kernel] app_1 [0x8020b360, 0x8020c730)
[kernel] app_2 [0x8020c730, 0x8020dcd8)
[kernel] app_3 [0x8020dcd8, 0x8020f090)
[kernel] app_4 [0x8020f090, 0x80210440)
[kernel] Loading app_0
Hello, world!
[kernel] Application exited with code 0
[kernel] Loading app_1
Into Test store_fault, we will insert an invalid store operation...
Kernel should kill this application!
[kernel] PageFault in application, kernel killed it.
[kernel] Loading app_2
3^10000=5079(MOD 10007)
3^20000=8202(MOD 10007)
3^30000=8824(MOD 10007)
3^40000=5750(MOD 10007)
3^50000=3824(MOD 10007)
3^60000=8516(MOD 10007)
3^70000=2510(MOD 10007)
3^80000=9379(MOD 10007)
3^90000=2621(MOD 10007)
3^100000=2749(MOD 10007)
Test power OK!
[kernel] Application exited with code 0
[kernel] Loading app_3
Try to execute privileged instruction in U Mode
Kernel should kill this application!
[kernel] IllegalInstruction in application, kernel killed it.
[kernel] Loading app_4
Try to access privileged CSR in U Mode
Kernel should kill this application!
[kernel] IllegalInstruction in application, kernel killed it.
All applications completed!
== Begin stack trace ==
0x0000000080201244, fp = 0x0000000080206cf0
0x0000000080201a78, fp = 0x0000000080206d30
0x0000000080200ed8, fp = 0x0000000080206da0
0x00000000802010bc, fp = 0x0000000080206e00
0x0000000080201724, fp = 0x0000000080206ef0
0x0000000080200a70, fp = 0x0000000080208fc0
0x0000000080400032, fp = 0x0000000080209000
0x0000000000000000, fp = 0x0000000000000000
== End stack trace ==
make: *** [Makefile:64: run-inner] Error 255

后续题目

问答题

1. 函数调用与系统调用有何区别?

  • 函数调用用普通的控制流指令,不涉及特权级的切换;系统调用使用专门的指令(如 RISC-V 上的 ecall),会切换到内核特权级。
  • 函数调用可以随意指定调用目标;系统调用只能将控制流切换给调用操作系统内核给定的目标。

这里注意去看之前提到的
trap
处理流程:

  1. print!
    ->
    print
    ->
    Stdout.write_fmt
    ->
    write
    ->
    sys_write
    ->
    syscall
    是在
    user
    层的函数调用链.
  2. syscall
    ->
    sys_write
    ->
    print!
    ->
    Stdout.write_fmt
    ->
    console_putchar
    是在
    os
    层的函数调用链.
  3. 使用
    x10~x17
    作为其中的桥梁

2. 为了方便操作系统处理,M态软件会将 S 态异常/中断委托给 S 态软件,请指出有哪些寄存器记录了委托信息,rustsbi 委托了哪些异常/中断?(也可以直接给出寄存器的值)

这个问题主要是在
rust-sbi
之中,这个题当然可以通过直接问GPT得出答案,但是为了增强我们的查询能力,假如GPT不知道或者我们需要更多的更准确更标准的信息,我们可以查看
RISC-V手册 (ustc.edu.cn)
,寻找其中内容.

我们在其中搜索委托,可以看到
10.5 现代操作系统的监管者模式
这一章中提到了关于RISC-V的委托机制的描述.
默认情况下,发生所有异常(不论在什么权限模式下)的时候,控制权都会被移交到 M 模式的异常处理程序。但是 Unix 系统中的大多数例外都应该进行 S 模式下的系统调 用。M 模式的异常处理程序可以将异常重新导向 S 模式,但这些额外的操作会减慢大多数 异常的处理速度。因此,RISC-V 提供了一种异常委托机制。通过该机制可以选择性地将中 断和同步异常交给 S 模式处理,而完全绕过 M 模式。

这里提到两个CSR,
mideleg

medeleg
,分别可以把中断和同步异常委托给S模式:
mideleg(Machine Interrupt Delegation,机器中断委托)CSR 控制将哪些中断委托给 S 模式。
M 模式还可以通过 medeleg CSR 将同步异常委托给 S 模式。

这里同步异常的名字可能比较奇怪,容易让人想到还有别的类型的异常,但是实际上异常只有两类:同步异常,中断:
RISC-V 将 异常分为两类。一类是同步异常,这类异常在指令执行期间产生,如访问了无效的存储器 地址或执行了具有无效操作码的指令时。另一类是中断,它是与指令流异步的外部事件, 比如鼠标的单击。

请注意,无论委派设置是怎样的,发生异常时控制权都不会移交给权限更低的模式。 在 M 模式下发生的异常总是在 M 模式下处理。在 S 模式下发生的异常,根据具体的委派 设置,可能由 M 模式或 S 模式处理,但永远不会由 U 模式处理。

图中含有关于中断和同步异常的描述,前半个表格是关于
中断
的后半个表格是关于
同步异常
的.把对应的位设置为高就可以把对应异常委托给S模式.

例如, mideleg[5]对应于 S 模式的时钟中断,如果把它置位,S 模式的时钟中断将会移交 S 模式 的异常处理程序,而不是 M 模式的异常处理程序。

例如,置上 medeleg[15]便会把 store page fault(store 过程中出现的缺页)委托给 S 模式。

使用
make debug
进入
debug
模式,使用GDB命令
break rust_main
打断点,然后使用
c
命令使得程序停在
rust_main
之前.

使用指令
info register mideleg

info register medeleg
可以读取到两个寄存器的值:

mideleg        0x666    1638
medeleg        0xf0b5ff 15775231 

这里发现和答案对应不上,对应不上就对了,看上图,实际上
mideleg
的有效位数为奇数位,应该让
0x666
按位与
一个
b1010 1010 1010
,得到的就是
0x222
了.

同理对于
medeleg

0xf0b5ff
应该取其
0~15
位缺
10

14
位,应该
按位与
一个
b1011 1011 1111 1111
得到
0xb1ff
,这里似乎还算和答案不一致,但是我们可以自圆其说,先不管了.

3. 如果操作系统以应用程序库的形式存在,应用程序可以通过哪些方式破坏操作系统?

这题主要是对比第一章和第二章操作系统的不同,找到第一章操作系统的问题.

第一印象产生出来主要是我们第二章忙活的部分,也就是在于关于调用更高级别指令的问题.但是调用更高级别的指令,我们如果用操作系统来拦截这些指令,就可以保证我们的操作都是已经定义的,就不会出现问题了.

这题所有的缺点,反过来就是第二章所有的优点.答案已经写得很完美了.

如果操作系统以应用程序库的形式存在,那么编译器在链接OS库时会把应用程序跟OS库链接成一个可执行文件,两者处于同一地址空间,这也是LibOS(Unikernel)架构,此时存在如下几个破坏操作系统的方式:

  • 缓冲区溢出:应用程序可以覆盖写其合法内存边界之外的部分,这可能会危及 OS;
  • 整数溢出:当对整数值的运算产生的值超出整数数据类型可以表示的范围时,就会发生整数溢出, 这可能会导致OS出现意外行为和安全漏洞。 例如,如果允许应用程序分配大量内存,攻击者可能会在内存分配例程中触发整数溢出,从而可能导致缓冲区溢出或其他安全漏洞;
  • 系统调用拦截:应用程序可能会拦截或重定向系统调用,从而可能损害OS的行为。例如,攻击者可能会拦截读取敏感文件的系统调用并将其重定向到他们选择的文件,从而可能危及 unikernel 的安全性。
  • 资源耗尽:应用程序可能会消耗内存或网络带宽等资源,可能导致拒绝服务或其他安全漏洞。

4. 编译器/操作系统/处理器如何合作,可采用哪些方法来保护操作系统不受应用程序的破坏?

仍然是参考官方文档的参考答案.

硬件操作系统运行在一个硬件保护的安全执行环境中,不受到应用程序的破坏;应用程序运行在另外一个无法破坏操作系统的受限执行环境中。 现代CPU提供了很多硬件机制来保护操作系统免受恶意应用程序的破坏,包括如下几个:

  • 特权级模式:处理器能够设置不同安全等级的执行环境,即用户态执行环境和内核态特权级的执行环境。处理器在执行指令前会进行特权级安全检查,如果在用户态执行环境中执行内核态特权级指令,会产生异常阻止当前非法指令的执行。
  • TEE(可信执行环境):CPU的TEE能够构建一个可信的执行环境,用于抵御恶意软件或攻击,能够确保处理敏感数据的应用程序(例如移动银行和支付应用程序)的安全。
  • ASLR(地址空间布局随机化):ASLR 是CPU的一种随机化进程地址空间布局的安全功能,其能够随机生成进程地址空间,例如栈、共享库等关键部分的起始地址,使攻击者预测特定数据或代码的位置。

5. RISC-V处理器的S态特权指令有哪些,其大致含义是什么,有啥作用?

这个问题之前我们说过它的分类,而具体的S态特权指令则应该查询参考书目
RISC-V手册 (ustc.edu.cn)
.

用户态软件为获得内核态操作系统的服务功能而执行特殊指令:
1. 指令本身属于高特权级的指令,如
sret
指令(表示从 S 模式返回到 U 模式)
2. 指令访问了
S模式特权级下才能访问的寄存器
或内存,如表示S模式系统状态的
控制状态寄存器
sstatus

通过查阅手册我们可以得出特权指令分为如下图所示:

sret的作用:

mret的作用:

wfi的作用:

sfence.vma的作用:

访问CSR的指令,这里直接列出M模式下CSR的列表,再S模式下的CSR命名则是把名字里的
m
,更换为
s

另外还有一些CSR专用的指令:

图中的CSR相关指令为:

6. RISC-V处理器在用户态执行特权指令后的硬件层面的处理过程是什么?

包括上部分画的图从来都是讲的怎么做特权级切换,但是我们总是忽略在切换上下文的时候的CSR的变化,这些部分想想就是通过硬件来改变的,这个问题刚好对我们是一个非常合适的提醒.

CPU 执行完一条指令(如 ecall )并准备从用户特权级 陷入( Trap )到 S 特权级的时候,硬件会自动完成如下这些事情:

  • sstatus 的 SPP 字段会被修改为 CPU 当前的特权级(U/S)。
  • sepc 会被修改为 Trap 处理完成后默认会执行的下一条指令的地址。
  • scause/stval 分别会被修改成这次 Trap 的原因以及相关的附加信息。
  • cpu 会跳转到 stvec 所设置的 Trap 处理入口地址,并将当前特权级设置为 S ,然后从Trap 处理入口地址处开始执行

CPU 完成 Trap 处理准备返回的时候,需要通过一条 S 特权级的特权指令 sret 来完成,这一条指令具体完成以下功能: * CPU 会将当前的特权级按照 sstatus 的 SPP 字段设置为 U 或者 S ; * CPU 会跳转到 sepc 寄存器指向的那条指令,然后继续执行。

7. 操作系统在完成用户态<–>内核态双向切换中的一般处理过程是什么?

这个更详细的要看关于上一部分的绿色部分,关于
ecall
->
x10~x17
->
trap_handler
->
sret
这一部分的描述,可以看看(019 在main中测试本章实现)里的大图.

当 CPU 在用户态特权级( RISC-V 的 U 模式)运行应用程序,执行到 Trap,切换到内核态特权级( RISC-V的S 模式),批处理操作系统的对应代码响应 Trap,并执行系统调用服务,处理完毕后,从内核态返回到用户态应用程序继续执行后续指令。

8. 程序陷入内核的原因有中断、异常和陷入(系统调用),请问 riscv64 支持哪些中断 / 异常?如何判断进入内核是由于中断还是异常?描述陷入内核时的几个重要寄存器及其值。

这里就还是要看这张图了

要判断当前的中断和异常情况只需要看
scause
,
RISC-V手册 (ustc.edu.cn)
对它的描述是:
scause 按图 10.3 根据异常类型设置,stval 被设置成出错的地址或者其它特定异常的信息字。

Trap
时重要的寄存器也还是看这张图,这里直接列出M模式下CSR的列表,再S模式下的CSR命名则是把名字里的
m
,更换为
s
:

9. 在哪些情况下会出现特权级切换:用户态–>内核态,以及内核态–>用户态?

用户态到内核态的切换已经重复了很多次了:

上层软件执行过程中出现了一些异常或
特殊情况
, 需要用到执行环境中提供的功能
1. 这里可以看到虽然都叫做
异常
但是实际上有一部分情况是特殊情况需要使用执行环境中的功能,不能非黑即白地把
异常
理解为
坏的
2. 用户态应用直接触发从用户态到内核态的异常的原因总体上可以分为两种
1. 其一是用户态软件为获得内核态操作系统的服务功能而执行特殊指令
1. 指令本身属于高特权级的指令,如
sret
指令(表示从 S 模式返回到 U 模式)
2. 指令访问了
S模式特权级下才能访问的寄存器
或内存,如表示S模式系统状态的
控制状态寄存器
sstatus

2. 其二是在执行某条指令期间产生了错误(如执行了用户态不允许执行的指令或者其他错误)并被 CPU 检测到

但是从内核态到用户态,我们马上能想到的只有
sret
,但是想想我们这一章实现的操作系统是什么时候开始运行APP的呢,运行的时候是怎么进入用户态的呢,更具体更详细的似乎没有?

想想上一部分我们在
官方手册
中可以看到:
唯一一种能够使得 CPU 特权级下降的方法就是执行 Trap 返回的特权指令,如
sret

mret

这时候我们再去
RISC-V手册 (ustc.edu.cn)
寻找关于
Trap
返回的特权指令:
监管者异常返回指令 sret 与 mret 的行为相同,但它作用于 S 模式的异常处理 CSR,而不 是 M 模式的 CSR

可以看到
Trap
的返回指令只有
sret

mret
.

这里再重复一下它们俩的作用.

sret的作用:

mret的作用:

10. Trap上下文的含义是啥?在本章的操作系统中,Trap上下文的具体内容是啥?如果不进行Trap上下文的保存于恢复,会出现什么情况?

这个直接联系到我们对于
TrapContext
的定义代码:

pub struct TrapContext {
    /// general regs[0..31]
    pub x: [usize; 32],
    /// CSR sstatus      
    pub sstatus: Sstatus,
    /// CSR sepc
    pub sepc: usize,
}

这里我们可以反过来回想到,
Trap
的上下文就是CSR保存的关于
Trap
的信息,以及通用寄存器.

如果不能进行
Trap
的上下文保存与恢复,CPU就不能正确恢复到原来的特权级.

实验练习

实践作业

sys_write 安全检查

ch2 中,我们实现了第一个系统调用
sys_write
,这使得我们可以在用户态输出信息。但是 os 在提供服务的同时,还有保护 os 本身以及其他用户程序不受错误或者恶意程序破坏的功能。

由于还没有实现虚拟内存,我们可以在用户程序中指定一个属于其他程序字符串,并将它输出,这显然是不合理的,因此我们要对 sys_write 做检查:

  • sys_write 仅能输出位于程序本身内存空间内的数据,否则报错。


AppManager
创建一个获取当前APP地址的方法,并且因为每次加载APP之后
current_app
号加一,所以需要读取的范围是
self.app_start[self.current_app]~self.app_start[self.current_app-1]
,计算出APP大小作为偏移量,然后通过基指针计算即可:

impl AppManager {
    pub fn print_app_info(&self) {
        println!("[kernel] num_app = {}", self.num_app);
        for i in 0..self.num_app {
            println!(
                "[kernel] app_{} [{:#x}, {:#x})",
                i,
                self.app_start[i],
                self.app_start[i + 1]
            );
        }
    }

    unsafe fn load_app(&self, app_id: usize) {
        if app_id >= self.num_app {
            println!("All applications completed!");
            //panic!("Shutdown machine!");
            shutdown(false);
        }
        println!("[kernel] Loading app_{}", app_id);
        // clear app area
        core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, APP_SIZE_LIMIT).fill(0);
        let app_src = core::slice::from_raw_parts(
            self.app_start[app_id] as *const u8,
            self.app_start[app_id + 1] - self.app_start[app_id],
        );
        let app_dst = core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, app_src.len());
        app_dst.copy_from_slice(app_src);
        // Memory fence about fetching the instruction memory
        // It is guaranteed that a subsequent instruction fetch must
        // observes all previous writes to the instruction memory.
        // Therefore, fence.i must be executed after we have loaded
        // the code of the next app into the instruction memory.
        // See also: riscv non-priv spec chapter 3, 'Zifencei' extension.
        asm!("fence.i");
    }

    pub fn get_current_app(&self) -> usize {
        self.current_app
    }

    pub fn move_to_next_app(&mut self) {
        self.current_app += 1;
    }

    pub fn get_current_app_range(&self) -> (usize, usize) {
        (APP_BASE_ADDRESS,APP_BASE_ADDRESS+self.app_start[self.current_app]-self.app_start[self.current_app-1])
    }
}

pub fn get_current_app_range() -> (usize, usize) {
    APP_MANAGER.exclusive_access().get_current_app_range()
}


batch.rs
中创建一个方法,计算用户栈的范围:

pub fn get_user_stack_range() -> (usize, usize) {
    (USER_STACK.get_sp() - USER_STACK_SIZE, USER_STACK.get_sp())
}

改造
sys_wirte
,如果变量不是在上述两个范围内,那么就
sys_exit()
:

/// write buf of length `len`  to a file with `fd`
pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize {
    let app_range = get_current_app_range();
    let stack_range = get_user_stack_range();
    //println!("range: [{:#x}, {:#x})\n", range.0,range.1);
    let buf_pointer = buf as usize;
    //println!("buf_pointer: {:#x}\n", buf_pointer);
    if  (buf_pointer < app_range.0 || buf_pointer >= app_range.1) && (buf_pointer < stack_range.0 || buf_pointer >= stack_range.1) {
        sys_exit(fd as i32)
    }
    match fd {
        FD_STDOUT => {
            let slice = unsafe { core::slice::from_raw_parts(buf, len) };
            let str = core::str::from_utf8(slice).unwrap();
            print!("{}", str);
            len as isize
        }
        _ => {
            panic!("Unsupported fd in sys_write!");
        }
    }
}

这里其实在编写过程中有一个小的踩坑的过程的(一直踩坑一直G),

最开始只想到了参数的值必须在被加载的范围内,也就是上述
app_range
的范围内,然后发现,APP2老报错,我把
sys_exit
换成
println!
,发现每次
访问变量
都会出问题,这时候也问了同义千问
sp
的作用,其中有一句让我豁然开朗:
栈指针寄存器用于跟踪当前栈顶的位置。每当函数调用发生时,参数和局部变量会被压入栈中,栈指针会相应地下移(在RISC-V中,栈向下增长)。

会想起牢丘的51单片机实验,他老说保存现场保存现场,其实保存的就是局部变量,局部变量就是存在通用寄存器里嘛!(也不知道我有没有融会贯通对)

这时候参考了别人的代码,就又加上了这部分.

测试的时候需要把自己的修改放到官方的代码
:~/App/rCore-Tutorial-v3
里去,
git checkout ch2-lab
,运行
make run TEST=1
.

这里运行的时候报错了:

(rustup target list | grep "riscv64gc-unknown-none-elf (installed)") || rustup target add riscv64gc-unknown-none-elf
riscv64gc-unknown-none-elf (installed)
cargo install cargo-binutils --vers =0.3.3
    Updating `ustc` index
  Installing cargo-binutils v0.3.3
error: failed to compile `cargo-binutils v0.3.3`, intermediate artifacts can be found at `/tmp/cargo-installvTJxCF`

Caused by:
  package `regex-automata v0.4.7` cannot be built because it requires rustc 1.65 or newer, while the currently active rustc version is 1.64.0-nightly
make: *** [Makefile:45: env] Error 101

说是
rustc
的等级太低,重新更新一下
rustc
,然后安装依赖:

rustup update

发现仍然无效.

但是这里踩了一个新坑,更新
rust
之后要看最新版本需要
重启命令行
.

仍然无效的原因是:

rustup show
Default host: x86_64-unknown-linux-gnu
rustup home:  /home/winddevil/.rustup

installed toolchains
--------------------

stable-x86_64-unknown-linux-gnu
nightly-2022-07-20-x86_64-unknown-linux-gnu
nightly-2024-05-01-x86_64-unknown-linux-gnu (default)
nightly-x86_64-unknown-linux-gnu

installed targets for active toolchain
--------------------------------------

riscv64gc-unknown-none-elf
x86_64-unknown-linux-gnu

active toolchain
----------------

nightly-2022-07-20-x86_64-unknown-linux-gnu (overridden by '/home/winddevil/App/rCore-Tutorial-v3/rust-toolchain.toml')
rustc 1.64.0-nightly (9a7b7d5e5 2022-07-19)

可以看到
rust-toolchain.toml
规定的
channel
影响了我们的版本问题.

进去把它修改为最新的
channel = "nightly-2024-07-30"
就可以了.

但是这样又产生新的冲突:

warning: the feature `panic_info_message` has been stable since 1.82.0-nightly and no longer requires an attribute to enable
 --> src/lib.rs:3:12
  |
3 | #![feature(panic_info_message)]
  |            ^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(stable_features)]` on by default

error[E0599]: no method named `unwrap` found for struct `PanicMessage` in the current scope
 --> src/lang_items.rs:5:36
  |
5 |     let err = panic_info.message().unwrap();
  |                                    ^^^^^^ method not found in `PanicMessage<'_>`

For more information about this error, try `rustc --explain E0599`.
warning: `user_lib` (lib) generated 1 warning
error: could not compile `user_lib` (lib) due to 1 previous error; 1 warning emitted
make[1]: *** [Makefile:20: binary] Error 101
make[1]: Leaving directory '/home/winddevil/App/rCore-Tutorial-v3/user'
make: *** [Makefile:53: kernel] Error 2

注释掉
App/rCore-Tutorial-v3/user/src/lib.rs

#![feature(panic_info_message)]
.

把所有的
panic_info.message().unwarp()
改成
panic_info.message().as_str().unwrap_or("no message")
就行了.

不修改的时候的输出:

[rustsbi] RustSBI version 0.2.0-alpha.6
.______       __    __      _______.___________.  _______..______   __
|   _  \     |  |  |  |    /       |           | /       ||   _  \ |  |
|  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  |
|      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  |
|  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  |
| _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__|

[rustsbi] Implementation: RustSBI-QEMU Version 0.0.2
[rustsbi-dtb] Hart count: cluster0 with 1 cores
[rustsbi] misa: RV64ACDFIMSU
[rustsbi] mideleg: ssoft, stimer, sext (0x222)
[rustsbi] medeleg: ima, ia, bkpt, la, sa, uecall, ipage, lpage, spage (0xb1ab)
[rustsbi] pmp0: 0x10000000 ..= 0x10001fff (rwx)
[rustsbi] pmp1: 0x80000000 ..= 0x8fffffff (rwx)
[rustsbi] pmp2: 0x0 ..= 0xffffffffffffff (---)
[rustsbi] enter supervisor 0x80200000
[kernel] Hello, world!
[kernel] num_app = 2
[kernel] app_0 [0x80209020, 0x8020c768)
[kernel] app_1 [0x8020c768, 0x8020ffa0)
[kernel] Loading app_0
[kernel] Panicked at src/trap/mod.rs:45 no message

可以看到加载app0的时候就出现了
panic
.

修改之后:

[rustsbi] RustSBI version 0.2.0-alpha.6
.______       __    __      _______.___________.  _______..______   __
|   _  \     |  |  |  |    /       |           | /       ||   _  \ |  |
|  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  |
|      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  |
|  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  |
| _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__|

[rustsbi] Implementation: RustSBI-QEMU Version 0.0.2
[rustsbi-dtb] Hart count: cluster0 with 1 cores
[rustsbi] misa: RV64ACDFIMSU
[rustsbi] mideleg: ssoft, stimer, sext (0x222)
[rustsbi] medeleg: ima, ia, bkpt, la, sa, uecall, ipage, lpage, spage (0xb1ab)
[rustsbi] pmp0: 0x10000000 ..= 0x10001fff (rwx)
[rustsbi] pmp1: 0x80000000 ..= 0x8fffffff (rwx)
[rustsbi] pmp2: 0x0 ..= 0xffffffffffffff (---)
[rustsbi] enter supervisor 0x80200000
[kernel] Hello, world!
[kernel] num_app = 2
[kernel] app_0 [0x80209020, 0x8020c768)
[kernel] app_1 [0x8020c768, 0x8020ffa0)
[kernel] Loading app_0
[kernel] Application exited with code 1
[kernel] Loading app_1
[kernel] Panicked at src/syscall/fs.rs:27 Unsupported fd in sys_write!

可以看到
app_0
的运行过程中触发了
Application exited with code 1
,没按照预期运行.而
app_1
则是更加重量级地跳了.

因为我们可以面向结果编程,所以我打算自己看一看
app_0
的测例是什么贵物;

#![no_std]
#![no_main]

use core::arch::asm;

#[macro_use]
extern crate user_lib;
extern crate core;
use core::slice;
use user_lib::{write, STDOUT};

/// 正确输出:
/// Test write0 OK!

const STACK_SIZE: usize = 0x1000;

unsafe fn r_sp() -> usize {
    let mut sp: usize;
    asm!("mv {}, sp", out(reg) sp);
    sp
}

unsafe fn stack_range() -> (usize, usize) {
    let sp = r_sp();
    let top = (sp + STACK_SIZE - 1) & (!(STACK_SIZE - 1));
    (top - STACK_SIZE, top)
}

#[no_mangle]
pub fn main() -> i32 {
    assert_eq!(
        write(STDOUT, unsafe {
            #[allow(clippy::zero_ptr)]
            slice::from_raw_parts(0x0 as *const _, 10)
        }),
        -1
    );
    let (bottom, top) = unsafe { stack_range() };
    assert_eq!(
        write(STDOUT, unsafe {
            slice::from_raw_parts((top - 5) as *const _, 10)
        }),
        -1
    );
    assert_eq!(
        write(STDOUT, unsafe {
            slice::from_raw_parts((bottom - 5) as *const _, 10)
        }),
        -1
    );
    // TODO: test string located in .data section
    println!("Test write0 OK!");
    0
}

第一点,我们可以看到实际上它希望我们每次尝试访问不在权限内的地址后不是退出app而是选择阻止,并且继续运行下面的部分.

因此可以把
sys_write
更改为:

pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize {
    let app_range = get_current_app_range();
    let stack_range = get_user_stack_range();
    //println!("range: [{:#x}, {:#x})\n", range.0,range.1);
    let buf_pointer = buf as usize;
    //println!("buf_pointer: {:#x}\n", buf_pointer);
    if  (buf_pointer < app_range.0 || buf_pointer >= app_range.1) && (buf_pointer < stack_range.0 || buf_pointer >= stack_range.1) {
        println!("Out of range!");
        return (len as isize)
        //sys_exit(fd as i32)
    }
    match fd {
        FD_STDOUT => {
            let slice = unsafe { core::slice::from_raw_parts(buf, len) };
            let str = core::str::from_utf8(slice).unwrap();
            print!("{}", str);
            len as isize
        }
        _ => {
            panic!("Unsupported fd in sys_write!");
        }
  

此时输出为:

[rustsbi] RustSBI version 0.2.0-alpha.6
.______       __    __      _______.___________.  _______..______   __
|   _  \     |  |  |  |    /       |           | /       ||   _  \ |  |
|  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  |
|      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  |
|  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  |
| _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__|

[rustsbi] Implementation: RustSBI-QEMU Version 0.0.2
[rustsbi-dtb] Hart count: cluster0 with 1 cores
[rustsbi] misa: RV64ACDFIMSU
[rustsbi] mideleg: ssoft, stimer, sext (0x222)
[rustsbi] medeleg: ima, ia, bkpt, la, sa, uecall, ipage, lpage, spage (0xb1ab)
[rustsbi] pmp0: 0x10000000 ..= 0x10001fff (rwx)
[rustsbi] pmp1: 0x80000000 ..= 0x8fffffff (rwx)
[rustsbi] pmp2: 0x0 ..= 0xffffffffffffff (---)
[rustsbi] enter supervisor 0x80200000
[kernel] Hello, world!
[kernel] num_app = 2
[kernel] app_0 [0x80209020, 0x8020c768)
[kernel] app_1 [0x8020c768, 0x8020ffa0)
[kernel] Loading app_0
Out of range!
Out of range!
Out of range!
[kernel] Application exited with code -1
[kernel] Loading app_1
[kernel] Panicked at src/syscall/fs.rs:29 Unsupported fd in sys_write!

可以看到
app_0
触发了三次
Out of range!
,但是最后还是以
-1
为结束.

这时候怀疑这个
-1
是用户态的
println!
不能正常运行导致的.

尝试运行
make run
而不启用
TEST
.这样根据
user/Makefile
文件的这处描述,就会运行普通的app :

TEST ?= 0
ifeq ($(TEST), 0)
	APPS :=  $(filter-out $(wildcard $(APP_DIR)/test*.rs), $(wildcard $(APP_DIR)/*.rs))
else
	APPS :=  $(wildcard $(APP_DIR)/test$(TEST)*.rs)
endif
ELFS := $(patsubst $(APP_DIR)/%.rs, $(TARGET_DIR)/%, $(APPS))

看一下
hello_world
能不能正常输出.

发现如果只留下
hello_world.rs
一个文件还是不能正常输出.

发现
ch2-lab

console.rs
的实现方式和我们之前的不一样.但是难道这样编译出来的指针就不在用户栈或者app加载的地方了吗?这里存疑.

尝试把测例移植到我们自己的代码里边.把
test1_write0.rs

test1_write1.rs
拷贝到
user/src/bin
下面,在
user
下运行
make build
.

运行之后发现
test1_write0
里第一个错误报了
out of range!
但是第二个没导致断言
assert_eq!
卡住程序,这时候具体看两次报错是什么情况,可以发现第一个尝试是在
0x0
写入,第二个是在距离栈顶大小为
5
的位置写入大小为
10
的数据,这提醒我们增加一个对于帧尾的判断:

/// write buf of length `len`  to a file with `fd`
pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize {
    let app_range = get_current_app_range();
    let stack_range = get_user_stack_range();
    // println!("app_range: [{:#x}, {:#x})", app_range.0,app_range.1);
    // println!("stack_range: [{:#x}, {:#x})", stack_range.0,stack_range.1);
    let buf_begin_pointer = buf as usize;
    let buf_end_pointer = unsafe{buf.offset(len as isize)} as usize;
    // println!("buf_pointer: {:#x}", buf_pointer);
    if  ((buf_begin_pointer < app_range.0 || buf_begin_pointer >= app_range.1) && 
        (buf_begin_pointer < stack_range.0 || buf_begin_pointer >= stack_range.1))||
        ((buf_end_pointer < app_range.0 || buf_end_pointer >= app_range.1) &&
        (buf_end_pointer  < stack_range.0 || buf_end_pointer  >= stack_range.1))
    {
        println!("out of range!");
        return -1 as isize;
        // sys_exit(fd as i32)
    }
    match fd {
        FD_STDOUT => {
            let slice = unsafe { core::slice::from_raw_parts(buf, len) };
            let str = core::str::from_utf8(slice).unwrap();
            print!("{}", str);
            len as isize
        }
        _ => {
            panic!("Unsupported fd in sys_write!");
        }
    }
}

这时候再次运行,发现第三次尝试报错,原因是它试图在距离栈底位置距离为
5
的地方写入
10
的数据,这下不管是帧头还算帧尾都在范围内了,那么为什么会是一个测试例子呢(
默认是错误的例子
),因为距离栈底为
5
的位置不在栈里,而后边
5
个单位在栈里,这样就导致了对于全局数据位置和临时数据位置的跨越写入,这时候把代码改成:

/// write buf of length `len`  to a file with `fd`
pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize {
    let app_range = get_current_app_range();
    let stack_range = get_user_stack_range();
    // println!("app_range: [{:#x}, {:#x})", app_range.0,app_range.1);
    // println!("stack_range: [{:#x}, {:#x})", stack_range.0,stack_range.1);
    let buf_begin_pointer = buf as usize;
    let buf_end_pointer = unsafe{buf.offset(len as isize)} as usize;
    // println!("buf_begin_pointer: {:#x}", buf_begin_pointer);
    // println!("buf_end_pointer: {:#x}", buf_end_pointer);
    if !(
            (buf_begin_pointer >= app_range.0 && buf_begin_pointer < app_range.1) && 
            (buf_end_pointer >= app_range.0 && buf_end_pointer < app_range.1)
        )&&
        !(
            (buf_begin_pointer >= stack_range.0 && buf_begin_pointer < stack_range.1) && 
            (buf_end_pointer >= stack_range.0 && buf_end_pointer < stack_range.1)
        )
    {
        println!("out of range!");
        return -1 as isize;
        // sys_exit(fd as i32)
    }
    match fd {
        FD_STDOUT => {
            let slice = unsafe { core::slice::from_raw_parts(buf, len) };
            let str = core::str::from_utf8(slice).unwrap();
            print!("{}", str);
            len as isize
        }
        _ => {
            panic!("Unsupported fd in sys_write!");
        }
    }
}

这时候的运行结果是:

[rustsbi] RustSBI version 0.3.1, adapting to RISC-V SBI v1.0.0
.______       __    __      _______.___________.  _______..______   __
|   _  \     |  |  |  |    /       |           | /       ||   _  \ |  |
|  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  |
|      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  |
|  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  |
| _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__|
[rustsbi] Implementation     : RustSBI-QEMU Version 0.2.0-alpha.2
[rustsbi] Platform Name      : riscv-virtio,qemu
[rustsbi] Platform SMP       : 1
[rustsbi] Platform Memory    : 0x80000000..0x88000000
[rustsbi] Boot HART          : 0
[rustsbi] Device Tree Region : 0x87000000..0x87000f02
[rustsbi] Firmware Address   : 0x80000000
[rustsbi] Supervisor Address : 0x80200000
[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
[rustsbi] pmp02: 0x80000000..0x80200000 (---)
[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
[rustsbi] pmp04: 0x88000000..0x00000000 (-wr)
[kernel] Hello, world!
[kernel] num_app = 7
[kernel] app_0 [0x8020b048, 0x8020c370)
[kernel] app_1 [0x8020c370, 0x8020d740)
[kernel] app_2 [0x8020d740, 0x8020ece8)
[kernel] app_3 [0x8020ece8, 0x802100a0)
[kernel] app_4 [0x802100a0, 0x80211450)
[kernel] app_5 [0x80211450, 0x80212d80)
[kernel] app_6 [0x80212d80, 0x802147c0)
[kernel] Loading app_0
Hello, world!
[kernel] Application exited with code 0
[kernel] Loading app_1
Into Test store_fault, we will insert an invalid store operation...
Kernel should kill this application!
[kernel] PageFault in application, kernel killed it.
[kernel] Loading app_2
3^10000=5079(MOD 10007)
3^20000=8202(MOD 10007)
3^30000=8824(MOD 10007)
3^40000=5750(MOD 10007)
3^50000=3824(MOD 10007)
3^60000=8516(MOD 10007)
3^70000=2510(MOD 10007)
3^80000=9379(MOD 10007)
3^90000=2621(MOD 10007)
3^100000=2749(MOD 10007)
Test power OK!
[kernel] Application exited with code 0
[kernel] Loading app_3
Try to execute privileged instruction in U Mode
Kernel should kill this application!
[kernel] IllegalInstruction in application, kernel killed it.
[kernel] Loading app_4
Try to access privileged CSR in U Mode
Kernel should kill this application!
[kernel] IllegalInstruction in application, kernel killed it.
[kernel] Loading app_5
out of range!
out of range!
out of range!
Test write0 OK!
[kernel] Application exited with code 0
[kernel] Loading app_6
== Begin stack trace ==
0x000000008020149a, fp = 0x0000000080206d40
0x0000000080201c02, fp = 0x0000000080206d80
0x000000008020073c, fp = 0x0000000080206e00
0x0000000080201790, fp = 0x0000000080206ef0
0x0000000080200d8c, fp = 0x0000000080209fc0
0x0000000080400032, fp = 0x000000008020a000
0x0000000000000000, fp = 0x0000000000000000
== End stack trace ==
make: *** [Makefile:64: run-inner] Error 255

第一个测试用例通过了,但是第二个还没有.果然如此,又要继续踩坑.

可以看到是加载之后就出现了BUG,而且报了调用栈,说明是触发了
panic!
.

查看代码,原来是进入了
sys_write

default
的分支,我们试着不报
panic
,把这句注释掉:

/// write buf of length `len`  to a file with `fd`
pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize {
    let app_range = get_current_app_range();
    let stack_range = get_user_stack_range();
    // println!("app_range: [{:#x}, {:#x})", app_range.0,app_range.1);
    // println!("stack_range: [{:#x}, {:#x})", stack_range.0,stack_range.1);
    let buf_begin_pointer = buf as usize;
    let buf_end_pointer = unsafe{buf.offset(len as isize)} as usize;
    // println!("buf_pointer: {:#x}", buf_pointer);
    if !(
            (buf_begin_pointer >= app_range.0 && buf_begin_pointer < app_range.1) && 
            (buf_end_pointer >= app_range.0 && buf_end_pointer < app_range.1)
        )||
        (
            (buf_begin_pointer >= stack_range.0 && buf_begin_pointer < stack_range.1) && 
            (buf_end_pointer >= stack_range.0 && buf_end_pointer < stack_range.1)
        )
    {
        println!("out of range!");
        return -1 as isize;
        // sys_exit(fd as i32)
    }
    match fd {
        FD_STDOUT => {
            let slice = unsafe { core::slice::from_raw_parts(buf, len) };
            let str = core::str::from_utf8(slice).unwrap();
            print!("{}", str);
            len as isize
        }
        _ => {
            -1 as isize
            //panic!("Unsupported fd in sys_write!");
        }
    }
}

OK,就此解决了:

[rustsbi] RustSBI version 0.3.1, adapting to RISC-V SBI v1.0.0
.______       __    __      _______.___________.  _______..______   __
|   _  \     |  |  |  |    /       |           | /       ||   _  \ |  |
|  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  |
|      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  |
|  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  |
| _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__|
[rustsbi] Implementation     : RustSBI-QEMU Version 0.2.0-alpha.2
[rustsbi] Platform Name      : riscv-virtio,qemu
[rustsbi] Platform SMP       : 1
[rustsbi] Platform Memory    : 0x80000000..0x88000000
[rustsbi] Boot HART          : 0
[rustsbi] Device Tree Region : 0x87000000..0x87000f02
[rustsbi] Firmware Address   : 0x80000000
[rustsbi] Supervisor Address : 0x80200000
[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
[rustsbi] pmp02: 0x80000000..0x80200000 (---)
[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
[rustsbi] pmp04: 0x88000000..0x00000000 (-wr)
[kernel] Hello, world!
[kernel] num_app = 7
[kernel] app_0 [0x8020b048, 0x8020c370)
[kernel] app_1 [0x8020c370, 0x8020d740)
[kernel] app_2 [0x8020d740, 0x8020ece8)
[kernel] app_3 [0x8020ece8, 0x802100a0)
[kernel] app_4 [0x802100a0, 0x80211450)
[kernel] app_5 [0x80211450, 0x80212d80)
[kernel] app_6 [0x80212d80, 0x802147c0)
[kernel] Loading app_0
Hello, world!
[kernel] Application exited with code 0
[kernel] Loading app_1
Into Test store_fault, we will insert an invalid store operation...
Kernel should kill this application!
[kernel] PageFault in application, kernel killed it.
[kernel] Loading app_2
3^10000=5079(MOD 10007)
3^20000=8202(MOD 10007)
3^30000=8824(MOD 10007)
3^40000=5750(MOD 10007)
3^50000=3824(MOD 10007)
3^60000=8516(MOD 10007)
3^70000=2510(MOD 10007)
3^80000=9379(MOD 10007)
3^90000=2621(MOD 10007)
3^100000=2749(MOD 10007)
Test power OK!
[kernel] Application exited with code 0
[kernel] Loading app_3
Try to execute privileged instruction in U Mode
Kernel should kill this application!
[kernel] IllegalInstruction in application, kernel killed it.
[kernel] Loading app_4
Try to access privileged CSR in U Mode
Kernel should kill this application!
[kernel] IllegalInstruction in application, kernel killed it.
[kernel] Loading app_5
out of range!
out of range!
out of range!
Test write0 OK!
[kernel] Application exited with code 0
[kernel] Loading app_6
string from data section
strinstring from stack section
strin
Test write1 OK!
[kernel] Application exited with code 0
All applications completed!

这里不要因为解决了就忘记解读第二个测例
test1_write1.rs
,查看它的源码,发现它分别测试的是:

  1. 能否跳过未支持的
    fd
  2. 能否访问
    1. 存在
      data
      段的静态变量
    2. 存在
      stack
      段的局部变量
    3. 存在
      stack
      段的局部变量的部分指针
#![no_std]
#![no_main]

#[macro_use]
extern crate user_lib;
use user_lib::write;
const DATA_STRING: &str = "string from data section\n";

const STDOUT: usize = 1;

/// 正确输出:
/// string from data section
/// strinstring from stack section
/// strin
/// Test write1 OK!

#[no_mangle]
pub fn main() -> i32 {
    assert_eq!(write(1234, DATA_STRING.as_bytes()), -1);
    assert_eq!(
        write(STDOUT, DATA_STRING.as_bytes()),
        DATA_STRING.len() as isize
    );
    assert_eq!(write(STDOUT, &DATA_STRING.as_bytes()[..5]), 5);
    let stack_string = "string from stack section\n";
    assert_eq!(
        write(STDOUT, stack_string.as_bytes()),
        stack_string.len() as isize
    );
    assert_eq!(write(STDOUT, &stack_string.as_bytes()[..5]), 5);
    println!("\nTest write1 OK!");
    0
}

问答作业

1. 正确进入 U 态后,程序的特征还应有:使用 S 态特权指令,访问 S 态寄存器后会报错。请自行测试这些内容 (运行 Rust 三个 bad 测例 ) ,描述程序出错行为,注明你使用的 sbi 及其版本。

这里可以直接看我们在上一部分的运行结果:

[rustsbi] RustSBI version 0.3.1, adapting to RISC-V SBI v1.0.0
.______       __    __      _______.___________.  _______..______   __
|   _  \     |  |  |  |    /       |           | /       ||   _  \ |  |
|  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  |
|      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  |
|  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  |
| _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__|
[rustsbi] Implementation     : RustSBI-QEMU Version 0.2.0-alpha.2
[rustsbi] Platform Name      : riscv-virtio,qemu
[rustsbi] Platform SMP       : 1
[rustsbi] Platform Memory    : 0x80000000..0x88000000
[rustsbi] Boot HART          : 0
[rustsbi] Device Tree Region : 0x87000000..0x87000f02
[rustsbi] Firmware Address   : 0x80000000
[rustsbi] Supervisor Address : 0x80200000
[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
[rustsbi] pmp02: 0x80000000..0x80200000 (---)
[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
[rustsbi] pmp04: 0x88000000..0x00000000 (-wr)
[kernel] Hello, world!
[kernel] num_app = 5
[kernel] app_0 [0x8020a038, 0x8020b360)
[kernel] app_1 [0x8020b360, 0x8020c730)
[kernel] app_2 [0x8020c730, 0x8020dcd8)
[kernel] app_3 [0x8020dcd8, 0x8020f090)
[kernel] app_4 [0x8020f090, 0x80210440)
[kernel] Loading app_0
Hello, world!
[kernel] Application exited with code 0
[kernel] Loading app_1
Into Test store_fault, we will insert an invalid store operation...
Kernel should kill this application!
[kernel] PageFault in application, kernel killed it.
[kernel] Loading app_2
3^10000=5079(MOD 10007)
3^20000=8202(MOD 10007)
3^30000=8824(MOD 10007)
3^40000=5750(MOD 10007)
3^50000=3824(MOD 10007)
3^60000=8516(MOD 10007)
3^70000=2510(MOD 10007)
3^80000=9379(MOD 10007)
3^90000=2621(MOD 10007)
3^100000=2749(MOD 10007)
Test power OK!
[kernel] Application exited with code 0
[kernel] Loading app_3
Try to execute privileged instruction in U Mode
Kernel should kill this application!
[kernel] IllegalInstruction in application, kernel killed it.
[kernel] Loading app_4
Try to access privileged CSR in U Mode
Kernel should kill this application!
[kernel] IllegalInstruction in application, kernel killed it.
All applications completed!

出错行为由
trap_handler
分类和处理:

#[no_mangle]
/// handle an interrupt, exception, or system call from user space
pub fn trap_handler(cx: &mut TrapContext) -> &mut TrapContext {
    let scause = scause::read(); // get trap cause
    let stval = stval::read(); // get extra value
    match scause.cause() {
        Trap::Exception(Exception::UserEnvCall) => {
            cx.sepc += 4;
            cx.x[10] = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]) as usize;
        }
        Trap::Exception(Exception::StoreFault) | Trap::Exception(Exception::StorePageFault) => {
            println!("[kernel] PageFault in application, kernel killed it.");
            run_next_app();
        }
        Trap::Exception(Exception::IllegalInstruction) => {
            println!("[kernel] IllegalInstruction in application, kernel killed it.");
            run_next_app();
        }
        _ => {
            panic!(
                "Unsupported trap {:?}, stval = {:#x}!",
                scause.cause(),
                stval
            );
        }
    }
    cx
}

把其中报错的部分挑出来.

app_1:

[kernel] Loading app_1
Into Test store_fault, we will insert an invalid store operation...
Kernel should kill this application!
[kernel] PageFault in application, kernel killed it.

其源码:

#![no_std]
#![no_main]

#[macro_use]
extern crate user_lib;

#[no_mangle]
fn main() -> i32 {
    println!("Into Test store_fault, we will insert an invalid store operation...");
    println!("Kernel should kill this application!");
    unsafe {
        core::ptr::null_mut::<u8>().write_volatile(0);
    }
    0
}

可以看到它是尝试写入了一个空指针.这时候就会触发
trap_handler
里的
Trap::Exception(Exception::StoreFault)
,意为访问了无效的内存地址.

app_3:

[kernel] Loading app_3
Try to execute privileged instruction in U Mode
Kernel should kill this application!
[kernel] IllegalInstruction in application, kernel killed it.

其源码:

#![no_std]
#![no_main]

#[macro_use]
extern crate user_lib;

use core::arch::asm;

#[no_mangle]
fn main() -> i32 {
    println!("Try to execute privileged instruction in U Mode");
    println!("Kernel should kill this application!");
    unsafe {
        asm!("sret");
    }
    0
}

可见它是尝试调用
sret
,这是需要S特权级的.因此触发
trap_handler

Trap::Exception(Exception::IllegalInstruction)
,意为使用了非法指令.

app_4:

[kernel] Loading app_4
Try to access privileged CSR in U Mode
Kernel should kill this application!
[kernel] IllegalInstruction in application, kernel killed it.

其源码:

#![no_std]
#![no_main]

#[macro_use]
extern crate user_lib;

use riscv::register::sstatus::{self, SPP};

#[no_mangle]
fn main() -> i32 {
    println!("Try to access privileged CSR in U Mode");
    println!("Kernel should kill this application!");
    unsafe {
        sstatus::set_spp(SPP::User);
    }
    0
}

可见它尝试写入
sstatus
,也即尝试访问
S

CSR
,因此触发
trap_handler

Trap::Exception(Exception::IllegalInstruction)
,意为使用了非法指令,但是这里不同的是用指令访问了
S
特权级才能访问的寄存器.

SBI的版本在
os/Cargo.toml
可以看到:

sbi-rt = { version = "0.0.2", features = ["legacy"] }

2. 请结合用例理解
trap.S
中两个函数
__alltraps

__restore
的作用,并回答如下几个问题:

这里是
trap.S
的内容:

.altmacro
.macro SAVE_GP n
    sd x\n, \n*8(sp)
.endm
.macro LOAD_GP n
    ld x\n, \n*8(sp)
.endm
    .section .text
    .globl __alltraps
    .globl __restore
    .align 2
__alltraps:
    csrrw sp, sscratch, sp
    # now sp->kernel stack, sscratch->user stack
    # allocate a TrapContext on kernel stack
    addi sp, sp, -34*8
    # save general-purpose registers
    sd x1, 1*8(sp)
    # skip sp(x2), we will save it later
    sd x3, 3*8(sp)
    # skip tp(x4), application does not use it
    # save x5~x31
    .set n, 5
    .rept 27
        SAVE_GP %n
        .set n, n+1
    .endr
    # we can use t0/t1/t2 freely, because they were saved on kernel stack
    csrr t0, sstatus
    csrr t1, sepc
    sd t0, 32*8(sp)
    sd t1, 33*8(sp)
    # read user stack from sscratch and save it on the kernel stack
    csrr t2, sscratch
    sd t2, 2*8(sp)
    # set input argument of trap_handler(cx: &mut TrapContext)
    mv a0, sp
    call trap_handler

__restore:
    # case1: start running app by __restore
    # case2: back to U after handling trap
    mv sp, a0
    # now sp->kernel stack(after allocated), sscratch->user stack
    # restore sstatus/sepc
    ld t0, 32*8(sp)
    ld t1, 33*8(sp)
    ld t2, 2*8(sp)
    csrw sstatus, t0
    csrw sepc, t1
    csrw sscratch, t2
    # restore general-purpuse registers except sp/tp
    ld x1, 1*8(sp)
    ld x3, 3*8(sp)
    .set n, 5
    .rept 27
        LOAD_GP %n
        .set n, n+1
    .endr
    # release TrapContext on kernel stack
    addi sp, sp, 34*8
    # now sp->kernel stack, sscratch->user stack
    csrrw sp, sscratch, sp
    sret

1. L40:刚进入
__restore
时,
a0
代表了什么值。请指出
__restore
的两种使用情景。

刚刚进入
__restore
的时候是
a0
是我们传入
__restore
的参数,我们在
run_next_app
中调用了这个函数:

/// run next app
pub fn run_next_app() -> ! {
    let mut app_manager = APP_MANAGER.exclusive_access();
    let current_app = app_manager.get_current_app();
    unsafe {
        app_manager.load_app(current_app);
    }
    app_manager.move_to_next_app();
    drop(app_manager);
    // before this we have to drop local variables related to resources manually
    // and release the resources
    extern "C" {
        fn __restore(cx_addr: usize);
    }
    unsafe {
        __restore(KERNEL_STACK.push_context(TrapContext::app_init_context(
            APP_BASE_ADDRESS,
            USER_STACK.get_sp(),
        )) as *const _ as usize);
    }
    panic!("Unreachable in batch::run_current_app!");
}

可以看到传入的是我们主动制造的
TrapContext
的指针,
TrapContext
内容是APP加载位置
APP_BASE_ADDRESS
和用户栈的指针.

它被调用的情景分为两种:

  1. 一个APP运行结束或者出错之后的APP切换
  2. 在内核工作之后开始APP的加载和运行

2. L46-L51:这几行汇编代码特殊处理了哪些寄存器?这些寄存器的的值对于进入用户态有何意义?请分别解释。

代码在此:

    # restore sstatus/sepc
    ld t0, 32*8(sp)
    ld t1, 33*8(sp)
    ld t2, 2*8(sp)
    csrw sstatus, t0
    csrw sepc, t1
    csrw sscratch, t2

这一部分是先把存在栈中的内容转移到
t0~t2
,然后再把它们还原回
sstatus
,
sepc

sscratch
.

这里不能直接移动的原因是
ld
可以操作寄存器的指针偏移,而
csrw
只能操作寄存器和寄存器.

sstatus
:
SPP
等字段给出 Trap 发生之前 CPU 处在哪个特权级(S/U)等信息.

sepc
:当 Trap 是一个异常的时候,记录 Trap 发生之前执行的最后一条指令的地址

sscratch
:是保存
Trap
时的
sscratch
的值,实际上是当时的用户栈的指针.

后续调用
csrrw sp, sscratch, sp
使得
sp
被交换回用户栈的指针.

3. L53-L59:为何跳过了
x2

x4

官方文档
:

  1. 但这里也有一些例外,如
    x0
    被硬编码为 0 ,它自然不会有变化;还有
    tp(x4)
    寄存器,除非我们手动出于一些特殊用途使用它,否则一般也不会被用到。
  2. 我们在这里也不保存 sp(x2),因为我们要基于它来找到每个寄存器应该被保存到的正确的位置。

4. L63:该指令之后,
sp

sscratch
中的值分别有什么意义?

此指令后,sp->user stack sscratch->kernel stack,因此
sp
重新指向用户栈.

5.
__restore
:中发生状态切换在哪一条指令?为何该指令执行之后会进入用户态?

发生状态切换是在
sret
.

硬件上是这个原因,调用它之后寄存器会发生变化.

6. L13:该指令之后,
sp

sscratch
中的值分别有什么意义?

这个指令之后就把
sp

sscratch
切换了,就会导致sp->kernel stack, sscratch->user stack.

7. 从 U 态进入 S 态是哪一条指令发生的?

应该是再用户态的
ecall
指令发生的.

3. 对于任何中断,
__alltraps
中都需要保存所有寄存器吗?你有没有想到一些加速
__alltraps
的方法?简单描述你的想法。

只需要保存中断过程中可能需要改变的寄存器.

这里可提前读取
scause
的内容,根据情况来处理,以本章
trap_handler
为例:

#[no_mangle]
/// handle an interrupt, exception, or system call from user space
pub fn trap_handler(cx: &mut TrapContext) -> &mut TrapContext {
    let scause = scause::read(); // get trap cause
    let stval = stval::read(); // get extra value
    match scause.cause() {
        Trap::Exception(Exception::UserEnvCall) => {
            cx.sepc += 4;
            cx.x[10] = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]) as usize;
        }
        Trap::Exception(Exception::StoreFault) | Trap::Exception(Exception::StorePageFault) => {
            println!("[kernel] PageFault in application, kernel killed it.");
            run_next_app();
        }
        Trap::Exception(Exception::IllegalInstruction) => {
            println!("[kernel] IllegalInstruction in application, kernel killed it.");
            run_next_app();
        }
        _ => {
            panic!(
                "Unsupported trap {:?}, stval = {:#x}!",
                scause.cause(),
                stval
            );
        }
    }
    cx
}

当检测到
scause

Environment call from U-mode
的时候进行相应的调用,因此需要保存上下文,而遇到
Store access fault

Illegal instruction
的时候则不需要保存.

这里是
mcause
的位描述,
scause
是它的一个子集,是一样的布局:

采集数据->采样率调整

  1. 使用
    torchaudio
    进行重采样(cpu版)

    • 首先导入相关包,既然使用torch作为我们的选项,安装torch环境我就不必多说了,如果你不想用torch可以使用后文提到的另一个库


      1 importtorch2 importtorchaudio3 from torchaudio.transforms importResample4 from time import time#仅计算时间,不影响主体
    • 使用
      torchaudio.load
      导入音频文件

    • 设定目标采样率并构造
      resample
      函数

    • 调用构造好的
      resample
      函数

    • 调用
      torchaudio
      的保存函数


    封装一下,总函数【记得先导入】:


    1 defresample_by_cpu():2     file_path = input("please input your file path:")3     start_time = time()#不影响,可去掉
    4     y, sr = torchaudio.load(file_path)  #使用torchaudio.load导入音频文件
    5 6     target_sample = 32000   #设定目标采样率
    7     resampler = Resample(orig_freq=sr, new_freq=target_sample)#构造resample函数,输入原始采样率和目标采样率
    8     resample_misic = resampler(y)                             #调用resample函数
    9 10     torchaudio.save("test.mp3", resample_misic, target_sample)#调用torchaudio的保存即可
    11     print(f"cost :{time() - start_time}s")#不影响,可去掉

    最后结果大概是几秒钟这样子


    1. 使用使用
      torchaudio
      进行重采样(gpu版):

      有了上面cpu的基础,其实调用gpu也就更换一下设备,和放入gpu的操作就好了,因此不过多赘述


      defresample_use_cuda():

      device
      = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
      start_time
      =time()
      file_path
      = input("please input your file path:")
      y, sr
      =torchaudio.load(file_path)

      y
      =y.to(device)
      target_sample
      = 32000resampler= Resample(orig_freq=sr, new_freq=target_sample).to(device)
      resample_misic
      =resampler(y)
      torchaudio.save(
      "test.mp3", resample_misic.to('cpu'), target_sample) #这里注意要把结果从gpu中拿出来到cpu,不然会报错。 print(f"cost :{time() - start_time}s")

      时间方面嘛,单个音频多了放入gpu取出gpu的步骤肯定会稍慢的,但是跑过cuda都知道它的强大,更多是用于后续的操作说是。

    2. 使用librosa库进行重采样

      具体步骤:


      • 导入两个库文件,
        librosa
        和音频文件读写库
        soundfile

        import librosa
        import soundfile as sf
        from time import time#仅计算时间,不影响主体
      • 导入音频文件

      • 设定目标采样率

      • 重采样

      • 输出


      综合封装成函数:


      1 defresample_by_lisa():2     file_path = input("please input your file path:")3     start_time =time()4     y, sr = librosa.load(file_path)     #使用librosa导入音频文件
      5     target_sample_rate = 32000
      6     y_32k = librosa.resample(y=y, orig_sr=sr, target_sr=target_sample_rate)         #使用librosa进行重采样至目标采样率
      7     sf.write("test_lisa.mp3", data=y_32k, samplerate=target_sample_rate)        #使用soundfile进行文件写入
      8     print(f"cost :{time() - start_time}s")

      总结:


      • 优点,简单小巧,
        ibrosa
        有很多能处理音频的功能

      • 缺点:无法调用cuda,保存的时候需要依赖
        soundfile
        库。

      • 时间:也是几秒左右,和
        torchaudio
        cpu版差不多

      • 小声bb:提取32k的效果好像没有torchaudio好【嘛,毕竟librosa历史有点久了,没有专注深度学习的torch好很正常啦】,你们也可以自己测一下


    all code:


    1 importtorch2 importtorchaudio3 from torchaudio.transforms importResample4 importlibrosa5 importsoundfile as sf6 from time importtime7 8 defresample_by_cpu():9     file_path = input("please input your file path:")10     start_time =time()11     y, sr = torchaudio.load(file_path)  #使用torchaudio.load导入音频文件
    12 13     target_sample = 32000   #设定目标采样率
    14     resampler = Resample(orig_freq=sr, new_freq=target_sample)#构造resample函数,输入原始采样率和目标采样率
    15     resample_misic = resampler(y)                             #调用resample函数
    16 17     torchaudio.save("test.mp3", resample_misic, target_sample)#调用torchaudio的保存即可
    18     print(f"cost :{time() - start_time}s")19 defresample_use_cuda():20 21     device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')22     start_time =time()23     file_path = input("please input your file path:")24     y, sr =torchaudio.load(file_path)25 26     y =y.to(device)27     target_sample = 32000
    28     resampler = Resample(orig_freq=sr, new_freq=target_sample).to(device)29     resample_misic =resampler(y)30     torchaudio.save("test.mp3", resample_misic.to('cpu'), target_sample)31     print(f"cost :{time() - start_time}s")32 33 defresample_by_lisa():34     file_path = input("please input your file path:")35     start_time =time()36     y, sr = librosa.load(file_path)#使用librosa导入音频文件
    37     target_sample_rate = 32000
    38     y_32k = librosa.resample(y=y, orig_sr=sr, target_sr=target_sample_rate)#使用librosa进行重采样至目标采样率
    39     sf.write("test_lisa.mp3", data=y_32k, samplerate=target_sample_rate)#使用soundfile进行文件写入
    40     print(f"cost :{time() - start_time}s")41 42 if __name__ == '__main__':43 resample_use_cuda()44 resample_by_cpu()45     resample_by_lisa()

2.2 提取pitch基频特征【音高提取】

  1. 使用
    torchaudio
    进行基频特征提取

    其实主要使用的这个函数:
    torchaudio.transforms._transforms.PitchShift

    让我们来看看它官方的
    example
    ,仿照着来写就好啦


    >>> waveform, sample_rate = torchaudio.load("test.wav", normalize=True)>>> transform = transforms.PitchShift(sample_rate, 4)>>> waveform_shift = transform(waveform)  #(channel, time)

    步骤:


    • 导入依赖


      importtorchaudioimporttorchaudio.transforms as Tfimport matplotlib.pyplot as plt     #画图依赖

    • 导入音频

    • 构造
      PitchShift

    • 使用这个函数对歌曲进行基频提取


    code:


    defget_pitch_by_torch():
    file_path
    = input("file path:")
    y, sr
    =torchaudio.load(file_path)"""specimen:
    >>> waveform, sample_rate = torchaudio.load("test.wav", normalize=True)
    >>> transform = transforms.PitchShift(sample_rate, 4)
    >>> waveform_shift = transform(waveform) # (channel, time)
    """pitch_tf= Tf.PitchShift(sample_rate=sr, n_steps=0)
    feature
    =pitch_tf(y)#绘制基频特征 这部分可以忽略,只是画图而已,可以直接复制不用理解 plt.figure(figsize=(16, 5))
    plt.plot(feature[0].numpy(), label
    ='Pitch')
    plt.xlabel(
    'Frame')
    plt.ylabel(
    'Frequency (Hz)')
    plt.title(
    'Pitch Estimation')
    plt.legend()
    plt.show()

    输出图片【总歌曲】效果:

    image-20240801144650461

    将输出的范围稍微改一下,切分特征的一部分,就是歌曲部分的音高特征啦,效果就很明显了

    改为:
    plt.plot(feature[0][5000:10000].numpy(), label='Pitch')

    image-20240801145201858

  2. 使用librosa提取基频特征

    • 步骤:


      • 导入包

      • 提取基频特征

      • (可选)绘制基频特征

    • 主要函数:
      librosa.pyin
      ,请见官方example


    #Computing a fundamental frequency (F0) curve from an audio input
    >>> y, sr = librosa.load(librosa.ex('trumpet'))
    >>> f0, voiced_flag, voiced_probs = librosa.pyin(y,
    ...                                              sr=sr,
    ...                                              fmin=librosa.note_to_hz('C2'),
    ...                                              fmax=librosa.note_to_hz('C7'))
    >>> times = librosa.times_like(f0, sr=sr)

    code:


    1 defget_pitch_by_librosa():2 3     file_path = input("请输入音频文件路径:")4     y, sr =librosa.load(file_path)5     """librosa.pyin(y,sr=sr,fmin=librosa.note_to_hz('C2'),fmax=librosa.note_to_hz('C7'))"""
    6     #使用pyin提取基频特征
    7     f0, voiced_flag, voiced_probs = librosa.pyin(y, sr=sr, fmin=librosa.note_to_hz('C2'), fmax=librosa.note_to_hz('C7'))8 9     #绘制基频特征,可忽略
    10     plt.figure(figsize=(14, 5))11     librosa.display.waveshow(y, sr=sr, alpha=0.5)12     plt.plot(librosa.times_like(f0), f0, label='f0 (fundamental frequency)', color='r')13     plt.xlabel('Time (s)')14     plt.ylabel('Frequency (Hz)')15     plt.title('Pitch (fundamental frequency) Estimation')16 plt.legend()17     plt.show()

    • 总结:


      • 比torchaudio略微麻烦一点,不过多了两个参数
        voiced_flag, voiced_probs
        ,看起来的视觉图好像也有些不一样,不过都是按照官方的这个来了,这也不对的话我也不会了

    • 输出:

      image-20240801151606191

  3. all code:

    importtorchaudioimporttorchaudio.transforms as Tfimportmatplotlib.pyplot as pltimportlibrosadefget_pitch_by_torch():
    file_path
    = input("file path:")
    y, sr
    =torchaudio.load(file_path)"""specimen:
    >>> waveform, sample_rate = torchaudio.load("test.wav", normalize=True)
    >>> transform = transforms.PitchShift(sample_rate, 4)
    >>> waveform_shift = transform(waveform) # (channel, time)
    """pitch_tf= Tf.PitchShift(sample_rate=sr, n_steps=0)
    feature
    =pitch_tf(y)#绘制基频特征 plt.figure(figsize=(16, 5))
    plt.plot(feature[0][
    5000:10000].numpy(), label='Pitch')
    plt.xlabel(
    'Frame')
    plt.ylabel(
    'Frequency (Hz)')
    plt.title(
    'Pitch Estimation')
    plt.legend()
    plt.show()
    defget_pitch_by_librosa():

    file_path
    = input("请输入音频文件路径:")
    y, sr
    =librosa.load(file_path)"""librosa.pyin(y,sr=sr,fmin=librosa.note_to_hz('C2'),fmax=librosa.note_to_hz('C7'))""" #使用pyin提取基频特征 f0, voiced_flag, voiced_probs = librosa.pyin(y, sr=sr, fmin=librosa.note_to_hz('C2'), fmax=librosa.note_to_hz('C7'))
    #绘制基频特征,可忽略 plt.figure(figsize=(14, 5))
    librosa.display.waveshow(y, sr
    =sr, alpha=0.5)
    plt.plot(librosa.times_like(f0), f0, label
    ='f0 (fundamental frequency)', color='r')
    plt.xlabel(
    'Time (s)')
    plt.ylabel(
    'Frequency (Hz)')
    plt.title(
    'Pitch (fundamental frequency) Estimation')
    plt.legend()
    plt.show()
    if __name__ == '__main__':#get_pitch_by_torch() #get_pitch_by_librosa()

    后续PPG特征、
    vec特征见下一章

1、OrthoFinder 教程:
用于比较基因组学的系统发育直系学推断

1.1 orthofinder介绍

OrthoFinder是一种快速、准确和全面的比较基因组学分析工具。它可以找到直系和正群,为所有的正群推断基因树,并为所分析的物种推断一个有根的物种树。OrthoFinder还为比较基因组分析提供全面的统计数据。OrthoFinder使用简单,只需运行一组FASTA格式的蛋白质序列文件(每个物种一个)。

1.2 Orthofinder安装

建议使用conda安装,简单方便,不用担心其他依赖和glibc版本的问题

1.创建一个工作目录。打开终端并运行以下命令:

mkdir ~/orthofinder_tutorial
cd ~/orthofinder_tutorial

2.下载最新版本的 OrthoFinder(如果您使用 Bioconda,您仍需要执行此步骤来获取示例数据集)

wget https://github.com/davidemms/OrthoFinder/releases/latest/download/OrthoFinder.tar.gz

如果你没有安装 wget,你可以试试 curl:

curl -L -O https://github.com/davidemms/OrthoFinder/releases/latest/download/OrthoFinder.tar.gz

或者去GitHub发布页面下载OrthoFinder:
https ://github.com/davidemms/OrthoFinder/releases

3.解压包,cd到OrthoFinder目录

tar xzvf OrthoFinder.tar.gz
cd OrthoFinder/

4.打印orthofinder帮助文件

 orthofinder -h

5.在示例数据集上运行 OrthoFinder

 orthofinder -f ExampleData/

你将会得到以下结果:

1.3 orhtofinder的使用示例

对一组模型生物进行系统基因组分析:小鼠、人类、青蛙、斑马鱼、日本河豚


https://www.ensembl.org/
,寻找相关基因
[2]

OrthoFinder 需要输入你感兴趣的物种中所有蛋白质编码基因的氨基酸序列。每个物种的序列应该在一个单独的文件中,文件扩展名为“.fa”、“.faa”、“.fasta”、“.fas”或“.pep”。当一个物种的基因组被测序并可用时,会执行两个主要步骤,即组装和注释。组装是将单个读数拼接到基因组序列中。注释是对基因组组装中感兴趣的特征的识别,例如蛋白质编码基因。因此,我们需要的文件通常位于名为“注释”的部分中。在 Ensembl 的右侧,在“基因注释”下单击“下载 FASTA”。如图:

单击“pep”文件夹(其中包含肽序列),然后将文件“Homo_sapiens.GRCh38.pep.all.fa.gz”下载到创建的文件夹中。

重复这一步骤下载其他物种序列信息,之后使用
gunzip *.gz
解压相关文件。

为加快运行时间,使用orthofinder提供的python脚本,提取每个基因的最长的转录变体

for f in *fa ; do python ~/orthofinder_tutorial/OrthoFinder/tools/primary_transcript.py $f ; done

你将会得到primary_transcripts文件夹,之后运行

nohup orthofinder -f primary_transcripts/ &

运行时间比较长,你将会得到以下结果

一些重要概念
[1、3]

  • Species-specific orthogroup: 一个仅来源于一个物种的直系同源组
  • Single-copy orthogroup: 在直系同源组中,每个物种里面只有一个基因。我们会用单拷贝直系同源组里的基因推断物种树以及其他数据分析。
  • Unassigned gene: 无法和其他基因进行聚类的基因。
  • G50和O50,指的是当你直系同源组按照基因数从大到小进行排列,然后累加,当加入某个组后,累计基因数大于50%的总基因数,那么所需要的直系同源组的数目就是O50,该组的基因树就是G50.
Orthogroups, Orthologs 和 Paralogs 这三个概念推荐看图理解。

1.4 结果文件解读[3]

运行标准OrthoFinder会产生一系列文件:直系同源组、直系同源、基因树、物种树、基因复制事件、比较基因组学数据。结果文件在所分析物种的.fa文件目录下。

(1)Orthogroups 直系同源组目录
[2]

Orthogroups.tsv:用制表符分隔的文件,每一行包含属于单个直系同源组的基因。每个物种的直系同源组的基因单独排成一列。

Orthogroups.txt: 类似于Orthogroups.tsv,只不过是 OrthoMCL的输出格式。

Orthogroups_UnassignedGenes.tsv:用制表符分隔开的文件,格式上与Orthogroups.tsv相同,但包含的是未分配到任何直系同源组的基因。

Orthogroups_SingleCopyOrthologues.txt:每个物种正好包含一个基因的直系同源群列表,即它们包含一对一的直系同源物。 这种直系同源组非常适合进行种间比较和种树推断。

Orthogroups.GeneCount.tsv:格式同Orthogroups.tsv。每一行为一个直系同源组,每一列是每个物种每个直系同源组的基因数目。

(2)Orthologues 直系同源目录

在这个目录中,每个物种都有一个子目录。该子目录又包括每个物种对的比较文件。直系同源可以是一对一、一对多或多对多,这取决于基因复制事件。文件中的每一行都包含一个物种中的基因,而该基因是另一物种中该基因的直系同源物,并且每一行都被交叉引用到包含这些基因的直系同源组中。

(3)Comparative_Genomics_Statistics 比较基因组学数据目录
[2]

Statistics_Overall.tsv:用制表符分隔开的文件,其中包含有直系同源组分析的常规统计信息。(总体统计信息)

Statistics_PerSpecies.tsv:用制表符分隔开的文件,内容与Statistics_Overall.tsv相似,但分物种。(分物种统计信息)

Duplications_per_Orthogroup.tsv:制表符分隔开的文件,该文件给出每个直系同源组中标识的复制项的数量。

Duplications_per_Species_Tree_Node.tsv:制表符分隔开的文件,该文件给出了沿物种树的每个分支发生的复制次数。

Orthogroups_SpeciesOverlaps.tsv:制表符分隔开的文件,包含每个物种对共享的直系同源群数目。

OrthologuesStats_?-to-?.tsv:制表符分隔开的文件,包含矩阵。这些矩阵给出了每对物种之间一对一,一对多和多对多关系的直系同源物数量。

(4)Gene_Duplication_Events 基因复制事件目录

Duplications.tsv:制表符分隔的文件,其中列出了所有基因复制事件,这些事件是通过检查每个直系同源群基因树的每个节点来标识的。

SpeciesTree_Gene_Duplications_0.5_Support.txt:提供了物种树分支上上述复制的总和。 它是newick格式的文本文件。 每个节点/物种名称后面的数字是在导致节点/物种的分支上发生的具有至少50%支持的基因复制事件的数量。

(5)Gene_Trees 基因树目录

为每个直系同源群推断的系统发育树。

(6)Orthogroup_Sequences 直系同源组序列目录

每个直系同源群的FASTA文件,给出了每个直系同源群中每个基因的氨基酸序列。

(7)Resolved_Gene_Trees 解析的基因树目录

为每个直系同源组推断出有根的系统发育树,使用OrthoFinder复制损失合并模型进行解析。

(8)Single_Copy_Orthologue_Sequences 单拷贝直系同源组序列目录

与直系同源组序列目录相似的文件。每个物种一对一的直系同源组。

(9)Species_Tree 物种树目录

SpeciesTree_rooted.txt:从所有直系同源组推断出的STAG物种树,包含内部节点上的STAG支持值,并以STRIDE(Species Tree Root Inference from Duplication Events)为根。STAG(Species Tree from All Genes)是一种从所有基因推测物种树的算法,不同于使用单拷贝的直系同源基因进行进化树构建。

SpeciesTree_rooted_node_labels.csv:同上,但是节点具有标签,以允许其他结果文件交叉引用物种树中的分支/节点(例如基因复制事件的位置)。

本节参考文献:

  1. 「基因组学」使用OrthoFinder进行直系同源基因分析
  2. OrthoFinder-用于比较基因组学的系统发育直系学推断
  3. orthofinder结果解读

2.系统发育树的构建

利用orthofinder寻找单拷贝基因构建系统发育树