​ 任何玩过Arm64架构的朋友都知道,我们的ARM64架构有异常:Exception Levels, ELs,它是其异常处理机制的核心组成部分,允许系统在不同的特权级别下执行代码。ARM64定义了四个异常级别,每个级别具有不同的特权、功能和访问权限。以下是对每个异常级别的详细介绍:

四层异常与他们的切换

1. EL0(用户模式)

  • 特权级别:最低。
  • 功能:咱们的程序一般都在这里,EL0运行普通用户应用程序,没有访问硬件资源的直接权限,所有对资源的访问都必须通过系统调用转发到EL1。
  • 访问限制:不能直接访问系统控制寄存器、内存管理单元等关键硬件组件。

2. EL1(内核模式)

  • 特权级别:中等特权。
  • 功能:执行操作系统内核(Linux跑在这里)和设备驱动程序。EL1可以管理硬件资源,提供对应用程序的支持。
  • 访问权限:可以访问大多数系统资源,包括中断控制器、定时器等。EL1能够处理来自EL0的系统调用和异常。

3. EL2(虚拟化模式)

  • 特权级别:高特权。
  • 功能:主要用于虚拟化环境,允许多个虚拟机共享硬件资源。EL2可以管理虚拟机监控器(hypervisor),提供对虚拟机的管理和资源分配。
  • 访问权限:除了访问EL1的资源外,还能控制和管理EL0和EL1的虚拟化环境。

4. EL3(安全监控模式)

  • 特权级别:最高特权。
  • 功能:处理安全相关任务,如可信执行环境(TEE)的管理。EL3主要用于处理与系统安全性相关的异常。
  • 访问权限:拥有对所有资源的完全访问权限,包括其他所有异常级别的资源。

异常级别的切换

当系统发生异常时,ARM64会根据当前的异常级别和异常类型自动转移到相应的更高异常级别。这个切换过程通常包括:

  • 保存上下文:保存当前处理程序的上下文(如PC和PSTATE)。
  • 转移到更高的异常级别:根据异常类型进入相应的处理程序。

两种异常

​ 我们的异常分为两个大类:依照我们执行时异常的发生方式分为同步异常和异步异常。

1. 同步异常

  • 系统调用:当用户模式(EL0)应用程序需要请求操作系统服务时,通过系统调用触发异常,转到内核模式(EL1)进行处理。
  • 页面错误:当程序试图访问未映射或无效的内存地址时,会引发页面错误。处理此异常通常涉及内存管理单元(MMU)的操作。
  • 非法指令:如果CPU执行了无效或未定义的指令,系统将引发此异常,并转入异常处理程序。
  • 浮点异常:与浮点运算相关的错误,如除以零或浮点溢出,触发此类型的异常。
  • 断点异常:用于调试的断点指令执行时,会触发该异常,转到调试处理程序。

2. 异步异常

  • 外部中断:由硬件设备(如键盘、网络适配器等)生成,通知CPU有事件需要处理。外部中断通常具有高优先级,能够打断当前的执行流程。
  • 定时器中断:由系统定时器生成,用于实现任务调度和时间管理。定时器中断确保系统能够进行周期性的任务调度。
  • 电源管理中断:与电源状态变化相关的中断,例如进入休眠或唤醒时生成的信号。

处理办法(艰辛!)

​ 笔者花费一些时间阅读了Entry.S文件,得到了这个结论。

​ 外面首先锁定到这里,找到这个文件:定位到这个代码段:

arch/arm64/kernel/entry.S

  1. SYM_CODE_START(vectors)
  2. kernel_ventry 1, t, 64, sync // Synchronous EL1t
  3. kernel_ventry 1, t, 64, irq // IRQ EL1t
  4. kernel_ventry 1, t, 64, fiq // FIQ EL1t
  5. kernel_ventry 1, t, 64, error // Error EL1t
  6. kernel_ventry 1, h, 64, sync // Synchronous EL1h
  7. kernel_ventry 1, h, 64, irq // IRQ EL1h
  8. kernel_ventry 1, h, 64, fiq // FIQ EL1h
  9. kernel_ventry 1, h, 64, error // Error EL1h
  10. kernel_ventry 0, t, 64, sync // Synchronous 64-bit EL0
  11. kernel_ventry 0, t, 64, irq // IRQ 64-bit EL0
  12. kernel_ventry 0, t, 64, fiq // FIQ 64-bit EL0
  13. kernel_ventry 0, t, 64, error // Error 64-bit EL0
  14. kernel_ventry 0, t, 32, sync // Synchronous 32-bit EL0
  15. kernel_ventry 0, t, 32, irq // IRQ 32-bit EL0
  16. kernel_ventry 0, t, 32, fiq // FIQ 32-bit EL0
  17. kernel_ventry 0, t, 32, error // Error 32-bit EL0
  18. SYM_CODE_END(vectors)

