2009年6月5日 星期五

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?

2009年6月4日 星期四

trace linux kernel source - ARM - 01

想開始閱讀文章的網友,建議是對assembly有大略的認識或者手上有語法可以查閱,雖然文章內容大多會解釋語法的用途,但畢竟不會做完整的解說。

以下的文章一些已經在某些討論區貼過,重新編排之後再貼重貼到blog,

此篇原文撰寫於2008-0806
--------
昨天下載了linux-2.6.26的kernel source,打算trace一下 (以ARM為例子)看看能不能多了解一下kernel booting時候的一些動作,一些文章提到是從/arch/arm/boot/bootp/init.S開始,所以就從它開始吧!!

19 .section .start,#alloc,#execinstr
20 .type _start, #function
21 .globl _start
22
23 _start: add lr, pc, #-0x8 @ lr = current load addr
24 adr r13, data
25 ldmia r13!, {r4-r6} @ r5 = dest, r6 = length
26 add r4, r4, lr @ r4 = initrd_start + load addr
27 bl move @ move the initrd
.....
76 .type data,#object
77 data: .word initrd_start @ source initrd address
78 .word initrd_phys @ destination initrd address
79 .word initrd_size @ initrd size
80

line19,宣告了叫做.start的section,line 20,21宣告了一個叫做_start的function,程式碼似乎從line 23開始line 23, 『add lr, pc, #-0x8』,add就是將pc+(-0x8)的結果放到lr之中,pc和lr都是ARM裡頭暫存器pc就是program counter,CPU用來指著目前要執行的指令,執行完CPU就會自動把PC+1這樣就會拿到下一道指令,lr通常是第14個register, r14,常被用來繼續functionreturn時候的返回位置。奇怪的是為什麼要-0x8??

原因應該是ARM本身有pipeline的設計,3 stages: prefetch->decode->execution,當指令被執行的時候,其實已經預先去偷偷抓下一道,所以PC值不是真的指在目前執行的地方,三級pipeline剛好多了兩個cycle所以4 bytes x 2必須要-8才是正在執行指令的位址。line 24, 『adr r13, data』adr會去讀取data所在的位址當作值,寫到r13裡頭。r13通常是用來放stack pointer,常縮寫成sp,所以現在r13就指到data所在的位置上去。line 25, 『ldmia r13!, {r4-r6}』ldmia, load multiple increment after,顧名思義就是可以做很多次load的動作,每此load完就把位址+1,執行完之後r4 = initrd_startr5 = initrd_physr6 = initrd_sizeline 26, 『add r4, r4, lr』r4 = r4+lr, lr是剛剛我們算過的,指到一開始執行指令的地方,看程式碼的解釋,被當成載入的位址,所以執行完 r4 = initrd_start+程式被載入的位置,但是意圖不明,看看之後有沒有被用到。line 27, 『bl move』bl, b是branch,相當於C語言goto的動作,l就是goto之前,先把目前位置存放到lr裡面,所以bl除了goto之外,也改寫了lr,應該是有利於等一下返回的時候,可以用lr的值。

由於上面的 code 裡面出現了一些還沒定義的symbol,例如 initrd_start, initrd_size等等,因此我們先岔斷出來做說明,其實這些是被定義在另外一個檔 ./arch/arm/boot/bootp/initrd.S

1 .type initrd_start,#object
2 .globl initrd_start
3 initrd_start:
4 .incbin INITRD
5 .globl initrd_end
6 initrd_end:

line 2, 3, 5, 6定義了兩個symbol initrd_start和initrd_end,中間還用.incbin INITRD 將 ramdisk 的 image include進來,這邊有點複雜,假如compiler kernel的時候

1) 有選擇使用ramdisk當成boot device的話,會對從環境變數去設置 INITRD 這個檔名會被帶入到MAKEFILE,並且在做assembler動作的時候丟進來。

2) 假如沒有使用initrd,那就.incbin應該就包不到東西,initrd_start 和 initrd_end 就會相等

另外有個 script ./arch/arm/boot/bootp/bootp.lds,它規範了所有link起來的object code裡面的section要怎麼編排,特定的編排方式,對於撰寫kernel的時候,程式本身可以很方便到特定的section取得想要的資料或是計算某個section的大小

10 OUTPUT_ARCH(arm)
11 ENTRY(_start)
12 SECTIONS
13 {
14 . = 0;
15 .text : {
16 _stext = .;
17 *(.start)
18 *(.text)
19 initrd_size = initrd_end - initrd_start;
20 _etext = .;
21 }
22
23 .stab 0 : { *(.stab) }
24 .stabstr 0 : { *(.stabstr) }
25 .stab.excl 0 : { *(.stab.excl) }
26 .stab.exclstr 0 : { *(.stab.exclstr) }
27 .stab.index 0 : { *(.stab.index) }
28 .stab.indexstr 0 : { *(.stab.indexstr) }
29 .comment 0 : { *(.comment) }
30 }

對於object file的格式不熟悉的話可以參考ELF format的相關文件

接著繼續看 init.S,之前的code已經goto到move這邊來,所以貼一些move的程式碼

