​ 任何玩过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

SYM_CODE_START(vectors)
kernel_ventry 1, t, 64, sync // Synchronous EL1t
kernel_ventry 1, t, 64, irq // IRQ EL1t
kernel_ventry 1, t, 64, fiq // FIQ EL1t
kernel_ventry 1, t, 64, error // Error EL1t kernel_ventry 1, h, 64, sync // Synchronous EL1h
kernel_ventry 1, h, 64, irq // IRQ EL1h
kernel_ventry 1, h, 64, fiq // FIQ EL1h
kernel_ventry 1, h, 64, error // Error EL1h kernel_ventry 0, t, 64, sync // Synchronous 64-bit EL0
kernel_ventry 0, t, 64, irq // IRQ 64-bit EL0
kernel_ventry 0, t, 64, fiq // FIQ 64-bit EL0
kernel_ventry 0, t, 64, error // Error 64-bit EL0 kernel_ventry 0, t, 32, sync // Synchronous 32-bit EL0
kernel_ventry 0, t, 32, irq // IRQ 32-bit EL0
kernel_ventry 0, t, 32, fiq // FIQ 32-bit EL0
kernel_ventry 0, t, 32, error // Error 32-bit EL0
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源码的人)

	.macro entry_handler el:req, ht:req, regsize:req, label:req
SYM_CODE_START_LOCAL(el\el\ht\()_\regsize\()_\label)
kernel_entry \el, \regsize
mov x0, sp
bl el\el\ht\()_\regsize\()_\label\()_handler # 跳转到这个异常的handler
.if \el == 0
b ret_to_user
.else
b ret_to_kernel
.endif
SYM_CODE_END(el\el\ht\()_\regsize\()_\label)
.endm /*
* Early exception handlers
*/
entry_handler 1, t, 64, sync
entry_handler 1, t, 64, irq
entry_handler 1, t, 64, fiq
entry_handler 1, t, 64, error entry_handler 1, h, 64, sync
entry_handler 1, h, 64, irq
entry_handler 1, h, 64, fiq
entry_handler 1, h, 64, error entry_handler 0, t, 64, sync
entry_handler 0, t, 64, irq
entry_handler 0, t, 64, fiq
entry_handler 0, t, 64, error entry_handler 0, t, 32, sync
entry_handler 0, t, 32, irq
entry_handler 0, t, 32, fiq
entry_handler 0, t, 32, error

​ 不着急一点点看:

宏定义结构

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

处理程序的入口逻辑

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

返回逻辑

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

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

宏结束

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

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

​ 回到这个文件的开头:

	.macro kernel_ventry, el:req, ht:req, regsize:req, label:req
.align 7
.Lventry_start\@:
.if \el == 0
/*
* This must be the first instruction of the EL0 vector entries. It is
* skipped by the trampoline vectors, to trigger the cleanup.
*/
b .Lskip_tramp_vectors_cleanup\@
.if \regsize == 64
mrs x30, tpidrro_el0
msr tpidrro_el0, xzr
.else
mov x30, xzr
.endif
.Lskip_tramp_vectors_cleanup\@:
.endif sub sp, sp, #PT_REGS_SIZE
#ifdef CONFIG_VMAP_STACK
/*
* Test whether the SP has overflowed, without corrupting a GPR.
* Task and IRQ stacks are aligned so that SP & (1 << THREAD_SHIFT)
* should always be zero.
*/
add sp, sp, x0 // sp' = sp + x0
sub x0, sp, x0 // x0' = sp' - x0 = (sp + x0) - x0 = sp
tbnz x0, #THREAD_SHIFT, 0f
sub x0, sp, x0 // x0'' = sp' - x0' = (sp + x0) - sp = x0
sub sp, sp, x0 // sp'' = sp' - x0 = (sp + x0) - x0 = sp
b el\el\ht\()_\regsize\()_\label 0:
/*
* Either we've just detected an overflow, or we've taken an exception
* while on the overflow stack. Either way, we won't return to
* userspace, and can clobber EL0 registers to free up GPRs.
*/ /* Stash the original SP (minus PT_REGS_SIZE) in tpidr_el0. */
msr tpidr_el0, x0 /* Recover the original x0 value and stash it in tpidrro_el0 */
sub x0, sp, x0
msr tpidrro_el0, x0 /* Switch to the overflow stack */
adr_this_cpu sp, overflow_stack + OVERFLOW_STACK_SIZE, x0 /*
* Check whether we were already on the overflow stack. This may happen
* after panic() re-enables interrupts.
*/
mrs x0, tpidr_el0 // sp of interrupted context
sub x0, sp, x0 // delta with top of overflow stack
tst x0, #~(OVERFLOW_STACK_SIZE - 1) // within range?
b.ne __bad_stack // no? -> bad stack pointer /* We were already on the overflow stack. Restore sp/x0 and carry on. */
sub sp, sp, x0
mrs x0, tpidrro_el0
#endif
b el\el\ht\()_\regsize\()_\label # 还是一样,最后跳转道实际的处理程序上去
.org .Lventry_start\@ + 128 // Did we overflow the ventry slot?
.endm

