好像没啥用的一些东西,之前想找个时间整理一下,结果弄明白之后懒得写了。
trap
第三章 陷入,中断和驱动程序 | xv6 中文文档 (gitbooks.io)
在执行
lw a3,0(zero)产生 page fault 后,到panic()被调用这段时间发生了什么?
在 RISC-V 架构的 xv6 操作系统中,当执行类似 lw a3, 0(zero) 的指令导致页面错误(page fault)时,系统会通过以下几个步骤处理该异常,最终导致 panic() 的调用。下面详细说明从页面错误产生到调用 panic() 的过程。
当执行类似 lw a3, 0(zero) 的指令在 RISC-V 架构的 QEMU 模拟器上运行时,因虚拟地址 0x0 无效,会触发 页面错误(page fault)。QEMU 和 xv6 协同工作来检测和处理这一错误,从错误发生到调用 panic() 的全过程可以分为几个主要步骤。以下将深入阐述这段时间 QEMU 和 xv6 各自做了什么:
1. 指令执行:QEMU 模拟 CPU 行为
QEMU 作为 RISC-V CPU 的模拟器,负责模拟指令的执行。当执行指令 lw a3, 0(zero) 时:
lw指令尝试从虚拟地址0x0加载数据到寄存器a3中。- 由于虚拟地址
0x0是无效的(通常情况下,这一地址并未被映射到合法的物理内存),这会导致 页面错误。
QEMU 负责检查虚拟地址并确保其有效性,具体通过其内存管理单元(MMU)模块来仿真地址转换和权限检查。
2. QEMU 的地址转换与页面错误检测
QEMU 模拟 RISC-V 处理器的分页机制。当指令试图访问内存时,QEMU 的 MMU 会根据当前的页表将虚拟地址转换为物理地址,并检查页面的有效性和权限。这个过程是 RISC-V 处理器硬件完成的部分,但在 QEMU 中由软件仿真。
- 地址检查:QEMU 通过页表判断虚拟地址
0x0是否已经映射到有效的物理地址。 - 权限检查:即使地址被映射,QEMU 也会检查访问权限,比如是否有读取、写入、执行的权限。
在这个例子中,虚拟地址 0x0 既没有映射,也没有权限,因此 QEMU 检测到 页面错误。
3. QEMU 触发异常并设置异常寄存器
一旦 QEMU 检测到页面错误,它会模拟 RISC-V 处理器的异常处理机制,触发一个 页面错误异常。QEMU 的异常处理流程包含以下步骤:
-
设置寄存器:
scause寄存器:设置为页面错误的异常原因。对于加载(load)操作导致的页面错误,scause的值为0x0f(Load Page Fault)。sepc寄存器:保存当前正在执行的指令的地址,即lw a3, 0(zero)指令所在的地址。stval寄存器:保存导致页面错误的虚拟地址,在此情况下为0x0。
QEMU 将这些寄存器设置为反映异常的具体信息。
-
跳转到异常处理程序: 根据 RISC-V 架构,当异常发生时,处理器会跳转到操作系统定义的异常处理程序。这个跳转由
stvec寄存器指定,该寄存器在xv6中设置为内核的异常处理入口地址kernelvec。
4. 进入 xv6 的异常处理程序
现在,QEMU 已经完成了对页面错误异常的仿真,控制权交给了 xv6 操作系统。此时,xv6 的内核接手处理异常。
进入 kernelvec: 当发生异常时,处理器会跳转到 kernelvec,这是一个汇编程序,负责从用户态切换到内核态,并保存当前的进程状态。这个过程包括:
- 保存用户进程的寄存器状态到内核栈,以便在处理异常后可以恢复进程执行。
- 将异常的关键信息(例如
scause,sepc,stval)传递给 C 语言编写的异常处理函数trap()。
.globl kernelvec
kernelvec:
// 保存当前的寄存器状态
// 跳转到 trap() 处理函数
调用 trap(): 通过 kernelvec 的汇编代码,系统最终会进入 C 语言的 trap() 函数,trap() 是 xv6 中用于处理所有中断和异常的核心函数。
5. xv6 的 trap() 处理页面错误
在 trap() 函数中,xv6 会根据 scause 的值判断异常的类型:
-
检查
scause:trap()函数通过读取
scause寄存器的值,判断当前异常是何种类型。
- 对于页面错误,
scause会包含一个表示页面错误的值,如0x0f(Load Page Fault)。
- 对于页面错误,
if ((r_scause() & 0xff) == 0xf) { // Load Page Fault
// 页面错误处理逻辑
} else {
panic("unexpected trap");
}
在此例中,trap() 确认是 Load Page Fault,由于虚拟地址 0x0 无效,xv6 无法处理这个页面错误。于是,它打印相关的寄存器信息,进入下一步的错误处理。
6. 打印调试信息
在决定系统无法处理页面错误后,trap() 会打印寄存器的信息以便调试。这些信息包括:
scause: 导致异常的原因。sepc: 发生异常的指令地址。stval: 导致异常的地址值(在此例中为0x0)。
printf("scause 0x%016lx\n", r_scause());
printf("sepc=0x%016lx\n", tf->epc);
printf("stval=0x%016lx\n", r_stval());
7. 调用 panic() 停止系统
在打印完调试信息后,由于页面错误不可恢复,xv6 会调用 panic() 停止系统。panic() 的实现如下:
void panic(char *s) {
cli(); // 关闭中断
cprintf("panic: %s\n", s); // 打印错误消息
for(;;) ; // 进入死循环,停止系统
}
- 关闭中断:
cli()函数关闭所有中断,防止系统在处理panic时受到干扰。 - 打印错误信息:通过
cprintf()打印panic消息,帮助开发人员调试问题。 - 死循环:系统进入无限循环停止运行,防止进一步的损坏。
8. QEMU 进入死循环,结束仿真
当 xv6 进入 panic() 的死循环后,QEMU 作为仿真器也停止继续仿真 CPU 指令,系统陷入一个无响应的状态。此时,QEMU 会持续仿真死循环中的操作,但系统已经不再进行有效的任务。
总结:QEMU 和 xv6 的协同工作
- QEMU 检测页面错误:QEMU 通过其 MMU 仿真模块检测到无效的虚拟地址(
0x0),并触发页面错误异常,设置寄存器scause,sepc, 和stval。 - QEMU 跳转到异常处理程序:QEMU 仿真处理器将控制权交给
xv6的异常处理入口kernelvec,开始处理异常。 xv6的异常处理:xv6通过trap()检查scause,确认是页面错误,并打印相关调试信息。- 系统崩溃并调用
panic():由于无法处理页面错误,xv6调用panic()停止系统运行,QEMU 进入死循环仿真。
QEMU 在这个过程中扮演了硬件模拟的角色,负责检测和触发异常;xv6 负责异常的具体处理和崩溃后的错误报告。这种协同使得开发者可以在仿真环境下调试复杂的操作系统问题。
为了深入理解 QEMU 和 xv6 在处理页面错误以及调用 panic() 的过程中各自执行了哪些操作,我将逐个部分大体介绍对应功能的代码实现位置。
1. 指令执行:QEMU 模拟 CPU 行为
在 QEMU 中,指令的执行由 CPU 仿真模块 负责,这里会调用与 RISC-V 架构相关的 CPU 仿真函数。
-
target/riscv/:QEMU 的 RISC-V 处理器仿真实现位于target/riscv/目录。 -
riscv_translate.c:此文件中包含指令翻译和执行的逻辑,比如lw指令会被解析为相应的加载操作。riscv/translate.c.inc:这个文件中包含 QEMU 对 RISC-V 指令的模拟执行逻辑。
QEMU 在执行 lw a3, 0(zero) 这条指令时,会进行内存访问,随后通过 MMU 执行虚拟地址到物理地址的转换。
2. QEMU 的地址转换与页面错误检测
QEMU 的 MMU(内存管理单元) 模块负责处理虚拟地址到物理地址的转换,并检查访问权限。
target/riscv/cpu_helper.c:QEMU 中的虚拟地址转换逻辑通常在cpu_helper.c中实现,这里包括访问虚拟内存的函数如riscv_cpu_tlb_fill,它会进行地址检查。accel/tcg/cputlb.c:QEMU 的 TLB(转换后备缓冲区)逻辑会用于加速地址转换,内存访问通过 TLB 缓存来快速进行映射。- 如果地址无效,QEMU 会触发页面错误,并调用异常处理函数。
3. QEMU 触发异常并设置异常寄存器
一旦检测到页面错误,QEMU 会通过设置 RISC-V 的异常寄存器并跳转到异常处理代码:
target/riscv/cpu.c:QEMU 中设置异常寄存器的代码位于target/riscv/cpu.c。这里会设置scause,sepc,stval等寄存器值。例如,函数riscv_cpu_do_interrupt()会处理各种异常,并设置相应的寄存器状态。riscv_cpu_do_interrupt():QEMU 调用这个函数来模拟 RISC-V CPU 中的异常处理机制,并设置寄存器值,准备进入操作系统的异常处理程序。
4. 进入 xv6 的异常处理程序
QEMU 完成对寄存器的设置后,控制权交给 xv6,通过跳转到 kernelvec 处理异常:
kernel/trampoline.S:xv6中的异常处理入口是kernelvec,这是汇编代码,它的实现位于xv6的kernel/trampoline.S文件中。这里定义了从用户态到内核态的上下文切换代码。kernelvec保存了异常时的寄存器状态,并调用 C 函数trap()处理异常。
5. xv6 的 trap() 处理页面错误
trap() 是 xv6 的核心异常处理函数,负责分析异常并根据情况进行处理:
kernel/trap.c:trap()函数的实现位于xv6的kernel/trap.c文件中。在trap()中,根据scause寄存器的值判断异常类型(页面错误),并执行相应的处理逻辑。
void trap() {
if ((r_scause() & 0xff) == 0xf) {
// Handle page fault here
} else {
panic("unexpected trap");
}
}
6. 打印调试信息
在无法处理页面错误时,trap() 函数会调用 printf() 打印相关的调试信息,包括 scause, sepc, 和 stval。
kernel/printf.c:printf()函数的实现位于xv6的kernel/printf.c文件中,这里会负责输出调试信息。printf调用会输出寄存器的值,如scause,sepc,stval。
7. 调用 panic() 停止系统
xv6 确认无法处理页面错误后,最终会调用 panic() 停止系统运行:
kernel/printf.c:panic()函数也位于xv6的kernel/printf.c中。panic()会关闭中断,打印错误信息,并进入死循环停止系统执行。
void panic(char *s) {
cli(); // Disable interrupts
cprintf("panic: %s\n", s);
for(;;);
}
8. QEMU 进入死循环,结束仿真
当 xv6 进入 panic() 的死循环后,QEMU 会持续模拟这个死循环:
- QEMU 不会中止模拟,它会继续模拟 CPU 执行死循环。这是通过 QEMU 中的 CPU 仿真主循环实现的,位于
accel/tcg/translate-all.c中的cpu_exec()函数中,QEMU 会不断执行同一个指令,进入无限循环。
为了深入理解 QEMU 和 xv6 在处理页面错误以及调用 panic() 的过程中各自执行了哪些操作,我将逐个部分大体介绍对应功能的代码实现位置。
1. 指令执行:QEMU 模拟 CPU 行为
在 QEMU 中,指令的执行由 CPU 仿真模块 负责,这里会调用与 RISC-V 架构相关的 CPU 仿真函数。
target/riscv/:QEMU 的 RISC-V 处理器仿真实现位于target/riscv/目录。riscv_translate.c:此文件中包含指令翻译和执行的逻辑,比如lw指令会被解析为相应的加载操作。riscv/translate.c.inc:这个文件中包含 QEMU 对 RISC-V 指令的模拟执行逻辑。
QEMU 在执行 lw a3, 0(zero) 这条指令时,会进行内存访问,随后通过 MMU 执行虚拟地址到物理地址的转换。
2. QEMU 的地址转换与页面错误检测
QEMU 的 MMU(内存管理单元) 模块负责处理虚拟地址到物理地址的转换,并检查访问权限。
target/riscv/cpu_helper.c:QEMU 中的虚拟地址转换逻辑通常在cpu_helper.c中实现,这里包括访问虚拟内存的函数如riscv_cpu_tlb_fill,它会进行地址检查。accel/tcg/cputlb.c:QEMU 的 TLB(转换后备缓冲区)逻辑会用于加速地址转换,内存访问通过 TLB 缓存来快速进行映射。- 如果地址无效,QEMU 会触发页面错误,并调用异常处理函数。
3. QEMU 触发异常并设置异常寄存器
一旦检测到页面错误,QEMU 会通过设置 RISC-V 的异常寄存器并跳转到异常处理代码:
target/riscv/cpu.c:QEMU 中设置异常寄存器的代码位于target/riscv/cpu.c。这里会设置scause,sepc,stval等寄存器值。例如,函数riscv_cpu_do_interrupt()会处理各种异常,并设置相应的寄存器状态。riscv_cpu_do_interrupt():QEMU 调用这个函数来模拟 RISC-V CPU 中的异常处理机制,并设置寄存器值,准备进入操作系统的异常处理程序。
4. 进入 xv6 的异常处理程序
QEMU 完成对寄存器的设置后,控制权交给 xv6,通过跳转到 kernelvec 处理异常:
kernel/trampoline.S:xv6中的异常处理入口是kernelvec,这是汇编代码,它的实现位于xv6的kernel/trampoline.S文件中。这里定义了从用户态到内核态的上下文切换代码。kernelvec保存了异常时的寄存器状态,并调用 C 函数trap()处理异常。
5. xv6 的 trap() 处理页面错误
trap() 是 xv6 的核心异常处理函数,负责分析异常并根据情况进行处理:
kernel/trap.c:trap()函数的实现位于xv6的kernel/trap.c文件中。在trap()中,根据scause寄存器的值判断异常类型(页面错误),并执行相应的处理逻辑。
void trap() {
if ((r_scause() & 0xff) == 0xf) {
// Handle page fault here
} else {
panic("unexpected trap");
}
}
6. 打印调试信息
在无法处理页面错误时,trap() 函数会调用 printf() 打印相关的调试信息,包括 scause, sepc, 和 stval。
kernel/printf.c:printf()函数的实现位于xv6的kernel/printf.c文件中,这里会负责输出调试信息。printf调用会输出寄存器的值,如scause,sepc,stval。
7. 调用 panic() 停止系统
xv6 确认无法处理页面错误后,最终会调用 panic() 停止系统运行:
kernel/printf.c:panic()函数也位于xv6的kernel/printf.c中。panic()会关闭中断,打印错误信息,并进入死循环停止系统执行。
void panic(char *s) {
cli(); // Disable interrupts
cprintf("panic: %s\n", s);
for(;;);
}
8. QEMU 进入死循环,结束仿真
当 xv6 进入 panic() 的死循环后,QEMU 不会中止模拟,它会继续模拟 CPU 执行死循环。这是通过 QEMU 中的 CPU 仿真主循环实现的,位于 accel/tcg/translate-all.c 中的 cpu_exec() 函数中,QEMU 会不断执行同一个指令,进入无限循环。
