ARM架构中断与异常向量表机制解析

往期内容

本专栏往期内容,interrtupr子系统:

  1. 深入解析Linux内核中断管理:从IRQ描述符到irq domain的设计与实现
  2. Linux内核中IRQ Domain的结构、操作及映射机制详解
  3. 中断描述符irq_desc成员详解
  4. Linux 内核中断描述符 (irq_desc) 的初始化与动态分配机制详解
  5. 中断的硬件框架
  6. GIC介绍
  7. GIC寄存器介绍

pinctrl和gpio子系统专栏:

  1. 专栏地址:pinctrl和gpio子系统

  2. 编写虚拟的GPIO控制器的驱动程序:和pinctrl的交互使用

    – 末片,有专栏内容观看顺序

input子系统专栏:

  1. 专栏地址:input子系统
  2. input角度:I2C触摸屏驱动分析和编写一个简单的I2C驱动程序
    – 末片,有专栏内容观看顺序

I2C子系统专栏:

  1. 专栏地址:IIC子系统
  2. 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-优快云博客
    – 末篇,有专栏内容观看顺序

总线和设备树专栏:

  1. 专栏地址:总线和设备树
  2. 设备树与 Linux 内核设备驱动模型的整合-优快云博客
    – 末篇,有专栏内容观看顺序

img

1.回顾中断的发生、处理过程

  • 中断发生的硬件过程

img

  • 中断处理的软件处理流程

    • CPU执行完当前指令,检查到发生了中断,跳到向量表
    • 保存现场、执行GIC提供的处理函数、恢复现场

中断的硬件框架-优快云博客

2.异常向量表的安装

对于arm架构,异常向量表有两个位置:0和0xffff0000。前者一般在裸机中会用到;后者是上了操作系统后的,并且这个地址是虚拟地址,需要在物理地址上存放好vector向量表后,将虚拟地址0xffff0000映射到该存放好向量表的物理地址

2.1 复制向量表

  • 汇编代码
// arch\arm\kernel\head.S
1. bl	__lookup_processor_type
   ...... 
2. bl	__create_page_tables  //创建页表:建立虚拟地址和物理地址之间的映射关系
3. ldr	r13, =__mmap_switched //address to jump to after mmu has been enabled 当使能mmu后会跳到mmap_switched函数,将该函数地址保存到r13
4. b	__enable_mmu
   b	__turn_mmu_on
   mov	r3, r13
   ret	r3
5. __mmap_switched: // arch\arm\kernel\head-common.S
6. b	start_kernel  //这里是跳转指令,也就是跳转去仔细start_kernel函数
  • 创建新向量表,将向量表的地址复制到新分配的vectors中,主要是在early_trap_init函数中实现的。
start_kernel // init\main.c
    setup_arch(&command_line); // arch\arm\kernel\setup.c
        paging_init(mdesc);    // arch\arm\mm\mmu.c
            devicemaps_init(mdesc); // arch\arm\mm\mmu.c
                vectors = early_alloc(PAGE_SIZE * 2); // 1.分配新向量表 -- 物理内存
                early_trap_init(vectors);             // 2.在代码中将vectors中的向量表复制到新向量表,具体看下图

                // 3. 映射新向量表到虚拟地址0xffff0000
                //存放向量表的物理地址和虚拟地址0xffff0000之间的关联
                /*
                 * Create a mapping for the machine vectors at the high-vectors
                 * location (0xffff0000).  If we aren't using high-vectors, also
                 * create a mapping at the low-vectors virtual address.
                 */
                map.pfn = __phys_to_pfn(virt_to_phys(vectors));
                map.virtual = 0xffff0000;
                map.length = PAGE_SIZE;
            #ifdef CONFIG_KUSER_HELPERS
                map.type = MT_HIGH_VECTORS;
            #else
                map.type = MT_LOW_VECTORS;
            #endif
                create_mapping(&map);

下面是devicemaps_init函数中具体的内容:

