之前貼了一些自己 trace linux kernel 的一些筆記文,筆記
式的文章難免寫得比較瑣碎,閱讀起來應該比較不容易,尤其逐
行trace,應該會有人很排斥或是不認同這樣的一種方式,之前
曾經看到一些文章甚至會明白地提醒讀者切記不要逐行研究程式
碼,要將其觀念記住才是重點。
這邊想針對這樣的論點提一些想法給大家參考,目的不是要訂出
一個好與壞,其實我覺得兩種方法並不相衝突,而是大家在學習
的階段,可以依照理解程度來做取捨。
以自己的經驗來說,以前一開始囫圇吞棗,試圖去理解書上提到
的觀念,似懂非懂的記了許多東西,但是往往人家問我:
『你能夠自己寫出一個OS或是其中一部分功能嗎?』
似乎就變得很心虛,只能告訴對方『我知道它的實作原理』,但
是說要自己要寫,好像就是少了點什麼? 好像懂,但是要怎麼真
正的寫出來,卻是不怎麼敢肯定。
面對這樣的狀況持續一段時間,讓人真正有自信能夠依樣畫葫蘆
弄出個什麼東西,卻是在花心思從很基本的instruction set
開始K和逐行逐行了解之後,才覺得似乎概念和實作有了那麼一
點連接。也由於這樣的基礎,有時候有助於資料不足的狀況下,
還能夠經由看程式碼來補足資料不足的部份,甚至可以用來印
證自己的想法。
這樣的說起來多看程式的好處多多囉?
好像也不盡然,自己的經驗是,看上老半天,一大段雖然每個字
都看得懂,但是兜起來就是不曉得他要做啥用? (看英文的時候
....恩...好像也是這樣 )有時回頭翻書,看看觀念,才會發
現這一段天書似的程式碼所隱含的意義,自然就理解了。
所以後來想想要深入kernel source的方式,似乎得要雙管齊
下,一邊看觀念,一邊找出相對應的程式碼出來,最後觀念與觀
念之間,必定有一些很細節的部份沒有被提到,玩家就得自己想
辦法將他們串起來,一旦書本上的觀念可以在實際程式上得到印
證,那這樣改天要改寫,也就遊刃有餘。
這邊想特別提到的還是閱讀指令集的重要性,對指令的熟悉,不
但對閱讀低階的程式碼有很大的幫助,還對於整個系統演進了解
更為透徹。以前會覺得了解太低階的東西用處不大,後來才覺得
這些部分有時卻影響很大。
以上無聊閒談,歡迎大家提出自己的經驗,交流一下~
顯示具有 trace linux kernel source - ARM 標籤的文章。 顯示所有文章
顯示具有 trace linux kernel source - ARM 標籤的文章。 顯示所有文章
2009年12月20日 星期日
2009年7月15日 星期三
trace linux kernel source - ARM - 03 part2
隔了很長一段時間沒update
之前程式碼走到 ./arch/arm/kernel/head.S 的 line 99 附近
99 ldr r13, __switch_data @ address to jump to after
100 @ mmu has been enabled
101 adr lr, __enable_mmu @ return (PIC) address
102 add pc, r10, #PROCINFO_INITFUNC
line 99, 將__switch_data放到r13。(留作之後用)
line 101, 將__enable_mmu的addr放到lr。(留作之後用)
line 102, 將 r10+#PROCINFO_INITFUNC 放到pc,也就是jump過去的意思。r10是proc_info的位址。PROCINFO_INITFUNC則是用之前提過的技巧,指向定義在./arch/arm/mm/proc-xxx.S的資料結構,以arm926為例,最後會指到
463 b __arm926_setup
所以程式碼就跳到了 __arm926_setup。
373 .type __arm926_setup, #function
374 __arm926_setup:
375 mov r0, #0
376 mcr p15, 0, r0, c7, c7 @ invalidate I,D caches on v4
377 mcr p15, 0, r0, c7, c10, 4 @ drain write buffer on v4
378 #ifdef CONFIG_MMU
379 mcr p15, 0, r0, c8, c7 @ invalidate I,D TLBs on v4
380 #endif
388 adr r5, arm926_crval
389 ldmia r5, {r5, r6}
390 mrc p15, 0, r0, c1, c0 @ get control register v4
391 bic r0, r0, r5
392 orr r0, r0, r6
396 mov pc, lr
397 .size __arm926_setup, . - __arm926_setup
這邊的程式碼就跟CPU有很大的相依性,
line 375~380, 主要就是invalidate CPU的I&D cache和清空write buffer。
line 388~392, 把cp15的設定讀出來,並且將一些預設狀態做好運算放到r0。(預設值從arm926_crval這邊可以取得。)
line 396, 直接把pc跳到lr,因為我們之前已經將lr = enable_mmu,所以直接跳過去。
155 __enable_mmu:
170 mov r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
171 domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
172 domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \
173 domain_val(DOMAIN_IO, DOMAIN_CLIENT))
174 mcr p15, 0, r5, c3, c0, 0 @ load domain access register
175 mcr p15, 0, r4, c2, c0, 0 @ load page table pointer
176 b __turn_mmu_on
177 ENDPROC(__enable_mmu)
line 170~174,設置好domain access。(domain access可以先當成設置access的權限,或許以後可以寫詳細的文章說明)
line 175~176,將create好的page table開頭丟給mmu。跳到turn_mmu_on
191 __turn_mmu_on:
192 mov r0, r0
193 mcr p15, 0, r0, c1, c0, 0 @ write control reg
194 mrc p15, 0, r3, c0, c0, 0 @ read id reg
195 mov r3, r3
196 mov r3, r3
197 mov pc, r13
198 ENDPROC(__turn_mmu_on)
顧名思義就是把mmu打開,將我們準備好的r0設定交給mmu,並讀取id到r3,接著pc跳到r13,r13剛剛在head.S已經先擺好__switch_data。所以會跳到head-common.S。
18 __switch_data:
19 .long __mmap_switched
20 .long __data_loc @ r4
21 .long _data @ r5
22 .long __bss_start @ r6
23 .long _end @ r7
24 .long processor_id @ r4
25 .long __machine_arch_type @ r5
26 .long __atags_pointer @ r6
27 .long cr_alignment @ r7
28 .long init_thread_union + THREAD_START_SP @ sp
29
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
39 __mmap_switched:
40 adr r3, __switch_data + 4
41
42 ldmia r3!, {r4, r5, r6, r7}
43 cmp r4, r5 @ Copy data segment if needed
44 1: cmpne r5, r6
45 ldrne fp, [r4], #4
46 strne fp, [r5], #4
47 bne 1b
48
49 mov fp, #0 @ Clear BSS (and zero fp)
50 1: cmp r6, r7
51 strcc fp, [r6],#4
52 bcc 1b
53
54 ldmia r3, {r4, r5, r6, r7, sp}
55 str r9, [r4] @ Save processor ID
56 str r1, [r5] @ Save machine type
57 str r2, [r6] @ Save atags pointer
58 bic r4, r0, #CR_A @ Clear 'A' bit
59 stmia r7, {r0, r4} @ Save control register values
60 b start_kernel
61 ENDPROC(__mmap_switched)
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
line 39,將__data_loc的addr放到r3
line 42,從r3的位址,連續讀取四筆資料到r4, r5, r6, r7
line 43~47,看看data segment是不是需要搬動。
line 49~52, clear BSS。
由於linux kernel在進入start_kernel前有一些前提必須要滿足:
r0 = cp#15 control register
r1 = machine ID
r2 = atags pointer
r9 = processor ID
所以line 54~59就是在做這些準備。
最後呢? 我們就跳到start_kernel了。(而且還是用b start_kernel,表示我們不會再回來了)
看一下start_kernel()在./init/main.c,終於跳出architecture specific的目錄,表示
我們真正的開始linux kernel的初始化。
像是 shedule init, console init, memory init, irq init等等都在start_kernel裡頭。
到這邊之後,應該就可以深入linux kernel中,各項比較跟hardware不那麼相關的軟體部分。
之前程式碼走到 ./arch/arm/kernel/head.S 的 line 99 附近
99 ldr r13, __switch_data @ address to jump to after
100 @ mmu has been enabled
101 adr lr, __enable_mmu @ return (PIC) address
102 add pc, r10, #PROCINFO_INITFUNC
line 99, 將__switch_data放到r13。(留作之後用)
line 101, 將__enable_mmu的addr放到lr。(留作之後用)
line 102, 將 r10+#PROCINFO_INITFUNC 放到pc,也就是jump過去的意思。r10是proc_info的位址。PROCINFO_INITFUNC則是用之前提過的技巧,指向定義在./arch/arm/mm/proc-xxx.S的資料結構,以arm926為例,最後會指到
463 b __arm926_setup
所以程式碼就跳到了 __arm926_setup。
373 .type __arm926_setup, #function
374 __arm926_setup:
375 mov r0, #0
376 mcr p15, 0, r0, c7, c7 @ invalidate I,D caches on v4
377 mcr p15, 0, r0, c7, c10, 4 @ drain write buffer on v4
378 #ifdef CONFIG_MMU
379 mcr p15, 0, r0, c8, c7 @ invalidate I,D TLBs on v4
380 #endif
388 adr r5, arm926_crval
389 ldmia r5, {r5, r6}
390 mrc p15, 0, r0, c1, c0 @ get control register v4
391 bic r0, r0, r5
392 orr r0, r0, r6
396 mov pc, lr
397 .size __arm926_setup, . - __arm926_setup
這邊的程式碼就跟CPU有很大的相依性,
line 375~380, 主要就是invalidate CPU的I&D cache和清空write buffer。
line 388~392, 把cp15的設定讀出來,並且將一些預設狀態做好運算放到r0。(預設值從arm926_crval這邊可以取得。)
line 396, 直接把pc跳到lr,因為我們之前已經將lr = enable_mmu,所以直接跳過去。
155 __enable_mmu:
170 mov r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
171 domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
172 domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \
173 domain_val(DOMAIN_IO, DOMAIN_CLIENT))
174 mcr p15, 0, r5, c3, c0, 0 @ load domain access register
175 mcr p15, 0, r4, c2, c0, 0 @ load page table pointer
176 b __turn_mmu_on
177 ENDPROC(__enable_mmu)
line 170~174,設置好domain access。(domain access可以先當成設置access的權限,或許以後可以寫詳細的文章說明)
line 175~176,將create好的page table開頭丟給mmu。跳到turn_mmu_on
191 __turn_mmu_on:
192 mov r0, r0
193 mcr p15, 0, r0, c1, c0, 0 @ write control reg
194 mrc p15, 0, r3, c0, c0, 0 @ read id reg
195 mov r3, r3
196 mov r3, r3
197 mov pc, r13
198 ENDPROC(__turn_mmu_on)
顧名思義就是把mmu打開,將我們準備好的r0設定交給mmu,並讀取id到r3,接著pc跳到r13,r13剛剛在head.S已經先擺好__switch_data。所以會跳到head-common.S。
18 __switch_data:
19 .long __mmap_switched
20 .long __data_loc @ r4
21 .long _data @ r5
22 .long __bss_start @ r6
23 .long _end @ r7
24 .long processor_id @ r4
25 .long __machine_arch_type @ r5
26 .long __atags_pointer @ r6
27 .long cr_alignment @ r7
28 .long init_thread_union + THREAD_START_SP @ sp
29
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
39 __mmap_switched:
40 adr r3, __switch_data + 4
41
42 ldmia r3!, {r4, r5, r6, r7}
43 cmp r4, r5 @ Copy data segment if needed
44 1: cmpne r5, r6
45 ldrne fp, [r4], #4
46 strne fp, [r5], #4
47 bne 1b
48
49 mov fp, #0 @ Clear BSS (and zero fp)
50 1: cmp r6, r7
51 strcc fp, [r6],#4
52 bcc 1b
53
54 ldmia r3, {r4, r5, r6, r7, sp}
55 str r9, [r4] @ Save processor ID
56 str r1, [r5] @ Save machine type
57 str r2, [r6] @ Save atags pointer
58 bic r4, r0, #CR_A @ Clear 'A' bit
59 stmia r7, {r0, r4} @ Save control register values
60 b start_kernel
61 ENDPROC(__mmap_switched)
switch_data的第一行就是 __mmap_switched,所以我們直接看line 39。
line 39,將__data_loc的addr放到r3
line 42,從r3的位址,連續讀取四筆資料到r4, r5, r6, r7
line 43~47,看看data segment是不是需要搬動。
line 49~52, clear BSS。
由於linux kernel在進入start_kernel前有一些前提必須要滿足:
r0 = cp#15 control register
r1 = machine ID
r2 = atags pointer
r9 = processor ID
所以line 54~59就是在做這些準備。
最後呢? 我們就跳到start_kernel了。(而且還是用b start_kernel,表示我們不會再回來了)
看一下start_kernel()在./init/main.c,終於跳出architecture specific的目錄,表示
我們真正的開始linux kernel的初始化。
像是 shedule init, console init, memory init, irq init等等都在start_kernel裡頭。
到這邊之後,應該就可以深入linux kernel中,各項比較跟hardware不那麼相關的軟體部分。
2009年7月3日 星期五
trace linux kernel source - ARM - 03
到目前為止,我們已經進展到kernel幫自己relocate完,並解將自己解壓縮到一個地方要準備開始執行,那疑問來了?到底是跳到哪裡去了,因為./compressed/head.S最後一行居然是『mov pc, r4』r4只代表了解壓縮完後kernel的位址,那究竟整包kernel編譯的時候,哪個function哪個東西被放在最前面咧?!所以我們又必須開始找於kernel link的時候是怎麼被安排的,有了前面的基礎,我們可以從Makefile知道程式碼如何被編譯。至於link上的細節,例如有那些section和section先後順序等等,可以從 lds 檔來規範。有興趣的人可以看一下 kernel source 根目錄裡頭的 Makefile,Makefile file裡面指定了使用vmlinux.lds來當做lds檔。
659 vmlinux-lds := arch/$(SRCARCH)/kernel/vmlinux.lds
打開 ./arch/arm/kernel/vmlinux.lds.S (會用來產生vmlinux.lds)我們可以發現第一個section是『.text.head』,裡頭的_stext從目前的位置開始放。於是我們曉得只要找到屬於.text.head這個section,並且是_stext這個symbol的程式碼,就是解壓縮完後的第一行程式碼。
26 .text.head : {
27 _stext = .;
28 _sinittext = .;
29 *(.text.head)
30 }
用指令搜尋一下,發現 ./arch/arm/kernel/head.S 裡頭有關鍵字(如下),結果我們從./arch/arm/boot/compressed/head.S跳到了./arch/arm/kernel/head.S 。
77 .section ".text.head", "ax"
78 .type stext, %function
79 ENTRY(stext)
80 msr cpsr_c, #PSR_F_BIT PSR_I_BIT SVC_MODE @ ensure svc mode
81 @ and irqs disabled
82 mrc p15, 0, r9, c0, c0 @ get processor id
83 bl __lookup_processor_type @ r5=procinfo r9=cpuid
84 movs r10, r5 @ invalid processor (r5=0)?
85 beq __error_p @ yes, error 'p'
86 bl __lookup_machine_type @ r5=machinfo
87 movs r8, r5 @ invalid machine (r5=0)?
88 beq __error_a @ yes, error 'a'
89 bl __vet_atags
90 bl __create_page_tables
既然找到了檔案,我們又可以開始繼續。看了一下,程式碼多了很多bl的跳躍動作,看來會在各個function跳來跳去。既然跳到真正的kernel開始跑,表示進入重頭戲,在進入kernel之前arm的平台有一些預設的狀況,也就是說arm kernel image會預設目前的cpu和系統的狀況是在某個狀態,這樣對一個剛要跑起來的OS比較決定目前要怎麼boot起來。可以看一下comment,有清楚的描述。這也幫助我們了解為什麼decompresser的程式跑完之後,還要把cache關掉。
59 /*
60 * Kernel startup entry point.
61 * ---------------------------
62 *
63 * This is normally called from the decompressor code. The requirements
64 * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
65 * r1 = machine nr, r2 = atags pointer.
基於以上的假設,當program counter指到kernel的程式的時候,就會從line 80開始跑。
line 80, msr指令會把, operand的值搬到cpsr_c裡面。這是用來確保arm cpu目前是跑在svc mode, irq&fiq都disable。(設成0x1就會disable,definition在./include/asm-arm /ptrace.h)
line 82, 讀取CPU ID到r9
line 83, 跳到 __lookup_processor_type 執行(bl會記住返回位址)。
77 .section ".text.head", "ax"
78 .type stext, %function
79 ENTRY(stext)
80 msr cpsr_c, #PSR_F_BIT PSR_I_BIT SVC_MODE @ ensure svc mode
81 @ and irqs disabled
82 mrc p15, 0, r9, c0, c0 @ get processor id
83 bl __lookup_processor_type @ r5=procinfo r9=cpuid
接著會跳到head-common.S這個檔。
line 158, (3f表示forware往前找叫做『3』label)將label 3的address放到r3。
line 159, 將r3指到的位址的資料,依序放到r7, r6, r5.(ldmda的da是要每次都位址減一次)
line l60, 161, 利用真實得到位址r3減去取得資料的位址得到一個offset值,這樣可計算出r5, r6真正應該要指到的地方。
line 163~169,這邊的程式應該不陌生,就是在各個CPU info裡面找尋對應的structure。找到的話就跳到line 171,返回head.S
line 170, 找不到的話,r5的processor id就放0x0.表示unknown id。__proc_info_xxx可以在 vmlinux.lds.S 找到,是用來包住CPU info的所有data.資料則是被定義在./arch/arm/mm/proc-xxx.S,例如arm926就有 proc-arm926.S,裡面有相對應的data宣告,compiling time的時候,這些資料會被編譯到這個區段當中。
156 .type __lookup_processor_type, %function
157 __lookup_processor_type:
158 adr r3, 3f
159 ldmda r3, {r5 - r7}
160 sub r3, r3, r7 @ get offset between virt&phys
161 add r5, r5, r3 @ convert virt addresses to
162 add r6, r6, r3 @ physical address space
163 1: ldmia r5, {r3, r4} @ value, mask
164 and r4, r4, r9 @ mask wanted bits
165 teq r3, r4
166 beq 2f
167 add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list)
168 cmp r5, r6
169 blo 1b
170 mov r5, #0 @ unknown processor
171 2: mov pc, lr
187 .long __proc_info_begin
188 .long __proc_info_end
189 3: .long . 190 .long __arch_info_begin
191 .long __arch_info_end
跳了很多檔案,建議指令和object code如何link的概念要有,就會很清楚了。
我們從 head-common.S返回之後,接著繼續看。
line 84, movs的意思是說,做mov的動作,並且將指令的S bit設起來,最後的結果也會update CPSR。這個指令執行的過程當中,會根據你要搬動的值去把N and Z flag設好。這個是有助於下個指令做check的動作。
line 85, 就是r5 = 0的話,就跳到__error_p去執行。line 86, 跳到__lookup_machine_type。原理跟剛剛找proc的資料一樣。
83 bl __lookup_processor_type @ r5=procinfo r9=cpuid
84 movs r10, r5 @ invalid processor (r5=0)?
85 beq __error_p @ yes, error 'p'
86 bl __lookup_machine_type @ r5=machinfo
看得出來跟proc很像,有個兩個小地方是
1. line 207,用ldmia不是用ldmda。原因就在於存放arch 和 proc info 的位址剛好相反。一個在lable3的上面。一個在的下方。設計上應該是可以做修改的。
2. arch定義的方式是透過macro,可以先看 ./include/asm-arm/mach/arch.h。裡頭有個 MACHINE_START ,這邊會設好macro,到時候直接使用就可以把arch的info宣告好。 例如 ./arch/arm/mach-omap1/board-generic.c
/* macro */
50 #define MACHINE_START(_type,_name) \
51 static const struct machine_desc __mach_desc_##_type \
52 __used \
53 __attribute__((__section__(".arch.info.init"))) = { \
54 .nr = MACH_TYPE_##_type, \
55 .name = _name,
56
57 #define MACHINE_END \
58 }; /* 用法 */
93 MACHINE_START(OMAP_GENERIC, "Generic OMAP1510/1610/1710")
94 /* Maintainer: Tony Lindgren */
95 .phys_io = 0xfff00000,
96 .io_pg_offst = ((0xfef00000) >> 18) & 0xfffc,
97 .boot_params = 0x10000100,
98 .map_io = omap_generic_map_io,
99 .init_irq = omap_generic_init_irq,
100 .init_machine = omap_generic_init,
101 .timer = &omap_timer,
102 MACHINE_END /* func */
204 .type __lookup_machine_type, %function
205 __lookup_machine_type:
206 adr r3, 3b
207 ldmia r3, {r4, r5, r6}
208 sub r3, r3, r4 @ get offset between virt&phys
209 add r5, r5, r3 @ convert virt addresses to
210 add r6, r6, r3 @ physical address space
211 1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type
212 teq r3, r1 @ matches loader number?
213 beq 2f @ found
214 add r5, r5, #SIZEOF_MACHINE_DESC @ next machine_desc
215 cmp r5, r6
216 blo 1b
217 mov r5, #0 @ unknown machine
218 2: mov pc, lr
接著我們又返回到head.S,line 87~88也是做check動作。line 89跳到vet_atags。在head-common.S
87 movs r8, r5 @ invalid machine (r5=0)?
88 beq __error_a @ yes, error 'a'
89 bl __vet_atags
90 bl __create_page_tables
line 245, tst會去做and動作,並且update flags。這邊的用意是判斷位址是不是aligned。
line 246, 沒有aligned跳到label 1,就返回了。
line 248~250, 讀取atags所在的address裡頭的值到r5,看看是否不等於ATAG_CORE_SIZE,不等於的話也是返回。
line 251~254, 判斷一下第一個達到的atag pointer是不是等於ATAG_CORE。如果正確的話,等一下要讀取atag的資料,才會正確。(atag是由bootloader帶給linux kernel的東西,用來告知booting所需要知道的參數。例如螢幕寬度,記憶體大小等等)
14 #define ATAG_CORE 0x54410001
15 #define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)
243 .type __vet_atags, %function 244 __vet_atags:
245 tst r2, #0x3 @ aligned?
246 bne 1f
247
248 ldr r5, [r2, #0] @ is first tag ATAG_CORE?
249 subs r5, r5, #ATAG_CORE_SIZE
250 bne 1f
251 ldr r5, [r2, #4]
252 ldr r6, =ATAG_CORE
253 cmp r5, r6
254 bne 1f
255
256 mov pc, lr @ atag pointer is ok
257
258 1: mov r2, #0
259 mov pc, lr接著我們又跳回去head.S。
line 90,又跳到 __create_page_tables。 (很累人....應該會死不少腦細胞)哇!page table?!!如雷貫耳的東西,不知道會怎麼做。剛剛偷看了一下,@@還蠻長了。
由於code看起來似乎越來越難解釋,會需要在檔案之間跳來跳去。建議一些基礎的東西可以再多複習幾次(其實是在說我自己 ),閱讀source code上收穫會比較多。例如:
1. arm instruction set - 這個最好每個指令的意思都大略看過一次,行有餘力多看幾個版本,armv4, v5 or v6。
2. compiler & assembler & linker - toolchain工具做的事情和功能,大致的流程和功能要有概念。
3. Makefile & link script - 這兩個功能和撰寫的方式要有簡單的概念。
以上1是非常重要的重點,2&3只要有大致上的概念就可以,因為trace code的時候,有時需要跳到Makeflie&Link script去看最後object code編排的位址。由於我們trace到了page table這個關鍵字,在開始之前,稍微簡短的解釋,為了幫助了解,儘量用易懂的概念講,有些用詞可能會和一些真實狀況不同,但是懂了之後,應該會有能力分辨,至於正式而學術上解說,很多書上應該都有,或是google一下就很多啦~
page table本身是很抽象的東西,尤其是對所謂的 user-mode application programmer 來說,大部分的狀況它是不需要被考慮到的部份,但是他的產生卻對os和driver帶來了很多影響。我們從一個小小的疑問出發。『產生page table到底是要給誰用的?』其實真正作用在它上面的H/W是MMU,一旦CPU啟用了MMU,當cpu嘗試去記憶體讀取一個operand的時候,cpu內部打出去的位址訊號都會先送到mmu,mmu會拿著這個位址去對照page table,看看這個位址是不是被轉換到另外一個位置(還會確認讀寫權限),最後才會到真正的位址去讀寫。這樣來看CPU其實一開始打出去的位址訊號其實不是最後可以拿到資料的位址,我們稱為virtual address(va),mmu打出來的位址才能真正拿到資料,所以我們把mmu打出去的位址稱為physical address(pa)。那用來查詢這個位址對照關係的表格,就是page table。到這邊我們有一個簡單的概念。來想像一個小問題,一個普通的周邊(例如你的顯示卡),因為他並不具有MMU功能,hw只看得懂pa,但是CPU卻是作用在va,假如我想去對hw的控制暫存器做讀寫,到底要用va還是pa?
這時,寫driver的人就必須要小心,設定給硬體看的位址必須要先轉成pa(像是設定DMA),給CPU執行的程式碼要使用va,這對一開始嘗試寫driver但是又不瞭解va pa之間的差別的人,常常產生很多疑問。
現在我們回頭想想OS,既然我們跑到create page table,可以預期的是他想要將MMU打開,因此希望預先建立起一個page table,讓MMU知道目前os想規劃的位址對應和讀取權限是如何被安排的。所以os必須考慮到現在的系統究竟是長怎樣?應該要如何被安排?是不是有那些要被保護?好吧~因為我們完全對os不了解,也不知道該安排什麼,只能祈禱在trace code完後,可以找到這些問題的答案,或者發現一些沒想到的問題。知道了page table的大致上的功能,下篇就可以專心的研究這個table的長相,和它想規劃出的系統模樣。
現在,讓我們跳入create_page_tables吧~
216 pgtbl r4 @ page table address
line 216,會跳到pgtbl的macro去執行。
其實只是載入一個位址。只是這個位址會因為你想將哪個位址開始的記憶體留給kernel使用,定一就會不同,理論上是可以改變的。一般會定義在./include/asm-arm/arch-你的平台 /memory.h。從下面的程式片斷,text 區段是從0x8000 offset(text_offset)開始算,中間的空白不知道有無其他用途。line 48,實際算page table 開頭位址的時候有減去0x4000,表示是從DRAM+0x8000-0x4000開始放pg table.
/* arch/arm/Makefile */
95 textofs-y := 0x00008000
152 TEXT_OFFSET := $(textofs-y)
/* include/asm-arm/arch-omap/memory.h */
40 #define PHYS_OFFSET UL(0x10000000)
/* arch/arm/kernel/head.S */
29 #define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET)
30 #define KERNEL_RAM_PADDR (PHYS_OFFSET + TEXT_OFFSET)
47 .macro pgtbl, rd
48 ldr \rd, =(KERNEL_RAM_PADDR - 0x4000)
49 .endm
得到pg table的開始位置之後,當然就是初始化囉。
line 221, 將pg table的base addr放到r0.
line 223, 將pg table的end addr放到r6.
line 224~228, 反覆地將0x0寫到pg table的區段裡頭,每個loop寫16bytes. (4x4),直到碰到end,結果就是把它全部clear成0x0.
221 mov r0, r4
222 mov r3, #0
223 add r6, r0, #0x4000
224 1: str r3, [r0], #4
225 str r3, [r0], #4
226 str r3, [r0], #4
227 str r3, [r0], #4
228 teq r0, r6
229 bne 1b
line 231, 將位址等於 r10+PROCINFO_MM_MMUFLAGS 裡頭的值放到r7。r10是proc_info的位址。proc的info data structure被定義在『./include/asm-arm/procinfo.h』,offset取得的方式用compiler的功能,以便以後新增structure的欄位的時候不需要更動程式碼。這邊的動作合起來就是讀預設要設給mmu flags的值。
231 ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
問題怎麼填值??拿出ARM的手冊,翻到MMU章節。一看發現page有很多種,還得分first level和second level。 念書時的印象,是從1st level在去查2nd level,先看怎麼設定1st level (不知以前老師有沒有亂教)
1. [31:20]存著section base addr
2. [19:2]存著mmu flags
3. [1:0]用來辨別這是存放哪種page, 有四種:
a. fault (00)
b. coarse page (01)
c. section (1st level) (10)
d. fine page (11)
4. pg tabel資料要存放到 [31:14] = translation base, [13:2] = table index , [1:0] = 0b00 的位址
來看code是怎麼設定。
line 239, 將pc的值往右shift 20次放到r6.這是取得前20高位元的資料,有點像是 1.。
line 240, 將r6往左shift 20次之後,和r7做or的動作,剛好也是 2.提到的mmu flags和1.處理好的資料做整理。所以前面兩個做完,就完成了bit[31:2]。
line 241, 將r3的資料寫到r4+(r6<<0x2)的地方去,剛好是4.提到的位址。(lucky)
239 mov r6, pc, lsr #20
240 orr r3, r7, r6, lsl #20
241 str r3, [r4, r6, lsl #2]
p.s. 用pc值剛好可以算出當前的page。
已經將pc所屬的page table entry(pte)設定好,接著繼續看
line 247, 248, 將KERNEL_START的位址往右shift 18算出pte的offset,(等於line239&241,239&241是先shift right 20然後shift left 2),並將剛剛r3的值設給pte。『!』的意思是會把address存到r0。
line 249~252, 算出KERNEL_END-1的pte位址放到r6, KERNEL_START的下一個pte的位址放到r0。r0 <= r6的話就持續對pte寫入初值的動作。但是這邊的r3有加上(0x1<<20),所以原本的section base會變成加1,目前不是很明瞭為什麼要加1,或許往後面會找到答案。
247 add r0, r4, #(KERNEL_START & 0xff000000) >> 18
248 str r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!
249 ldr r6, =(KERNEL_END - 1)
250 add r0, r0, #4
251 add r6, r4, r6, lsr #18
252 1: cmp r0, r6
253 add r3, r3, #1 << 20
254 strls r3, [r0], #4
255 bls 1b
line279,PAGE_OFFSET是規範RAM mapping完後Kernel要用的virtual address,我們將這個位址所屬的pte算出來放到r0。
line 280~283,將要 map 的physical address的方式算出來放到r6。
line 284,最後將結果存到r0所指到的pte。以上三個動作,就是做好 ram 起頭的位址一開始map,還沒做完整塊map。由於我們目前的設定方式每塊是1MB,所以這邊是將RAM開始的1MB做map。
line 327,返回,結束一開始的create page table的動作,我們可以看出其實並沒有做完整的初始化page table,所以之後應該會有其他page table的細節。
279 add r0, r4, #PAGE_OFFSET >> 18
280 orr r6, r7, #(PHYS_OFFSET & 0xff000000)
281 .if (PHYS_OFFSET & 0x00f00000)
282 orr r6, r6, #(PHYS_OFFSET & 0x00f00000)
283 .endif
284 str r6, [r0]
327 mov pc, lr
附帶一提,我們這邊省略了一些用ifdef包起來的程式碼,像是一開始會印一些output message的程式碼(line286~326)。
自create page table返回後,我們偷偷看一下接下來的程式碼,
line 99, 將switch_data擺到r13
line 101, 將enable_mmu擺到lr
line 102, 將pc改跳到r10+PROCINFO_INITFUNC的地方去
其實這邊有點玄機,switch_data和enable_mmu都是function,結果位址都只是被存起來,並沒有直接跳過去執行。因為程式碼雖然順序是 switch_data->enable_mmu->proc init function,但執行的順序會是 procinfo 的init function-> enable_mmu -> switch_data 。至於為什麼要這樣寫的原因?就不清楚了,也還沒仔細推敲過。 switch_data最後就會跳到大家都很熟悉的start_kernel().
99 ldr r13, __switch_data @ address to jump to after
100 @ mmu has been enabled
101 adr lr, __enable_mmu @ return (PIC) address
102 add pc, r10, #PROCINFO_INITFUNC
未完待續~
659 vmlinux-lds := arch/$(SRCARCH)/kernel/vmlinux.lds
打開 ./arch/arm/kernel/vmlinux.lds.S (會用來產生vmlinux.lds)我們可以發現第一個section是『.text.head』,裡頭的_stext從目前的位置開始放。於是我們曉得只要找到屬於.text.head這個section,並且是_stext這個symbol的程式碼,就是解壓縮完後的第一行程式碼。
26 .text.head : {
27 _stext = .;
28 _sinittext = .;
29 *(.text.head)
30 }
用指令搜尋一下,發現 ./arch/arm/kernel/head.S 裡頭有關鍵字(如下),結果我們從./arch/arm/boot/compressed/head.S跳到了./arch/arm/kernel/head.S 。
77 .section ".text.head", "ax"
78 .type stext, %function
79 ENTRY(stext)
80 msr cpsr_c, #PSR_F_BIT PSR_I_BIT SVC_MODE @ ensure svc mode
81 @ and irqs disabled
82 mrc p15, 0, r9, c0, c0 @ get processor id
83 bl __lookup_processor_type @ r5=procinfo r9=cpuid
84 movs r10, r5 @ invalid processor (r5=0)?
85 beq __error_p @ yes, error 'p'
86 bl __lookup_machine_type @ r5=machinfo
87 movs r8, r5 @ invalid machine (r5=0)?
88 beq __error_a @ yes, error 'a'
89 bl __vet_atags
90 bl __create_page_tables
既然找到了檔案,我們又可以開始繼續。看了一下,程式碼多了很多bl的跳躍動作,看來會在各個function跳來跳去。既然跳到真正的kernel開始跑,表示進入重頭戲,在進入kernel之前arm的平台有一些預設的狀況,也就是說arm kernel image會預設目前的cpu和系統的狀況是在某個狀態,這樣對一個剛要跑起來的OS比較決定目前要怎麼boot起來。可以看一下comment,有清楚的描述。這也幫助我們了解為什麼decompresser的程式跑完之後,還要把cache關掉。
59 /*
60 * Kernel startup entry point.
61 * ---------------------------
62 *
63 * This is normally called from the decompressor code. The requirements
64 * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
65 * r1 = machine nr, r2 = atags pointer.
基於以上的假設,當program counter指到kernel的程式的時候,就會從line 80開始跑。
line 80, msr指令會把, operand的值搬到cpsr_c裡面。這是用來確保arm cpu目前是跑在svc mode, irq&fiq都disable。(設成0x1就會disable,definition在./include/asm-arm /ptrace.h)
line 82, 讀取CPU ID到r9
line 83, 跳到 __lookup_processor_type 執行(bl會記住返回位址)。
77 .section ".text.head", "ax"
78 .type stext, %function
79 ENTRY(stext)
80 msr cpsr_c, #PSR_F_BIT PSR_I_BIT SVC_MODE @ ensure svc mode
81 @ and irqs disabled
82 mrc p15, 0, r9, c0, c0 @ get processor id
83 bl __lookup_processor_type @ r5=procinfo r9=cpuid
接著會跳到head-common.S這個檔。
line 158, (3f表示forware往前找叫做『3』label)將label 3的address放到r3。
line 159, 將r3指到的位址的資料,依序放到r7, r6, r5.(ldmda的da是要每次都位址減一次)
line l60, 161, 利用真實得到位址r3減去取得資料的位址得到一個offset值,這樣可計算出r5, r6真正應該要指到的地方。
line 163~169,這邊的程式應該不陌生,就是在各個CPU info裡面找尋對應的structure。找到的話就跳到line 171,返回head.S
line 170, 找不到的話,r5的processor id就放0x0.表示unknown id。__proc_info_xxx可以在 vmlinux.lds.S 找到,是用來包住CPU info的所有data.資料則是被定義在./arch/arm/mm/proc-xxx.S,例如arm926就有 proc-arm926.S,裡面有相對應的data宣告,compiling time的時候,這些資料會被編譯到這個區段當中。
156 .type __lookup_processor_type, %function
157 __lookup_processor_type:
158 adr r3, 3f
159 ldmda r3, {r5 - r7}
160 sub r3, r3, r7 @ get offset between virt&phys
161 add r5, r5, r3 @ convert virt addresses to
162 add r6, r6, r3 @ physical address space
163 1: ldmia r5, {r3, r4} @ value, mask
164 and r4, r4, r9 @ mask wanted bits
165 teq r3, r4
166 beq 2f
167 add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list)
168 cmp r5, r6
169 blo 1b
170 mov r5, #0 @ unknown processor
171 2: mov pc, lr
187 .long __proc_info_begin
188 .long __proc_info_end
189 3: .long . 190 .long __arch_info_begin
191 .long __arch_info_end
跳了很多檔案,建議指令和object code如何link的概念要有,就會很清楚了。
我們從 head-common.S返回之後,接著繼續看。
line 84, movs的意思是說,做mov的動作,並且將指令的S bit設起來,最後的結果也會update CPSR。這個指令執行的過程當中,會根據你要搬動的值去把N and Z flag設好。這個是有助於下個指令做check的動作。
line 85, 就是r5 = 0的話,就跳到__error_p去執行。line 86, 跳到__lookup_machine_type。原理跟剛剛找proc的資料一樣。
83 bl __lookup_processor_type @ r5=procinfo r9=cpuid
84 movs r10, r5 @ invalid processor (r5=0)?
85 beq __error_p @ yes, error 'p'
86 bl __lookup_machine_type @ r5=machinfo
看得出來跟proc很像,有個兩個小地方是
1. line 207,用ldmia不是用ldmda。原因就在於存放arch 和 proc info 的位址剛好相反。一個在lable3的上面。一個在的下方。設計上應該是可以做修改的。
2. arch定義的方式是透過macro,可以先看 ./include/asm-arm/mach/arch.h。裡頭有個 MACHINE_START ,這邊會設好macro,到時候直接使用就可以把arch的info宣告好。 例如 ./arch/arm/mach-omap1/board-generic.c
/* macro */
50 #define MACHINE_START(_type,_name) \
51 static const struct machine_desc __mach_desc_##_type \
52 __used \
53 __attribute__((__section__(".arch.info.init"))) = { \
54 .nr = MACH_TYPE_##_type, \
55 .name = _name,
56
57 #define MACHINE_END \
58 }; /* 用法 */
93 MACHINE_START(OMAP_GENERIC, "Generic OMAP1510/1610/1710")
94 /* Maintainer: Tony Lindgren
95 .phys_io = 0xfff00000,
96 .io_pg_offst = ((0xfef00000) >> 18) & 0xfffc,
97 .boot_params = 0x10000100,
98 .map_io = omap_generic_map_io,
99 .init_irq = omap_generic_init_irq,
100 .init_machine = omap_generic_init,
101 .timer = &omap_timer,
102 MACHINE_END /* func */
204 .type __lookup_machine_type, %function
205 __lookup_machine_type:
206 adr r3, 3b
207 ldmia r3, {r4, r5, r6}
208 sub r3, r3, r4 @ get offset between virt&phys
209 add r5, r5, r3 @ convert virt addresses to
210 add r6, r6, r3 @ physical address space
211 1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type
212 teq r3, r1 @ matches loader number?
213 beq 2f @ found
214 add r5, r5, #SIZEOF_MACHINE_DESC @ next machine_desc
215 cmp r5, r6
216 blo 1b
217 mov r5, #0 @ unknown machine
218 2: mov pc, lr
接著我們又返回到head.S,line 87~88也是做check動作。line 89跳到vet_atags。在head-common.S
87 movs r8, r5 @ invalid machine (r5=0)?
88 beq __error_a @ yes, error 'a'
89 bl __vet_atags
90 bl __create_page_tables
line 245, tst會去做and動作,並且update flags。這邊的用意是判斷位址是不是aligned。
line 246, 沒有aligned跳到label 1,就返回了。
line 248~250, 讀取atags所在的address裡頭的值到r5,看看是否不等於ATAG_CORE_SIZE,不等於的話也是返回。
line 251~254, 判斷一下第一個達到的atag pointer是不是等於ATAG_CORE。如果正確的話,等一下要讀取atag的資料,才會正確。(atag是由bootloader帶給linux kernel的東西,用來告知booting所需要知道的參數。例如螢幕寬度,記憶體大小等等)
14 #define ATAG_CORE 0x54410001
15 #define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)
243 .type __vet_atags, %function 244 __vet_atags:
245 tst r2, #0x3 @ aligned?
246 bne 1f
247
248 ldr r5, [r2, #0] @ is first tag ATAG_CORE?
249 subs r5, r5, #ATAG_CORE_SIZE
250 bne 1f
251 ldr r5, [r2, #4]
252 ldr r6, =ATAG_CORE
253 cmp r5, r6
254 bne 1f
255
256 mov pc, lr @ atag pointer is ok
257
258 1: mov r2, #0
259 mov pc, lr接著我們又跳回去head.S。
line 90,又跳到 __create_page_tables。 (很累人....應該會死不少腦細胞)哇!page table?!!如雷貫耳的東西,不知道會怎麼做。剛剛偷看了一下,@@還蠻長了。
由於code看起來似乎越來越難解釋,會需要在檔案之間跳來跳去。建議一些基礎的東西可以再多複習幾次(其實是在說我自己 ),閱讀source code上收穫會比較多。例如:
1. arm instruction set - 這個最好每個指令的意思都大略看過一次,行有餘力多看幾個版本,armv4, v5 or v6。
2. compiler & assembler & linker - toolchain工具做的事情和功能,大致的流程和功能要有概念。
3. Makefile & link script - 這兩個功能和撰寫的方式要有簡單的概念。
以上1是非常重要的重點,2&3只要有大致上的概念就可以,因為trace code的時候,有時需要跳到Makeflie&Link script去看最後object code編排的位址。由於我們trace到了page table這個關鍵字,在開始之前,稍微簡短的解釋,為了幫助了解,儘量用易懂的概念講,有些用詞可能會和一些真實狀況不同,但是懂了之後,應該會有能力分辨,至於正式而學術上解說,很多書上應該都有,或是google一下就很多啦~
page table本身是很抽象的東西,尤其是對所謂的 user-mode application programmer 來說,大部分的狀況它是不需要被考慮到的部份,但是他的產生卻對os和driver帶來了很多影響。我們從一個小小的疑問出發。『產生page table到底是要給誰用的?』其實真正作用在它上面的H/W是MMU,一旦CPU啟用了MMU,當cpu嘗試去記憶體讀取一個operand的時候,cpu內部打出去的位址訊號都會先送到mmu,mmu會拿著這個位址去對照page table,看看這個位址是不是被轉換到另外一個位置(還會確認讀寫權限),最後才會到真正的位址去讀寫。這樣來看CPU其實一開始打出去的位址訊號其實不是最後可以拿到資料的位址,我們稱為virtual address(va),mmu打出來的位址才能真正拿到資料,所以我們把mmu打出去的位址稱為physical address(pa)。那用來查詢這個位址對照關係的表格,就是page table。到這邊我們有一個簡單的概念。來想像一個小問題,一個普通的周邊(例如你的顯示卡),因為他並不具有MMU功能,hw只看得懂pa,但是CPU卻是作用在va,假如我想去對hw的控制暫存器做讀寫,到底要用va還是pa?
這時,寫driver的人就必須要小心,設定給硬體看的位址必須要先轉成pa(像是設定DMA),給CPU執行的程式碼要使用va,這對一開始嘗試寫driver但是又不瞭解va pa之間的差別的人,常常產生很多疑問。
現在我們回頭想想OS,既然我們跑到create page table,可以預期的是他想要將MMU打開,因此希望預先建立起一個page table,讓MMU知道目前os想規劃的位址對應和讀取權限是如何被安排的。所以os必須考慮到現在的系統究竟是長怎樣?應該要如何被安排?是不是有那些要被保護?好吧~因為我們完全對os不了解,也不知道該安排什麼,只能祈禱在trace code完後,可以找到這些問題的答案,或者發現一些沒想到的問題。知道了page table的大致上的功能,下篇就可以專心的研究這個table的長相,和它想規劃出的系統模樣。
現在,讓我們跳入create_page_tables吧~
216 pgtbl r4 @ page table address
line 216,會跳到pgtbl的macro去執行。
其實只是載入一個位址。只是這個位址會因為你想將哪個位址開始的記憶體留給kernel使用,定一就會不同,理論上是可以改變的。一般會定義在./include/asm-arm/arch-你的平台 /memory.h。從下面的程式片斷,text 區段是從0x8000 offset(text_offset)開始算,中間的空白不知道有無其他用途。line 48,實際算page table 開頭位址的時候有減去0x4000,表示是從DRAM+0x8000-0x4000開始放pg table.
/* arch/arm/Makefile */
95 textofs-y := 0x00008000
152 TEXT_OFFSET := $(textofs-y)
/* include/asm-arm/arch-omap/memory.h */
40 #define PHYS_OFFSET UL(0x10000000)
/* arch/arm/kernel/head.S */
29 #define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET)
30 #define KERNEL_RAM_PADDR (PHYS_OFFSET + TEXT_OFFSET)
47 .macro pgtbl, rd
48 ldr \rd, =(KERNEL_RAM_PADDR - 0x4000)
49 .endm
得到pg table的開始位置之後,當然就是初始化囉。
line 221, 將pg table的base addr放到r0.
line 223, 將pg table的end addr放到r6.
line 224~228, 反覆地將0x0寫到pg table的區段裡頭,每個loop寫16bytes. (4x4),直到碰到end,結果就是把它全部clear成0x0.
221 mov r0, r4
222 mov r3, #0
223 add r6, r0, #0x4000
224 1: str r3, [r0], #4
225 str r3, [r0], #4
226 str r3, [r0], #4
227 str r3, [r0], #4
228 teq r0, r6
229 bne 1b
line 231, 將位址等於 r10+PROCINFO_MM_MMUFLAGS 裡頭的值放到r7。r10是proc_info的位址。proc的info data structure被定義在『./include/asm-arm/procinfo.h』,offset取得的方式用compiler的功能,以便以後新增structure的欄位的時候不需要更動程式碼。這邊的動作合起來就是讀預設要設給mmu flags的值。
231 ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
問題怎麼填值??拿出ARM的手冊,翻到MMU章節。一看發現page有很多種,還得分first level和second level。 念書時的印象,是從1st level在去查2nd level,先看怎麼設定1st level (不知以前老師有沒有亂教)
1. [31:20]存著section base addr
2. [19:2]存著mmu flags
3. [1:0]用來辨別這是存放哪種page, 有四種:
a. fault (00)
b. coarse page (01)
c. section (1st level) (10)
d. fine page (11)
4. pg tabel資料要存放到 [31:14] = translation base, [13:2] = table index , [1:0] = 0b00 的位址
來看code是怎麼設定。
line 239, 將pc的值往右shift 20次放到r6.這是取得前20高位元的資料,有點像是 1.。
line 240, 將r6往左shift 20次之後,和r7做or的動作,剛好也是 2.提到的mmu flags和1.處理好的資料做整理。所以前面兩個做完,就完成了bit[31:2]。
line 241, 將r3的資料寫到r4+(r6<<0x2)的地方去,剛好是4.提到的位址。(lucky)
239 mov r6, pc, lsr #20
240 orr r3, r7, r6, lsl #20
241 str r3, [r4, r6, lsl #2]
p.s. 用pc值剛好可以算出當前的page。
已經將pc所屬的page table entry(pte)設定好,接著繼續看
line 247, 248, 將KERNEL_START的位址往右shift 18算出pte的offset,(等於line239&241,239&241是先shift right 20然後shift left 2),並將剛剛r3的值設給pte。『!』的意思是會把address存到r0。
line 249~252, 算出KERNEL_END-1的pte位址放到r6, KERNEL_START的下一個pte的位址放到r0。r0 <= r6的話就持續對pte寫入初值的動作。但是這邊的r3有加上(0x1<<20),所以原本的section base會變成加1,目前不是很明瞭為什麼要加1,或許往後面會找到答案。
247 add r0, r4, #(KERNEL_START & 0xff000000) >> 18
248 str r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!
249 ldr r6, =(KERNEL_END - 1)
250 add r0, r0, #4
251 add r6, r4, r6, lsr #18
252 1: cmp r0, r6
253 add r3, r3, #1 << 20
254 strls r3, [r0], #4
255 bls 1b
line279,PAGE_OFFSET是規範RAM mapping完後Kernel要用的virtual address,我們將這個位址所屬的pte算出來放到r0。
line 280~283,將要 map 的physical address的方式算出來放到r6。
line 284,最後將結果存到r0所指到的pte。以上三個動作,就是做好 ram 起頭的位址一開始map,還沒做完整塊map。由於我們目前的設定方式每塊是1MB,所以這邊是將RAM開始的1MB做map。
line 327,返回,結束一開始的create page table的動作,我們可以看出其實並沒有做完整的初始化page table,所以之後應該會有其他page table的細節。
279 add r0, r4, #PAGE_OFFSET >> 18
280 orr r6, r7, #(PHYS_OFFSET & 0xff000000)
281 .if (PHYS_OFFSET & 0x00f00000)
282 orr r6, r6, #(PHYS_OFFSET & 0x00f00000)
283 .endif
284 str r6, [r0]
327 mov pc, lr
附帶一提,我們這邊省略了一些用ifdef包起來的程式碼,像是一開始會印一些output message的程式碼(line286~326)。
自create page table返回後,我們偷偷看一下接下來的程式碼,
line 99, 將switch_data擺到r13
line 101, 將enable_mmu擺到lr
line 102, 將pc改跳到r10+PROCINFO_INITFUNC的地方去
其實這邊有點玄機,switch_data和enable_mmu都是function,結果位址都只是被存起來,並沒有直接跳過去執行。因為程式碼雖然順序是 switch_data->enable_mmu->proc init function,但執行的順序會是 procinfo 的init function-> enable_mmu -> switch_data 。至於為什麼要這樣寫的原因?就不清楚了,也還沒仔細推敲過。 switch_data最後就會跳到大家都很熟悉的start_kernel().
99 ldr r13, __switch_data @ address to jump to after
100 @ mmu has been enabled
101 adr lr, __enable_mmu @ return (PIC) address
102 add pc, r10, #PROCINFO_INITFUNC
未完待續~
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?
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包進來。
以下的文章一些已經在某些討論區貼過,重新編排之後再貼重貼到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包進來。
訂閱:
文章 (Atom)