Skip to content

问题

一、异常状态保存:硬件保存 vs 软件保存

为什么通常需要硬件保存?

  1. 原子性
    异常或中断发生时,必须立刻、原子地保存当前状态(如 PC、状态寄存器)以避免状态不一致。硬件能在极短时间内完成这一操作,而软件做不到这一点。

  2. 时序要求
    异常和中断处理必须在指令执行的特定阶段完成,只有硬件能够精确掌控这一时序,确保不遗漏任何关键状态。

  3. 特权级保护
    异常处理常涉及从用户态切换到内核态,必须在此过程中确保操作系统核心状态不被篡改;软件在特权转换前后很难确保对所有寄存器的安全访问与保存。

  4. 重入问题
    如果依赖软件来保存状态,在保存过程中如果再次发生中断,可能会导致保存状态的不一致问题。

各架构比较

  • x86 架构
  • EIP 和 CS:发生中断或异常时需要跳转到新的代码位置。如果由软件保存,会面临“先有鸡还是先有蛋”的问题,因为保存时必须知道中断触发前的精确位置。
  • EFLAGS:包含中断使能标志等关键信息。如果保存不当,可能会导致状态不一致。

  • MIPS32 架构

  • EPC:保存异常时的程序计数器;软件保存难以在已进入异常处理程序后获取触发中断前的精确 PC。
  • Status 和 Cause:用于控制处理器操作模式;在特权级转换期间,软件保存这些值容易引起安全问题。

  • RISC-V 架构

  • mepc、mstatus、mcause:与 MIPS 类似,同样要求硬件原子性、时序精准地保存这些状态。如果依赖纯软件方式,难以确保这些要求,必需由硬件完成保存。

软件保存的场景与限制

在某些受限场合下,如计划好的上下文切换、协作式多任务或用户级线程切换时,软件可以保存部分状态,但存在以下限制: - 特权级转换:用户态到内核态的转变需要硬件支持以保证安全性。 - 精确异常捕捉:软件难以在异常发生瞬间捕捉确切的执行状态。 - 原子性:处理异常过程中需要的原子性保存难以通过纯软件保证。

因此,x86 架构可以通过约定将异常号压栈,但 MIPS32 和 RISC-V32 的异常处理依赖于专门硬件寄存器来保存异常原因和值,不能仅靠软件完成。


二、函数调用状态保存 vs 上下文切换(CTE)状态保存

函数调用中的状态保存

  • 内容
  • 返回地址:保存调用指令执行后,返回的地址。
  • 调用者保存寄存器:根据调用约定,需要调用者保存的寄存器。
  • 局部变量和参数:通过栈帧自动分配保存。

  • 特点
    在同一进程/线程内,执行环境连续,仅保存必要信息即可恢复。

上下文切换(CTE)中的状态保存

  • 内容
  • 所有通用寄存器:无论调用者保存还是被调用者保存的寄存器。
  • 程序计数器(PC)
  • 状态寄存器:如标志寄存器、条件码寄存器等。
  • 浮点寄存器(如果使用)。
  • 内存管理相关寄存器:如页表基址。
  • 特权级信息:当前执行的特权等级。
  • 处理器其他特定状态:例如 MMU 配置、缓存状态等。
  • 操作系统调度信息:如进程优先级、时间片管理等。

  • 特点
    上下文切换用于不同进程或线程间,环境完全改变,需要保存完整状态,以便恢复到原任务时“无感知”中断状态。

主要原因区别

  1. 执行环境的连续性
    函数调用在同一环境中,仅需局部保存;而任务切换需保存全部状态以构造完整的执行环境。

  2. 特权级别变化
    函数调用通常不会涉及特权级别切换,而上下文切换可能涉及从用户态切换到内核态。

  3. 保存目的
    函数调用关注返回现场正确性;而上下文切换要求完全还原当时的硬件状态,确保任务在恢复后行为不变。

  4. 不可预测性
    函数调用由编译器生成代码控制,保存的状态有限;而上下文切换常由不可预知的外部事件(中断、异常)触发,必须完整保存以应对复杂情况。

  5. 资源管理需求
    上下文切换有时伴随系统资源重新分配,因此需要保存更多信息,位置通常在内核栈或 PCB 中,而函数调用保存在用户栈中。


三、__am_irq_handle() 中 Context 结构的来源