static void __init devicemaps_init(const struct machine_desc *mdesc)
{
    struct map_desc map;  // 用于定义映射区域的结构体
    unsigned long addr;   // 循环使用的地址变量
    void *vectors;        // 向量表的虚拟地址指针

    /*
     * 1. 提前分配异常向量页的内存,通常大小为两个页面。
     *    异常向量用于处理处理器的各种异常(如中断、错误等)。
     */
    vectors = early_alloc(PAGE_SIZE * 2);

    // 提前初始化异常向量,将 vectors 地址传递给异常初始化函数
    early_trap_init(vectors);

    /*
     * 2. 清除页表,排除顶层 PMD 页面,以便稍后使用 early_fixmaps。
     *    VMALLOC_START 表示虚拟内存的起始地址。
     */
    for (addr = VMALLOC_START; addr < (FIXADDR_TOP & PMD_MASK); addr += PMD_SIZE)
        pmd_clear(pmd_off_k(addr));

    /*
     * 3. 如果内核是 XIP (Execute In Place) 模式,则在 modulearea 中映射内核代码。
     *    XIP 模式允许直接从 ROM 中执行代码,不必将代码拷贝到 RAM 中。
     */
#ifdef CONFIG_XIP_KERNEL
    map.pfn = __phys_to_pfn(CONFIG_XIP_PHYS_ADDR & SECTION_MASK);  // XIP 的物理基地址
    map.virtual = MODULES_VADDR;   // 模块区的起始虚拟地址
    map.length = ((unsigned long)_exiprom - map.virtual + ~SECTION_MASK) & SECTION_MASK;  // 映射长度
    map.type = MT_ROM;  // 映射类型为只读
    create_mapping(&map);  // 创建内核的只读映射
#endif

    /*
     * 4. 映射缓存刷新区域。此区域用于缓存清除和同步 CPU 缓存。
     */
#ifdef FLUSH_BASE
    map.pfn = __phys_to_pfn(FLUSH_BASE_PHYS);  // 映射物理地址
    map.virtual = FLUSH_BASE;  // 缓存刷新区域的虚拟地址
    map.length = SZ_1M;  // 映射长度为 1 MB
    map.type = MT_CACHECLEAN;  // 设置为缓存清理类型
    create_mapping(&map);  // 创建映射
#endif
#ifdef FLUSH_BASE_MINICACHE
    map.pfn = __phys_to_pfn(FLUSH_BASE_PHYS + SZ_1M);  // 小缓存物理地址
    map.virtual = FLUSH_BASE_MINICACHE;  // 小缓存虚拟地址
    map.length = SZ_1M;  // 映射长度 1 MB
    map.type = MT_MINICLEAN;  // 小缓存清理类型
    create_mapping(&map);  // 创建映射
#endif

    /*
     * 5. 为处理器的高向量位置(0xffff0000)创建一个映射。
     *    如果不使用高向量,也在低向量位置(0x00000000)创建映射。
     */
    map.pfn = __phys_to_pfn(virt_to_phys(vectors));  // 计算向量的物理页帧号
    map.virtual = 0xffff0000;  // 设置向量的虚拟地址为高向量位置
    map.length = PAGE_SIZE;  // 设置映射大小
#ifdef CONFIG_KUSER_HELPERS
    map.type = MT_HIGH_VECTORS;  // 高向量类型
#else
    map.type = MT_LOW_VECTORS;  // 低向量类型
#endif
    create_mapping(&map);  // 创建高向量映射

    // 如果不使用高向量,则在低向量地址(0x00000000)创建映射
    if (!vectors_high()) {
        map.virtual = 0;
        map.length = PAGE_SIZE * 2;  // 为两个页面创建映射
        map.type = MT_LOW_VECTORS;  // 低向量类型
        create_mapping(&map);  // 创建低向量映射
    }

    /*
     * 6. 为内核创建一个只读映射,用于保护高向量的内核页面。
     */
    map.pfn += 1;  // 下一个物理页面
    map.virtual = 0xffff0000 + PAGE_SIZE;  // 设置为高向量第二页面的虚拟地址
    map.length = PAGE_SIZE;  // 映射一个页面
    map.type = MT_LOW_VECTORS;  // 低向量类型
    create_mapping(&map);  // 创建只读映射

    /*
     * 7. 如果机器描述结构中包含 map_io 函数,则调用它映射静态映射设备;
     *    否则使用默认的 debug_ll_io_init() 进行初始化。
     */
    if (mdesc->map_io)
        mdesc->map_io();
    else
        debug_ll_io_init();

    // 填补 PMD 缺口
    fill_pmd_gaps();

    /*
     * 8. 为 VMALLOC 区域保留固定的 I/O 空间,用于 PCI 设备。
     */
    pci_reserve_io();

    /*
     * 9. 刷新缓存和 TLB 确保所有内存操作都一致。
     *    此外确保写缓存中的向量页已写回。
     */
    local_flush_tlb_all();  // 刷新整个 TLB
    flush_cache_all();      // 刷新缓存

    /*
     * 10. 启用异步中止异常处理。
     */
    early_abt_enable();
}
  • 向量页分配与初始化:提前分配两个页面的大小用于异常向量表,然后调用 early_trap_init 初始化异常向量。
  • 页表清理:清除页表,以便为早期的固定映射区域做准备。
  • XIP 模式下的内核映射:如果配置了 XIP(即在 ROM 中直接执行内核代码),则为 XIP 内核创建只读映射。
  • 缓存刷新区域映射:创建缓存清理区域的映射,用于 CPU 缓存同步。
  • 向量表映射:如果使用高向量位置,则创建映射在 0xffff0000 地址;若不使用高向量,还会在 0x00000000 地址创建低向量映射。
  • 只读映射创建:为向量表的只读区域创建映射,提供额外保护。
  • 静态映射设备的 I/O 映射:根据机器描述结构 mdesc 中的 map_io 函数映射 I/O 设备,否则使用默认方法初始化。
  • VMALLOC 区域 I/O 空间保留:为 PCI 设备保留固定的 I/O 空间。
  • 缓存与 TLB 刷新:刷新缓存和 TLB 确保内存一致性,防止写缓存中的向量页未写回而影响异常处理。
  • 启用异步异常:开启处理器的异步异常(如数据预取异常)。

