找回密码
 立即注册
首页 业界区 安全 从源码分析arm64中断与GIC

从源码分析arm64中断与GIC

羡渥蛛 2025-6-11 13:56:58
本文以树莓派4b(armv8)来实现,4b支持两种

  • 传统的中断控制器
  • gic-400
    但是使用的qemu和实际的板子都是默认支持gic-400的,所以主要是借助gic-400实现中断的功能
异常处理

相关寄存器


  • PSTATE 就是cpu状态

    • DAIF 调试异常 SError(系统异常) IRQ(中断) FIQ(快速中断)

  • esr_elx 用来保存返回地址
  • spsr_elx 用来保存对应级别的PSTATE
  • elr_elx 用来保存异常的原因
处理异常时自动发生的事

CPU捕获到异常时

  • 将PSTATE保存到对应的SPSR_ELx中
  • 返回地址保存到ELR_ELx中
  • PSTATE DAIF关闭
  • 如果是同步异常把原因写入ESR_ELx,如果是中断,原因保存在GIC-400的寄存器中
  • 切换SP到对应的SP_ELx中
  • 跳转到中断向量表里
  • 执行对应的处理函数
CPU处理完异常执行eret,会

  • ELR中恢复PC
  • SPSR中恢复PSTATE (DAIF也会变)
    1.png

中断向量表

2.png

可见通过保存到对应异常的vbar中,CPU就可以在对应级别是发生异常进入中断向量表中
由于内核目前一直在EL1阶段,所以在el1的初始化函数el1_entry中
  1. el1_entry:
  2.     // 加入向量表
  3.     adr x0, vectors
  4.     msr vbar_el1, x0
复制代码
其中vector的实现是参考linux的实现
  1.     .macro kernel_ventry, el, label, regsize=64
  2.     .align 7
  3.     sub sp, sp, #S_FRAME_SIZE
  4.     b el\()\el\()_\label
  5.     .endm
  6.     .pushsection ".entry.text", "ax"
  7.     .align 11
  8. ENTRY(vectors)
  9.         kernel_ventry        1, sync_invalid                        // Synchronous EL1t
  10.         kernel_ventry        1, irq_invalid                        // IRQ EL1t
  11.         kernel_ventry        1, fiq_invalid                        // FIQ EL1t
  12.         kernel_ventry        1, error_invalid                // Error EL1t
  13.         kernel_ventry        1, sync_invalid                                // Synchronous EL1h
  14.         kernel_ventry        1, irq                      // IRQ EL1h
  15.         kernel_ventry        1, fiq_invalid                        // FIQ EL1h
  16.         kernel_ventry        1, error_invalid                        // Error EL1h
  17.         kernel_ventry        0, sync_invalid                                // Synchronous 64-bit EL0
  18.         kernel_ventry        0, irq_invalid                                // IRQ 64-bit EL0
  19.         kernel_ventry        0, fiq_invalid                        // FIQ 64-bit EL0
  20.         kernel_ventry        0, error_invalid                        // Error 64-bit EL0
  21.         kernel_ventry        0, sync_invalid, 32                // Synchronous 32-bit EL0
  22.         kernel_ventry        0, irq_invalid, 32                // IRQ 32-bit EL0
  23.         kernel_ventry        0, fiq_invalid, 32                // FIQ 32-bit EL0
  24.         kernel_ventry        0, error_invalid, 32                // Error 32-bit EL0
  25. END(vectors)
复制代码
kernel_ventry 值得说一下 el\()\el\()_\label 中\()表示一个符号的结尾,el 然后是结尾\() ,接着\el 这个就是传入宏第一个的参数,然后是结尾\(),然后是_,在跟着\label也就是第二个参数
vectors 就是个位置,依次放着各个异常的入口,只要依次实现这些入口函数,CPU发生异常的时候就会进入对应的处理函
数。
以el0_sync_invalid为例,现在先忽略kernel_entry,这个是用来现场保护的,最后会到bad_mode中进行异常的处理
  1. /*
  2. * Invalid mode handlers
  3. */
  4.         .macro        inv_entry, el, reason, regsize = 64
  5.         bl kernel_entry
  6.         mov        x0, sp
  7.         mov        x1, #\reason
  8.         mrs        x2, esr_el1
  9.         b        bad_mode
  10.         .endm
  11.        
  12. el0_sync_invalid:
  13.         inv_entry 0, BAD_SYNC
  14. ENDPROC(el0_sync_invalid)
