ARM Linux中斷向量表搬移設計過程
Preface引言
本文引用地址:http://m.butianyuan.cn/article/201611/317931.htm我在這里用一些篇幅來描述一下arm體系結構下Linux中怎樣來初始化中斷向量表的,因為這個方法很具有通用性,我把它叫做代碼大挪移。您說搬代碼誰不會阿,不就是拷貝嗎,的確如此,但是拷貝也有技巧??截惡芎唵卫?,其實就是memcpy,這不用提,我在這里想說的是,你怎么把你的代碼設計成能隨便拷貝的,換句專業(yè)的術語,叫與位置無關的代碼,拷到哪都能用。我以前也用過類似的方法作啟動,今天拿來說說。
Scenario 1第一場景copy
我們先看實際動作。代碼的位置在arch/arm/traps.c中,kernel version: 2.6.27。這個是初始化部分的代碼,setup_arch()->early_trap_init().熟悉初始化部分的朋友們可能見到過這段代碼。
void __init early_trap_init(void)
{
unsigned long vectors = CONFIG_VECTORS_BASE;
extern char __stubs_start[], __stubs_end[];
&nsp;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動作一目了然,就是兩個memcpy(第三個實際上是拷貝一些別的東西,原理是一樣的,這里不提了). Copy的源是vectors,這個值是CONFIG_VECTORS_BASE,一般來講,是0xffff0000,當然你可以根據硬件的設定自己配制這個值。把什么東西往那copy呢?第一部分是從__vectors_start到__vectors_end之間的代碼,第二部分是從__stubs_start到__stubs_end之間的代碼,而第二部分是copy到vectors + 0x200起始的位置。也就是說,兩部分之間的距離是0x200,即512個字節(jié)。
我們來看__vectors_start,__vectors_end,font face="Times New Roman">__stubs_start,__stubs_end到底是什么東西,只要知道它們在哪里定義的,就知道怎么回事了。
Scenario 2第二場景主角閃亮登場
它們埋伏在arch/arm/kernel/entry-armv.S中,這個文件是arm中各個模式的入口代碼,熟悉arm的朋友們知道arm有幾種模式,不知道的自己查查,不說了。我們取一個片斷,和我們的闡述相關的部分。為了讓大家看得更清楚,我刪掉了部分代碼和注釋,把主干凸顯出來。有興趣的朋友可以查看源代碼,研究全部,里面還是比較有內涵的。
.globl__stubs_start
__stubs_start:
/*
* Interrupt dispatcher
*/
vector_stubirq, IRQ_MODE, 4
//請注意這里:vector_stub是一個宏,展開后是一塊代碼,下面是個跳轉表,我們將代碼結//構展開,大致是這樣的結構: (后面的vector_stubdabt, ABT_MODE, 8等展開過程全一樣,在此略過不提)
// -------------------------------- begin展開
.align5
vector_irq:
sublr, lr, 4
@ Save r0, lr_
@ (parent CPSR)
@
stmiasp, {r0, lr}@ save r0, lr
mrslr, spsr
strlr, [sp, #8]@ save spsr
@ Prepare for SVC32 mode.IRQs remain disabled.
@
mrsr0, cpsr
eorr0, r0, IRQ_MODE ^ SVC_MODE)
msrspsr_cxsf, r0
@ the branch table must immediately follow this code
@
andlr, lr, #0x0f
movr0, sp
ldrlr, [pc, lr, lsl #2]
movspc, lr@ branch to handler in SVC mode
// -------------------------------- end展開
.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@f
/*
* Data abort dispatcher
* Enter in ABT mode, spsr = USR CPSR, lr = USR PC
*/
vector_stubdabt, ABT_MODE, 8
.long__dabt_usr@0(USR_26 / USR_32)
.long__dabt_invalid@1(FIQ_26 / FIQ_32)
.long__dabt_invalid@2(IRQ_26 / IRQ_32)
.long__dabt_svc@3(SVC_26 / SVC_32)
。。。
.long__dabt_invalid@f
/*
* Prefetch abort dispatcher
* Enter in ABT mode, spsr = USR CPSR, lr = USR PC
*/
vector_stubpabt, ABT_MODE, 4
.long__pabt_usr@0 (USR_26 / USR_32)
.long__pabt_invalid@1 (FIQ_26 / FIQ_32)
.long__pabt_invalid@2 (IRQ_26 / IRQ_32)
.long__pabt_svc@3 (SVC_26 / SVC_32)
。。。
.long__pabt_invalid@f
/*
* Undef instr entry dispatcher
* Enter in UND mode, spsr = SVC/USR CPSR, lr = SVC/USR PC
*/
vector_stubund, 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@f
.align5
vector_fiq:
disable_fiq
subspc, lr, #4
vector_addrexcptn:
bvector_addrexcptn
/*
* We group all the following data together to optimise
* for CPUs with separate I & D caches.
*/
.align5
.LCvswi:
.wordvector_swi
.globl__stubs_end
__stubs_end:
.equstubs_offset, __vectors_start + 0x200 - __stubs_start
.globl__vectors_start
__vectors_start:
swiSYS_ERROR0
bvector_und + stubs_offset
ldrpc, .LCvswi + stubs_offset
bvector_pabt + stubs_offset
bvector_dabt + stubs_offset
bvector_addrexcptn + stubs_offset
bvector_irq + stubs_offset
bvector_fiq + stubs_offset
.globl__vectors_end
__vectors_end:
為了讓大家看得更清,我把代碼的結構再次簡化成這樣:
.globl__stubs_start
__stubs_start:
.align5
vector_irq:
[code part]//展開代碼
[jump table part]//地址跳轉表
。。。
.align5
vector_dabt:
[code part]
[jump table part]
。。。
.align5
vector_ pabt:
[code part]
[jump table part]
。。。
.align5
vector_und:
[code part]
[jump table part]
。。。
.align5
vector_fiq:
。。。
.globl__stubs_end
__stubs_end:
.globl__vectors_start
__vectors_start:
swiSYS_ERROR0
bvector_und + stubs_offset
ldrpc, .LCvswi + stubs_offset
bvector_pabt + stubs_offset
bvector_dabt + stubs_offset
bvector_addrexcptn + stubs_offset
bvector_irq + stubs_offset
bvector_fiq + stubs_offset
.globl__vectors_end
__vectors_end:
在這里我不花過多的篇幅去解釋代碼的意思,這不是本文的目的,只要你把結構看清,就達到目的了。但我會花點時間研究一下展開代碼部分(藍色)的特征,這部分代碼是與位置無關的代碼,我們稍微研究一下,它為什么會這么寫。
.align5
vector_irq:
[code part]//展開代碼
[jump table part]//地址跳轉表
。。。
首先這部分代碼大致都是一樣的結構,前面是一些代碼,后面跟著一個跳轉表。跳轉表里面定義了一些地址。我們截取這部分看
。。。
@ the branch table must immediately follow this code
@
andlr, lr, #0x0f(1)// lr中當前存儲了上一個狀態(tài)寄存器的值,對后幾位做與,
//就是取在中斷前處在用戶態(tài)還是核心態(tài),這個值用作跳
//轉表的索引
movr0, sp(2)//用做他用,sp值當第一個參數傳給后面函數
ldrlr, [pc, lr, lsl #2](3)// pc是當前執(zhí)行指令地址加8,即跳轉表的基地址,lr是索引
//很好的技巧,取pc找當前地址什么時候都沒錯
movpc, lr@ branch to handler in SVC mode
[jump table]
.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)
真正的跳轉在最后一句完成,大家都看得很清楚。跳到哪里去了,如果中斷以前是svc模式,就會跳到__irq_svc。我們發(fā)現這里不會直接用b(bl,bx等)個,
ü一是b跳轉后面是個偏移,而這個偏移是有限制的,不能太大
ü二是b跳轉后面的偏移你不知道在代碼拷貝后還是不是那個樣子,因為我們要搬移代碼,所以如果你不能確定搬移后的偏移不變,那你就用絕對地址,而上面的代碼前三句就是算出絕對地址來,然后用絕對地址賦值給pc直接完成跳轉。
這些都是一些技巧,總之你要注意的是寫位置無關的代碼時涉及到跳轉部分,用b跳轉還是直接賦成絕對地址(通過跳轉表實現),如果你不能保證搬移后的偏移一致,寫這部分就要注意了,要用一些技巧的。
大家可以去用gcc的-fPIC和-S選項匯編一個小的函數看看,fPIC就是與位置無關選項,相信編譯過動態(tài)庫的人都熟悉,看看它是怎么做的。你會發(fā)現異曲同工。
Scenario 3第三場景大搬移
我用一個章節(jié)來介紹大搬移的過程,以及一些在搬移中Linux出現的問題及解決方案。我把整個的搬移過程做成一張圖里,然后討論了一些技術細節(jié)。我們看到這是一個巨大無比的圖,我們這章節(jié)的所闡述的內容都在圖里。
我們將搬移前的代碼組織稱為Code/Load視圖,因為這是代碼中的(或image中的)組織情況,把搬移后的代碼組織稱為Exec視圖,反映的是代碼執(zhí)行時代碼在內存中的情況。我剛才講過了第一場景的情況,忘了的回到第一場景中去看,兩個memcpy的執(zhí)行過程在圖中也有表示,就是藍色和紅色的帶箭頭的虛線,這就是代碼從code view到exec view的拷貝過程,一目了然,不用多說。
現在出現了一個問題,就是我們發(fā)現在__vector_start和__vector_end之間的代碼有點怪異,我們再次摘到這里來看:
.equstubs_offset, __vectors_start + 0x200 - __stubs_start
.globl__vectors_start
__vectors_start:
swiSYS_ERROR0
bvector_und + stubs_offset
ldrpc, .LCvswi + stubs_offset
bvector_pabt + stubs_offset
bvector_dabt + stubs_offset
bvector_addrexcptn + stubs_offset
bvector_irq + stubs_offset
bvector_fiq + stubs_offset
.globl__vectors_end
__vectors_end:
在第二個場景中我們說過,這叫做位置無關的代碼,因為要拷貝到別的地方。而且里面都是跳轉指令。我們發(fā)現了除了第三個行代碼用了絕對地址進行了跳轉,其它都是用的b跳轉。舉個例子,bvector_dabt + stubs_offset,(vector_dabt在__stubs_start和__stubs_end之間),如果你用b vector_dabt,這肯定是有問題的,因為copy之后exec view的組織(map)是不一樣的,所以b后這個偏移就不對了。這里面,我們就要對這個偏移進行一次調整。Stubs_offset就是這個調整值,是可以計算出來的,具體的計算過程在圖中講得比較清楚,這里不提了。大家可以在圖中看到詳細的推導過程。
其實盡管ldrpc, .LCvswi + stubs_offset這條指令用的是絕對地址跳轉,用得跳轉表的方法,但找地址的過程也用到了這個技術。我們看到
.align5
.LCvswi:
.wordvector_swi
.LCvswi這個位置存儲的是一個地址,就是要跳到這個地方。.align 5的意思是32字節(jié)對齊,這個是保證cache line對齊的,不提了。在exec view中找這個地址,就得加上個offset.原理是一樣的,因為.LCvswi在__stubs_start和__stubs_end之間,這個區(qū)域被搬移了,不能直接用這個標簽地址了,vector_swi沒有被搬移,所以可以直接用。
總結一下。我覺得我要講的東西雖然是Linux中的技術細節(jié),描述的確是代碼搬移過程原理和注意事項。其實更重要的是,我們如何把這一個過程倒過來,即在涉及到代碼搬移的場合中如何進行設計,如何運用這些技術實現這一設計過程。你可以遵循這樣的指導步驟:
1.畫出那個大圖來,按自己的要求確定Code view和Exec view,設計搬移區(qū)段和設計搬移的位置
2.寫出要搬移的代碼,運用位置無關的技術(上面提到的)進行編碼和檢驗
3.用類似memcpy的代碼進行搬移
評論