而该函数中的 early_trap_init(vectors);就是将vectors段进行初始化,比如向量表起始地址设置为__vectors_start

void __init early_trap_init(void *vectors_base)
{
#ifndef CONFIG_CPU_V7M
    unsigned long vectors = (unsigned long)vectors_base;  // 向量页的起始地址
    extern char __stubs_start[], __stubs_end[];  // 存根代码的起始和结束地址
    extern char __vectors_start[], __vectors_end[];  // 向量表的起始和结束地址
    unsigned i;  // 用于遍历向量页的循环变量

    // 将传入的向量页基地址存入全局变量 vectors_page,供其他地方使用
    vectors_page = vectors_base;

    /*
     * 1. 初始化向量页,使所有未定义的指令都跳转到同一个未定义指令。
     *    这里使用的指令 0xe7fddef1 在 ARM 和 Thumb 两种指令集下都是未定义的。
     *    这种处理方式确保任何未捕获的异常都将被捕获。
     */
    for (i = 0; i < PAGE_SIZE / sizeof(u32); i++)
        ((u32 *)vectors_base)[i] = 0xe7fddef1;

    /*
     * 2. 将向量表和存根代码复制到向量页(地址 0xffff0000 处);
     *    向量表用于异常处理入口,存根代码提供了具体的异常处理逻辑。
     */
    memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);  // 复制向量表
    memcpy((void *)vectors + 0x1000, __stubs_start, __stubs_end - __stubs_start);  // 复制存根代码

    // 初始化用户空间的辅助代码(kuser helpers),用于用户态和内核态的交互
    kuser_init(vectors_base);

    /*
     * 3. 刷新指令缓存确保向量表和存根代码对处理器指令流可见。
     *    刷新范围为向量页的两个页面大小(即 8 KB)。
     */
    flush_icache_range(vectors, vectors + PAGE_SIZE * 2);

#else /* ifndef CONFIG_CPU_V7M */
    /*
     * 在 ARM Cortex-M 处理器(如 Cortex-M3 和 Cortex-M4)上,向量表位置可以通过配置寄存器指定,
     * 因此不需要复制向量表到专用内存区域。直接在内核镜像中使用即可。
     */
#endif
}
  • 指令未定义初始化:对整个向量页填充一条未定义的指令 0xe7fddef1。该指令在 ARM 和 Thumb 指令集中都是未定义的,因此任何未捕获的异常都会导致未定义的指令异常,从而统一跳转到异常处理逻辑中。这种方法在初始化阶段为所有异常提供一个默认处理入口。

  • 向量表与存根代码复制

    • 将向量表和存根代码从内核镜像复制到向量页。向量表用于存放异常入口地址,例如中断、系统调用等入口。
    • 存根代码是实际的异常处理逻辑,它们与向量表中的入口地址配合实现对异常的处理。
  • 用户辅助功能初始化kuser_init 函数用于初始化 kuser helpers。kuser helpers 是一组用于用户空间与内核进行低级交互的辅助函数。

  • 指令缓存刷新:调用 flush_icache_range 函数刷新指令缓存,使得新的向量表和存根代码对处理器的指令流可见。这确保处理器在异常发生时能够正确获取最新的异常处理代码。

  • Cortex-M 特殊处理:在 Cortex-M 系列处理器(如 ARMv7-M 架构)上,向量表的位置可以直接配置,而不需要复制向量表到一个特定的内存区域。

