2010年9月8日 星期三

trace linux kernel - 2

文章出自於 trace linux kernel source - ARM - 02

開始跳進去head.S之前,先來了解一下bootloader大略概念,當板子通電,最先被執行到的通常是bootloader,透過它才有機會去改變載入過程例如更換這次使用的kernel image或者是選擇要用tftp download kernel image,還是從flash上的某個image跑起來,由於kernel本身也對自己所處的環境一無所知,為了讓單純起見,因此linux也有限制bootloader必須在進入到kernel之前必須設置好的狀態,例如:

1. r0 = 0
2. r1 = architecture ID
3. r2 = atag list
4. mmu off
5. I&D cache off

如此一來kernel就可以在已知的狀態,去預先規定好的暫存器拿資料,有了以上的概念有助於看head.S。

我們首先來看一開始的程式碼進入點

114 start:
115 .type start,#function
116 .rept 8
117 mov r0, r0
118 .endr
119
120 b 1f
121 .word 0x016f2818 @ Magic numbers to help the loader
122 .word start @ absolute load/run zImage address
123 .word _edata @ zImage end address
124 1: mov r7, r1 @ save architecture ID
125 mov r8, r2 @ save atags pointer

line 116~118, rept = repeat, endr = end of repeat, 意思是將move r0, r0的程式碼重複八次,也就是說build成kernel image的時候這邊一開始的code會有8個指令都在作『move r0, r0』的事情,很怪!!還看不出是做什麼的。可能之後會看到如何被運用。(有些文章寫說是作出中斷向量表的空間,我們這邊先不預先作猜測~)line 120, branch到1的地方,f是指forward的方式找branch。line 124, 125, 分別將r1, r2的資料丟到r7, r8存起來,回想兩件事情:

1) init.S執行的過程中,始終沒有用到r1, r2,那r1, r2的資料到底放著什麼東西?
2) 一開始我們提到,bootloader會預先設定好狀態才跳到kernel

原來!! r1, r2就是bootloader準備好的。 這讓我想到一個問題,假設我們不想跑bootloader,是不是可以寫一小段程式碼,直接將狀態設置好,就直接進入linux kernel呢??line 121~123, 純粹將一些資訊記住,.start就是 kernel 起始位置,這邊看起來是忽略掉init.S和initrd.S佔去的位置,直接將.start這個section當成kernel image的開始起點。接著繼續往下看(我們預設arch已經超過v2,現在應該大多是v4以上)

133 mrs r2, cpsr @ get current mode
134 tst r2, #3 @ not user?
135 bne not_angel
136 mov r0, #0x17 @ angel_SWIreason_EnterSVC
137 swi 0x123456 @ angel_SWI_ARM
138 not_angel:
139 mrs r2, cpsr @ turn off interrupts to
140 orr r2, r2, #0xc0 @ prevent angel from running
141 msr cpsr_c, r2

line 133, mrs 是特殊用來讀取cpsr和spsr暫存器裡頭紀錄processor模式值的指令,這兩個reg是用來控制和表示processor目前狀態的。

line 134, tst = test, 看看r2是不是等於3。

line 135, r2不等於3的話就跳到 not_angel 這個地方開始執行,記得以前有個angelboot可以用來boot armlinux,應該是angelboot會特別跑在3這個mode。line 136, 137, 用來觸發angelboot裡頭的swi的function,作用應該是要切回去SVC mode。SVC mode是一開始ARM processor預設執行模式。line 139~141, 用來關掉interrupt,避免被中斷booting的過程。因為目前沒有處理中斷的函式和環境,中斷萬一觸發可能無法處理。

程式繼續往下跑

157 adr r0, LC0
158 ldmia r0, {r1, r2, r3, r4, r5, r6, ip, sp}
159 subs r0, r0, r1 @ calculate the delta offset
160
161 @ if delta is zero, we are
162 beq not_relocated @ running at the address we
163 @ were linked at.

288 .type LC0, #object
289 LC0: .word LC0 @ r1
290 .word __bss_start @ r2
291 .word _end @ r3
292 .word zreladdr @ r4
293 .word _start @ r5
294 .word _got_start @ r6
295 .word _got_end @ ip
296 .word user_stack+4096 @ sp
297 LC1: .word reloc_end - reloc_start
298 .size LC0, . - LC0

line 157, 將LC0的位址當作值放到r0。line 158, 從r0指到的位址開始,將值依序讀到r1, r2, r3, r4, r5, r6, ip, sp, 其中 ip=r12, sp=r13