复制代码
中断现场保护和恢复

这时候就不得不说中断发生时的现场保护了
与异常不同最后需要恢复现场
  1. el1_irq:
  2.     bl kernel_entry
  3.     bl irq_handle
  4.     bl kernel_exit
  5. ENDPROC(el1_irq)
复制代码
一个线框的定义是
  1. struct pt_regs {
  2.         struct {
  3.                 u64 regs[31];
  4.                 u64 sp;
  5.                 u64 pc;
  6.                 u64 pstate;
  7.         };
  8.         u64 orig_x0;
  9. #ifdef __AARCH64EB__
  10.         u32 unused2;
  11.         s32 syscallno;
  12. #else
  13.         s32 syscallno;
  14.         u32 unused2;
  15. #endif
  16.         u64 orig_addr_limit;
  17.         u64 unused; // maintain 16 byte alignment
  18.         u64 stackframe[2];
  19. };
复制代码
主要就是32个寄存器和SP、PC、PSTATE,所以只要按照这个顺序依次保存就可以
  1. // 保护 pt_regs
  2. kernel_entry:
  3.         //开辟空间
  4.     sub sp, sp, #S_FRAME_SIZE
  5.     // 保存普通寄存器
  6.     stp        x0, x1, [sp, #16 * 0]
  7.         stp        x2, x3, [sp, #16 * 1]
  8.         stp        x4, x5, [sp, #16 * 2]
  9.         stp        x6, x7, [sp, #16 * 3]
  10.         stp        x8, x9, [sp, #16 * 4]
  11.         stp        x10, x11, [sp, #16 * 5]
  12.         stp        x12, x13, [sp, #16 * 6]
  13.         stp        x14, x15, [sp, #16 * 7]
  14.         stp        x16, x17, [sp, #16 * 8]
  15.         stp        x18, x19, [sp, #16 * 9]
  16.         stp        x20, x21, [sp, #16 * 10]
  17.         stp        x22, x23, [sp, #16 * 11]
  18.         stp        x24, x25, [sp, #16 * 12]
  19.         stp        x26, x27, [sp, #16 * 13]
  20.         stp        x28, x29, [sp, #16 * 14]
  21.         //保存最开始sp的位置到x21
  22.     add x21, sp, #S_FRAME_SIZE
  23.     mrs x22, elr_el1
  24.     mrs x23, spsr_el1
  25.     stp lr, x21, [sp, #S_LR]
  26.     stp x22, x23, [sp, #S_PC]
  27.     ret
复制代码
stp 会依次保存两个寄存器到 sp+第二个数的位置
接下来就是恢复时候
  1. // 恢复 pt_regs
  2. kernel_exit:
  3.     // 先把pc 和 pstate恢复
  4.     ldp x22, x23, [sp, #S_PC]
  5.     msr        elr_el1, x22                        // set up the return data
  6.         msr        spsr_el1, x23
  7.     ldp        x0, x1, [sp, #16 * 0]
  8.         ldp        x2, x3, [sp, #16 * 1]
  9.         ldp        x4, x5, [sp, #16 * 2]
  10.         ldp        x6, x7, [sp, #16 * 3]
  11.         ldp        x8, x9, [sp, #16 * 4]
  12.         ldp        x10, x11, [sp, #16 * 5]
  13.         ldp        x12, x13, [sp, #16 * 6]
  14.         ldp        x14, x15, [sp, #16 * 7]
  15.         ldp        x16, x17, [sp, #16 * 8]
  16.         ldp        x18, x19, [sp, #16 * 9]
  17.         ldp        x20, x21, [sp, #16 * 10]
  18.         ldp        x22, x23, [sp, #16 * 11]
  19.         ldp        x24, x25, [sp, #16 * 12]
  20.         ldp        x26, x27, [sp, #16 * 13]
  21.         ldp        x28, x29, [sp, #16 * 14]
  22.     //最后再恢复lr 和 之前的sp
  23.         ldr        lr, [sp, #S_LR]
  24.         add        sp, sp, #S_FRAME_SIZE                // restore sp   
  25.     eret
复制代码
就是保存的逆操作,最后通过 eret 返回到elr_el1指向的位置
GIC-400

基本介绍

上面就已经把异常发生时候的软件部分部分说完了,接下来就是控制中断的GIC-400驱动的实现了。
GIC支持的中断有:

  • SGI 软件中断
  • PPI 私有外设中断
  • SPI 共享外设中断,只有SPI可以设置分发的CPU
GIC是中断控制器,分为分发器(dist) 和 CPU接口
3.png


从GIC角度来看,一个中断发生过程

  • 当GIC 检测到一个中断发生时,会将该中断状态从inactive状态标记为pending状态。
  • 对于处在penging状态的中断,分发器会确定目标 CPU, 将中断请求发给这个CPU。
  • 对于每个 CPU, 分发器会从众多处于等待状态的中断中选择一个优先级最高的中断,发送到目标CPU 的 CPU 接口。
  • CPU 接口会决定这个中断是否可以发送给CPU, 如果这个中断的优先级满足要求,GIC 会发送一个中断请求信号给 CPU.
  • CPU 进入中断异常, 读取 GICC_IAR 来响应该中断(一般是由 Linux 内核的中断处理程序来读寄存器)。寄存器会返回硬件中断号(hardware interrupt ID)。对于 SGI 来说,返回源CPU 的ID (source processor ID) 。当GIC感知到软件读取了该寄存器后,根据如下情况处理。

    • 如果该中断源处于pending 状态,则将该中断状态切换到 active 状态
    • 如果该中断又重新产生,那么该中断状态则变成 active and pending 状态

  • 如果该中断正在忙,正在处理其他中断, 则该中断状态其切换为 active and pending 状态,等待CPU将当前当前的中断处理结束之后,再将该中断切换到 active 状态
  • 处理器完成中断服务,发送一个完成信号结束中断(End of Interupt,EOI) 给 GIC。该中断状态再切换到 inactive 状态。
从CPU来看

  • 接受到一个中断信号,进入中断向量表
  • 执行gic driver中的中断handle函数
  • 读取GICC_IAR得到原因,执行对应的处理函数
寄存器描述

GIC-400 分发器寄存器 (Distributor Registers)
偏移地址 (Hex)寄存器名中文名类型位域描述 (位宽: 32-bit)0x000GICD_CTLR分发器控制寄存器RW- [0]: 全局中断转发使能
- [1]: Group1 中断使能
- [2]: Group0 中断使能0x004GICD_TYPER分发器类型寄存器RO- [4:0]: 支持的中断数(ITLinesNumber = N/32 -1)
- [7:5]: CPU 数量 -1
- [10:8]: 共享中断数(LSPI)0x008GICD_IIDR分发器标识寄存器RO- [31:0]: 厂商和版本信息0x080GICD_IGROUPRn中断组寄存器RW每 bit 对应一个中断:
- 0: Group0(安全)
- 1: Group1(非安全)0x100GICD_ISENABLERn中断使能寄存器RW每 bit 对应一个中断:
- 1: 使能中断0x180GICD_ICENABLERn中断禁用寄存器RW每 bit 对应一个中断:
- 1: 禁用中断0x400GICD_IPRIORITYRn中断优先级寄存器RW每中断占 8 位:
- [7:0]: 优先级(值越低优先级越高)0x800GICD_ITARGETSRn中断目标 CPU 寄存器RW每中断占 8 位:
- [7:0]: 目标 CPU 掩码(每 bit 对应一个 CPU)0xC00GICD_ICFGRn中断配置寄存器RW每中断占 2 位:
- 00: 电平触发
- 01: 边沿触发CPU 接口寄存器 (CPU Interface Registers)如下
偏移地址 (Hex)寄存器名中文名类型位域描述 (位宽: 32-bit)0x0000GICC_CTLRCPU 控制寄存器RW- [0]: CPU 接口使能
- [1]: Group0 FIQ 旁路
- [2]: Group1 IRQ 旁路0x0004GICC_PMR优先级屏蔽寄存器RW- [7:0]: 优先级阈值(仅高 4 位有效)0x0008GICC_BPR二进制点寄存器RW- [2:0]: 优先级分组值0x000CGICC_IAR中断应答寄存器RO- [9:0]: 中断 ID
- [12:10]: 源 CPU ID0x0010GICC_EOIR中断结束寄存器WO- [9:0]: 结束中断的 ID0x0014GICC_RPR运行优先级寄存器RO- [7:0]: 当前中断优先级0x0018GICC_HPPIR最高挂起中断寄存器RO- [9:0]: 最高优先级挂起中断 ID0x001CGICC_ABPR别名二进制点寄存器RW- [2:0]: Group0 二进制点值0x00D0GICC_DIR停用中断寄存器WO- [9:0]: 停用中断 ID(虚拟化扩展)说明

  • 偏移地址:相对基地址(如 GICD_BASE 或 GICC_BASE)。
  • 寄存器数组(如 GICD_IGROUPRn):每个寄存器管理 32 个中断(例如 n=0 对应中断 0-31)。
  • 优先级:实际有效位数由实现决定(例如 4 位或 8 位)。
  • 目标 CPU 掩码:例如 0x01 表示 CPU0,0x03 表示 CPU0 和 CPU1。
  • GICD_ITARGETSRn用来控制中断号的目标CPU,每八位描述一个中断号,前32个中断号(GICD_ITARGETSR0-7)是只读的,只有SPI可以配置到哪个CPU
GIC初始化

5.png

gic的结构体如下
  1. struct gic_chip_data {
  2.         u64 raw_dist_base;
  3.         u64 raw_cpu_base;
  4.         struct irq_domain *domain;
  5.         struct irq_chip *chip;
  6.         u32 gic_irqs;
  7. };
  8. #define gic_dist_base(d) ((d)->raw_dist_base)
  9. #define gic_cpu_base(d) ((d)->raw_cpu_base)
复制代码
初始化函数,可以对照上面的寄存器看一下所需要的寄存器
  1. int gic_init(int chip, u32 dist_base, u32 cpu_base)
  2. {
  3.         printk("gic init ...\n");
  4.         struct gic_chip_data *gic;
  5.         gic = &gic_data[chip];
  6.         gic->raw_dist_base = dist_base;
  7.         gic->raw_cpu_base = cpu_base;
  8.         u32 irq_num = (readl(gic_dist_base(gic) + GIC_DIST_TYPER) & 0x1f);
  9.         irq_num = (irq_num + 1) * 32;
  10.         gic->gic_irqs = irq_num;
  11.         printk("cpu_base:0x%x, dist_base:0x%x, gic_irqs:%d\n",
  12.                gic_dist_base(gic), gic_cpu_base(gic), gic->gic_irqs);
  13.         gic_dist_init(gic);
  14.         gic_cpu_init(gic);
  15.         return 0;
  16. }
复制代码
[code]static void gic_dist_init(struct gic_chip_data *gic){        u64 base = gic_dist_base(gic);        writel(GICD_ENABLE, base + GIC_DIST_CTRL);        u32 cpu_mask = gic_get_cpumask(gic);        cpu_mask |= cpu_mask
您需要登录后才可以回帖 登录 | 立即注册