Update chapter2.md

pull/1/head
Zhiyuan Shao 4 years ago
parent e9677edc38
commit 1ed8cc61b8

@ -1,6 +1,6 @@
## 第二章实验1非法指令的截获
# 第二章实验1非法指令的截获
### 2.1 实验环境搭建
## 2.1 实验环境搭建
实验环境我们推荐采用Ubuntu 16.04LTS或18.04LTSx86_64操作系统我们未在其他系统如archRHEL等上做过测试但理论上只要将实验中所涉及到的安装包替换成其他系统中的等效软件包就可完成同样效果。另外我们在EduCoder实验平台网址https://www.educoder.net 上创建了本书的同步课程课程的终端环境中已完成实验所需软件工具的安装所以如果读者是在EduCoder平台上选择的本课程则可跳过本节的实验环境搭建过程直接进入通过终端命令行进入实验环境。
@ -8,7 +8,7 @@ PKE实验涉及到的软件工具有RISC-V交叉编译器、spike模拟器
**我们强烈建议读者在新装环境中完整构建buildRISC-V交叉编译器以及spike模拟器**。如果强调环境的可移植性,可以考虑在虚拟机中安装完整系统和环境,之后将虚拟机进行克隆和迁移。
#### 2.1.1 RISC-V交叉编译器
### 2.1.1 RISC-V交叉编译器
RISC-V交叉编译器是与Linux自带的GCC编译器类似的一套工具软件集合不同的是x86_64平台上Linux自带的GCC编译器会将源代码编译、链接成为适合在x86_64平台上运行的二进制代码称为native code而RISC-V交叉编译器则会将源代码编译、链接成为在RISC-V平台上运行的代码。后者RISC-V交叉编译器生成的二进制代码是无法在x86_64平台即x86_64架构的Ubuntu环境下直接运行的它的运行需要模拟器我们采用的spike的支持。
@ -44,8 +44,6 @@ RISC-V交叉编译器的构建需要一些本地支撑软件包可使用以
`$ make`
`$ make install`
以上命令中,[your.RISCV.install.path]指向的是你的RISC-V交叉编译器安装目录。如果安装是你home目录下的一个子目录如~/riscv-install-dir则最后的make install无需sudoer权限。但如果安装目录是系统目录如/opt/riscv-install-dir则需要sudoer权限即在make install命令前加上sudo
● 第四步,设置环境变量
@ -54,13 +52,9 @@ RISC-V交叉编译器的构建需要一些本地支撑软件包可使用以
`$ export PATH=$PATH:$RISCV/bin`
以上命令设置了RISCV环境变量指向在第三步中的安装目录并且将交叉编译器的可执行文件所在的目录加入到了系统路径中。这样我们就可以在PKE的工作目录调用RISC-V交叉编译器所包含的工具软件了。这时你也可以在任意目录编写helloworld.c文件并使用以下命令测试下安装是否成功
`$ riscv64-unknown-elf-gcc helloworld.c -o helloworld`
以上命令设置了RISCV环境变量指向在第三步中的安装目录并且将交叉编译器的可执行文件所在的目录加入到了系统路径中。这样我们就可以在PKE的工作目录调用RISC-V交叉编译器所包含的工具软件了。
若编译成功当前的目录下会出现名为helloworld的elf文件。
#### 2.1.2 spike模拟器
### 2.1.2 spike模拟器
接下来安装spkie模拟器。首先取得spike的源代码有两个途径一个是从github代码仓库中获取
@ -80,7 +74,7 @@ RISC-V交叉编译器的构建需要一些本地支撑软件包可使用以
在以上命令中我们假设RISCV环境变量已经指向了RISC-V交叉编译器的安装目录。如果未建立关联可以将$RISCV替换为2.1.1节中的[your.RISCV.install.path]。
#### 2.1.3 PKE
### 2.1.3 PKE
到github下载课程仓库
@ -132,7 +126,7 @@ RISC-V交叉编译器的构建需要一些本地支撑软件包可使用以
以上命令完成后会在当前目录下会生产obj子目录里面就包含了我们的pke代理内核。pke代理内核的构建过程将在2.2节中详细讨论。
#### 2.1.4 环境测试
### 2.1.4 环境测试
全部安装完毕后你可以对环境进行测试在pke目录下输入
@ -151,18 +145,12 @@ you need add your code!
这里,代理内核的作用是只为特定的应用服务(如本例中的./app/elf/app1_2应用所以可以做到“看菜吃饭”的效果。因为我们这里的应用非常简单所以pke就可以做得极其精简它没有文件系统、没有物理内存管理、没有进程调度、没有操作终端shell等等传统的完整操作系统“必须”具有的组件。在后续的实验中我们将不断提升应用的复杂度并不断完善代理内核。通过这个过程读者将深刻体会操作系统内核对应用支持的机制以及具体的实现细节。
## 2.2 实验内容
### 2.2 实验内容
实验要求在用户模式APP里调用非法指令如S或M级别的指令或进行非法内存访问导致系统报错。如illegal instruction或者内存访问越界报警。
注意:以后的实验,要基于本实验,使得代理内核能够捕捉非法指令和内存访问。
### 应用: ###
**2.2.1 练习一hello world**
应用1helloworld.c代码如下
首先进入app目录下。我们先来编写一个简单的hello world程序我们编写helloworld.c源文件如下
```
1 #include <stdio.h>
2 int global_init=1;
3 int global_uninit;
@ -171,36 +159,81 @@ you need add your code!
6 printf("hello world!\n");
7 return 0;
8 }
```
例2.1 helloworld.c
应用说明以上应用通过调用库函数printf再标准输出上打印出字符串`hello world`。
应用2源代码见pke/app/elf/app1_1.c代码如下
1 #include<stdio.h>
2
3 long unsigned int addr_line=0x7f7ea000;
4 int main()
5 {
6
7 long unsigned int addr_u = 0x7f7ecc00;
8 long unsigned int addr_m = 0x8f000000;
9
10 //在用户模式下访问用户空间地址
11 printf("APP: addr_u 0x%016lx\n",addr_u);
12 *(int *)(addr_u)=1;
13
14 //用户模式下访问内核空间地址,会引发段错误
15 printf("APP: addr_m 0x%016lx\n",addr_m);
16 *(int *)(addr_m)=1;
17
18 return 0;
19 }
应用说明以上应用在第七行、第八行分别指定了两个地址其中第七行的地址属于用户空间第八行的地址属于内核空间。随即再分别对这两个地址进行访问在用户模式APP里对内核空间地址进行访问属于非法内存访问这会导致异常的产生。
应用3源代码见pke/app/elf/app1_2.c代码如下
1 #include<stdio.h>
2
3 #define test_csrw() ({ \
4 asm volatile ("csrw sscratch, 0"); })
5
6 int main()
7 {
8 printf("user mode test illegal instruction!\n");
9 // 在用户态调用内核态的指令属于非法,会引发非法指令错误!
10 test_csrw();
11 return 0;
12 }
应用说明:以上应用,使用内联汇编`csrw`对`sscratch`CSR寄存器进行写操作在用户模式APP里调用非法指令S级别的指令会抛出异常`illegal instruction`。
----------
### 任务1 打印hello world(编程) ###
任务描述: 使用编辑器将以上的helloworld.c的代码输入并存放在pke/app目录中。
使用riscv64-unknown-elf-gcc编译该文件得到的ELF文件helloworld。
确认以上交叉编译器、spike、pke已安装成功接下来进入pke/app目录编译helloworld.c执行以下命令
`$riscv64-unknown-elf-gcc helloworld.c -o elf/helloworld`
现在回到上一级目录使用pke来运行二进制文件
接下来回到上一级目录使用spike+pke来运行helloworld二进制文件执行以下命令
`$spike obj/pke app/elf/helloworld`
​你可以得到以下输出:
```
PKE IS RUNNING
hello world!
```
**2.2.2 练习二:中断入口探寻**
CPU 运行到一些情况下会产生异常exception 例如访问无效的内存地址、执行非法指令除零、发生缺页等。用户程序进行系统调用syscall 或程序运行到断点breakpoint 时,也会主动触发异常。
### 任务2 : 中断入口探寻(理解) ###
任务描述: CPU 运行到一些情况下会产生异常exception 例如访问无效的内存地址、执行非法指令除零、发生缺页等。用户程序进行系统调用syscall 或程序运行到断点breakpoint 时,也会主动触发异常。
当发生中断或异常时CPU 会立即跳转到一个预先设置好的地址执行中断处理程序最后恢复原程序的执行。这个地址。我们称为中断入口地址。在RISC-V中设有专门的CSR寄存器保存这个地址即stvec寄存器。
下面请你阅读pk/pk.c文件找出pk中设置中断入口函数的位置。
任务目标: 请你阅读pk/pk.c文件找出pk中设置中断入口函数的位置。
**2.2.3 练习三:中断过程详究**
### 任务3 : 中断过程详究(理解) ###
中断的处理过程可以分为3步骤
任务描述: 中断的处理过程可以分为3步骤
- 保存当前环境寄存器
- 进入具体的中断异常处理函数
@ -208,7 +241,6 @@ CPU 运行到一些情况下会产生异常exception ,例如访问无效
pk中使用trapframe_t结构体pk/pk.h来保存中断发生时常用的32个寄存器及部分特殊寄存器的值其结构如下。
```
typedef struct
{
long gpr[32];
@ -218,27 +250,27 @@ typedef struct
long cause;
long insn;
} trapframe_t;
```
下面请阅读pk/entry.S详细分析同上述三个过程相对应的代码。
**2.2.4 练习四:中断的具体处理(需要编程)**
当中断异常发生后中断帧将会被传递给pk/handlers.c中的handle_trap函数。接着通过trapframe中的scause寄存器的值可以判断属于哪种中断异常从而选择相对应的中断处理函数。
任务目标: 阅读pk/entry.S详细分析同上述三个过程相对应的代码。
### 任务4: 中断的具体处理 (编程) ###
任务描述: 当中断异常发生后中断帧将会被传递给pk/handlers.c中的handle_trap函数。接着通过trapframe中的scause寄存器的值可以判断属于哪种中断异常从而选择相对应的中断处理函数。
在pk/handlers.c中的各种中断处理函数的实现其中segfault段错误的处理函数与illegal_instruction的处理函数并不完善。请你在pk/handlers.c中找到并完善segfault与handle_illegal_instruction两个函数。
提示:
预期输出
当完成你的segfault代码后,重新make然后输入如下命令
当完成segfault代码后重新make pke然后输入如下命令
`$riscv64-unknown-elf-gcc ../app/app1_1.c -o ../app/elf/app1_1`
`$spike ./obj/pke app/elf/app1_1`
预期的输出如下:
得到以下预期的输出
```
PKE IS RUNNING
APP: addr_u 0x7f7ecc00
APP: addr_m 0x8f000000
@ -252,7 +284,7 @@ s8 0000000000000000 s9 0000000000000000 sA 0000000000000000 sB 0000000000000000
t3 0000000000000000 t4 0000000000000078 t5 0000000000000000 t6 0000000000000000
pc 0000000000010198 va 000000008f000000 insn ffffffff sr 8000000200046020
User store segfault @ 0x000000008f000000
```
接着当你完成handle_illegal_instruction函数后输入如下命令
@ -260,9 +292,8 @@ User store segfault @ 0x000000008f000000
`$ spike ./obj/pke app/elf/app1_2`
预期的输出如下
得到以下预期的输出:
```
PKE IS RUNNING
user mode test illegal instruction!
z 0000000000000000 ra 0000000000010162 sp 000000007f7ecb40 gp 0000000000013de8
@ -275,28 +306,25 @@ s8 0000000000000000 s9 0000000000000000 sA 0000000000000000 sB 0000000000000000
t3 0000000000000000 t4 000000005f195e48 t5 0000000000000000 t6 0000000000000000
pc 0000000000010162 va 0000000014005073 insn 14005073 sr 8000000200046020
An illegal instruction was executed!
```
如果你的两个测试app都可以正确输出的话那么运行检查的python脚本
如果你的两个测试app都可以以上输出的话可以接着运行检查的python脚本
`$./pke-lab1`
若得到如下输出,那么恭喜你,你已经成功完成了实验一
若得到如下输出,那么恭喜你,你已经成功完成了实验一
```
build pk : OK
running app1 : OK
test1 : OK
running app2 : OK
test2 : OK
Score: 30/30
```
## 2.3 实验指导
### 2.2 基础知识
**2.2.1 程序编译连接与ELF文件**
**2.3.1 程序编译连接与ELF文件**
ELF的全称为Executable and Linkable Format是一种可执行二进制文件。
@ -326,7 +354,7 @@ l .comment节注释部分这一部分不会被加载到内存。
在pke中ELF文件头结构的定义如下
```
typedef struct {
uint8_t e_ident[16]; //ELF文件标识包含用以表示ELF文件的字符
uint16_t e_type; //文件类型
@ -343,7 +371,7 @@ typedef struct {
uint16_t e_shnum; //节头部个数
uint16_t e_shstrndx; //节头部字符索引
} Elf64_Ehdr;
```
ELF文件头比较重要的几个结构体成员是e_entry、e_phoff、e_phnum、e_shoff、e_shnum。其中e_entry是可执行程序的入口地址即从内存的这个位置开始执行在这里入口地址是虚拟地址也就是链接地址e_phoff和e_phnum可以用来找到所有的程序头表项e_phoff是程序头表的第一项相对于ELF文件的开始位置的偏移而e_phnum则是表项的个数同理e_ shoff和e_ shnum可以用来找到所有的节头表项。
@ -355,7 +383,7 @@ ELF文件头比较重要的几个结构体成员是e_entry、e_phoff、e_phnum
得到的输入文件helloworld.txt的主要内容如下
```
hellowrold: file format elf64-littleriscv
architecture: riscv:rv64, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
@ -390,13 +418,12 @@ Idx Name Size VMA LMA File off Algn
CONTENTS, READONLY
10 .riscv.attributes 00000035 0000000000000000 0000000000000000 00003551 2**0
CONTENTS, READONLY
```
可以看到解析出来的文件结构与图2.1的结构相对应。其中值得注意的是,.bss节与.comment节在文件中的偏移是一样的这就说明.bss在硬盘中式不占用空间的仅仅只是记载了它的长度。
Program Header程序头表实际上是将文件的内容分成了好几个段而每个表项就代表了一个段这里的段是不同于之前节的概念有可能就是同时几个节包含在同一个段里。程序头表项的数据结构如下所示
```
typedef struct {
uint32_t p_type; //段类型
uint32_t p_flags; //段标志
@ -407,7 +434,7 @@ typedef struct {
uint64_t p_memsz; //段在内存中的长度
uint64_t p_align; //段在内存中的对齐标志
} Elf64_Phdr;
```
下面我们通过一个图来看看用ELF文件头与程序头表项如何找到文件的第i段。
@ -417,7 +444,7 @@ typedef struct {
Sections而另一个节头表的功能则是让程序能够找到特定的某一节其中节头表项的数据结构如下所示
```
typedef struct {
uint32_t sh_name; //节名称
uint32_t sh_type; //节类型
@ -430,95 +457,93 @@ typedef struct {
uint64_t sh_addralign; //字节对齐标志
uint64_t sh_entsize; //表项大小
} Elf64_Shdr;
```
而通过ELF文件头与节头表找到文件的某一节的方式和之前所说的找到某一段的方式是类似的。
**2.2.2** 代理内核与应用程序的加载
**2.3.2 代理内核与应用程序的加载**
阅读pke.lds文件可以看到整个PK程序的入口为reset_vector函数
```
3 OUTPUT_ARCH( "riscv" )
4
5 ENTRY( reset_vector )
```
我们在machine/mentry.S中找的这个符号。
```
36 reset_vector:
37 j do_reset
```
首先初始化x0~x31共32个寄存器其中x10a0寄存器与x11a1寄存器存储着从之前boot loader中传来的参数而不复位。
```
223 do_reset:
224 li x1, 0
.....
255 li x31, 0
```
将mscratch寄存器置0
```
256 csrw mscratch, x0
```
将trap_vector的地址写入t0寄存器trap_vector是mechine模式下异常处理的入口地址。再将t0的值写入mtvec寄存器中。然后读取mtvec寄存器中的地址到t1寄存器。比较t0于t1。
```
259 la t0, trap_vector
260 mtvec, t0
261 rr t1, mtvec
262 1:bne t0, t1, 1b
```
正常情况下t1自然是的等于t0的于是程序顺序执行将栈地址写入sp寄存器中
```
264 la sp, stacks + RISCV_PGSIZE - MENTRY_FRAME_SIZE
```
读取mhartid到a3寄存器调整sp
```
266 csrr a3, mhartid
267 slli a2, a3, RISCV_PGSHIFT
268 add sp, sp, a2
```
当a3不等于0时跳转到 init_first_hart
```
270 # Boot on the first hart
271 beqz a3, init_first_hart
```
此时进入"machine/minit.c"文件在init_first_hart中对外设进行初始化
```
154 void init_first_hart(uintptr_t hartid, uintptr_t dtb)
155 {
…… //初始化外设
180 boot_loader(dtb);
181 }
```
在init_first_hart的最后一行调用boot_loader函数
```
160 void boot_loader(uintptr_t dtb)
161 {
……. //CSR寄存器设置
169 enter_supervisor_mode(rest_of_boot_loader, pk_vm_init(), 0);
170 }
```
在boot_loader中经历设置中断入口地址清零sscratch寄存器关中断等一系列操作后。最后会调用enter_supervisor_mode函数正式切换至Supervisor模式。
```
204 void enter_supervisor_mode(void (*fn)(uintptr_t), uintptr_t arg0, uintptr_t arg1)
205 {
206 uintptr_t mstatus = read_csr(mstatus);
@ -537,15 +562,13 @@ typedef struct {
219 asm volatile ("mret" : : "r" (a0), "r" (a1));
220 __builtin_unreachable();
221 }
```
在enter_supervisor_mode函数中将 mstatus的MPP域设置为1表示中断发生之前的模式是Supervisor将mstatus的MPIE域设置为0表示中发生前MIE的值为0。随即将机器模式的内核栈顶写入mscratch寄存器中设置mepc为rest_of_boot_loader的地址并将kernel_stack_top与0作为参数存入a0和a1。
在enter_supervisor_mode函数中将 mstatus的MPP域设置为1表示中断发生之前的模式是Supervisor将mstatus的MPIE域设置为0表示中发生前MIE的值为0。随即将机器模式的内核栈顶写入mscratch寄存器中设置mepc为rest_of_boot_loader的地址并将kernel_stack_top与0作为参数存入a0和a1。
最后执行mret指令该指令执行时程序从机器模式的异常返回将程序计数器pc设置为mepc即rest_of_boot_loader的地址将特权级设置为mstatus寄存器的MPP域即方才所设置的代表Supervisor的1MPP设置为0将mstatus寄存器的MIE域设置为MPIE即方才所设置的表示中断关闭的0MPIE设置为1。
于是当mret指令执行完毕程序将从rest_of_boot_loader继续执行。
```
144 static void rest_of_boot_loader(uintptr_t kstack_top)
145 {
146 arg_buf args;
@ -561,6 +584,6 @@ typedef struct {
156
157 run_loaded_program(argc, args.argv, kstack_top);
158 }
```
这个函数中我们对应用程序的ELF文件进行解析并且最终运行应用程序。
Loading…
Cancel
Save