​ ARM64架构中不同异常向量的处理入口点(vector entry points)。每个kernel_ventry行代表一个特定异常类型在不同异常级别和模式下的处理方式。下面列个表说明:

  1. 异常级别

    • EL1t(EL1 Synchronous): 内核模式下的同步异常处理。
    • EL1h(EL1 Hypervisor): 虚拟化环境下的同步异常处理。
    • EL0: 用户模式下的异常处理,分为64位和32位两种模式。
  2. 异常类型
    • sync: 同步异常,如系统调用或页面错误。
    • irq: 硬件中断请求(IRQ),由外部设备生成的中断。
    • fiq: 快速中断请求(FIQ),用于更高优先级的中断处理。
    • error: 处理其他类型的错误,如未定义指令或故障。
  3. 指令解释
    • kernel_ventry: 这是一个宏或函数,表示定义一个异常处理程序的入口点。
    • 第一个参数(如10)通常表示异常级别(1表示EL1,0表示EL0)。
    • 第二个参数(如th)表示异常类型(t表示异常,h表示虚拟化)。
    • 第三个参数(如6432)表示处理器的位数(64位或32位)。
    • 第四个参数是具体的异常类型(如syncirq等)。

也就是说这个汇编文件实际上就是描述了不同的Exception Level下的不同的异常类型的处理机制表。

​ 很好,但是当你browse大部分博客准备查看我们的异常exception vector handler在哪里的时候,你会惊喜的发现自己找不到handler了!在哪里呢?在这里!

​ 当然,这是为了简化在ARM64内核中定义异常处理程序的流程,通过参数化异常级别、处理类型和寄存器大小,方便地创建多个处理程序。这种结构化的方式有助于保持代码的整洁和可维护性。(但是苦了我这种喜欢读recent源码的人)

  1. .macro entry_handler el:req, ht:req, regsize:req, label:req
  2. SYM_CODE_START_LOCAL(el\el\ht\()_\regsize\()_\label)
  3. kernel_entry \el, \regsize
  4. mov x0, sp
  5. bl el\el\ht\()_\regsize\()_\label\()_handler # 跳转到这个异常的handler
  6. .if \el == 0
  7. b ret_to_user
  8. .else
  9. b ret_to_kernel
  10. .endif
  11. SYM_CODE_END(el\el\ht\()_\regsize\()_\label)
  12. .endm
  13. /*
  14. * Early exception handlers
  15. */
  16. entry_handler 1, t, 64, sync
  17. entry_handler 1, t, 64, irq
  18. entry_handler 1, t, 64, fiq
  19. entry_handler 1, t, 64, error
  20. entry_handler 1, h, 64, sync
  21. entry_handler 1, h, 64, irq
  22. entry_handler 1, h, 64, fiq
  23. entry_handler 1, h, 64, error
  24. entry_handler 0, t, 64, sync
  25. entry_handler 0, t, 64, irq
  26. entry_handler 0, t, 64, fiq
  27. entry_handler 0, t, 64, error
  28. entry_handler 0, t, 32, sync
  29. entry_handler 0, t, 32, irq
  30. entry_handler 0, t, 32, fiq
  31. entry_handler 0, t, 32, error

​ 不着急一点点看:

宏定义结构

  1. SYM_CODE_START_LOCAL(el\el\ht\()_\regsize\()_\label)
  • SYM_CODE_START_LOCAL:这个宏用于开始定义一个本地符号,通常用于异常处理程序的入口点。
  • el:表示异常级别(例如EL0、EL1等)。
  • ht:表示处理类型(可能指同步或异步)。
  • regsize:表示寄存器大小(如32位或64位)。
  • label:定义处理程序的标签名称。

