闲话

从前都在X86上分析内核,做开发、trouble shooting,对于其他架构了解较少,对于新架构的学习,甚至还有些抵触,这次趁分析问题的机会,顺便学习了一下ARM架构的基础知识,权当笔记。

这里主要是AArch32架构(即32位,后面就都简写成ARM了),相对比较简单,入门必备。

ARM处理器模式

在引入安全扩展之前,ARM有7中处理器模式,其中6种是特权模式,剩下一种为用户程序运行的非特权模式。特权模式下,可以做一些user模式下不能做的操作,比如mmu配置和cache操作。

TrustZone Security Extensions引入了独立于模式的两种安全状态,和一种新的Monitor模式。如此就有8种CPU模式了。

TrustZone Security Extensions的环境中,通过将所有的硬件和软件资源按设备来划分,来提升系统安全性。 在CPU处于Normal (Non-secure) state,其不能访问在Secure state下分配的内存。

当前处理器所处模式,由CPSR(当前程序状态寄存器)寄存器的值决定,改变cpu模式可以通过特权软件显示设置该寄存器,也可以由异常触发(发生异常后,CPU就会自动切换到对应的模式)。

嵌入式进阶教程分门别类整理好了,看的时候十分方便,由于内容较多,这里就截取一部分图吧。

armcortex内核架构(ARMCortex-A编程手册学习笔记)(1)

需要的朋友私信【内核】即可领取

寄存器

ARM架构提供16个32位的通用寄存器(R0-R15),R0-R14可用作普通的数据存储。

R15为PC寄存器,修改R15可以改变程序的执行流程。PC值指向当前执行指令的地址加8处,PC是有读写限制的。

R14也称LR(Link register),通常用于保存当前函数的返回地址(上一级函数),当其空闲时,也可以用作普通寄存器用。每种模式下都有自己独立(物理上)的R14。

R13也称SP,即用于保存堆栈的栈顶指针,当其空闲时,也可以用作普通寄存器用。每一种异常模式都有其自己独立的r13,它通常指向异常模式所专用的堆栈。当ARM进入异常模式的时候,程序就可以把一般通用寄存器压入堆栈,返回时再出栈,保证了各种模式下程序的状态的完整性。

程序也可以访问CPSR,SPSR是前一个执行模式下CPSR的副本。

虽然软件可以访问这些寄存器,但是不同模式下,部分寄存器实际对应的物理存储位置可能不同,也就是说,不同模式下,部分寄存器(除R0-7和R15外)实际物理上是不同(虽然对软件来说是透明的,软件看到的是相同的逻辑上的寄存器),这些寄存器只能在相应模式下才能访问。

Program Status Registers。CPSR用于存储:flags、当前CPU模式、中断禁用标记、当前CPU状态等。在user模式下,CPSR对应的寄存器为APSR(限制版本的CPSR)。

Coprocessor 15

CP15,系统控制协处理器,用于控制Core的需要特性,包括c0-c15主要的32位寄存器,这些寄存器通常也通过名称访问,如CP15.SCTLR

cache问题

让程序执行的时间变得不可知。

对于实时性要求高的系统来说有点不可接受。

外设需要用up-to-date的数据,需要cache控制。

其他

tag占物理空间,但不计算在cache size内

invalidate操作:丢掉cache。

clean操作:将脏数据刷入内存,保证一致。

ARM64中包含如下几种类型的异常:

  1. 中断(Interrupts),就是我们平常理解的中断,主要由外设触发,是典型的异步异常。 ARM64中主要包括两种类型的中断:IRQ(普通中断)和FIQ(高优先级中断,处理更快)。 Linux内核中好像没有使用FIQ,还没有仔细看代码,具体不详述了。
  2. Aborts。 可能是同步或异步异常。包括指令异常(取指令时产生)、数据异常(读写内存数据时产生),可以由MMU产生(比如典型的缺页异常),也可以由外部存储系统产生(通常是硬件问题)。
  3. Reset。复位被视为一种特殊的异常。
  4. Exception generating instructions。由异常触发指令触发的异常,比如Supervisor Call (SVC)、Hypervisor Call (HVC)、Secure monitor Call (SMC)
