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

未完待續~

沒有留言:

張貼留言

搜尋此網誌