轉自:http://blog.csdn.net/xiaojsj111/article/details/14129661
以外部中斷irq為例來說明,當外部硬體産生中斷時,linux的處理過程。首先先說明當外部中斷産生時,硬體處理器所做的工作如下:
R14_irq = address of next instruction to be executed + 4/*将寄存器lr_mode設定成傳回位址,即為目前pc的值,因為pc是目前執行指令的下兩條指令*/
SPSR_irq = CPSR /*儲存處理器目前狀态、中斷屏蔽位以及各條件标志位*/
CPSR[4:0] = 0b10010 /*設定目前程式狀态寄存器CPSR中相應的位進入IRQ模式,注意cpsr是所有模式共享的*/
CPSR[5] = 0 /*在ARM狀态執行*/
/*CPSR[6] 不變*/
CPSR[7] = 1 /*禁止正常中斷*/
If high vectors configured then
PC=0xFFFF0018 /*将程式計數器(PC)值設定成該異常中斷的中斷向量位址,進而跳轉到相應的異常中斷處理程式處執行,對于ARMv7向量表普遍中斷是0xFFFF0018*/
else
PC=0x00000018 /*對于低向量*/
假設在使用者空間時,産生了外部硬體中斷,則這個時候的指令跳轉流程如下:
[cpp] view plain copy
- __vectors_start:---------------〉在中斷向量表被拷貝後,該位址就是0xffff0000.
- ARM( swi SYS_ERROR0 )
- THUMB( svc #0 )
- THUMB( nop )
- W(b) vector_und + stubs_offset
- W(ldr) pc, .LCvswi + stubs_offset
- W(b) vector_pabt + stubs_offset
- W(b) vector_dabt + stubs_offset
- W(b) vector_addrexcptn + stubs_offset
- W(b)vector_irq + stubs_offset----------〉當外部中斷産生時,pc直接指向這個位址。
- W(b) vector_fiq + stubs_offset
- .globl __vectors_end
下面的vector_stubirq, IRQ_MODE, 4語句,展開就是vector_irq,是以上述語句跳轉到如下語句執行:
- __stubs_start:
- /*
- * Interrupt dispatcher
- */
- vector_stub irq, IRQ_MODE, 4
- .long __irq_usr@ 0 (USR_26 / USR_32)
- .long __irq_invalid@ 1 (FIQ_26 / FIQ_32)
- .long __irq_invalid@ 2 (IRQ_26 / IRQ_32)
- .long __irq_svc@ 3 (SVC_26 / SVC_32)
- .long __irq_invalid@ 4
- .long __irq_invalid@ 5
- .long __irq_invalid@ 6
- .long __irq_invalid@ 7
- .long __irq_invalid@ 8
- .long __irq_invalid@ 9
- .long __irq_invalid@ a
- .long __irq_invalid@ b
- .long __irq_invalid@ c
- .long __irq_invalid@ d
- .long __irq_invalid@ e
- .long __irq_invalid@ f
vector_stubirq, IRQ_MODE, 4語句展開如下:
- <span style="font-size:18px">/*
- * 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
- .align 5
- vector_\name:
- .if \correction
- sub lr, lr, #\correction //因為硬體處理器是将目前指令的下兩條指令的位址存儲在lr寄存器中,是以這裡需要減4,讓他指向被中斷指令的下一條,這樣當中斷被恢複時,可以繼續被中斷的指令繼續執行。
- .endif<span style="white-space:pre"> </span> //需要注意的是,這個時候的lr寄存器,已經是irq模式下的私有寄存器了,在中斷産生時,硬體處理器已經自動為他賦了值。
- @
- @ Save r0, lr_<exception> (parent PC) and spsr_<exception>
- @ (parent CPSR)
- stmia sp, {r0, lr} @ save r0, lr//儲存r0和lr寄存器,即被中斷的下一條指令
- mrs lr, spsr
- str lr, [sp, #8] @ save spsr
- @ Prepare for SVC32 mode. IRQs remain disabled.//準備從中斷模式切換到管理模式,不同的模式,對應各自不同的堆棧。
- mrs r0, cpsr
- eor r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)
- msr spsr_cxsf, r0
- @ the branch table must immediately follow this code
- and lr, lr, #0x0f //擷取被中斷前,處理器所處的模式
- THUMB( adr r0, 1f )
- THUMB( ldr lr, [r0, lr, lsl #2] )
- mov r0, sp<span style="white-space:pre"> </span>//讓r0寄存器指向中斷模式下堆棧的基位址
- ARM( ldr lr, [pc, lr, lsl #2] )
- movs pc, lr @ branch to handler in SVC mode,同時将中斷模式下的spsr_irq(irq私有的)指派給cpsr(該寄存器所有模式共享)
- ENDPROC(vector_\name)</span>
此時中斷模式下的私有棧sp的存儲情況如下:(注意這個時候的sp是中斷模式下的堆棧sp),并且這個時候r0寄存器中,儲存有sp的指針值,由于r0已經被儲存到堆棧,是以可以放心被使用
根據被中斷時,處理器模式的不同,分别跳轉到__irq_usr和__irq_svc兩個分支。
在這裡我們以__irq_usr為例來說明:
- <span style="font-size:18px">__irq_usr:
- usr_entry //進行中斷前的硬體上下文的儲存
- kuser_cmpxchg_check
- irq_handler
- get_thread_info tsk//擷取被中斷的使用者程序或核心線程所對應的核心棧所對應的thread info結構。
- mov why, #0
- b ret_to_user_from_irq//恢複被中斷時的上下文,然後繼續被中斷的程序或線程的執行
- UNWIND(.fnend )
- ENDPROC(__irq_usr)</span>
usr_entry展開如下:
- .macro usr_entry
- UNWIND(.fnstart )
- UNWIND(.cantunwind ) @ don't unwind the user space
- sub sp, sp, #S_FRAME_SIZE // #S_FRAME_SIZE的值為72
- ARM( stmib sp, {r1 - r12} ) //盡管目前是處于管理模式,但由于svc和usr的r0-r12是公共的,是以相當于儲存使用者模式的r1-r12寄存器
- THUMB( stmia sp, {r0 - r12} )
- ldmia r0, {r3 - r5} //将之前儲存在中斷模式堆棧中的r0_usr,lr,spsr分别存儲到r3-r5中
- add r0, sp, #S_PC @ here for interlock avoidance #S_PC=60
- mov r6, #-1 @ "" "" "" ""
- str r3, [sp] @ save the "real" r0 copied
- @ from the exception stack
- @
- @ We are now ready to fill in the remaining blanks on the stack:
- @ r4 - lr_<exception>, already fixed up for correct return/restart
- @ r5 - spsr_<exception>
- @ r6 - orig_r0 (see pt_regs definition in ptrace.h)
- @ Also, separately save sp_usr and lr_usr
- stmia r0, {r4 - r6}
- ARM( stmdb r0, {sp, lr}^ )//儲存使用者模式下的sp_usr,lr_usr
- THUMB( store_user_sp_lr r0, r1, S_SP - S_PC )
- @ Enable the alignment trap while in kernel mode
- alignment_trap r0
- @ Clear FP to mark the first stack frame
- zero_fp
- ifdef CONFIG_IRQSOFF_TRACER
- bl trace_hardirqs_off
- endif
- .endm
至此,使用者模式下所有的寄存器都被正确儲存了,并且處理器模式中irq模式成功切換到管理模式,并且sp這個時候是指向儲存r0_usr寄存器值得地方。此時的管理模式的核心棧分布如下:
需要說明的是:上圖中的lr_irq即為使用者模式下被中斷指令的下一條指令,spsr_irq即為使用者模式下被中斷時的cpsr寄存器。
在這裡說明下,中斷時寄存器的儲存是有固定的順序的,他們順序即如下所示:
cpsr(r16) |
pc(r15) |
lr(r14) |
sp(r13) |
r12(ip) |
r11(fp) |
r10 |
r9 |
r8 |
r7 |
r6 |
r5 |
r4 |
r3 |
r2 |
r1 |
r0 |
上圖中的S_FRAME_SIZE, S_PC在arch/arm/kernel/Asm-offsets.c:中定義
DEFINE(S_FRAME_SIZE, sizeof(struct pt_regs));
DEFINE(S_PC, offsetof(struct pt_regs, ARM_pc));
include/asm-arm/Ptrace.h:
struct pt_regs {
long uregs[18];
};
#define ARM_pc uregs[15]
呵呵,pt_regs中對應的就是上面棧上的18個寄存器,ARM_pc是pc寄存器存放在這個數組中的偏移。
接着看get_thread_info, 它也是個宏,用來擷取目前線程的位址。他的結構體定義如下:
include/linux/Sched.h:
union thread_union {
struct thread_info thread_info; /*線程屬性*/
unsigned long stack[THREAD_SIZE/sizeof(long)]; /*棧*/
由它定義的線程是8K位元組對齊的, 并且在這8K的最低位址處存放的就是thread_info對象,即該棧擁有者線程的對象,而get_thread_info就是通過把sp低13位清0(8K邊界)來擷取目前thread_info對象的位址。
arch/arm/kernel/entry-armv.S:
.macro get_thread_info, rd
mov /rd, sp, lsr #13
mov /rd, /rd, lsl #13
.endm
調用該宏後寄存器tsk裡存放的就是目前線程的位址了, tsk是哪個寄存器呢,呵呵我們在看:
arch/arm/kernel/entry-header.S:
tsk .req r9 @ current thread_info
呵呵,tsk隻是r9的别名而已, 是以這時r9裡儲存的就是目前線程的位址。
為了将彙編部分講完,我們繼續研究ret_to_user_from_irq函數,該函數展開後,如下:
- <span style="font-size:18px">ENTRY(ret_to_user_from_irq)
- ldr r1, [tsk, #TI_FLAGS] //tsk如上所述,是r9寄存器的别名,并且是指向thread_info結構體的
- tst r1, #_TIF_WORK_MASK //檢測是否有待處理的任務
- bne work_pending
- no_work_pending:
- #if defined(CONFIG_IRQSOFF_TRACER)
- asm_trace_hardirqs_on
- #endif
- /* perform architecture specific actions before user return */
- arch_ret_to_user r1, lr //針對arm,是dummy的
- restore_user_regs fast = 0, offset = 0//恢複之前使用者模式時被中斷時所儲存的寄存器上下文
- ENDPROC(ret_to_user_from_irq)</span>
restore_user_regs展開如下:
- <span style="font-size:18px"> .macro restore_user_regs, fast = 0, offset = 0
- ldr r1, [sp, #\offset + S_PSR] @ get calling cpsr 即為被中斷時,處理器的cpsr值
- ldr lr, [sp, #\offset + S_PC]! @ get pc 即為被中斷指令的,下一條指令
- msr spsr_cxsf, r1 @ save in spsr_svc //将r1指派給管理模式下的spsr_svc,這樣在movs時,會自動将該值指派為cpsr
- #if defined(CONFIG_CPU_V6)
- strex r1, r2, [sp] @ clear the exclusive monitor
- #elif defined(CONFIG_CPU_32v6K)
- clrex @ clear the exclusive monitor
- .if \fast
- ldmdb sp, {r1 - lr}^ @ get calling r1 - lr
- .else
- ldmdb sp, {r0 - lr}^ @ get calling r0 - lr,将儲存在核心棧中的r0到r14恢複到使用者模式中的寄存器
- .endif
- mov r0, r0 @ ARMv5T and earlier require a nop
- @ after ldm {}^
- add sp, sp, #S_FRAME_SIZE - S_PC //恢複核心棧到中斷産生之前的位置。
- movs pc, lr @ return & move spsr_svc into cpsr
- .endm
- </span>
至此中斷彙編部分已經全部處理完成。
最後摘錄部門網上經典的問題解答:
問題1:vector_irq已經是異常、中斷處理的入口函數了,為什麼還要加stubs_offset?( b vector_irq + stubs_offset)
答:(1)核心剛啟動時(head.S檔案)通過設定CP15的c1寄存器已經确定了異常向量表的起始位址(例如0xffff0000),是以需要把已經寫好的核心代碼中的異常向量表考到0xffff0000處,隻有這樣在發生異常時核心才能正确的處理異常。
(2)從上面代碼看出向量表和stubs(中斷處理函數)都發生了搬移,如果還用b vector_irq,那麼實際執行的時候就無法跳轉到搬移後的vector_irq處,因為指令碼裡寫的是原來的偏移量,是以需要把指令碼中的偏移量寫成搬移後的。至于為什麼搬移後的位址是vector_irq+stubs_offset,請參考我的上篇blog:linux中斷系統那些事之----中斷初始化過程
問題2:為什麼在異常向量表中,用b指令跳轉而不是用ldr絕對跳轉?
答:因為使用b指令跳轉比絕對跳轉(ldr pc,XXXX)效率高,正因為效率高,是以把__stubs_start~__stubs_end之間的代碼考到了0xffff0200起始處。
注意:
因為b跳轉指令隻能在+/-32MB之内跳轉,是以必須拷貝到0xffff0000附近。
b指令是相對于目前PC的跳轉,當彙編器看到 B 指令後會把要跳轉的标簽轉化為相對于目前PC的偏移量寫入指令碼。
問題3:為什麼首先進入head.S開始執行?
答:核心源代碼頂層目錄下的Makefile制定了vmlinux生成規則:
# vmlinux image - includingupdated kernel symbols
vmlinux: $(vmlinux-lds)$(vmlinux-init) $(vmlinux-main) vmlinux.o $(kallsyms.o)FORCE
其中$(vmlinux-lds)是編譯連接配接腳本,對于ARM平台,就是arch/arm/kernel/vmlinux-lds檔案。vmlinux-init也在頂層Makefile中定義:
vmlinux-init := $(head-y)$(init-y)
head-y 在arch/arm/Makefile中定義:
head-y:=arch/arm/kernel/head$(MMUEX T).o arch/arm/kernel/init_task.o
…
ifeq ($(CONFIG_MMU),)
MMUEXT := -nommu
endif
對于有MMU的處理器,MMUEXT為空白字元串,是以arch/arm/kernel/head.O 是第一個連接配接的檔案,而這個檔案是由arch/arm/kernel/head.S編譯産生成的。
綜合以上分析,可以得出結論,非壓縮ARM Linux核心的入口點在arch/arm/kernel/head.s中。
問題4: 中斷為什麼必須進入svc模式?
一個最重要原因是:
如果一個中斷模式(例如從usr進入irq模式,在irq模式中)中重新允許了中斷,并且在這個中斷例程中使用了BL指令調用子程式,BL指令會自動将子程式傳回位址儲存到目前模式的sp(即r14_irq)中,這個位址随後會被在目前模式下産生的中斷所破壞,因為産生中斷時CPU會将目前模式的PC儲存到r14_irq,這樣就把剛剛儲存的子程式傳回位址沖掉。為了避免這種情況,中斷例程應該切換到SVC或者系統模式,這樣的話,BL指令可以使用r14_svc來儲存子程式的傳回位址。
問題5:為什麼跳轉表中有的用了b指令跳轉,而有的用了ldr px,xxxx?
W(b) vector_und+ stubs_offset
W(ldr) pc, .LCvswi + stubs_offset
W(b) vector_pabt+ stubs_offset
W(b) vector_dabt+ stubs_offset
W(b) vector_addrexcptn+ stubs_offset
W(b) vector_irq+ stubs_offset
W(b) vector_fiq+ stubs_offset
.LCvswi:
.word vector_swi
由于系統調用異常的代碼編譯在其他檔案中,其入口位址與異常向量相隔較遠,使用b指令無法跳轉過去(b指令隻能相對目前pc跳轉32M範圍)。是以将其位址存放到LCvswi中,并從記憶體位址中加載其入口位址,原理與其他調用是一樣的。這也就是為什麼系統調用的速度稍微慢一點的原因。
問題6:為什麼ARM能進行中斷?
因為ARM架構的CPU有一個機制,隻要中斷産生了,CPU就會根據中斷類型自動跳轉到某個特定的位址(即中斷向量表中的某個位址)。如下表所示,既是中斷向量表。
ARM中斷向量表及位址
問題7:什麼是High vector?
A:在Linux3.1.0,arch/arm/include/asm/system.hline121 有定義如下:
#if __LINUX_ARM_ARCH__ >=4
#define vectors_high() (cr_alignment & CR_V)
#else
#define vectors_high() (0)
#endif
意思就是,如果使用的ARM架構大于等于4,則定義vectors_high()=cr_alignment&CR_V,該值就等于0xffff0000
在Linux3.1.0,arch/arm/include/asm/system.hline33有定義如下:
#define CR_V (1 << 13) /* Vectors relocated to 0xffff0000 */
arm下規定,在0x00000000或0xffff0000的位址處必須存放一張跳轉表。
問題8:中斷向量表是如何存放到0x00000000或0xffff0000位址的?
A:Uboot執行結束後會把Linux核心拷貝到記憶體中開始執行,linux核心執行的第一條指令是linux/arch/arm/kernel/head.S,此檔案中執行一些參數設定等操作後跳入linux/init/main.c檔案的start_kernel函數,此函數調用一系列初始化函數,其中trip_init()函數實作向量表的設定操作。
【作者】sky
【出處】http://www.cnblogs.com/sky-heaven/
【部落格園】 http://www.cnblogs.com/sky-heaven/
【知乎】 http://www.zhihu.com/people/zhang-bing-hua
【我的作品---旋轉倒立擺】 http://v.youku.com/v_show/id_XODM5NDAzNjQw.html?spm=a2hzp.8253869.0.0&from=y1.7-2
【我的作品---自平衡自動循迹車】 http://v.youku.com/v_show/id_XODM5MzYyNTIw.html?spm=a2hzp.8253869.0.0&from=y1.7-2
【大餅教你學系列】https://edu.csdn.net/course/detail/10393
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利.