异常与CTE
📆需求的层级递交:从批处理到操作系统到特权级与异常响应机制的具体实现(逻辑链)⛓️¶
逻辑:从效率到控制
-
起源:批处理的需求 (The Need for Batch Processing) 痛点:计算机(硬件)太快,人太慢。跑完一个程序,计算机要停下来等人插卡片/输命令,CPU 就在空转浪费。 需求:需要一个自动化程序,能自动把用户的程序(Job)一个接一个地加载到内存里运行,不需要人干预。 产物:这个“程序”就是操作系统(OS)的雏形(在 PA 中叫 Nanos-lite)。
-
需求:操作系统的安全性需求 (The Need for Protection) 痛点:如果把控制权完全交给用户程序(A),万一程序 A 写得烂(死循环、乱改内存),或者它是恶意的: (破坏OS):用户可以在调用前故意把 SP 设置为一个非法地址(比如指向只读段或空指针)。OS 代码执行第一条 push 指令时,就会触发 Page Fault 导致 OS 崩溃(Kernel Panic)。 (窃取数据):OS 在栈上留下的敏感数据(如密码散列、密钥),在函数返回后,依然留在用户的栈里,用户程序可以直接读取。 术语:我们需要特权级隔离 (Privilege Isolation) 和 保护域 (Protection Domain)。 原则:OS 必须对计算机拥有绝对的、高于用户程序的控制权。
-
实现:怎样满足需求?(Hardware Support & ISA Mechanism) 为了实现上述的“保护”和“自动化”,软件(OS)自己是做不到的,必须求助于硬件(CPU/ISA)。 这就引入了硬件机制: A. 森严的等级制 硬件实现:CPU 增加一个状态位(如 mstatus 中的 MPP 位),区分 M-mode (管家/内核态) 和 U-mode (用户/用户态)。 逻辑:只有在 M-mode 下,才能执行特权指令(如修改 CSR、操作设备)。用户程序运行在 U-mode,被关在笼子里。 B. 开设“窗口” (Exception/Trap Mechanism) 问题:用户程序被关在笼子里,它需要输出结果(IO操作),不能直接操控硬件,需要通过OS帮助实现。 硬件实现:异常响应机制 (CTE)。 逻辑: 自陷 (Trap):用户执行 ecall。 硬件接管:CPU 瞬间暂停用户程序,强行切换到 M-mode。 引导安置:CPU 查 mtvec(异常入口地址),强制跳转。 C. 状态保存与恢复 (Context Switch) 问题:OS实现用户程序的需求(或者处理完异常)后,回到用户刚才断开的地方继续运行 硬件实现:CSR 寄存器 (mepc, mcause, mstatus)。 逻辑: 进门时:硬件自动把异常相关状态(当前的 PC 和状态)存入 CSR。 出门时:OS 执行 mret,硬件从 CSR 读回数据,恢复,切回 U-mode。
-
抽象层具体实现 痛点:虽然硬件提供了机制(ecall, mtvec),但存在架构差异,需要尽量抽象。 方案:抽象与封装 (Abstraction & Encapsulation via CTE) 结果:OS 只需要通过 switch(event.event) 判断是 EVENT_SYSCALL (求助) 还是 EVENT_ERROR (捣乱),完全不用关心底层是几号异常。 封装“现场” (Context Abstraction) 底层差异:不同 CPU 的寄存器数量、名字、压栈顺序完全不同。 CTE 封装:CTE 定义一个叫 Context 的结构体。把所有寄存器打包塞进去。 结果:OS 拿到一个指向 Context 的指针。OS 不需要知道里面是 eax 还是 a0,如果需要修改返回值,调用 CTE 提供的统一接口即可。 高级应用:OS 甚至可以通过修改 Context 中的 PC 或指令,用软件来模拟硬件不支持的指令(如 FPU 浮点运算)。
-
串联动作(The Layered Architecture) OS 启动:初始化硬件,设置好 mtvec。 加载用户程序:OS 把用户代码搬进内存,把 CPU 权限降级(M->U),给用户程序执行。 用户执行: 情况 A (正常):做加减乘除 -> 硬件 $fex=0$ -> 继续。 情况 B (坏事):除零/非法访问 -> 硬件 $fex=1$ -> 触发异常 -> 转OS -> OS 杀掉程序 -> 加载下一个。 情况 C (求助):ecall (系统调用) -> 硬件 $fex=1$ -> 触发自陷 -> 转OS -> OS 打印字符 -> mret 回到用户程序。 初始化:OS 调用 cte_init(handler),把自己的函数注册给 CTE。 用户请求:用户程序调用 yield()(或者触发异常)。 CTE 接管 (硬件层):CTE 的汇编代码 (trap.S) 捕获异常,保存所有寄存器到栈上,形成 Context。 CTE 翻译 (抽象层):CTE 的 C 代码 (irq.c) 读取硬件原因,打包成 Event。 OS 处理 (逻辑层):CTE 调用 OS 的 handler(Event, Context)。 OS 看到 EVENT_SYSCALL -> 办事 -> 修改 Context 中的返回值。 返回:OS return context -> CTE 恢复现场 (pop 寄存器) -> mret 回到用户程序。
-
逻辑 最终总结 (The Big Picture) PA3 的完整逻辑链是: 批处理需求 $\rightarrow$ 引入操作系统 $\rightarrow$ 安全需求 $\rightarrow$ 引入硬件特权级与异常 $\rightarrow$ 跨平台需求 $\rightarrow$ 引入 CTE 抽象层。
概念清晰与机制认知(自陷,环境调用是什么,特权级审查机制具体怎么实现)¶
概念辨析:自陷与环境调用¶
- 环境调用 (Environment Call) —— 程序的需求,触发自陷的原因之一
科学定义:一种异常(Synchronous Exception)。由指令
ecall触发,用于向更高特权级的执行环境(OS 或 SBI)请求服务。环境一般指“宿主”,比如用户程序的环境就是操作系统,操作系统的环境就是HAL(硬件抽象层)or硬件 认知升级:它只是触发自陷的一种方式,其他方式还包括断点(ebreak)、非法指令、地址不对齐等。
- 自陷 (Trap) —— 处理异常和中断的一种机制
科学定义:处理器中止当前指令流,保存上下文,并强制跳转到预设处理程序(Trap Handler)的过程。
小概念辨析:异常(一般认为)就是同步的,中断(一般认为)就是异步的,可以省去(adj)因为名词本身就含(adj)
逻辑链条:从请求到执行流切换¶
发出请求 (User): 用户程序执行 ecall 指令。此时 CPU 处于 User Mode。 硬件介入 (Hardware - The Magic Moment): CPU 译码器识别出 ecall,触发 Trap 机制。原子性地完成以下动作: 保存原因:将 mcause 设为 8/9/11 (Environment Call)。 保存现场:将当前 PC 存入 mepc。 权限飞升 (关键):硬件强制将 CPU 内部状态 Current Mode 修改为 Machine/Supervisor Mode。 强制跳转:将 PC 修改为 mtvec (异常入口地址)。 软件接管 (OS): PC 指向了 OS 的 trap_handler。此时 CPU 已处于高特权级,可以执行特权指令。 服务处理: OS 根据 mcause 和寄存器参数(如 a7 中的系统调用号),执行具体服务(如 sys_write)。 返回 (Return): OS 执行 mret 指令。硬件再次介入,恢复 mstatus,将 Current Mode 降回 User Mode,并跳转回 mepc (或 mepc+4)。
特权级审查机制:谁在监视?标签在哪?(不是很重要)¶
解决了水木石疑惑:“扁平二进制流如何区分权限”的答案。 * 监视者 (The Monitor) 实体:CPU 的译码与执行部件 (Decode & Execute Unit)。 依据:CPU 内部的一个不可见状态寄存器(或
mstatus中的位),记录着 Current Privilege Mode (U/S/M)。 工作方式:它是硬件级的“安检门”,每一条指令执行前都要经过它的逻辑电路比对。 * 标签 (The Label) —— 藏在操作数里 大多数指令(如 add,因为平庸)无标签,只有特权相关指令(系统类指令)有标签。RISC-V 将标签巧妙地编码在 CSR 寄存器的地址中:
CSR 地址 (12-bit): Bit [9:8] (Privilege Level):指示访问该寄存器所需的最低特权级。 00 = User 01 = Supervisor 11 = Machine Bit [11:10] (R/W Status):指示读写属性(11 通常表示只读)。 3. 审查逻辑 (The Logic) 当 CPU 执行 csrrw (读写 CSR) 等指令时,硬件逻辑门自动执行以下判断:
IF (CPU.Current_Mode<CSR_Addr[9:8]) ⟹ Trigger Illegal Instruction Exception IF (CPU.Current_Mode<CSR_Addr[9:8]) ⟹ Trigger Illegal Instruction Exception
🗺️ 第一部分:今天的 Debug 路线复盘 1. 现象(The Symptom) 你实现了 yield() 测试,程序成功触发了 ecall,NEMU 也跳转到了异常处理入口。但是,当 C 语言的 __am_irq_handle 试图读取异常号(mcause)时,读出来的是一堆乱码(或者看起来像地址的值)。
- 溯源(The Investigation) 我们对比了“生产者”和“消费者”:
生产者(trap.S):这是手写的汇编。它简单粗暴地按照 GPRs -> mcause -> mstatus -> mepc 的顺序把数据压入栈。 消费者(struct Context):这是 C 语言定义的结构体。它原本期望的顺序是 mepc -> mcause -> GPRs...。 3. 真相(The Root Cause) ABI(二进制接口)失配。 编译器(处理 C)和汇编器(处理 S)互不沟通。链接器把它们拼在一起时,C 代码拿着错误的“地图”(结构体定义)去读汇编代码造好的“地形”(栈数据),导致读到了错误内存偏移量的数据。
- 解决(The Fix) 手动对齐。修改 struct Context,使其内存布局严格对应 trap.S 的压栈顺序。 🧠 第二部分:核心学到的东西 编译器的局限性:C 编译器看不懂手写的汇编代码,它只能根据 struct 定义生成指令。 汇编的霸道:汇编指令直接操作物理内存地址(Offset),它不关心高级语言的类型系统。 ABI 是契约:在系统级编程中,程序员必须充当“人肉编译器”,确保 C 语言的数据结构和汇编的内存布局在物理上严丝合缝。 CPU 是盲目的:它不会报错,只会执行。如果你让它从错误的地方读数据,它就会读出错误的数据给你。
事件分发:事件区分如何实现?¶
为了简化ISA的设计, 处理器通常只会提供一条自陷指令, 软件层次上的多个事件可能都通过相同的自陷指令来实现, 因此CTE需要额外的方式区分它们. 如果自陷指令本身可以携带参数, 就可以用不同的参数指示不同的事件, 例如x86和mips32都可以采用这种方式; 如果自陷指令本身不能携带参数, 就需要通过其他状态来区分, 例如通过某个寄存器的值来区分, riscv32采用这种方式. 因此, 要识别一个事件, 不仅要看异常号, 有时候还需要考虑其他状态。
在 RISC‑V 的异常处理机制中,mcause 只能标识硬件层面的异常类型,例如ecall、page fault、illegal instruction 等。它无法直接描述更高层的软件语义。
当 mcause 表示 ecall 时,操作系统通常需要进一步读取通用寄存器(如 a7)中的 syscall number 来确定具体的系统调用类型,例如read、write、exit 或 yield。
因此,异常处理在语义上存在多个层次:
* 1. 硬件异常层(ISA)
由 mcause 提供,例如 ecall、page fault、interrupt 等。
* 2. 系统调用层(OS ABI)
当异常为 ecall 时,通过寄存器(如 a7)中的 syscall number
进一步区分具体的系统调用。
* 3. 抽象机事件层(AM)
AM 将底层异常重新封装为统一的 Event,例如
EVENT_YIELD、EVENT_SYSCALL、EVENT_PAGEFAULT、EVENT_IRQ_TIMER,
从而屏蔽不同 ISA 的差异。
因此 Event 枚举中包含的事件实际上来源于不同层次的异常,它们并不是硬件层的直接分类,而是 AM 对各种异常的统一抽象。
对蓝标问题的研究与回答¶
x86通过软件保存异常号,没有类似 cause 的寄存器。mips32 和 riscv32 可以这样吗?为什么?¶
结论:
理论上可以,但设计上不合理,因此 MIPS32 和 RISC‑V 由硬件将异常号保存到 cause/mcause 寄存器。
原因:
* 1. 异常发生时软件尚未开始执行
异常是由硬件检测并立即改变控制流的:
执行指令
→ 硬件检测到异常
→ 跳转到异常入口
mtvec),因此软件必须通过 mcause/cause 寄存器读取异常号,才能区分:
ecall
page fault
illegal instruction
interrupt
cause 寄存器。
总结:
x86:通过异常向量区分异常
MIPS/RISC‑V:通过统一入口 + cause/mcause 区分异常
为什么异常处理需要保存比函数调用更多的信息?¶
结论: 因为函数调用是 可预期的控制流转移,而异常是 不可预期的控制流转移。 函数调用: 函数调用由程序主动发起,调用者在调用前可以按照 calling convention 保存必要寄存器,因此通常只需要保存:
返回地址
部分寄存器
所有通用寄存器
PC
处理器状态寄存器
异常号
(可能还有地址空间信息)
函数调用:主动控制流 → 保存最少必要信息
异常处理:被动控制流 → 必须保存完整上下文
保存上下文:上下文形成机制(执行流在文件层和抽象层之间的变化与汇编理解)¶
首先,从执行流角度看,异常发生时 CPU 会跳转到 mtvec 指向的异常入口,异常入口必须是汇编代码。原因在于异常刚发生时,寄存器状态尚未保存,C 语言的调用环境(如栈帧、调用约定等)也尚未建立。如果直接进入 C 代码,编译器生成的指令可能会破坏寄存器状态。因此必须先由汇编完成寄存器保存并构造栈帧,然后再调用 C 函数。
在 NEMU 的实现中,trap.S 通过一系列 store 指令将所有通用寄存器以及 mcause、mstatus、mepc 等 CSR 保存到栈中,从而在栈上构造出一段连续的内存区域,这一段内存实际上就是异常发生时 CPU 状态的完整快照。
随后 trap.S 将当前栈指针 sp 作为参数传递给 __am_irq_handle()。在 C 语言中,这个指针被解释为 Context * 类型。需要注意的是,Context 并不是通过复制栈中的数据生成的结构体,而只是对这段内存布局的一种结构化描述。编译器在访问结构体成员时,本质上只是根据成员偏移量对指针地址进行偏移访问。
因此,Context 结构体成员的顺序必须与 trap.S 中保存寄存器的顺序完全一致,否则通过结构体访问成员时就会读取到错误的内存位置。
意识到,操作系统实现中的许多结构(例如 Context、寄存器保存区等)本质上都是对底层内存布局的一种类型解释,而不是独立的数据结构。
突破点:思维复杂度的再提升🔴¶
从上下文形成机制的了解中,或者说自pa2设备实现部分开始至今,逐渐感受到自己的思维困局大多因:没有本能的复杂系统思维视角来对程序的执行流进行分析解读
-
1.在不同抽象层之间切换视角的能力 从 C 代码的抽象结构,到汇编指令对寄存器和内存的操作,再到硬件异常机制的控制流转移,这些层次之间需要不断来回切换才能真正理解系统行为。
-
2.阅读和理解汇编代码 很多底层机制(例如异常入口、寄存器保存、栈帧构造)在高级语言层面是看不到的,只有通过汇编才能理解系统真正做了什么。
🚀穿越时空的旅程:完整的异常响应机制(控制流与执行流)¶
测试函数中调用yield(测试函数中),跳转到cte中真正使用——执行ecall指令(cte文件中),此时触发了自陷异常,处理器检测到异常后保存mepc,并将执行流跳转到异常入口 (ecall指令内嵌isaraiseintr实现),执行流进入入口后直接走进了trap.s(汇编文件),汇编文件中做了:把mcause,mstatus,mepc保存到栈中,随后调用回调函数,(中间有个关键点就是调用函数后,设置第一个函数参数(即a0,其所存的是sp(栈指针))——>意味着context结构体中的数据排列直接映射在栈空间中,也就是汇编中直接写入的数据排布)通过context对内存布局的模仿直接读取内存中相关的数据(context本身也作为快照保存了异常触发瞬间的上下文)把这个异常打包成一个事件(event),进行事件分发(event中枚举类型并配合其他状态来分发),分发交给操作系统执行(也就是操作系统中的处理函数),对于测试函数是simple trap函数。函数执行结束后返回原本的context(那个快照),控制流随后返回到 __am_asm_trap,恢复之前保存的寄存器状态,并通过 mret 指令从异常返回。由于软件对保存的 PC 进行了+4 处理,因此返回到 ecall 指令之后的下一条指令继续执行。从程序的视角来看,这次异常处理过程就像一次普通的函数调用,程序状态在返回后被完全恢复。
yield test $\rightarrow$ yield() $\rightarrow$ ecall $\rightarrow$ NEMU执行ecall $\rightarrow$ isa_raise_intr() $\rightarrow$ 保存 mepc / mcause $\rightarrow$ pc = mtvec $\rightarrow$ 进入 __am_asm_trap $\rightarrow$ 保存寄存器到栈 $\rightarrow$ 构造 Context $\rightarrow$ 调用 __am_irq_handle $\rightarrow$ 识别 EVENT_YIELD $\rightarrow$ simple_trap 处理 $\rightarrow$ 返回 Context $\rightarrow$ __am_asm_trap 恢复寄存器 $\rightarrow$ mret $\rightarrow$ pc = mepc $\rightarrow$ 回到 ecall + 4 $\rightarrow$ yield() 返回 $\rightarrow$ yield test 继续执行
📈从etrace实现看自己的系统思维成长与不足¶
逻辑推理过程:¶
既然为了追踪异常处理,那么本质应该是:将异常可视化,(为了让etrace对程序来说是非侵入式的)讲义企图在硬件层即实现对异常处理的追踪,而不是在软件层(am层),那么似乎是没法屏蔽架构差异的,于是etrace成了针对特定架构的etrace了,因此,我可以撒手:针对现架构进行设计:对于riscv32,能想到的就是在指令中直接内嵌一个异常可视化函数,即当ecall执行时,输出mcause(后期可以考虑增加a7值读取)(判断出异常类型)
逻辑方向基本正确而且科学,但是由于对ecall在整个异常中断机制中的地位和角色认识不清,使得认为etrace的实现可以硬编码进ecall中
事实上¶
ecall仅仅是系统调用,是软件由于自身权限不足而向操作系统发出的“请帮忙”请求,仅仅是异常与中断机制的一个部分; 而真正被整个异常中断机制所需要的东西是:isa_raise_intr函数,由它进行异常/中断具体类型的判断(具体判断方法RTFM),并记录瞬发时的地址,返回异常入口地址。
用户系统和程序调用(开始进入操作系统)¶
为什么实现异常响应机制后才开始对操作系统进行施工?¶
-
1.异常机制不属于OS,而是软硬件的桥 异常响应机制(在 PA 中对应 AM 层的 CTE 模块)本质上并不是操作系统本身的业务逻辑,而是硬件架构(ISA)提供的一种特权级切换与控制流劫持机制。 它是连接“无特权的用户态”与“高特权的内核态”的唯一物理桥梁。如果不先把这座桥修好,OS 和用户程序就处于物理隔离状态,无法进行任何交互。
-
- 控制权的底线:OS 的“安全绳”与“后门” 操作系统的核心职责是管理硬件资源。当 OS 通过 loader 把程序加载进内存,并将 PC(程序计数器)交给用户程序时,OS 实际上是主动放弃了 CPU 的控制权。
如果不先做异常机制: 这种控制权的交接就是单向的“自杀”。用户程序(哪怕是一个死循环)将永远占用 CPU,系统彻底失控。 先做异常机制的意义: 异常机制(如时钟中断、非法指令拦截)是 OS 留在硬件底层的“后门”。它确保了无论用户程序在做什么,底层硬件都有能力强行剥夺其执行权,将控制流切回 OS。只有确保能随时“夺回”权力,OS 才敢把权力“交出去”。 * 3. 用户程序的生存基础:系统调用(Syscall)的物理通道 在引入真正的 OS(Nanos-lite)后,用户程序(Navy-apps)将运行在低特权级(如 RISC-V 的 U-mode)下。硬件保护机制决定了它绝对无法直接调用任何硬件接口(如显卡打印、磁盘读写)。
用户程序的“第一声啼哭”(例如打印 Hello World 或执行 exit 退出)必须请求 OS 帮忙。 这种请求的唯一方式,就是通过执行特定的汇编指令(如 ecall)触发自陷(Trap),通过异常机制的桥梁回到 OS。 如果 OS 没有预先建立好异常处理的入口和分发逻辑,用户程序一旦尝试输出或退出,就会触发未处理的异常,导致系统直接崩溃(Kernel Panic)。 总结与反思 构建操作系统的过程,不能使用“使用者”的顺向思维,而必须具备“造物主”的自底向上(Bottom-up)架构思维。 先制定宇宙法则(特权级与异常机制),确保对全局的绝对掌控力;然后再创造世界(内存管理与加载器);最后才能投放生命(运行用户程序)。异常响应机制,正是这套宇宙法则的物理基石。
讲义强调的——程序和进程的本质差异:静态蓝图与动态状态机的内存映射¶
-
- 静态的程序 (Program) —— 冻结在磁盘上的 ELF 程序是一个死的实体,它是编译器和链接器固化下来的绝对状态。
- 物理载体:磁盘上的二进制文件。
- 契约验证:通过头部的 Magic Number(如
\x7fELF)向加载器证明自己的合法性。 - 空间极简主义:ELF 只存储必须在运行前确定的信息。它包含
.text(代码)、.rodata(只读数据)和.data(已初始化数据)。 -
无状态性:ELF 中绝对没有堆和栈的实体。甚至连
.bss(未初始化数据)在文件中都不占实际体积,只用一个元数据记录“这里需要多少字节的零”。 -
- 动态的进程/实例 (Process/Instance) —— 物理内存中的状态机
当 ELF 被加载到物理内存(
pmem),且 CPU 的PC寄存器指向第一条指令时,程序就“活”了,蜕变为一个动态演化的状态机。
- 动态的进程/实例 (Process/Instance) —— 物理内存中的状态机
当 ELF 被加载到物理内存(
- 物理载体:物理内存 (
pmem) + CPU 寄存器(PC,sp, 通用寄存器)。 - 栈 (Stack) 的无中生有:
- 映射:栈是程序控制流(Control Flow)的实时描摹。
- 诞生:由启动汇编(
start.S)将链接脚本中的_stack_pointer赋值给sp寄存器的那一刻瞬间激活。 - 特征:向下生长,记录函数调用链、局部变量和寄存器上下文。它是自动的、短暂的、极易越界(UB)的。
-
堆 (Heap) 的圈地运动:
- 映射:堆是程序数据流(Data Flow)的长期仓库。
- 诞生:由 C 语言运行时(
trm.c)通过_heap_start和PMEM_END强行圈出物理内存的所有剩余荒地。 - 特征:向上生长,由程序员通过
malloc/free显式管理。它是庞大的、持久的。
-
- 桥梁:链接脚本 (Linker Script)
最关键一点是:堆栈不是凭空出现的,而是由链接脚本静态规划,由启动代码动态激活的。
链接脚本(
linker.ld)就是连接静态 ELF 和动态内存的图纸。它用.(定位器)和_stack_top、_end等符号,在物理内存的坐标系里提前画好了红线。ELF 里面到底是什么? ELF 里面全是扁平化的 0 和 1。 但它不是一锅粥,它是一个带有说明书的集装箱。 可以把 ELF 文件严格分为两部分: 1. 元数据(说明书):ELF Header 和 Program Headers。这部分是给加载器(Loader)看的。它用纯二进制记录着:“我是 RISC-V 架构的”、“我的入口点(Entry Point)在
0x80000000”、“请把文件偏移0x1000开始的 4KB 数据,原封不动地搬到物理内存的0x80000000去”。 2. 有效载荷(货物):.text(代码段)和.data(数据段)。这部分才是真正给 CPU 看的。你在start.S里写的la sp, _stack_pointer,在这一层已经变成了类似0x00000197这样的 32 位机器码,死死地冻结在.text段里。
- 桥梁:链接脚本 (Linker Script)
最关键一点是:堆栈不是凭空出现的,而是由链接脚本静态规划,由启动代码动态激活的。
链接脚本(
大一统视角:从冻结到爆发的物理推演 1. 动作一:剥离与搬运(加载过程) 当 NEMU(或任何加载器)加载程序时,它根本不执行代码。它只搬运: a.读取 ELF 的“说明书”。 b. 根据说明书,把 ELF 里的“货物”(纯粹的机器码和初始数据)复制到 NEMU 的物理内存数组(
pmem)中。 c. 注意:搬运完成后,ELF 的“说明书”就被扔掉了! 物理内存里现在只剩下纯粹的指令流和数据。此时,内存依然是死寂的。 2. 动作二:点火(交接控制权) 加载器完成搬运后,做最后一件事情:强行修改 CPU 的PC(程序计数器)寄存器,将其指向刚才搬运过去的指令流的起始地址(比如0x80000000,也就是start.S的第一条指令)。 3. 动作三:状态机的展开(动态激活) 从这一微秒开始,CPU 接管了一切。CPU 根本不知道什么是 ELF,什么是 AM。它只遵循一个死循环:取指 -> 译码 -> 执行 -> 更新 PC。终极困惑 “ELF 之所以能被解读出动态过程,就是因为他有对动态过程的描述?” 极其准确。-----from gemini ELF 并没有直接保存“栈”这个动态实体,但它保存了“如何创造栈、如何使用栈”的机器码指令。 静态数据(
.data,.rodata)是被直接冻结在 ELF 里的。 动态过程(堆栈的生长、控制流的跳转)是被编码成指令冻结在 ELF 里的。 当 CPU 这台状态机开始吞噬这些指令时,冻结的逻辑就被解冻成了物理内存中电平的疯狂翻转。这就是“激活”。