处理程序的入口逻辑

  1. kernel_entry \el, \regsize
  • kernel_entry:这通常是另一个宏,用于设置内核入口环境,根据elregsize初始化异常处理的上下文。
  1. mov x0, sp
  • 将堆栈指针(SP)的值移动到寄存器x0,这通常用于传递参数或保存上下文。
  1. bl el\el\ht\()_\regsize\()_\label\()_handler
  • bl指令用于调用指定的异常处理程序。在这里,它根据传入的参数构造处理程序名称,调用实际的异常处理函数。

返回逻辑

  1. if \el == 0
  2. b ret_to_user
  3. .else
  4. b ret_to_kernel
  5. .endif
  • 这部分代码根据当前的异常级别决定返回的处理逻辑。

    • 如果el是0,表示用户模式,则跳转到ret_to_user,该标签通常处理返回用户态的逻辑。
    • 如果el不是0,表示内核模式,则跳转到ret_to_kernel,用于返回内核态的逻辑。

宏结束

  1. SYM_CODE_END(el\el\ht\()_\regsize\()_\label)
  2. .endm
  • SYM_CODE_END:结束符号定义,标记宏的结束。

很好!我们大致明白了:实际上这个handler就是处理完毕后判断需要调回哪个层级(内核态还是用户态),最后,我们需要揭露以下kernel_entry的神秘面纱:

​ 回到这个文件的开头:

  1. .macro kernel_ventry, el:req, ht:req, regsize:req, label:req
  2. .align 7
  3. .Lventry_start\@:
  4. .if \el == 0
  5. /*
  6. * This must be the first instruction of the EL0 vector entries. It is
  7. * skipped by the trampoline vectors, to trigger the cleanup.
  8. */
  9. b .Lskip_tramp_vectors_cleanup\@
  10. .if \regsize == 64
  11. mrs x30, tpidrro_el0
  12. msr tpidrro_el0, xzr
  13. .else
  14. mov x30, xzr
  15. .endif
  16. .Lskip_tramp_vectors_cleanup\@:
  17. .endif
  18. sub sp, sp, #PT_REGS_SIZE
  19. #ifdef CONFIG_VMAP_STACK
  20. /*
  21. * Test whether the SP has overflowed, without corrupting a GPR.
  22. * Task and IRQ stacks are aligned so that SP & (1 << THREAD_SHIFT)
  23. * should always be zero.
  24. */
  25. add sp, sp, x0 // sp' = sp + x0
  26. sub x0, sp, x0 // x0' = sp' - x0 = (sp + x0) - x0 = sp
  27. tbnz x0, #THREAD_SHIFT, 0f
  28. sub x0, sp, x0 // x0'' = sp' - x0' = (sp + x0) - sp = x0
  29. sub sp, sp, x0 // sp'' = sp' - x0 = (sp + x0) - x0 = sp
  30. b el\el\ht\()_\regsize\()_\label
  31. 0:
  32. /*
  33. * Either we've just detected an overflow, or we've taken an exception
  34. * while on the overflow stack. Either way, we won't return to
  35. * userspace, and can clobber EL0 registers to free up GPRs.
  36. */
  37. /* Stash the original SP (minus PT_REGS_SIZE) in tpidr_el0. */
  38. msr tpidr_el0, x0
  39. /* Recover the original x0 value and stash it in tpidrro_el0 */
  40. sub x0, sp, x0
  41. msr tpidrro_el0, x0
  42. /* Switch to the overflow stack */
  43. adr_this_cpu sp, overflow_stack + OVERFLOW_STACK_SIZE, x0
  44. /*
  45. * Check whether we were already on the overflow stack. This may happen
  46. * after panic() re-enables interrupts.
  47. */
  48. mrs x0, tpidr_el0 // sp of interrupted context
  49. sub x0, sp, x0 // delta with top of overflow stack
  50. tst x0, #~(OVERFLOW_STACK_SIZE - 1) // within range?
  51. b.ne __bad_stack // no? -> bad stack pointer
  52. /* We were already on the overflow stack. Restore sp/x0 and carry on. */
  53. sub sp, sp, x0
  54. mrs x0, tpidrro_el0
  55. #endif
  56. b el\el\ht\()_\regsize\()_\label # 还是一样,最后跳转道实际的处理程序上去
  57. .org .Lventry_start\@ + 128 // Did we overflow the ventry slot?
  58. .endm