line 159, 將r0-r1,這邊的意思是說,r0是真正被載入到記憶體上的address,r1是被compile完就已經決定好的位址(也就是line 289中LC0這個symbole的address),兩個相減,剛好可以算出『compile好』跟『被load到位址』之間的offset,這樣做有什麼意義? 繼續往下看。line 162, 如果相減等於0,表示載入的位址和complie好的位址是一樣的,那程式碼就可以被直接執行,要是不為0的話,表示compile本來以為這些執行碼會被放到 r1 的位址,可是卻被放到r0的位址去,這樣一來,有一些預先compile好的程式碼,可以會找不到一些symbol的所在位置,因為整個image被load到不對的offset的地方。那...怎麼辦勒??往下看

172 add r5, r5, r0
173 add r6, r6, r0
174 add ip, ip, r0

202 1: ldr r1, [r6, #0] @ relocate entries in the GOT
203 cmp r1, r2 @ entry < bss_start
204 cmphs r3, r1 @ _end < entry
205 addlo r1, r1, r0 @ table. This fixes up the
206 str r1, [r6], #4 @ C references.
207 cmp r6, ip
208 blo 1b

line 172~174, 將r0這個offset,加到r5, r6, ip,也就是r5=zImage base address, r6=GOT start, ip=GOT end. GOT全名是global offset table, 它是ELF format執行檔裡面用來放一些和位址無關的code的地方。詳細的東西可以參照http://www.itee.uq.edu.au/~emmerik/elf.html。總之,可以看得出來我們將一些位址加上了offset,因為我們載入的位置跟原本執行碼所預期的位址不同,因此必須做一些relocate的動作,若是不做的話,很可能程式碼會拿到不對的資料,或是jump到錯誤的地方執行。line 202~208, r1指向GOT table start,將GOT table start到bss區塊之前的資料都作relocate。line 203, 204,應該是用來避免r1指到bss區塊。關於BSS也必須參考ELF format的東西, BSS是用來放置,未經初始化的變數的地方。以上,我們發現kernel意識到自己被載入到某個地方,並且查看被載入到的地方是不是和compile time決定一樣,不一樣的話,自己手動修改一些需要做offset的資訊,等於是手動作relocate的事情,這樣一來linux kernel才可以被放到memory中的許多地方都可以被執行起來。

接著繼續trace

211 not_relocated: mov r0, #0
212 1: str r0, [r2], #4 @ clear bss
213 str r0, [r2], #4
214 str r0, [r2], #4
215 str r0, [r2], #4
216 cmp r2, r3
217 blo 1b
218
224 bl cache_on

上面trace到kernel做了一些判斷,如果被載入的位址和compile time決定的位址不同,就會自己做relocate的動作,將一些ELF binary的特定pointer和value加上offset。那做完初步的relocate之後要做什麼?

line 212~215, 都是做store的動作,把r0存到r2所指到的位址,做完之後r2=r2+4。r2= bss start的位址. 換句話說,開始將bss裡頭的值都初始化成0。lin3 216, 217, 確認一下是不是到了bss的底部,不到底部的話,jump到line 212繼續做搬移的動作。line 224, 做完bss初始化,jump cache_on

328 cache_on: mov r3, #8 @ cache_on function
329 b call_cache_fn

537 call_cache_fn: adr r12, proc_types
539 mrc p15, 0, r6, c0, c0 @ get processor ID
543 1: ldr r1, [r12, #0] @ get value
544 ldr r2, [r12, #4] @ get mask
545 eor r1, r1, r6 @ (real ^ match)
546 tst r1, r2 @ & mask
547 addeq pc, r12, r3 @ call cache function
548 add r12, r12, #4*5
549 b 1b

line 328, 將r3填入8, 不知道r3會拿做什麼用,繼續看。
line 329, jump到call_cache_fn。
line 537, 將proc_types的位址讀到r12。
line 539, 將coprocessor裡頭的CPU id讀出來放到r6
line 543, 544, 將r12所指到的第一個位址的資料放到r1, offset 4bytes的資料放到r2,我們可以先觀察一下proc_types的長相(如下)

566 proc_types:
567 .word 0x41560600 @ ARM6/610
568 .word 0xffffffe0
569 b __arm6_mmu_cache_off @ works, but slow
570 b __arm6_mmu_cache_off
571 mov pc, lr
......
640 .word 0x00050000 @ ARMv5TE
641 .word 0x000f0000
642 b __armv4_mmu_cache_on
643 b __armv4_mmu_cache_off
644 b __armv4_mmu_cache_flush

註解上面寫了很多arm的家族的名稱,例如arm 6, armv5te等等,而且不難發現都是先兩個.word,然後跟著三個『b xxxx_cache_xxx』,感覺很像是一組一組的資料。line 545, 546, 將r6裡頭的CPU ID和讀出來的r1做exclusive OR,並且取mask,看看是否相等,相等的話,就將pc設定r12+r3。換句話說,就是用CPU ID去確認值是否相等,值相等的話,就jump到r12+r3的位址。line 548, 549, 不相等的話,就把r12加上5x4byte的offset跳回去繼續找。

整理一下,這邊的程式碼就是去proc_types的地方,比對CPU ID,比對成功的話,就呼叫該筆資料裡面的cache function,至於呼叫第幾個function,就由r3控制,那所有CPU對應到的data structure就從proc_types開始。以ARMv5TE來說,r3=8,就剛好是cache_on的function。所以我們知道如果我自己發明了一個新的ARM CPU,也弄了一個新的id,這邊就需要修改出相對應的CPU的infomation,不然可能會找不到CPU ID。

到這邊我們,找到了CPU對應的cache on的function,必且要準備呼叫它。

接著開始首先我們偷看一下code,line 226, 將sp的值放到r1。line 227 將sp的值加上0x10000放到sp。為什麼kernel之前花了一些功夫將自己relocate到某個位置之後,要把cache打開,然後要開始對stack pointer(sp)做動作?目前還看不出來,所以接著trace下去。line 238,比較r4和r2的值,r4的值從line 158載入之後就一直沒被用到過,這個值是從一些makefile或是被makefile include進來的,然後在linking time的時候會被帶入,每個平台不一定一樣,通常你可到./arch/arm/mach-xxx/Makefile.boot去設定,這個值是用來指定 kernel應該要被load到哪個位址上面執行。以omap1來說,

zreladdr-y := 0x10008000

就是表示kernel會被載入到 0x10008000 的地方。這邊將r2和r4比較的用意是看看sp+0x10000之後會不會超過zreladdr的位置,應該是怕stack爆掉了會蓋到kernel的地方。(記住我們現在的kernel其實還在壓縮狀態,zreladdr是指解壓縮完要開始執行的狀態。)line238~line243, 比較了r4和r2,假如不會蓋到,就會跳到wont_overwrite去執行,假如會蓋到,就看目前sp到之後解壓縮image位址之間的距離有沒有比 image四倍的大小來得大,假如有,表示空間還夠用,還是可以跳去wont_overwrite去,假如不到四倍大,就跑到line 262去把kernel搬到遠一點的地方,試看看能不能正常boot起來,line262先不做解釋,一般來說位址設錯的話,這邊的correction 失敗的機率還是很大,著眼在correction的意義不大。所以我們就直接跳去wont_overwrite吧!

226 mov r1, sp @ malloc space above stack
227 add r2, sp, #0x10000 @ 64k max
238 cmp r4, r2
239 bhs wont_overwrite
240 sub r3, sp, r5 @ > compressed kernel size
241 add r0, r4, r3, lsl #2 @ allow for 4x expansion
242 cmp r0, r5
243 bls wont_overwrite
244
245 mov r5, r2 @ decompress after malloc space
246 mov r0, r5
247 mov r3, r7
248 bl decompress_kernel
249
250 add r0, r0, #127 + 128 @ alignment + stack
251 bic r0, r0, #127 @ align the kernel length

跳到wont_overwrite之後,當然就是要開始把kernel解壓縮,line 283,把r4搬到r0,r4就是我們剛剛說的kernel被解壓縮之後的位址。(也就是解完之後應該要執行的位置)line 284,把r7搬到r3,r7從一開始讀進來之後,也沒用過,理論上是architecture ID。line 285,是跳到decompress_kernel,這邊我們發現decompress_kernel是被定義在misc.c檔,所以這是第一次從 assembly code跳到c code的地方。這樣一來我們就知道原來剛剛要把cache打開和設定好sp的用意,原來就是為了要執行c code,因為c的程式碼有固定的執行方式,會需要用到sp,這部份可以參考『procedure call standard for the ARM architecture』,這也是r4和r7被搬到r0, r3的原因,因為r0~r3是用來傳遞C function的參數用的,r0就是arg0, r1=arg1, etc.

283 wont_overwrite: mov r0, r4
284 mov r3, r7
285 bl decompress_kernel
286 b call_kernel

偷看misc.c

346 decompress_kernel(ulg output_start, ulg free_mem_ptr_p, ulg free_mem_ptr, int arch_id)

果然r0~r3就是的參數。

由於解壓縮不是我們的重點沒有trace假設一切都順利decompress_kernel結束後我們就得到一個解壓縮完的kernel放在r4指向的位置line 286,會jump到call_kernel,如下:

516 call_kernel: bl cache_clean_flush
517 bl cache_off
518 mov r0, #0 @ must be zero
519 mov r1, r7 @ restore architecture number
520 mov r2, r8 @ restore atags pointer
521 mov pc, r4 @ call kernel

line 516, flush cache
line 517, 關掉 cacheline 518~520,將r0, r1, r2分別填值。
line 521,將program counter指到r4,也就是解壓縮的kernel的一開頭。到這邊我們終於結束head.S的工作,解壓縮並且跳到另外一個object code的開始。跳到解壓縮的開始位置,究竟會進入哪一個function?

沒有留言:

張貼留言