__am_irq_handle(Context *c) 中,c 指向的上下文结构体是异常发生时 CPU 状态的快照,其形成过程和各部分之间的联系如下:

  1. 位置与传递
  2. 该上下文结构保存在当前进程或线程使用的栈上,由汇编代码(在 trap.S 中)分配栈空间并填充 CPU 的各个通用寄存器和 CSR 寄存器的值。
  3. 汇编代码将栈指针作为参数传递给 C 函数 __am_irq_handle

  4. 各部分内容的联系

  5. riscv.h:定义了 Context 结构体,其成员需要与汇编中保存的栈布局严格匹配;这是 C 代码与汇编代码之间传递 CPU 状态的一致接口。
  6. trap.S:底层汇编代码负责在异常入口处分配栈上的上下文空间、保存通用寄存器和 CSR 寄存器(如 mepc, mstatus, mcause 等),并把保存成功后的栈指针传递给 __am_irq_handle
  7. 讲义文字:解释了整个异常处理过程以及为什么需要硬件自动保存状态,并描述了状态保存、上下文传递、异常处理及恢复的整个链路。
  8. NEMU 新指令:如 ecallmret 指令的模拟实现提供了调试异常状态的工具,帮助确认上下文保存和恢复机制是否正确,还原硬件异常处理行为。

  9. 状态机视角总结

  10. 初始状态:CPU 正常执行程序。
  11. 异常触发:例如通过 ecall 指令触发异常。
  12. 硬件响应:自动保存 PC 到 mepc,设置 mcause,并跳转到 mtvec 指定的异常处理入口。
  13. 上下文保存:汇编代码将所有必要 CPU 状态保存到栈中后,将栈指针传递给 C 函数(__am_irq_handle)。
  14. 异常处理:C 函数基于 mcause 判断异常类型,调用用户注册的事件处理函数(如 simple_trap)。
  15. 上下文恢复:汇编代码从栈中恢复状态,使用 mret 等指令返回原执行点。

四、ELF 文件视角与程序加载

ELF 文件的两种视角

  1. Section 视角(面向链接过程)
  2. 用于链接器解析符号、重定位、合并目标文件,包含符号表、重定位表和调试信息等。
  3. 这些信息在链接时至关重要,但在程序运行时并不需要全部加载内存中。

  4. Segment 视角(面向执行)

  5. 用于加载器将程序映射到内存中执行,描述程序在内存中的布局、必须加载的部分以及权限设定。
  6. 使用 Program Header Table 管理,加载器仅加载那些类型为 PT_LOAD 的 segments。

Section 与 Segment 的映射关系

  • 一个 segment 可以包含多个 section;而某些 section(如调试信息)不属于任何 segment。
  • 加载器读取 Program Header Table,根据 PT_LOAD 属性加载必要段,同时忽略不影响程序运行的调试、符号信息。

Program Header Table

  • 每个表项描述一个 segment 的属性:
  • Type:例如 PT_LOAD 表示需要加载的 segment。
  • Virtual Address:segment 在内存中的加载地址。
  • Flags:设定读、写、执行权限。
  • Alignment:内存对齐要求。
  • Offset、FileSiz 与 MemSiz:描述在文件中数据的位置与大小,以及加载到内存后所需的空间大小。

文件大小与内存大小(FileSiz 与 MemSiz)

  • FileSiz:表示在 ELF 文件中存储的字节数。
  • MemSiz:表示加载到内存后需要的总字节数。
  • 差异原因:
  • 未初始化数据(.bss):.bss 段在文件中不存储实际数据,但载入到内存时需要分配相应空间(此时 FileSiz 小于 MemSiz)。
  • 内存对齐:为了内存访问效率,可能需要额外空间。

清零操作的必要性

  • 必须将 Virtual Address + FileSiz, Virtual Address + MemSiz 区域清零,以初始化 .bss 段中未初始化的全局变量和静态变量为 0,从而确保程序在启动时具有正确的初始状态。

程序加载与执行流程

  1. 编译链接
  2. 源文件(如 navy-apps/tests/hello/hello.c)通过编译和链接生成 ELF 文件(含代码、数据、.bss 等段)。

  3. 加载过程

  4. 操作系统(如 Nanos-lite)加载 ELF 文件,解析头表,分配内存(包括为堆与栈预留空间),并将代码和数据复制到指定内存位置,同时对 .bss 区域进行清零操作。

  5. 入口点设定与执行

  6. 加载器设置程序入口点(通常在 crt0 中,如 call_main),CPU 将 PC 指向入口地址。
  7. crt0 初始化运行环境(设置栈、全局数据初始化等),然后调用 main 函数开 始执行程序。

  8. 字符串打印流程

  9. 程序(hello 程序)通过调用 write 或 printf 系统调用触发异常(如通过 ecall 指令),进入内核处理输出。
  10. 内核系统调用处理程序将请求传递到设备驱动层,再由硬件(或模拟硬件,如 NEMU 中的终端模拟)将字符输出到终端。