詳解Linux內核異常處理體系結構

本節內容:Linux內核異常處理的的初始化過程和異常發生時的處理流程

【首先來區分一下兩個概念:中斷(Interrupt)和異常(Exception)。中斷屬於異常的一種,就拿2440開發板來說,他有60多種中斷源,例如來自DMA控制器、UART、IIC和外部中斷等。2440有一個專門的中斷控制器來處理這些中斷,中斷控制器在接收到這些中斷信號之後就需要ARM920T進入IRQ或FIQ模式進行處理,這兩種模式也是中斷異常的僅有模式。而異常的概念要廣的多,它包括復位、未定義指令、軟中斷、IRQ等等。還有一點知識就是,中斷這種異常在響應之前到來之前是需要程序員進行什麼優先級、是否要屏蔽信號之類的初始化的,而其他比如未定義指令是不用的,只要發生了就跳到異常向量入口取址執行。因此下面初始化內容中的第(2)點是針對中斷這種異常的設置的】

一、初始化設置:

(1)異常向量相關的設置:start_kernel()-->setup_arch()-->early_trap_init()函數來擔任這個任務。在arch/arm/kernel/traps.c文件件中定義:這個函數很有分量,值得細細分析!!!

void __init early_trap_init(void)
{
	unsigned long vectors = CONFIG_VECTORS_BASE;
	extern char __stubs_start[], __stubs_end[];
	extern char __vectors_start[], __vectors_end[];
	extern char __kuser_helper_start[], __kuser_helper_end[];
	int kuser_sz = __kuser_helper_end - __kuser_helper_start;

	/*
	 * 看下面這段英文註釋,代碼就一目瞭然了,就是把異常向量表、和異常處理那部分代碼複製到指定的地址處
	 * Copy the vectors, stubs and kuser helpers (in entry-armv.S)
	 * into the vector page, mapped at 0xffff0000, and ensure these
	 * are visible to the instruction stream.
	 */
	memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start); 
	memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start); 
	memcpy((void *)vectors + 0x1000 - kuser_sz, __kuser_helper_start, kuser_sz); 

	/*
	 * Copy signal return handlers into the vector page, and
	 * set sigreturn to be a pointer to these.
	 */
	memcpy((void *)KERN_SIGRETURN_CODE, sigreturn_codes,
	       sizeof(sigreturn_codes));

	flush_icache_range(vectors, vectors + PAGE_SIZE);
	modify_domain(DOMAIN_USER, DOMAIN_CLIENT);
}

詳細函數分析:
將異常向量表複製到vectors地址處,vectors在函數的第一句就被賦值爲“CONFIG_VECTORS_BASE”,經驗告訴我們它是個內核編譯配置項,去內核的頂層目錄裏邊的“.config”文件搜索就出來,果然就有“CONFIG_VECTORS_BASE=0xffff0000”這麼一句話。
好,同樣問題就來了,我們之前瞭解過的中斷向量是放到0x00000000地址開始處,把中斷向量放到0xffff0000 異常觸發時cpu還能自動找到?答案是能!
在ARM920T的使用手冊裏邊有涉及相關的內容:協處理控制寄存器CP15的C1寄存器的第[13]位就是用來設置異常向量的存放位置的,該位爲0存放到0x0000000開始處,爲1存放到0xffff0000開始處。

到這裏Linux內核異常向量設置的工作就算是完成了。可是想想:設置完這些異常向量之後,異常發生了,CPU是怎麼一個處理過程???接着往下分析:Linux內核處理異常主要流程

繼續分析就得從異常向量表來開始入手,__vectors_start和__vectors_end在arch/arm/kernel/entry-armv.S文件中有定義。他們就是內核異常向量表的起始和結束地址。

........
	.globl	__vectors_start
__vectors_start:
	swi	SYS_ERROR0    ;arm在復位異常發生時來這裏執行
	b	vector_und + stubs_offset
	ldr	pc, .LCvswi + stubs_offset
	b	vector_pabt + stubs_offset
	b	vector_dabt + stubs_offset
	b	vector_addrexcptn + stubs_offset
	b	vector_irq + stubs_offset
	b	vector_fiq + stubs_offset

	.globl	__vectors_end
	........
下面以第一個調轉指令“bvector_und + stubs_offset”的分析爲例,發現怎麼在源碼裏面都找不到vector_und這個東東,各種查資料之後發現特麼是個彙編宏定義,幹!展開來解解氣,建議先熟悉一下彙編宏定義規則。