异常级别(EL)

ARM中,异常由级别之分,具体如下图所示,只要关注:

普通的用户程序处于EL0,级别最低

内核处于EL1,HyperV处于EL2,EL1-3属于特权级别。

异常处理

Arm中的异常处理过程与X86比较相似,同样包括硬件自动完成部分和软件部分,同样需要设置中断向量,保存上下文,不同的异常类型的处理方式可能有细微差别。 这里不详述了。

需要关注:用户态(EL0)不能处理异常,当异常发生在用户态时,异常级别(EL)会发生切换,默认切换到EL1(内核态)。

中断向量表

Arm64架构中的中断向量表有点特别(相对于X86来说~),包含16个entry,这16个entry分为4组,每组包含4个entry,每组中的4个entry分别对应4种类型的异常:

  1. SError
  2. FIQ
  3. IRQ
  4. Synchronous Aborts

4个组的分类根据发生异常时是否发生异常级别切换、和使用的堆栈指针来区别。分别对应于如下4组:

  1. 异常发生在当前级别且使用SP_EL0(EL0级别对应的堆栈指针),即发生异常时不发生异常级别切换,可以简单理解为异常发生在内核态(EL1),且使用EL0级别对应的SP。 这种情况在Linux内核中未进行实质处理,直接进入bad_mode()流程。
  2. 异常发生在当前级别且使用SP_ELx(ELx级别对应的堆栈指针,x可能为1、2、3),即发生异常时不发生异常级别切换,可以简单理解为异常发生在内核态(EL1),且使用EL1级别对应的SP。 这是比较常见的场景。
  3. 异常发生在更低级别且在异常处理时使用AArch64模式。 可以简单理解为异常发生在用户态,且进入内核处理异常时,使用的是AArch64执行模式(非AArch32模式)。 这也是比较常见的场景。
  4. 异常发生在更低级别且在异常处理时使用AArch32模式。 可以简单理解为异常发生在用户态,且进入内核处理异常时,使用的是AArch32执行模式(非AArch64模式)。 这中场景基本未做处理。
代码分析

## 中断向量表

Linux内核中,中断向量表实现在entry.S文件中,代码如下:

/* * Exception vectors. */ .align 11 /*el1代表内核态,el0代表用户态*/ ENTRY(vectors) ventry el1_sync_invalid // Synchronous EL1t ventry el1_irq_invalid // IRQ EL1t ventry el1_fiq_invalid // FIQ EL1t /*内核态System Error ,使用SP_EL0(用户态栈)*/ ventry el1_error_invalid // Error EL1t ventry el1_sync // Synchronous EL1h ventry el1_irq // IRQ EL1h ventry el1_fiq_invalid // FIQ EL1h /*内核态System Error ,使用SP_EL1(内核态栈)*/ ventry el1_error_invalid // Error EL1h ventry el0_sync // Synchronous 64-bit EL0 ventry el0_irq // IRQ 64-bit EL0 ventry el0_fiq_invalid // FIQ 64-bit EL0 /*用户态System Error ,使用SP_EL1(内核态栈)*/ ventry el0_error_invalid // Error 64-bit EL0 #ifdef CONFIG_COMPAT ventry el0_sync_compat // Synchronous 32-bit EL0 ventry el0_irq_compat // IRQ 32-bit EL0 ventry el0_fiq_invalid_compat // FIQ 32-bit EL0 ventry el0_error_invalid_compat // Error 32-bit EL0 #else ventry el0_sync_invalid // Synchronous 32-bit EL0 ventry el0_irq_invalid // IRQ 32-bit EL0 ventry el0_fiq_invalid // FIQ 32-bit EL0 ventry el0_error_invalid // Error 32-bit EL0 #endif END(vectors)

可以明显看出分组和分类的情况。

invalid类处理

