1、異常的作用
異常,就是可以打斷CPU正在運行流程的一些事情,比如外部中斷、未定義指令、試圖修改只讀數據、執行swi指令(中斷指令)等。當這些事情發生時,CPU暫停當前的程序,先處理異常事件,然后再繼續執行被中斷的程序。
- 未定義指令異常: CPU在執行一些未定義的機器指令時,觸發“未定義指令異常”,操作系統可以利用這個特點使用一些自定義指令。
- 數據訪問終止異常: 將一塊數據設為只讀的,提供給多個進程共用, 這樣可以節省內存。當某個進程試圖修改其中的數據時,將觸發"數據訪問終止異常", 在異常處理函數中將這塊數據復制出一份可寫的副本,提供給這個進程使用。
- 當用戶程序試圖讀寫的數據或執行的指令不在內存中時,也會出發一個“數據訪問中止異常”或“指令預取中止異常”,在異常處理函數中將這些數據或指令讀入內存(內存不足時還可以將不用的數據、指令換出內存),然后重新執行被中斷的程序。這樣可以節省內存, 還使得操作系統可以運行這類程序:它們使用的內存遠大於實際的物理內存。
- 當程序使用不對齊的地址訪問時,也會觸發"數據訪問終止異常", 在異常處理程序中先使用多個對齊的地址讀出數據。對於讀操作,從中選取數據組合好后返回給被中斷的程序;對於寫操作,修改其中的部分數據后再寫入內存。這使得程序(特別是應用程序)不用考慮地址的對齊的問題。
- 用戶程序可以 "swi" 指令觸發 "swi異常" ,操作系統在swi異常處理函數中實現各種系統調用。
2、arm9的異常向量表
異常類型 | 處理器模式 | 異常向量 | 高地址向量 |
---|---|---|---|
復位異常 (reset) | 特權模式 | 0x00000000 | 0xFFFF0000 |
未定義指令異常(undefined interrupt) | 未定義指令終止模式 | 0x00000004 | 0xFFFF0004 |
軟件中斷異常(software abort) | 特權模式 | 0x00000008 | 0xFFFF0008 |
預取中止異常(prefetch) | 數據訪問終止模式 | 0x0000000C | 0xFFFF000C |
數據中止異常(data abort) | 數據訪問終止模式 | 0x00000010 | 0xFFFF0010 |
外部中斷請求(IRQ) | 外部中斷模數 | 0x00000018 | 0xFFFF0018 |
快速中斷請求(FIQ) | 快速中斷模式 | 0x0000001C | 0xFFFF001C |
3、linux內核對異常的設置
內核在
start_kernel()
函數(源碼在init/main.c中)調用了setup_arch(&command_line)
->early_trap_init()
函數和init_IRQ()
函數設置了異常(說明:在之前的版本是在trap_init()
和init_IRQ()
設置)。
1)early_trap_init()函數分析(\arch\arm\kernel\traps.c):
early_trap_init 函數被用來設置各種異常向量,包括中斷向量。ARM架構的CPU的異常向量基地址可以是
0x00000000
,也可以是0xffff0000
,Linux 內核使用0xffff0000
,early_trap_init 函數將異常向量復制0xffff0000處
; 部分代碼如下:
void __init early_trap_init(void)
{
unsigned long vectors = CONFIG_VECTORS_BASE; // CONFIG_VECTORS_BASE 這個宏是一個內核配置項.在 .config 里面。 CONFIG_VECTORS_BASE = 0xffff0000
extern char __stubs_start[], __stubs_end[];
extern char __vectors_start[], __vectors_end[];
...
/*vectors 等於 0xffff0000; __vectors_start 和 __vectors_end 之間的代碼就是異常向量*/
memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
/* __stubs_start和__stubs_end 之間的代碼從異常向量跳轉去執行跟復雜的代碼 */
memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);
...
}
__vectors_start和__vectors_end之間的代碼就是異常向量! 異常向量的代碼只是一些跳轉指令。發生異常時,CPU自動執行這些指令,跳轉去執行跟復雜的代碼;比如保存被中斷程序的執行環境,調用異常處理函數,恢復被中斷程序的執行環境並重新運行。
.....
.equ stubs_offset, __vectors_start + 0x200 - __stubs_start /* */
.....
.globl __vectors_start
__vectors_start:
swi SYS_ERROR0 /* 復位時,CPU將執行這條指令 */
b vector_und + stubs_offset /* 未定義指令 */
ldr pc, .LCvswi + stubs_offset /* swi異常 */
b vector_pabt + stubs_offset /* 指令預取中止 */
b vector_dabt + stubs_offset /* 數據訪問中止 */
b vector_addrexcptn + stubs_offset /* 沒有用到 */
b vector_irq + stubs_offset /* irq異常 */
b vector_fiq + stubs_offset /* fiq異常 */
/* 上面這些表示發生異常時,要跳轉去執行的代碼。 */
.globl __vectors_end
__vectors_end:
.....
以
vector_und
為例。當發生異常時,跳轉到異常向量,執行b vector_und + stubs_offset
,然后跳轉到下面的代碼:
.....
vector_stub und, UND_MODE /* "vector_stub" 是一個宏,根據 "und, UND_MODE" 定義一段代碼*/
/* 下面這些代碼是跳轉去執行更復雜的代碼 */
.long __und_usr @ 0 (USR_26 / USR_32) /* 用戶模式下執行了未定義指令 */
.long __und_invalid @ 1 (FIQ_26 / FIQ_32)
.long __und_invalid @ 2 (IRQ_26 / IRQ_32)
.long __und_svc @ 3 (SVC_26 / SVC_32) /* 在管理模式下執行了未定義指令 */
.long __und_invalid @ 4
.long __und_invalid @ 5
.long __und_invalid @ 6
.long __und_invalid @ 7
.long __und_invalid @ 8
.long __und_invalid @ 9
.long __und_invalid @ a
.long __und_invalid @ b
.long __und_invalid @ c
.long __und_invalid @ d
.long __und_invalid @ e
.long __und_invalid @ f
.....
vector_stub
宏的功能, 計算處理完異常后的返回地址,保存一些寄存器,然后進入管理模式,最后根據異常的工作模式跳轉到上面 代碼
的某個分支 。 這個宏的代碼如下:
/*
* Vector stubs.
*
* This code is copied to 0xffff0200 so we can use branches in the
* vectors, rather than ldr's. Note that this code must not
* exceed 0x300 bytes.
*
* Common stub entry macro:
* Enter in IRQ mode, spsr = SVC/USR CPSR, lr = SVC/USR PC
*
* SP points to a minimal amount of processor-private memory, the address
* of which is copied into r0 for the mode specific abort handler.
*/
.macro vector_stub, name, mode, correction=0 /* “.macro” 這個偽匯編 是定義一個宏 ,使用方法可以參考這個 https://www.cnblogs.com/Widesky/p/9006954.html */
.align 5 /* 4字節對齊 linux下交叉 編譯器的對齊方式 2^5bit對齊 */
vector_\name:
.if \correction
sub lr, lr, #\correction /* 根據不同的異常,計算返回地址 */
.endif
@
@ Save r0, lr_<exception> (parent PC) and spsr_<exception>
@ (parent CPSR)
@
stmia sp, {r0, lr} @ save r0, lr /* 將r0,lr 壓入到各自異常的堆棧中 */
mrs lr, spsr /* 將spsr賦給lr */
str lr, [sp, #8] @ save spsr /* 將lr入棧,即spsr入棧 */
@
@ Prepare for SVC32 mode. IRQs remain disabled.
@
mrs r0, cpsr
eor r0, r0, #(\mode ^ SVC_MODE)
msr spsr_cxsf, r0 /* 將r0的值賦給spsr_cxsf,此時的狀態還是處於und模式 */
@
@ the branch table must immediately follow this code
@
and lr, lr, #0x0f /* lr=lr&0x0f,lr起始就是spsr的值,它保存了進入IRQ模式前的CPU模式,其實是5位控制的,這里只用到4位,用來跳轉到不同的處理函數 */
mov r0, sp /* 將管理模式的sp的值給r0 */
ldr lr, [pc, lr, lsl #2] /* lr = *(pc+lr<<2)。如果在進入IRQ之前是用戶模式即是從應用層進入的,那么lr = pc = __und_usr.否則是管理模式也就是處於內核層時發生了IRQ異常 lr = pc+12=__und_svc */
movs pc, lr @ branch to handler in SVC mode /* 將lr的值給pc,同時將spsr的值賦給cpsr,此時才是進入了管理模式 */
.endm
__und_usr
、__und_svc
兩個不同的分支,只是在它們的入口處(比如保存被中斷程序的寄存器)稍有差別,后續的處理大體,相同,都是調用相應的C函數。未定義指令異常最會調用do_undefinstr函數來處理,跳轉的代碼如下。
用戶模式下發生未定義指令異常
/* __und_usr 用戶模式下發生未定義指令異常的處理代碼 */
__und_usr:
usr_entry
tst r3, #PSR_T_BIT @ Thumb mode?
bne __und_usr_unknown @ ignore FP //跳轉到__und_usr_unknown
sub r4, r2, #4
........ //下面還有一些匯編指令
__und_usr_unknown:
mov r0, sp /* 將棧頂地址,作為參數傳入 */
adr lr, ret_from_exception /* 將返回地址寫入到 r0, 處理完C函數后將返回到這里 */
b do_undefinstr /*C函數入口,處理未定義指令異常*/
管理模式下發生未定義指令異常
__und_svc:
svc_entry
@
@ call emulation code, which returns using r9 if it has emulated
@ the instruction, or the more conventional lr if we are to treat
@ this as a real undefined instruction
@
@ r0 - instruction
@
ldr r0, [r2, #-4]
adr r9, 1f
bl call_fpe
mov r0, sp @ struct pt_regs *regs
bl do_undefinstr /*C函數入口,處理未定義指令異常*/
@
@ IRQs off again before pulling preserved data off the stack
@
1: disable_irq
@
@ restore SPSR and restart the instruction
@
ldr lr, [sp, #S_PSR] @ Get SVC cpsr
msr spsr_cxsf, lr
ldmia sp, {r0 - pc}^ @ Restore SVC registers /* 將之前保存到堆棧的寄存器,出棧,並且恢復相應的cpsr寄存器,然后返回斷點處繼續執行 */
4、小結
以
未定義指令異常
簡單的分析了一下linux 下的異常處理體系。
linux 下地異常處理 和 裸機地差不多,只不過在linux下使用了高地址
異常向量和處理異常的代碼,搬移前和搬以后的地址如下圖:
中斷也是異常的一種,因為中斷的處理一些中斷的處理函數,必須由驅動開發者提供,下一章會單獨分析中斷管理的框架。