.macro    MACRO_NAME   PARA1   PARA2   ......

......內容......

.endm

同樣在這個文件中找到了vector_stub這個宏:

.macro	vector_stub, name, mode, correction=0
	.align	5  			@將異常入口強制進行2^5字節對齊,即一個cache line大小對齊,出於性能考慮
vector_\name:
	.if \correction @correction=0 所以分支無效
	sub	lr, lr, #\correction
	.endif
	.endif
	...........
	movs	pc, lr			@ branch to handler in SVC mode
ENDPROC(vector_\name)
.endm

以宏“vector_stub  und,   UND_MODE”爲例將其展開爲:

vector_und:   
	@
	@ 此時已進入UND_MOD,lr=上一個模式被打斷時的PC值,下面三條指令是保護上個模式的現場
	@
	stmia	sp, {r0, lr}		<span style="white-space:pre">	</span>@ save r0, lr
	mrs	lr, spsr               <span style="white-space:pre">		</span>@ 準備保存上個模式的cpsr值,因爲他被放到了UND_MODE的spsr中
	str	lr, [sp, #8]			@ save spsr to stack
	@
	@ Prepare for SVC32 mode.  IRQs remain disabled. 注意前面的“Prepare”,這裏還不是真正切換到SVC,只是準備!!不要緊張
	@
	mrs	r0, cpsr             		@ r0=0x1b  (UND_MODE)
	eor	r0, r0, #(\mode ^ SVC_MODE)<span style="white-space:pre">	</span>@ 邏輯異或指令
	msr	spsr_cxsf, r0   		@ cxsf是spsr寄存器的控制域(C)、擴展域(X)、狀態域(S)、標誌域(F),注意這裏的spsr是UND管理模式的
	@
	@ the branch table must immediately follow this code 下一級跳轉表必須要緊跟在這一段代碼之後(這一點很重要)
	@
	and	lr, lr, #0x0f		@ 執行這條指令之前:lr = 上個模式的cpsr值,現在取出其低四位--模式控制位的[4:0],關鍵點又來了:查看2440芯片手冊可以知道,這低4位二進制值爲十進制數值的 0-->User_Mode; 1-->Fiq_Mode; 2-->Irq_Mode; 3-->SVC_Mode; 7-->Abort_Mode; 11-->UND_Mode,明白了這些下面的處理就會恍然大悟,原來找到那些異常處理分支是依賴這4位的值來實現的
	mov	r0, sp			@ 將SP值保存到R0是爲了之後切換到SVC模式時將這個模式下堆棧中的信息轉而保存到SVC模式下的堆棧中
	ldr	lr, [pc, lr, lsl #2] @ 我第一次遇到LDR的這種用法,找了一下LDR的資料發現是這個意思:將pc+lr*4的計算結果重新保存到lr中,我們知道pc是指向當前指令的下兩條指令處的地址的,也就是指向了“.long	__und_usr”
	movs	pc, lr			@ branch to handler in SVC mode 前方高能!關鍵的地方來了!在跳轉到第二級分支的同時CPU的工作模式從UND_MODE強制切換到SVC_MODE,這是由於MOVS指令在賦值的同時會將spsr的值賦給cpsr
ENDPROC(vector_und)
	.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 其他模式下面不能發生未定義指令異常,否則都使用__und_invalid分支處理這種異常
	.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

【附加註釋:在arch\arm\include\asm\ptrace.h中有:#define  SVC_MODE  0x00000013 和 #define UND_MODE  0x0000001b

這樣做的目的是什麼?有什麼天大的好處?肯定有!因爲,現在的我還沒敢懷疑內核會做一些沒用的事情
Linux的中斷管理的設計思路都是這樣的:異常事件觸發,cpu自動跳到異常向量表處執行,同時也切換到對應的模式,有種變色龍的感覺,但是隨後立即有段代碼強制讓cpu切換到SVC管理模式進行異常處理,當然有一點值得一說,reset異常是進入用戶模式的,此時的異常向量存放的是swi指令,swi指令是進入svc管理模式的(也叫內核模式)結果可想而知,也得聽話,乖乖進入管理模式。如此一來,內核管理異常就方便多了,從宏觀的角度來看,cpu絕大部分時間是停留在user和svc模式的,要不就是user模式下正常工作,要不就是svc模式下異常處理,那段切換的時間完全被忽略,有種英雄就被人們忘記的感覺。也就是說可以看做內核要不就是在user模式下要不就是在svc模式下被其他各種異常中斷打斷咯。如果這些都意會到了,下面的內容分析,肯定妥妥的,就是這麼自信得意

================================================================================================================
執行到“movs pc, lr”這一句,找到了branch table中的一項,現在我們繼續往下分析,假設進入UND_MODE之前是User模式,那麼接下來會到__und_usr分支去繼續執行
__und_usr標號也是在該文件中定義,代碼如下:

__und_usr:
	usr_entry   @搜一下發現這是一個宏定義,先猜測一下功能是:將usr模式下的寄存器、中斷返回地址保存到堆棧中。可以說是接管UND_MODE下保存的信息和未保存信息
	@
	@ fall through to the 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
	@
	adr	r9, ret_from_exception
	adr	lr, __und_usr_unknown
	tst	r3, #PSR_T_BIT			@ Thumb mode?
	subeq	r4, r2, #4			@ ARM instr at LR - 4
	subne	r4, r2, #2			@ Thumb instr at LR - 2
1:	ldreqt	r0, [r4]
	beq	call_fpe
	@ Thumb instruction
#if __LINUX_ARM_ARCH__ >= 7
2:	ldrht	r5, [r4], #2
	and	r0, r5, #0xf800			@ mask bits 111x x... .... ....
	cmp	r0, #0xe800			@ 32bit instruction if xx != 0
	blo	__und_usr_unknown  		@blo小於跳轉指令。找到真正異常處理函數入口
3:	ldrht	r0, [r4]
	add	r2, r2, #2			@ r2 is PC + 2, make it PC + 4
	orr	r0, r0, r5, lsl #16
#else
	b	__und_usr_unknown 
#endif
 UNWIND(.fnend		)
ENDPROC(__und_usr)

=================================================================================================================
usr_entry宏內容:

.macro	usr_entry
 UNWIND(.fnstart	)
 UNWIND(.cantunwind	)
	sub	sp, sp, #S_FRAME_SIZE   @ 通過查找和計算S_FRAME_SIZE=4*18=72
	stmib	sp, {r1 - r12}	  <span style="white-space:pre">	</span>@ 從開始的Usr_MODE到UND_MODE,再到現在的SVC_MODE,程序中都沒有去操作通用寄存器中的R1-R12,因此可以直接將他們入棧。接下來就可以隨便使用這些寄存器了。

	ldmia	r0, {r1 - r3}	  <span style="white-space:pre">	</span>@ 之前已將UND_MODE下棧頂指針保存到R0,出棧後r1=Usr_r0,r2=Usr_lr,r3=Usr_cpsr
	add	r0, sp, #S_PC		@ here for interlock avoidance  從這往下一小部分代碼尚未消化
	mov	r4, #-1			
	str	r1, [sp]	<span style="white-space:pre">	</span>@ save the "real" r0 copied
					@ from the exception stack

	@
	@ We are now ready to fill in the remaining blanks on the stack:
	@
	@  r2 - lr_<exception>, already fixed up for correct return/restart
	@  r3 - spsr_<exception>
	@  r4 - orig_r0 (see pt_regs definition in ptrace.h)
	@
	@ Also, separately save sp_usr and lr_usr
	@
	stmia	r0, {r2 - r4}
	stmdb	r0, {sp, lr}^

	@
	@ Enable the alignment trap while in kernel mode
	@
	alignment_trap r0

	@
	@ Clear FP to mark the first stack frame
	@
	zero_fp
.endm

=========================================================================================================
__und_usr_unknown也是在這個文件中定義:

__und_usr_unknown:
	enable_irq
	mov	r0, sp
	adr	lr, ret_from_exception  @ 這裏就是異常中斷的返回,先將返回前處理的處理函數的地址給lr寄存器,下面調用完C函數之後直接就可以返回
	b	do_undefinstr   	@ 最終調用C函數進行復雜的處理 在arch/arm/kernel/traps.c中
ENDPROC(__und_usr_unknown)

小結一下Linux異常處理流程:

異常發生前工作狀態,到異常發生,去異常向量表找到入口地址,(這算異常發生之後跳轉到第一個處理分支),進入異常模式,保護部分現場,強制進入SVC管理模式,根據異常發生前的工作模式找到異常處理的第二級分支,在該模式下面接過異常模式堆棧中的信息,接着保存異常發生時異常模式還未保存的信息,準備好處理完畢返回處理程序的地址,調用異常處理函數。


(2)中斷相關初始化:init_IRQ()函數來完成,他直接由srart_kernel()函數來調用。定義於arch/arm/kernel/irq.c。【這一點內容在下一篇博文中發表】

















發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章