​ 下面慢慢说:

  1. .macro kernel_ventry, el:req, ht:req, regsize:req, label:req
  2. .align 7
  3. .Lventry_start\@:
  4. .if \el == 0
  5. b .Lskip_tramp_vectors_cleanup\@
  6. .if \regsize == 64
  7. mrs x30, tpidrro_el0
  8. msr tpidrro_el0, xzr
  9. .else
  10. mov x30, xzr
  11. .endif
  12. .Lskip_tramp_vectors_cleanup\@:
  13. .endif
  14. sub sp, sp, #PT_REGS_SIZE
  • kernel_ventry宏定义了内核在不同异常级别和寄存器大小下的入口逻辑。
  • 指令说明
    • b .Lskip_tramp_vectors_cleanup\@用于跳过清理操作。
    • 根据寄存器大小(64位或32位),适当设置x30寄存器。

堆栈溢出检查

  1. #ifdef CONFIG_VMAP_STACK
  2. add sp, sp, x0
  3. sub x0, sp, x0
  4. tbnz x0, #THREAD_SHIFT, 0f
  5. sub x0, sp, x0
  6. sub sp, sp, x0
  7. b el\el\ht\()_\regsize\()_\label
  8. 0:
  9. msr tpidr_el0, x0
  10. sub x0, sp, x0
  11. msr tpidrro_el0, x0
  12. adr_this_cpu sp, overflow_stack + OVERFLOW_STACK_SIZE, x0
  13. mrs x0, tpidr_el0
  14. sub x0, sp, x0
  15. tst x0, #~(OVERFLOW_STACK_SIZE - 1)
  16. b.ne __bad_stack
  17. sub sp, sp, x0
  18. mrs x0, tpidrro_el0
  19. #endif

此段代码检测堆栈溢出并执行相应的处理。

  • 通过调整sp来检查堆栈溢出情况。
  • 如果检测到溢出,保存相关寄存器值并切换到溢出堆栈。

入口跳转

  1. b el\el\ht\()_\regsize\()_\label
  2. .org .Lventry_start\@ + 128 // Did we overflow the ventry slot?
  3. .endm
  • 功能:最终跳转到对应的内核处理函数,确保进入正确的异常处理逻辑。

​ 现在我们总结以下:

​ 我们的现代内核采用汇编宏的办法定义了一个及其灵活的内核异常句柄表。他排列开来,使用一个handler模板生成对应的注册机制。进入中断向量,他首先要检查堆栈情况(看看是堆栈溢出还是其他错误),然后跳转道实际的处理函数(这个要在外面找,但是遗憾的是目前没有找到 ),随后要做清理退出,根据进入处理句柄的时候是内核态还是用户态来决定是返回恢复哪里的堆栈信息。

​ 就是这样!