​ 下面慢慢说:

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

堆栈溢出检查

#ifdef CONFIG_VMAP_STACK
add sp, sp, x0
sub x0, sp, x0
tbnz x0, #THREAD_SHIFT, 0f
sub x0, sp, x0
sub sp, sp, x0
b el\el\ht\()_\regsize\()_\label 0:
msr tpidr_el0, x0
sub x0, sp, x0
msr tpidrro_el0, x0
adr_this_cpu sp, overflow_stack + OVERFLOW_STACK_SIZE, x0
mrs x0, tpidr_el0
sub x0, sp, x0
tst x0, #~(OVERFLOW_STACK_SIZE - 1)
b.ne __bad_stack
sub sp, sp, x0
mrs x0, tpidrro_el0
#endif

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

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

入口跳转

b el\el\ht\()_\regsize\()_\label
.org .Lventry_start\@ + 128 // Did we overflow the ventry slot?
.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. JWT浅了解

    JWT通过数字签名的方式(让我想起了软考),以json对象为载体,在不同的服务终端之间安全传输信息 是一种授权认证 生成token的原理:通过header的加密方式,对payload进行加密.然后把h ...

  2. 买卖股票相关算法-动态规划-python

    要求1: 给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格. 你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票.设计 ...

  3. argparse学习笔记

    argparse是 Python 的一个内置模块,用于编写用户友好的命令行接口.使用 argparse,你可以很容易地为 Python 脚本添加参数解析功能,使得脚本可以接受命令行选项和参数.学起来也 ...

  4. 【Excel】VBA编程 01 入门

    视频地址: https://www.bilibili.com/video/BV1Q5411p71p 在Excel种需要打开[开发工具]和[启用所有宏]两点 打开开发工具选项 宏启用 菜单栏才会有开发工 ...

  5. maven实战教程-含视频讲解

    1.背景 2.什么是maven? 通俗的说就是,不用手动拷贝jar包,帮我们管理项目结构,只需要配置坐标,自动从中央仓库下载(其他介绍请百度...). 3.Maven的安装与配置 注意:Maven在使 ...

  6. [JLOI2015] 城池攻占 题解

    前言 题目链接:洛谷. 题目分析 其他人要么倍增,要么左偏树,那我就来讲讲朴实无华的 dfs 序加上线段树的做法. 首先发现题目中明确指出了作乘法的时候一定是乘上一个大于零的数,这是为什么呢?首先把可 ...

  7. 随时随地与 LLMs 聊天的开源项目「GitHub 热点速览」

    众所周知,本地运行 LLMs 需要下载模型(体积大),并且还比较吃硬件配置.近日 GitHub 推出了 GitHub Models 服务,让开发者可以在 GitHub 上免费测试 Llama.Phi ...

  8. AtCoder Beginner Contest 314

    AtCoder Beginner Contest 314 - AtCoder A - 3.14 (atcoder.jp) 题目提供了100位,所以直接用字符串输出 #include <bits/ ...

  9. 【牛客刷题】HJ10 字符个数统计

    题目链接 简单的说这题就是字符串去重以后检查长度.如果用Java的话,可以遍历字符串,然后利用Set来进行去重,最后统计Set的size就可以了.但是如果是Go语言,则稍微麻烦点.基本思路就是用map ...

  10. kube-flannel.yml

    --- apiVersion: policy/v1beta1 kind: PodSecurityPolicy metadata: name: psp.flannel.unprivileged anno ...