66 move: ldmia r4!, {r7 - r10} @ move 32-bytes at a time
67 stmia r5!, {r7 - r10}
68 ldmia r4!, {r7 - r10}
69 stmia r5!, {r7 - r10}
70 subs r6, r6, #8 * 4
71 bcs move
72 mov pc, lr

line 66, 將r4所指到的位置,分別將值讀出來放到r7, r8, r9, r10, 可以發現剛剛計算過的r4這邊被用到了,但是為什麼r4不是用initrd_start,卻還要加上load addr??

原因應該是bootp.lds的14行『. = 0;』表示最後被link好的address會從0x0開始,所以 initrd_start 所記錄的位置可以當成是offset,加上load到DRAM或是擺在flash上的位址後就剛好是initrd所在的地方。

line 67, 『stmia r5!, {r7 - r10}』stmia, store multiple increment after, 和ldmia動作相同,只是用來寫資料。r5是存放著initrd要擺放的位置,猜測應該是為了一開始image放在flash上,但是可以將initrd拷貝到DRAM上r7寫到r5指到的位置

r8->r5+1
r9->r5+2
r10->r5+3

所以我們發現,66,67行就是將r4所指的東西搬到r5。line 68, 69也是一樣copy了4x4bytes,一共是32bytes。line 70,『subs r6, r6, #8 * 4』,將length - 32bytesline 71,『bcs move』,b是branch的意思,cs是表示condition的條件,要是條件符合的話,就做branch的動作,這邊的用意是判斷前一個length是不是已經到0,如果不為零就繼續copy。line 72,『mov pc, lr』接著就把剛剛bl指令預先存放好的lr 填入pc,這樣CPU就會跳回去原本的return address。以上的動作,慢慢看得出來有在做些什麼事

1. 找出initrd的所在位置
2. 將它copy到一個指定的destination去

程式返回之後我們接著看下一行

33 ldmia r13, {r5-r9} @ get size and addr of initrd
34 @ r5 = ATAG_CORE
35 @ r6 = ATAG_INITRD2
36 @ r7 = initrd start
37 @ r8 = initrd end
38 @ r9 = param_struct address
39
40 ldr r10, [r9, #4] @ get first tag
41 teq r10, r5 @ is it ATAG_CORE?

line 33, 繼續從r13的地方取出資料到r5, r6, r7 ,r8, r9,註解的說明有提到各個資料的意義,注意一下這邊的r7是initrd的destination address不是source address。line 40, 讀入第一個tag,這邊的tag是指bootloader丟給kernel的一個boot arguments,會被用一個叫做ATAG的structure包起來,並且放到系統的某個地方。然後kernel跑init.S,的時候就會去這個地方拿ATAG的資料,這些資訊包括記憶體要使用多大,螢幕的解析度多大等等。line 41, t是test, eq是equal, 判斷拿到的第一個tag是不是等於atag core. 應該是看 atag list 是不是成立的。繼續接著看

45 movne r10, #0 @ terminator
46 movne r4, #2 @ Size of this entry (2 words)
47 stmneia r9, {r4, r5, r10} @ Size, ATAG_CORE, terminator

發現45, 46, 47的指令都帶有condition "ne", not equal,表示是剛剛 line 41發現atag不成立所做的事情,注釋是寫『If we didn't find a valid tag list, create a dummy ATAG_CORE entry.』所以以上三行就是用來創造一個假的entry,假設一切順利這三行指令會bypass過去不會被執行到。接著來看init.S最後一段程式碼 (終於~)

54 taglist: ldr r10, [r9, #0] @ tag length
55 teq r10, #0 @ last tag (zero length)?
56 addne r9, r9, r10, lsl #2
57 bne taglist
58
59 mov r5, #4 @ Size of initrd tag (4 words)
60 stmia r9, {r5, r6, r7, r8, r10}
61 b kernel_start @ call kernel

line 54, 將r9指到的位址的offset 0x0的值載入到r10。看註解是tag length,所以這邊得要去翻翻atag的規範這邊有個文章有提到 http://www.simtec.co.uk/products ... ooting_article.html ,一開始應該是去讀atag_header所看第一個欄位,確認一下是size,應該沒問題。

struct atag_header {
u32 size; /* legth of tag in words including this header */
u32 tag; /* tag value */
};

line 55,測試一下size是不是0。line 56, 57也有condition ne,表示是不為0的時候做的。將拿到的length(r10)乘以4,這邊的lsl是將r10往左shift的意思,因為一個欄位是4bytes,所以乘4之後就跳到下一個tag,一直跳到最後沒東西。line 59, 將r5設成4line 60, 將r5, r6, r7, r8 ,r10存到r9所指到的位置,應該就是跟在atag list的後面。line 61, jump 到 kernel_start ,注意這邊是用b而不是bl,因為跳過去kernel就不需要返回了。BL會用到lr紀錄返回位置。以上,走過一整個init.S,我們可以粗略的知道他會

1) 處理ramdisk
2) 處理atag (也就是bootargs)

接著會跳到./arch/arm/boot/compressed/head.S。kernel_start的定義方式跟initrd_start有點類似,中間有透過 kernel.S去用.incbin把kernel image包進來。

搜尋此網誌