带invalid后缀的向量都是Linux做未做进一步处理的向量,默认都会进入bad_mode()流程,说明这类异常Linux内核无法处理,只能上报给用户进程(用户态,sigkill或sigbus信号)或die(内核态)

带invalid后缀的向量最终都调用了inv_entry,inv_entry实现如下:

/* * Invalid mode handlers */ /*Invalid类异常都在这里处理,统一调用bad_mode函数*/ .macro inv_entry, el, reason, regsize = 64 kernel_entry el, \regsize /*传入bad_mode的三个参数*/ mov x0, sp /*reason由上一级传入*/ mov x1, #\reason /*esr_el1是EL1(内核态)级的ESR(异常状态寄存器),用于记录异常的详细信息,具体内容解析需要参考硬件手册*/ mrs x2, esr_el1 /*调用bad_mode函数*/ b bad_mode .endm

调用bad_mode,是C函数,通知用户态进程或者panic。

/* * bad_mode handles the impossible case in the exception vector. */ asmlinkage void bad_mode(struct pt_regs *regs, int reason, unsigned int esr) { siginfo_t info; /*获取异常时的PC指针*/ void __user *pc = (void __user *)instruction_pointer(regs); console_verbose(); /*打印异常信息,messages中可以看到。*/ pr_crit("Bad mode in %s handler detected, code 0xx -- %s\n", handler[reason], esr, esr_get_class_string(esr)); /*打印寄存器内容*/ __show_regs(regs); /*如果发生在用户态,需要向其发送信号,这种情况下,发送SIGILL信号,所以就不会有core文件产生了*/ info.si_signo = SIGILL; info.si_errno = 0; info.si_code = ILL_ILLOPC; info.si_addr = pc; /*给用户态进程发生信号,或者die然后panic*/ arm64_notify_die("Oops - bad mode", regs, &info, 0); }

arm64_notify_die:

void arm64_notify_die(const char *str, struct pt_regs *regs, struct siginfo *info, int err) { /*如果发生异常的上下文处于用户态,则给相应的用户态进程发送信号*/ if (user_mode(regs)) { current->thread.fault_address = 0; current->thread.fault_code = err; force_sig_info(info->si_signo, info, current); } else { /*如果是内核态,则直接die,最终会panic*/ die(str, regs, err); } }

IRQ中断处理

场景的场景中(不考虑EL2和EL3),IRQ处理分两种情况:用户态发生的中断和内核态发生的中断,相应的中断处理接口分别为:

el0_syncel1_sync

相应代码如下:

el1_sync:

.align 6 el1_irq: /*保存中断上下文*/ kernel_entry 1 enable_dbg #ifdef CONFIG_TRACE_IRQFLAGS bl trace_hardirqs_off #endif /*调用中断处理默认函数*/ irq_handler /*如果支持抢占,处理稍复杂*/ #ifdef CONFIG_PREEMPT get_thread_info tsk ldr w24, [tsk, #TI_PREEMPT] // get preempt count cbnz w24, 1f // preempt count != 0 ldr x0, [tsk, #TI_FLAGS] // get flags tbz x0, #TIF_NEED_RESCHED, 1f // needs rescheduling? bl el1_preempt 1: #endif #ifdef CONFIG_TRACE_IRQFLAGS bl trace_hardirqs_on #endif /*恢复上下文*/ kernel_exit 1 ENDPROC(el1_irq)

代码非常简单,主要就是调用了irq_handler()函数,不做深入解析了,有兴趣可以自己再看看代码。

el0_sync处理类似,主要区别在于:其涉及用户态和内核态的上下文切换和恢复。

.align 6 el0_irq: kernel_entry 0 el0_irq_naked: enable_dbg #ifdef CONFIG_TRACE_IRQFLAGS bl trace_hardirqs_off #endif /*退出用户上下文*/ ct_user_exit irq_handler #ifdef CONFIG_TRACE_IRQFLAGS bl trace_hardirqs_on #endif /*返回用户态*/ b ret_to_user ENDPROC(el0_irq)

,