以前, armv6 如 (arm9, arm11等) 有7种异常模式。 分别为:
User 用户模式, 应用程序运行于该模式。
Svc (Supervisor) 超级模式, bootloader及内核运行于该模式。 系统调用通过swi陷入内核态, 切换到Svc模式。
Sys (System) 系统模式, 一种特权模式, 貌似有些特殊场景会用, 比如中断嵌套等场景, 后续研究。
Abort 异常模式, 分为预取指令异常(prefetch abort)及数据异常(data abort)等, 有些会导致真实的崩溃, 有些linux拿来用于缺页异常和按需换页(demand paging)等用途。
Undef 未定义指令异常, 根据arm指令执行的流水线, 最早分为三条流水线, 取指, 译码, 执行, 未定义异常就是发生在译码阶段。
IRQ - 中断请求, interrupt request, 实在的外设中断, 核间中断等。
FIQ - 快速中断请求, fast interrupt request, 需要快速处理的中断可用该中断实现。
armv7, armv8 如cortex a7, a15, a53等芯片, 多了两种异常模式,
Hyp (Hypervisor) 虚拟化用途, 具体需要学习...
Mon (Monitor) 监视者模式, 不知道咋翻译... 主要用于trustzone方案上, 利用SMC(Secure Monitor Call)这条指令 可以 陷入 monitor模式, 切换运行模式, 到secure mode, 配置cp15某个协处理器寄存器, 可运行于安全的执行环境中。
前面介绍了arm的异常模式, 即cpu运行过程中发生的异常事件。 会打断cpu目前正在做的事, 转而进行异常处理。
具体异常行为该如何处理, 可由软件进行处理。 故以下介绍下软件行为。
bootrom, uboot, linux 都有定义异常向量表, 根据arm规范,
Exception |
Offset from vector base |
Mode on entry |
A bit on entry |
F bit on entry |
I bit on entry |
---|---|---|---|---|---|
Reset |
|
Supervisor |
Disabled |
Disabled |
Disabled |
Undefined instruction |
|
Undefined |
Unchanged |
Unchanged |
Disabled |
Software interrupt |
|
Supervisor |
Unchanged |
Unchanged |
Disabled |
Prefetch Abort |
|
Abort |
Disabled |
Unchanged |
Disabled |
Data Abort |
|
Abort |
Disabled |
Unchanged |
Disabled |
Reserved |
|
Reserved |
- | - |
- |
IRQ |
|
IRQ |
Disabled |
Unchanged |
Disabled |
FIQ |
|
FIQ |
Disabled |
Disabled |
Disabled |
以上为随便搜索的ARM1136的异常向量表, arm32的芯片应该差不多。(aarch64/armv7/8? 引入了exception level, 即异常等级的概念, 差别较大。)
bootrom, uboot 都含有start.S, 查看开头几行。
17 /*
18 *************************************************************************
19 *
20 * Startup Code (reset vector)
21 *
22 * do important init only if we don't start from memory!
23 * setup Memory and board specific bits prior to relocation.
24 * relocate armboot to ram
25 * setup stack
26 *
27 *************************************************************************
28 */
29
30 › .globl› reset
31
32 reset:
33 › /*
34 › * set the cpu to SVC32 mode
35 › */
36 › mrs›r0,cpsr
37 › bic›r0,r0,#0x1f
38 › orr›r0,r0,#0xd3
39 › msr›cpsr,r0
40
41 › /* the mask ROM code should have PLL and others stable */
42 #ifndef CONFIG_SKIP_LOWLEVEL_INIT
43 › bl cpu_init_crit
44 #endif
以上为uboot/arch/arm/cpu/arm1136/start.S
嗯, 只有reset向量的代码了, 即只包含复位/启动代码了||...
异常向量表放其他地方去了...
不过想来也是uboot/linux代码越来越庞大, 光arm片子就从arm7 ~ armv8 cortex a53啥的一坨... 挺多arm片子的异常向量表的代码都是一样的。 所以根据可复用的设计理念, 异常向量表被移到公用的地方去了。
继续找, 在uboot/arch/arm/lib里面找到了
vim uboot/arch/arm/lib/vector.S
17 /*
18 * A macro to allow insertion of an ARM exception vector either
19 * for the non-boot0 case or by a boot0-header.
20 */
21 .macro ARM_VECTORS
22 #ifdef CONFIG_ARCH_K3
23 › ldr pc, _reset
24 #else
25 › b› reset
26 #endif
27 › ldr›pc, _undefined_instruction
28 › ldr›pc, _software_interrupt
29 › ldr›pc, _prefetch_abort
30 › ldr›pc, _data_abort
31 › ldr›pc, _not_used
32 › ldr›pc, _irq
33 › ldr›pc, _fiq
34 › .endm
对照arm官方的定义, 就匹配上了, 如此当芯片发生硬件异常时(如reset, abort, interrupt等), 我们就能做异步/及时的处理了。
具体异常处理后面继续讨论。
看下该目录下的makefile
10 ifdef CONFIG_CPU_V7M
11 obj-y› += vectors_m.o crt0.o
12 else ifdef CONFIG_ARM64
13 obj-y› += crt0_64.o
14 else
15 obj-y› += vectors.o crt0.o
16 endif
可以看到, armv7, arm64和arm32是不一样的。
贴一下vectors_m.S的异常向量表,
36 .section .vectors
37 ENTRY(_start)
38 › .long› CONFIG_SYS_INIT_SP_ADDR›› @ 0 - Reset stack pointer
39 › .long› reset› › › › @ 1 - Reset
40 › .long› __invalid_entry›› › @ 2 - NMI
41 › .long› __hard_fault_entry› › @ 3 - HardFault
42 › .long› __mm_fault_entry› › @ 4 - MemManage
43 › .long› __bus_fault_entry› › @ 5 - BusFault
44 › .long› __usage_fault_entry›› @ 6 - UsageFault
45 › .long› __invalid_entry›› › @ 7 - Reserved
46 › .long› __invalid_entry›› › @ 8 - Reserved
47 › .long› __invalid_entry›› › @ 9 - Reserved
48 › .long› __invalid_entry›› › @ 10 - Reserved
49 › .long› __invalid_entry›› › @ 11 - SVCall
50 › .long› __invalid_entry›› › @ 12 - Debug Monitor
51 › .long› __invalid_entry›› › @ 13 - Reserved
52 › .long› __invalid_entry›› › @ 14 - PendSV
53 › .long› __invalid_entry›› › @ 15 - SysTick
54 › .rept› 255 - 16
55 › .long› __invalid_entry›› › @ 16..255 - External Interrupts
56 › .endr
挺多不认识了... 后面再研究...
哦,makefile里面还有个crt0.S, crt0_64.S的代码,
全称C RunTime startup Code, 关于初始化C语言程序运行环境的。 比如bss段的初始化, 堆栈区域的定义等等。
ok, 这里有个疑问, bootrom, uboot, linux都有异常向量表的代码, 都有啥区别呢?
嗯, 没错, 都是给自己用的。
比如bootrom 代码控制启动方式, 是sd卡启动, 还是emmc启动, 还是nor flash启动, 甚至串口/usb启动。。。
如果内部发生异常, 走的就是bootrom的异常向量表... 所以bootrom启动时有程序异常... 那就片子起不来咯...
* 关于irq/fiq 中断, 通常情况下bootrom, uboot都不怎么用, 通常都采用轮训方式去实现功能, 不采用中断。
当然想用也是可以的^^。
ok, 关于linux的异常向量表,
linux没有start.S, 开头是start_kernel
进去找找, 找到了
start_kernel->setup_arch->paging_init->devicemaps_init->early_trap_init
823 void __init early_trap_init(void *vectors_base)
824 {
825 #ifndef CONFIG_CPU_V7M
826 › unsigned long vectors = (unsigned long)vectors_base;
827 › extern char __stubs_start[], __stubs_end[];
828 › extern char __vectors_start[], __vectors_end[];
829 › unsigned i;
830
831 › vectors_page = vectors_base;
832
833 › /*
834 › * Poison the vectors page with an undefined instruction. This
835 › * instruction is chosen to be undefined for both ARM and Thumb
836 › * ISAs. The Thumb version is an undefined instruction with a
837 › * branch back to the undefined instruction.
838 › */
839 › for (i = 0; i < PAGE_SIZE / sizeof(u32); i++)
840 › › ((u32 *)vectors_base)[i] = 0xe7fddef1;
841
842 › /*
843 › * Copy the vectors, stubs and kuser helpers (in entry-armv.S)
844 › * into the vector page, mapped at 0xffff0000, and ensure these
845 › * are visible to the instruction stream.
846 › */
847 › memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
848 › memcpy((void *)vectors + 0x1000, __stubs_start, __stubs_end - __stubs_start);
849
850 › kuser_init(vectors_base);
851
852 › flush_icache_range(vectors, vectors + PAGE_SIZE * 2);
853 #else /* ifndef CONFIG_CPU_V7M */
854 › /*
855 › * on V7-M there is no need to copy the vector table to a dedicated
856 › * memory area. The address is configurable and so a table in the kernel
857 › * image can be used.
858 › */
859 #endif
860 }
ok, 整个函数都贴了, 也没几行, 主要就是把__vectors_start到__vectors_end的异常向量表拷贝到vectors地址。
vectors地址是哪里, 分两种情况, 有mmu和没开mmu
没mmu, 所以是物理实际地址,
720 #define vectors_base()› (vectors_high() ? 0xffff0000 : 0)
根据宏定义, 有两个, 0x0 或者0xffff0000,
45 #if __LINUX_ARM_ARCH__ >= 4
46 #define vectors_high()› (get_cr() & CR_V)
47 #else
48 #define vectors_high()› (0)
49 #endif
70 static inline unsigned long get_cr(void)
71 {
72 › unsigned long val;
73 › asm("mrc p15, 0, %0, c1, c0, 0› @ get CR" : "=r" (val) : : "cc");
74 › return val;
75 }
有个cp15协处理寄存器可以配置, 告诉arm核心 用默认的0还是高位地址0xffff0000作为异常向量表开始地址。
如果有mmu, 那cpu看到的就是mmu映射的虚拟内存...即分配0x0, 0xffff0000这两个地方内存来使用, 具体实际物理内存在哪里,
那就要看mmu的 映射表如何配置的了....
/*
1322 › * Allocate the vector page early.
1323 › */
1324 › vectors = early_alloc(PAGE_SIZE * 2);
1326 › early_trap_init(vectors);
这部分 找到些博客:
https://www.cnblogs.com/arnoldlu/p/8060121.html
后面找时间研究下。
找下具体的异常向量表代码, __vectors_start,
trap.c同级目录下,
kernel/arch/arm/kernel/entry-armv.S
1217 .L__vectors_start:
1218 › W(b)› vector_rst
1219 › W(b)› vector_und
1220 › W(ldr)› pc, .L__vectors_start + 0x1000
1221 › W(b)› vector_pabt
1222 › W(b)› vector_dabt
1223 › W(b)› vector_addrexcptn
1224 › W(b)› vector_irq
1225 › W(b)› vector_fiq
是的, 和bootrom, uboot类似。
其中1220行, __vectors_start + 0x1000, 对比trap.c中的代码,
memcpy((void *)vectors + 0x1000, __stubs_start, __stubs_end - __stubs_start);
这部分__stubs_start的代码是干什么的, 继续探究以下:
1024 /*
1025 * Vector stubs.
1026 *
1027 * This code is copied to 0xffff1000 so we can use branches in the
1028 * vectors, rather than ldr's. Note that this code must not exceed
1029 * a page size.
1030 *
1031 * Common stub entry macro:
1032 * Enter in IRQ mode, spsr = SVC/USR CPSR, lr = SVC/USR PC
1033 *
1034 * SP points to a minimal amount of processor-private memory, the address
1035 * of which is copied into r0 for the mode specific abort handler.
......
1076 › .section .stubs, "ax", %progbits
1077 › @ This must be the first word
1078 › .word› vector_swi
__stubs_start没找到..., 但感觉就是vector_swi处。
即软件中断处理模块。后续介绍系统调用等会提及swi。
关于irq handler暂时没看明白怎么跳转的, 后续研究。
linux有哪些内容是用异常向量表来实现的呢??
1. 虚拟内存/按需换页
操作系统的设计思路, 每个进程的映射空间都是0-4G空间(不考虑arm64)。其中3G-4G是内核空间,进程无法直接访问(需要通过系统调用等)
每个进程使用独立的虚拟内存空间, 互不干扰。
虚拟内存和物理内存间的映射关系由mmu进行, mmu参考页表进行转换。
故可以理解, 每个进程都有独立的页表, 存放在内核 内存的某个地方, 当前cpu需要执行哪个进程, 需要加载当前进程页表的基地址。是的, 在操作系统切换进程时, 需要将需要执行的进程页表基地址 写入 cp15 某个寄存器中。如此该cpu就能进行正确的内存寻址。
每个cpu核心硬件资源上都有对应的逻辑计算器, 算术计算器, 寄存器(以前是37个), 协处理器, 协处理器专用寄存器。
所以每个cpu跑 某个线程 都需要加载 对应的进程页表。 如果2个cpu跑同一个进程的不同线程, 他们使用的页表就是一样的。且能访问同一片内存区域。所以多线程需要进行关键数据区的同步/保护。
ok, 废话很多, 其实虚拟内存本身和异常模式没有关系... 有关系的是内存的加载方式。
Linux操作系统采用类似晚绑定的机制。用户运行程序, 该程序是在文件系统中, 即磁盘上。 程序开始运行时, 操作系统(加载器, loader)并不会把所有代码/依赖库全一股脑加载到内存中。而是分配好虚拟内存, 即相应页表映射。 只有当程序开始运行时, 发现
诶, main, funcA, funcB 都不在内存中, 此时就会发生pre-fetch abort, 即预取指令异常; 如果是访问某个全局变量, 如果数据不在内存, 则会发生data abort, 即数据异常。 Linux操作系统碰到此类异常, 会尝试去把相应数据从磁盘/flash加载到内存。然后程序就能继续执行了。 常见的如do_pagefault, demand_page等函数。
这里只提到了内存加载, 那如果物理内存不够用了, 该进程又在后台, 那么可以理解的, 操作系统应该会去释放这部分内存。
代码段及RO Data这些没关系, 都是只读的, 内核把这部分内存释放给别人用就可以了。 但是可读可写的全局变量等 该如何处理呢? 肯定不能简单得释放, 释放了下次重新加载值就不对了。。。也不能回写到磁盘, 这是全局变量, 不是文件系统的文件....
这部分需要研究下, 但根据我的理解, 无外乎两种方式,
1). RW的数据就不释放了, 程序销毁前一旦加载就常驻内存了。。。
2). 利用linux的swap交换分区技术。 swap交换分区 也叫虚拟内存.... 跟虚拟内存地址不一样, 这是利用磁盘扩充内存容量。。。将后台, 不活跃的程序交换到swap分区。(Linux中mount下看看, 一般都会分一个...)
2. 中断处理....
bootrom, uboot不怎么用中断... 但是linux操作系统肯定是必须要用中断的啦....
3. swi, software interrupt, 软件中断,
linux使用swi作为系统调用的入口, 用户态进程调用open, read, write, ioctl, close等系统调用时, 需要陷入内核态。
用户态运行在 user模式, 内核态运行在svc模式。
查看glibc的系统调用实现, glibc-2.30/sysdeps/unix/sysv/linux/arm/sysdep.h
338 #if defined(__thumb__)
339 /* We can not expose the use of r7 to the compiler. GCC (as
340 of 4.5) uses r7 as the hard frame pointer for Thumb - although
341 for Thumb-2 it isn't obviously a better choice than r11.
342 And GCC does not support asms that conflict with the frame
343 pointer.
344
345 This would be easier if syscall numbers never exceeded 255,
346 but they do. For the moment the LOAD_ARGS_7 is sacrificed.
347 We can't use push/pop inside the asm because that breaks
348 unwinding (i.e. thread cancellation) for this frame. We can't
349 locally save and restore r7, because we do not know if this
350 function uses r7 or if it is our caller's r7; if it is our caller's,
351 then unwinding will fail higher up the stack. So we move the
352 syscall out of line and provide its own unwind information. */
353 # undef INTERNAL_SYSCALL_RAW
354 # define INTERNAL_SYSCALL_RAW(name, err, nr, args...)› › \
355 ({› › › › › › › › \
356 register int _a1 asm ("a1");› › › › \
357 int _nametmp = name;› › › › › \
358 LOAD_ARGS_##nr (args)›› › › › \
359 register int _name asm ("ip") = _nametmp;›› › \
360 asm volatile ("bl __libc_do_syscall"›› › \
361 : "=r" (_a1)› › › › \
362 : "r" (_name) ASM_ARGS_##nr›› › \
363 : "memory", "lr");› › › › \
364 _a1; })
365 #else /* ARM */
366 # undef INTERNAL_SYSCALL_RAW
367 # define INTERNAL_SYSCALL_RAW(name, err, nr, args...)› › \
368 ({› › › › › › › › \
369 register int _a1 asm ("r0"), _nr asm ("r7");›› \
370 LOAD_ARGS_##nr (args)› › › › › \
371 _nr = name;› › › › › › \
372 asm volatile ("swi› 0x0›@ syscall " #name› \
373 › › : "=r" (_a1)› › › › \
374 › › : "r" (_nr) ASM_ARGS_##nr› › › \
375 › › : "memory");› › › › \
376 _a1; })
377 #endif
可以看到, arm的系统调用使用swi 0x0 陷入内核, 后面跟的寄存器保存相关参数。 (read, write, ioctl都有各自不同参数)
查看kernel/arch/arm/kernel/entry-common.S
210 › /* saved_psr and saved_pc are now dead */
211
212 › uaccess_disable tbl
213
214 › adr›tbl, sys_call_table›› @ load syscall table pointer
215
216 #if defined(CONFIG_OABI_COMPAT)
217 › /*
218 › * If the swi argument is zero, this is an EABI call and we do nothing.
219 › *
220 › * If this is an old ABI call, get the syscall number into scno and
221 › * get the old ABI syscall table address.
222 › */
223 › bics› r10, r10, #0xff000000
224 › eorne› scno, r10, #__NR_OABI_SYSCALL_BASE
225 › ldrne› tbl, =sys_oabi_call_table
226 #elif !defined(CONFIG_AEABI)
227 › bic›scno, scno, #0xff000000›› @ mask off SWI op-code
228 › eor›scno, scno, #__NR_SYSCALL_BASE› @ check OS number
229 #endif
230 › get_thread_info tsk
231 › /*
232 › * Reload the registers that may have been corrupted on entry to
233 › * the syscall assembly (by tracing or context tracking.)
234 › */
235 TRACE(›ldmia› sp, {r0 - r3}› › )
236
237 local_restart:
238 › ldr›r10, [tsk, #TI_FLAGS]› › @ check for syscall tracing
239 › stmdb› sp!, {r4, r5}› › › @ push fifth and sixth args
240
241 › tst›r10, #_TIF_SYSCALL_WORK›› @ are we tracing syscalls?
242 › bne›__sys_trace
243
244 › cmp›scno, #NR_syscalls› › @ check upper syscall limit
245 › badr› lr, ret_fast_syscall› › @ return address
246 › ldrcc› pc, [tbl, scno, lsl #2]›› @ call sys_* routine
有些只了解个大概, 后续再深究...