Linux内核源码阅读:AArch64的异常处理机制详谈(内核版本6.11)的更多相关文章

  1. ubuntu下linux内核源码阅读工具和调试方法总结

    http://blog.chinaunix.net/uid-20940095-id-66148.html 一 linux内核源码阅读工具 windows下当然首选source insight, 但是l ...

  2. 鸿蒙内核源码分析(进程管理篇) | 谁在管理内核资源 | 百篇博客分析OpenHarmonyOS | v2.07

    百篇博客系列篇.本篇为: v02.xx 鸿蒙内核源码分析(进程管理篇) | 谁在管理内核资源 | 51.c.h .o 进程管理相关篇为: v02.xx 鸿蒙内核源码分析(进程管理篇) | 谁在管理内核 ...

  3. Linux内核源码阅读记录一之分析存储在不同段中的函数调用过程

    在写驱动的过程中,对于入口函数与出口函数我们会用一句话来修饰他们:module_init与module_exit,那会什么经过修饰后,内核就能狗调用我们编写的入口函数与出口函数呢?下面就来分析内核调用 ...

  4. 内核源码阅读vim+cscope+ctags+taglist

    杜斌博客:http://blog.db89.org/kernel-source-read-vim-cscope-ctags-taglist/ 武特博客:http://edsionte.com/tech ...

  5. linux内核源码阅读之facebook硬盘加速利器flashcache

    从来没有写过源码阅读,这种感觉越来越强烈,虽然劣于文笔,但还是下定决心认真写一回. 源代码下载请参见上一篇flashcache之我见 http://blog.csdn.net/liumangxiong ...

  6. linux内核源码阅读之facebook硬盘加速flashcache之八

    前面我们的分析中重点关注正常的数据流程,这一小节关注如果有异常,那么流程是怎么走完的呢? 1)创建新任务时kcached_job申请不到 2)读写命中时cache块为忙 3)系统关机时处理,系统开机时 ...

  7. linux内核源码阅读之facebook硬盘加速flashcache之四

    这一小节介绍一下flashcache读写入口和读写的基础实现. 首先,不管是模块还是程序,必须先找到入口,用户态代码会经常去先看main函数,内核看module_init,同样看IO流时候也要找到入口 ...

  8. linux内核源码阅读之facebook硬盘加速flashcache之三

    上一节讲到在刷缓存的时候会调用new_kcahed_job创建kcached_job,由此我们也可以看到cache数据块与磁盘数据的对应关系.上一篇:http://blog.csdn.net/lium ...

  9. linux内核源码阅读之facebook硬盘加速flashcache之二

    flashcache数据结构都在flashcache.h文件中,但在看数据结构之前,需要先过一遍flashcache是什么,要完成哪些功能?如果是自己设计这样一个系统的话,大概要怎么设计. 前面讲过, ...

  10. linux内核源码阅读之facebook硬盘加速flashcache之六

    其实到目前为止,如果对读流程已经能轻松地看懂了,那么写流程不需要太多脑细胞.我觉得再写下去没有太大的必要了,后面想想为了保持flashcache完整性,还是写出来吧.接着到写流程: 1530stati ...

随机推荐

  1. python中基于tcp协议与udp的通信

    python中基于tcp协议与udp的通信(数据传输)   一.TCP协议介绍 流式协议(以数据流的形式通信传输) 安全协议(收发信息都需收到确认信息才能完成收发,是一种双向通道的通信) tcp协议在 ...

  2. 【RabbitMQ】08 深入部分P1 可靠性投递

    1.消息投递确认 这里的代码延用了06的东西: https://www.cnblogs.com/mindzone/p/15374684.html 删除之前的整合案例,重新写了一份案例的队列和交换机配置 ...

  3. 面向分布式强化学习的经验回放框架——Reverb: A Framework for Experience Replay

    论文题目: Reverb: A Framework for Experience Replay 地址: https://arxiv.org/pdf/2102.04736.pdf 框架代码地址: htt ...

  4. 记录一次Ubuntu20.04死机经过!!!在Ubuntu下使用Chrome的“无痕式”窗口,如果打开标签页过多就会造成死机

    这里要说的事情就是自己刚刚经历的事情,而且尝试了多次最后证明,在Ubuntu下使用Chrome的"无痕式"窗口,如果打开标签页过多就会造成死机. 如何在Ubuntu下安装Chrom ...

  5. 免费领取云主机,在华为开发者空间玩转YOLOV3

    摘要:YOLOv3(You Only Look Once version 3)是一种高效的目标检测算法,旨在实现快速而准确的对象检测. 本文分享自华为云社区<华为云开发者云主机体验[玩转华为云] ...

  6. blender-点线面操作

  7. 【动画进阶】神奇的卡片 Hover 效果与 Blur 的特性探究

    本文,我们将一起探讨探讨,如下所示的一个卡片 Hover 动画,应该如何实现: 这个效果的几个难点: 鼠标移动的过程中,展示当前卡片边缘的 border 以及发光效果: 效果只出现在鼠标附近?这一块的 ...

  8. grpc断路器之hystrix

    上一章介绍了grpc断路器sentinel, grpc断路器之sentinel 但是由于公司线上系统用的告警与监控组件是prometheus,而sentinel暂时还没有集成prometheus,所以 ...

  9. [nRF24L01+] 3. Radio Control 无线电控制

    3. Radio Control 无线电控制 nRF24L01+可以配置为:power down, standby, Rx/Tx mode 3.1. 无线控制状态图 当VDD电压大于1.9V时,进入上 ...

  10. 颗粒流 + Janssen 定律 + Bagnold 数

    对于 \(n\) 个球,易得有 \[\begin{array}{c} \displaystyle\frac\pi2>\theta_i>-\frac\pi2,\theta_1>\cdo ...