首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

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

  • 25-03-04 14:01
  • 2776
  • 9312
blog.csdn.net

往期内容

本专栏往期内容,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-CSDN博客
    – 末篇,有专栏内容观看顺序

总线和设备树专栏:

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

img

目录

  • 往期内容
  • 1.回顾中断的发生、处理过程
  • 2.异常向量表的安装
    • 2.1 复制向量表
    • 2.2 向量表在哪
  • 3.中断向量
  • 4.处理流程
  • 5.处理函数

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

  • 中断发生的硬件过程

img

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

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

中断的硬件框架-CSDN博客

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函数
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 创建新向量表,将向量表的地址复制到新分配的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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

下面是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();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 向量页分配与初始化:提前分配两个页面的大小用于异常向量表,然后调用 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
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 指令未定义初始化:对整个向量页填充一条未定义的指令 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 的段
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • __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)向量
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

其中每一行的 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

注:本文转载自blog.csdn.net的憧憬一下的文章"https://blog.csdn.net/caiji0169/article/details/143783341"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top