指令集(ISA),微架构,操作系统

指令集简单来说就是一套语言规范,定义指令类型,寄存器,数据类型,还有cpu的管理模式等。而微架构是依据指令集设计出的处理器硬件结构与实现方案。相同的指令集设计出来的微架构可能完全不同,比如Intel和amd的芯片都是x86指令集。但是设计出来的cpu结构完全不同,性能和能耗也天差地别。影响cpu性能的大部分是微架构设计,工艺制程,系统级整合等,而指令集带来的差异往往只是小头。指令集的流行主要是商业和生态决定的。

操作系统的内核及驱动源码,绝大部分都由高级语言(如 C 语言)编写,核心逻辑在不同 CPU 架构上是相同的。只有少量直接与硬件交互的底层代码(例如上下文切换、中断处理、MMU 配置,以及驱动的外设寄存器访问、DMA、中断注册等),需要依赖内核提供的架构抽象层。这部分抽象层会根据 x86、ARM 等指令集分别实现,驱动本身则通过统一接口调用,因此源码通常也跨架构通用。把这些代码按目标架构编译后,生成对应的内核镜像和驱动模块,就能在该 CPU 上使用了。

riscv指令集

riscv只是64位宽的数据和地址寻址,为什么指令的编码宽度只有32位

指令长度和 CPU 位数没有必然联系,它是由“编码效率”和“代码密度”的权衡决定的。 RISC-V 的基础整数指令集虽然需要操作 64 位数据,但指令本身只需要编码32 位.

为什么指令格式中字段位置不统一,比如有些指令立即数被分散在不同位。

  • 固定字段(rs1, rs2, rd):为了并行读寄存器,绝对位置恒定。
  • 立即数碎片化:是被固定字段挤压后的必然形态,但通过巧妙的位映射,硬件拼接零开销。
  • 共享位位置:不同格式的立即数符号位、高位等有意重叠,让加法器和扩展电路能复用
  • riscv的体系结构是基于加载和存储的体系结构设计的,所以所有的数据处理都要在通用寄存器中完成

函数调用栈

描述:分配一个全新的虚拟内存页作为栈,左上角为低地址右下角为高地址。