img

2.2 向量表在哪

上面说到在代码中就有将向量表的起始地址__vectors_start复制到新分配的vectors段中,vectros段的内容怎么去找到它??就是靠__vectors_start

上面代码中可以看到代码中向量表位于__vectors_start处( 向量表起始地址),它在arch/arm/kernel/vmlinux.lds中定义, 也就是下面的连接脚本,指定 .vectors 段的加载位置(0xffff0000),确保在 CPU 触发异常时可以正确找到中断向量表的位置(也就是找到vectors段中的向量表) :

__vectors_start = .;                            // 定义向量表起始地址
.vectors 0xffff0000 : AT(__vectors_start) {     // 将 .vectors 段加载到物理地址0xffff0000
  *(.vectors)                                    // 匹配名为 .vectors 的段
}
. = __vectors_start + SIZEOF(.vectors);          // 更新当前地址指针到 .vectors 结束
__vectors_end = .;                               // 定义向量表结束地址
__stubs_start = .;                               // 定义 stubs 区域起始地址
.stubs ADDR(.vectors) + 0x1000 : AT(__stubs_start) { // 将 .stubs 段放置在偏移 0x1000 处
  *(.stubs)                                      // 匹配名为 .stubs 的段
}
  • __vectors_start = .;:定义 .vectors 段的起始地址为当前地址。
  • **.vectors 0xffff0000 : AT(__vectors_start) { \*(.vectors) }**:将 .vectors 段分配到内存地址 0xffff0000,并将 __vectors_start 设置为该段的物理加载地址。*(.vectors) 指令将所有 .vectors 名称的段匹配到这里。
  • **. = __vectors_start + SIZEOF(.vectors);**:更新当前地址到 .vectors 段结束处,计算 .vectors 段大小。
  • .stubs ADDR(.vectors) + 0x1000 : AT(__stubs_start) { \*(.stubs) }:将 .stubs 段放置在 .vectors 段的偏移 0x1000 位置,为跳转指令提供目的地。

在arch\arm\kernel\entry-armv.S里搜.vectors,可以找到vectors段的内容。 这段汇编代码定义了一个 .vectors 段,其中包含 ARM 处理器的中断向量表。ARM 处理器在发生异常时,会跳转到固定的地址来执行特定的中断向量代码:

.section .vectors, "ax", %progbits
.L__vectors_start:                 // 向量表起始地址标签
    W(b)   vector_rst              // 重置向量
    W(b)   vector_und              // 未定义指令异常向量
    W(ldr) pc, .L__vectors_start + 0x1000 // 软件中断向量,跳转到偏移0x1000的位置
    W(b)   vector_pabt             // 预取指令异常向量
    W(b)   vector_dabt             // 数据访问异常向量
    W(b)   vector_addrexcptn       // 地址异常向量
    W(b)   vector_irq              // 外部中断(IRQ)向量
    W(b)   vector_fiq              // 快速中断(FIQ)向量

其中每一行的 W(b)W(ldr) 指令表示一个 ARM 指令:

  • W(b):生成一个跳转指令 b,用于跳转到指定的中断向量处理函数。
  • W(ldr):生成一个加载指令 ldr,将 .L__vectors_start + 0x1000 处的地址加载到 pc 寄存器,实现对软件中断的处理。

中断向量定义如下:

  • vector_rst:复位向量,发生复位时跳转执行。
  • vector_und:未定义指令异常向量。
  • vector_pabt:预取指令异常向量,指令预取时发生异常。
  • vector_dabt:数据访问异常向量,数据访问时发生异常。
  • vector_addrexcptn:地址异常向量。
  • vector_irq:外部中断(IRQ)向量。
  • vector_fiq:快速中断(FIQ)向量。

img

关联

  • .vectors 段包含了 ARM 异常中断向量,指向特定异常的处理函数。
  • 连接脚本将 .vectors 段放置在 0xffff0000 地址,该地址是 ARM 处理器默认的异常向量位置。
  • ldr 指令将 .stubs 段(偏移 0x1000)作为跳转目标,从而执行不同的异常处理代码。

3.中断向量

发生中断时,CPU跳到向量表去执行b vector_irq

vector_irq函数使用宏来定义:

img

4.处理流程

img

5.处理函数

img

评论 60
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值