1. 入口点与主函数

  • main 不是进程的起点。在 main 执行之前,操作系统和运行时库会完成一系列初始化工作(如设置栈指针、全局指针、调用全局构造函数等),然后才跳转到 main
  • main 的返回地址指向其调用者(通常是 __libc_start_main 或启动代码中的下一条指令。该返回地址在调用 main 时已被保存在特定的位置(在 RISC‑V 中为 ra 寄存器;在 x86‑64 中被压入栈)。

2. 函数栈帧的创建与使用

  • 当一个函数(例如 main)被调用时,它会通过减小栈指针sp的值分配栈帧。

    栈帧用于:

    • 保存返回地址(若该函数还会调用其他函数,即非叶子函数,则必须将返回地址保存到栈上;叶子函数可以视调用约定省略此步)。
    • 保存被调用者保存的寄存器(callee‑saved registers),以便函数返回时恢复调用者的状态。
    • 存放局部变量和临时数据。
    • 为传递超出寄存器数量的参数预留空间(通常放在调用者栈帧的顶部,或由调用者分配的“参数构造区”)。

3. 参数传递

  • 调用函数前,参数先被放入特定的寄存器(如 RISC‑V 的 a0a7,x86‑64 的 rdirsi 等)。
  • 若参数数量超过寄存器容量,多余的参数被压入栈(通常放在当前调用者的栈帧中,由调用者负责分配和清理)。

4. 调用与返回的通用流程

  • call 指令:将程序计数器(PC)跳转到被调用函数的起始地址,并同时保存返回地址(保存方式因架构而异:RISC‑V 写入 ra,x86‑64 压入栈)。

  • 被调用函数入口:

    1. 分配栈帧(addi sp, sp, -N 或等效操作)。
  1. 返回地址被调用者保存的寄存器(如 rafps0s11 等)存入栈帧。
  2. 执行函数体。
  • 函数返回前:

    1. 将返回值放入约定的寄存器(如 a0)或内存地址。
  1. 从栈帧中恢复之前保存的返回地址和寄存器。
  2. 释放栈帧(addi sp, sp, +N)。
  • ret 指令:将程序计数器恢复为返回地址,继续执行调用者的后续指令。

异常处理

异常类型

  • 中断:异步
  • 异常:同步
    • 指令访问异常
    • 数据访问异常
      • 加载异常
      • 存储异常
  • 系统调用:同步

异常时cpu做的事

CPU 硬件动作 具体内容(以现代通用处理器为例)
1. 识别并仲裁 判断事件优先级(如不可屏蔽中断 > 错误异常 > 普通中断),选择一个最高优先级的事件处理
2. 保存关键上下文(部分) 程序计数器(PC/EPC)程序状态字(PSR/EFLAGS/SPSR) 自动保存到特定寄存器或栈上(不同架构不同:x86自动压入内核栈,ARM存入ELR/SPSR,RISC-V存入mepc/mcause
3. 切换执行特权级 自动提升到内核模式/超级用户模式(如x86的Ring0,ARM的EL1,RISC-V的M-mode或S-mode),并切换到内核栈(通过TSS、SP_EL1或mscratch交换)
4. 查找处理程序入口 根据事件类型读取中断/异常向量表(x86的IDT,ARM的异常向量表,RISC-V的mtvec表),获得入口地址
5. 跳转执行 将PC设置为入口地址,开始执行异常入口代码(通常是操作系统的底层汇编)
6. (可选)屏蔽同类事件 很多CPU会自动清除中断使能位(如x86的IF位),防止处理过程中被同一类型的事件打断

总结:硬件完成的工作是“强拉”CPU进入一个已知的安全状态,并交给操作系统软件。这些操作只需要几个时钟周期,极其高效。

异常时os做的事

此时CPU已经跳转到OS预置的异常处理入口地址(例如 vector_swi, irq_handler, exception_vectors 等)。这部分代码必须用汇编编写,因为它需要操作CPU特殊寄存器、切换栈指针、并协调与高层C代码的调用约定。

OS 底层入口动作 详细说明
1. 保存所有通用寄存器 硬件只保存了PC和状态标志,OS必须将剩余的所有通用寄存器(如R0-R31,x0-x29等)压入内核栈,形成结构化的“pt_regs”或“trap frame”,以便后续恢复
2. 确定事件原因 读取硬件寄存器的值,跳转到合适的异常处理程序中
3. 恢复上下文 异常或者中断处理完成后,恢复保存在栈上的上下文
4. 返回异常现场 从C函数返回后,恢复所有保存的寄存器,执行一条特殊的“异常返回”指令,由硬件恢复PC和特权级,继续执行原程序

异常的处理策略

异常类型 OS 高级处理动作(C代码)
外部硬件中断 1. 关中断(通常已经在入口处由硬件或汇编做了) 2. 调用中断控制器接口读取中断号 3. 根据中断号查找注册的设备驱动处理函数(如 request_irq 注册的handler) 4. 执行驱动的中断服务例程(ISR) 5. 处理完后,判断是否需要进行软中断(下半部)或唤醒等待进程 6. 开中断(可能)
系统调用(陷阱) 1. 判断系统调用号是否合法 2. 从用户空间拷贝参数(用 copy_from_user) 3. 执行对应的内核服务函数(如文件系统、进程管理、网络) 4. 将结果返回给用户 5. 检查是否需要重新调度
缺页(故障) 1. 获取发生故障的虚拟地址(读取控制寄存器) 2. 检查地址是否合法(是否在进程地址空间内) 3. 如果合法,执行页面分配或磁盘读取(换页) 4. 修改页表,TLB失效 5. 返回后硬件会自动重试那条指令
非法指令/除法错误 1. 向当前进程发送信号(如 SIGILL, SIGFPE) 2. 如果用户自定义了信号处理函数,则调用它;否则终止进程(core dump)
调试异常/断点 1. 暂停当前进程,唤醒调试器(如gdb) 2. 允许调试器查看或修改内存/寄存器

关键点:在这一阶段,OS可能修改进程的内存空间、调度状态、甚至切换到另一个进程。这些操作都不会影响硬件对下一次异常的处理。