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不那麼相關的軟體部分。

2009年7月3日 星期五

CPU exception vector 淺談

因為再深入我也不懂,所以就淺談吧~
有錯請指正~

話說,身為一個程式設計人員,常常遇到一些程式跑著跑著就當掉的狀況也很合理的。
例如:『segmentation fault』。簡單講就是程式寫錯,然後它就當了。

大家也應該都聽過,『開機之後CPU就會到固定位置去拿指令,然後開始跑,通
常這個位置都是放BIOS,而這個位置通常是0』。

那這個第一行指令究竟是長怎樣呢?這跟程式當掉有什麼關係?跟程式碼寫錯
又有什麼關係?

我們來看通常一般冷開機之後開始跑的程式碼都是怎樣的
( 通常就是跳到bootload or BIOS )

b start
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq
ldr pc, _fiq


上面是組語,我們只看兩行,第一行是『b start』,『b』的意思就是branch的
意思,也就是jump到start這個地方開始執行。換句換說,CPU拿到手的第一個
指令就是要它去找別人,顯然是相當不負責任的行為。讓人不禁懷疑那第二行第
三行到底是幹麼的。

我們接著看『ldr pc, _undefined_instruction』,『ldr』的意思是說,將
_undefined_instruction這個位址,放到pc這個暫存器當中。大家一定也聽過
program counter這個東西,一個正常的CPU就是靠program counter所指
的位置去抓指令進來執行。換句話說,第二行程式居然也是要CPU跳到別的地方
執行。而且前面七八行都是做這件事情。很怪吧?!!!

到這裡我們解答了上面第一個疑問,第一行程式其實就是一個Jump的指令,跳到
真正的開機程序。

看了程式碼之後,我們不禁好奇這些看起來排列很有規則的程式碼到底是做啥用
的?其實答案就是『exception vector』。

什麼是exception vector呢?其實就是CPU出錯了(其實不完全是出錯),它就會
來這個vector找看相對應的處理函式,例如遇到看不懂得指令,他就固定會抓0x4
位址的指令,也就是『ldr pc, _undefined_instruction』,接著就會跳到處理undefined instruction的函式去處理。

到這邊我們回答了第二個問題,程式當掉的時候,就是會跑來這個地方,接著再
Jump到處理的程式碼。同時我們也發現,一個CPU就是靠著這個vector在處理
各種不同的exception。那....萬一這些vector被改掉了呢?
例如:你程式碼拿到一個null pointer,然後又對這個null pointer開始做寫入動
作。

char *ptr = NULL;
memcpy( ptr, 0x0, 10 );

那整個OS就完全不能動了。可是我們也知道我們現在這樣寫,頂多 segmentation
fault,並不會怎樣,OS還是照樣活著。why why why?

原因很簡單,就是這些區段被一些方法保護住了。一般常見的有兩種方法,一種是
MMU,一種是cpu有兩個exception vector存上的位址。

MMU可以設定某個區段的存取權力,只能readable的話,那就不會有被改掉的問
題,另外一個方式是可以將exception vector的位址擺到不容易改到的地方,通常
是往高位址的地方擺(high vector address),例如,0xffff0000接近4GB的位
置。(32bit 最多只能定址4GB)

第二種方式,通常是一開始開機vector還是從0x0那邊抓,可是經過設定CPU,可
以改變,在不斷電的狀態下,發生exception就會跳到高位位址去找exception
vector。

p.s. 關於定址模式可能有些人沒感覺,可能要懂整個CPU的memory map之後,
會比較有感覺。

p.s. 眼尖的人一定覺得很怪,為什麼vector第一行用『b』,其它卻是用『ldr』

上面貼文本來的用意是要點出,為什麼一個CPU需要有擺放兩個exception vector的
功能。如果有看懂的話,答案很簡單,就是怕被程式人員亂改改到了,這樣會讓整個OS
當掉,很容易一個null pointer亂填,整個系統就掛了。可是一開始不知道怎麼帶出這
麼問題,所以文章就變長了。恰巧,也提到一些其他的東西,我們就稍微聊一下CPU
的模式吧。

從 exception vector 看得出來,CPU本身除了拿到指令一直run以外,還會接受一些
exception發生,發生之後呢,便會到這個vector來拿相對應處理的指令。

舉例來說,一個程式碼被compile成使用某個硬體的特殊指令,可是卻剛好被放到沒支
援這個指令的 CPU 上執行,那CPU跑著跑著發現一個自己看不懂得指令,就會發出一
個undefined instruction 的 exception,exception發出後,依照CPU的設計,以
ARM來說,就會抓位址在0x4地方的指令開始執行。

而這時候,我們的處理函式就有機會可以透過這個exception hanlder去模擬這個指令
,利用多個指令或運算計算出那個特殊硬體指令的值。這是其中一種undefined
instruction exception 的應用。

另外,我們要提的是,這許多的exception其實隱含著CPU的運行模式,例如我們以前
常聽到x86 CPU切到什麼保護模式之類的,其實就是因為發生了一些exception,為了
處理這些exception,所以切到相對應的模式去做處理。

切換模式聽起來很單純,其實對實作OS來說,牽涉到的事情還不少,例如切換模式之
後,為了要讓處理完exception之後,原本的程式能夠繼續執行,相對的exception
handler必須將目前狀態存起來,所以就必須實現stack來存放目前的register status
(context),以便將來可以resume回來。

看到這邊,應該有人會問想說,了解exception vector的意義何在?當你要了解如何
實做一個OS的時候,其實就是要去控制CPU,而CPU的運作,其實就是控制這些模
式。而exception vector就是這些模式的進入點。當你對一個OS可以有透徹的了解
的時候,對於系統表現出來的行為,會有更新的了解和解釋,對於debug application
和 driver 其實是有幫助的。

對於各個cpu mode的說明,例如一些有趣的問題:有哪些模式?每個模式有什麼不
同?它是專門處理那些問題?有哪些種應用?這些因為要牽涉到很多assembly的程
式片段,所以就先不深入探討,有興趣的朋友可以一起討論。

從system bus來看系統

有些基礎不夠深厚 遣詞用句也不是很精確以下試圖以自己的體認和觀點 以簡單的辭彙去解釋一些processor與系統演進的方式,如果有誤或是清楚的地方,歡迎大家討論

[本文開始]
在學校學computer architecture或者做一些簡單的單晶片介面卡實驗課程的時候
常常會被一下子多出來的專有名詞搞混像是DMA, memory, cache, mmu, virtual address, physical address 等等,尤其大家一開始接觸電腦都是已經發展很久的x86系統,即使有單晶片系統的實驗課程也很難將單晶片的經驗運用到x86或是已經較為複雜的CPU系統上,因為CPU上頭又開始加入了越來越複雜的作業系統,像是作業系統常提到的kernel mode和user mode,常常跟上述的專有名詞physical address or virtural address交互使用。例如:刻板印象的『作業系統處於kernel mode時,使用physical address』經常讓一剛要學習撰寫driver或是OS的軟體人員產生非常多的疑問,那應該怎麼簡單來看待一個系統?

首先,我們回歸到CPU這個字的字義上,我們稱呼它為『處理器』他是用來處理各種『資料』那麼這些資料又放在哪邊?他是如何去存取這些資料?通常可以被處理的資料都在memory當中,CPU透過所謂的 bus 去存取memory到目前為止,我們得到一個簡單的系統

CPU + bus + memory

接著,我們用兩個角度去看這個簡單系統的發展

1. 硬體加速
2. 作業系統

我們先從硬體加速的觀點來看這個系統如何改進:

首先,硬體加速通常就是把某種特殊格式的資料準備好 (例如電影)放在某個地方,然後請你設計好的加速硬體去處理這些資料,可以看得出來,這些資料勢必會被放在某塊地方,讓硬體可以直接讀取。這個想法造就了 DMA ,也讓系統也開始複雜化,因為這樣一來,同時之間會有兩個東西去存取記憶體。所以bus的設計上變得複雜,必須處理DMA和CPU同時發出request 的狀況,這邊bus變成需要多了arbiter的功能,以便決定目前誰是master可以存取memory。

從另外一個角度來說,她們會互搶資料,也就是你加入越多DMA,CPU那邊的效率有可能更差,因為他要不到資料,必須等待,但另一方面他多出了一些時間可以處理其他資料,而將一些固定特殊的工作交給加速硬體去完成。所以系統改變成:

CPU + arbiter bus + memory + DMA

接著我們從作業系統的角度來看:

一開始的作業系統沒有分 kernel mode 和 user mode,也沒有出現virtual address,只有一種模式和一種定址方式。可是這樣會因為程式撰寫錯誤,直接改寫到OS所處記憶體位置,變成整個系統都不能工作。例如:

OS被載入到 memory 0 的開頭位置,某個程式寫錯資料,把資料也寫到0的位置,這樣原本的OS就被亂改到,萬一程式跑到這邊系統可能就掛掉了。面對這樣的問題,發展了user mode和 kernel mode,並藉由硬體的保護,使得user mode下不能直接存取某塊被保護的memory區塊,這個硬體就是現在的MPU。到目前為止我們可以看出來整個系統更加複雜了,CPU必須多一些模式,以便支持作業系統的kernel mode和user mode,kernel mode可以去改寫MPU設定要保護的memory區段,等到設定好了,切換成user mode,這樣 user program 就可以安心的使用記憶體,也不擔心惡意程式去搞壞OS。所以我們的系統變成了

multi-mode CPU + MPU + arbiter bus + memory + DMA

接著我們看 virtual address 和 physical address 的誕生,這邊相當抽象,需要一些想像。

隨著時代進步,越來越多user program產生,user mode的program目前雖然已經不會因為隨意的存取記憶體,造成OS掛點,可是還是有可能去改到別人的記憶體,因為每個程式共同擁有並使用同一個記憶體和所謂的記憶體空間。例如:

程式A放在0~128 KBytes,程式B放在512~768KBytes,程式A寫錯位置到B程式的地方,變成程式B毀損,B程式就掛掉了。

上述的記憶體使用情況,不僅給programmer帶來困擾,而且變成記憶體必須被事先決定好誰只能用哪邊?只能使用多大?這樣不但得事先規劃,對於記憶體的使用也不是很高,因此MMU的硬體便被提出來。簡單來看,MMU記錄著某個虛擬位置所對應到的實體記憶體的位置。作業系統利用這個特性,為每一個user program先做了一個假的記憶體,每個程式都以為他有很大很大的記憶體空間,其實當程式呼叫malloc的時候,才會去設置MMU將這個user program的假記憶體空間和真實記憶體做個連結。這邊很難解釋清楚,其實每個程式都有一個假的記憶體空間的紀錄表格,當OS做context switch到某個program的時候,MMU也會跟著切到這個table上,以便用到正確的紀錄表格。例如:

程式A的表格記錄了他配置0~4K,而這4K對應到真實記憶體的128K~132K的區段。
程式B的表格記錄了他配置0~4K,而這4K對應到真實記憶體的132K~136K的區段。

雖然兩個程式自己都以為他是用到0~4K,可是其實真實記憶體所配到的位置不同,作業系統在切換的時候,會同步將虛擬記憶體的紀錄表格做切換,以便使用到正確的對應表。以上是一種例子,各種OS可能採取的設計方式不同,但是概念大多是一樣的,我們可以看得出來,對每個程式來說,它可以有擁有許多假的連續記憶體空間,對程式撰寫,還有記憶體使用率來說,很有效果。到此,CPU多了MMU,現在大多包含MPU的功能。

muti-mode CPU + MMU + arbiter bus + memory + DMA

因此OS在kernel mode的時候,如果MMU是打開的,這時候還是使用virtual address。要讓mmu正常工作,必須要設定,要讓他知道你對位址的規劃是如何。例如:

RAM被硬體人員安排到0xFF00 0000的位址,可是你希望RAM被當成從0x0開始,你就可以設定MMU將 0xFF00 0000 對應到 0x0(有些CPU會提供特別開機時特別的remap功能,將ram map到0x0開始),這樣一來你讀取0x0就會自動轉到0xFF00 0000那邊去。0x0000 0000 --<>-- 0xFF00 0000作業系統一開始就會初始化這些位址轉換對應的資訊,把它放在所謂的page table裡面,然後把table位址交給MMU,MMU就會依照table的資訊工作。假如你今天有兩張table,兩張table設定不一樣,MMU只要在這兩張table之間切換,那記憶體對應的方式也會跟著換。這是一個很重要的想法。假如,我每一個process都有自己的 page table,切換process的時候,table也跟著切換,這樣我可以控制每一個process去存取記憶體的方式。假如我是讓所有的process 都共用一個 table,這樣這個table的設定值,就會影響到所有的process。共用一個table表示每個process用同一個位址去讀寫,透過MMU轉換到的位址是相同的,會讀寫到同一塊。不共用的話,表示processA和processB即使拿同一個位址0x8000 0000去讀寫,可能最後對應到的位址是不同的。

但是這樣的差別就是有什麼好處??

因為感覺上複雜化了整個系統,本來我可以很直覺的拿processA傳給我的位址,去讀取它為我準備的資料,現在我可能會因為我使用的 table 不同,就不能在其他process直接使用。感覺很麻煩,要是大家都在同一個 table 工作,不是很簡單嗎?位址空間都是一樣的。why?!

一、如果大家都在同一個page table上

因為彼此用的記憶體應對方式是一樣的所以processA要了某個區段 0x0~0x100這樣processB就不能使用,很可能跑了一段時間之後整個系統的memory fragment就會變多,對記憶體使用率來講不好,很可能要一次找到大塊的不好找,雖然virtual address有4G但是process分一分還是可能不太夠

二、processB可以看到processA的address space

表示我可以拿到相對應的資源這樣惡意程式很容易就得到一些記憶體上的個人資料有些地方的說明不太容易,太簡略容易造成誤解,贅述又怕不容易理解。沒表達清楚的地方歡迎提出來討論。

Linux 下的 mmap()

以下希望藉由一個實際可以在user mode運作的API, mmap()讓programmer能夠感受到MMU這個硬體在系統中所扮演的角色

寫linux底下driver的人常常會看到這個東西 mmap()
在user mode program裡面假如你用open( "/dev/xxx", ... )去打開一個檔案系統的節點
就可以用這個file descriptor的handler對他做mmap()的動作
那這個mmap究竟背後藏了哪些意義?又有哪些硬體在工作才能達成?

mmap的字面意義是memory map顧名思義就是『記憶體映對』簡單來看,就是用mmap()幫你做對映
對映好了,對著傳回的address作存取就等於對檔案作存取

1. 首先來看mmap()一般是怎麼被使用的 (這邊可以先不用管要傳什麼參數)

int fd, mapSize, offset, start;
char* ptr;mapSize = 0x1000; /* 希望映對多大的區塊 */

offset = 0;start = 0;

/* 打開檔案 */
fd = open( "/home/tester/a.txt", O_RDWR O_SYNC );

/* 作mmap動作,取得一個對應好的address */
ptr = mmap( 0, map_size, PROT_READPROT_WRITE, MAP_SHARED, fd, offset );

假如一切順利的話 ptr 就會接到一個address,這個address會對應到a.txt這個檔案所在的起始位置,如果這時候我們用 strcpy( ptr, "hello!!" );a.txt裡面就會被寫入"hello!!"的字串

2. 假如我寫了兩個程式,都是用mmap()到同一個a.txt,程式會不會出問題? 如果不會,那我既然對同一個檔案作mmap(),那我拿到的ptr不是應該是相同的address?

答案是:兩個程式可以正常執行,但是這兩個回傳的address不一定相同。why?

為什麼對同一個檔案寫入,也都做同樣的mmap()動作,甚至傳的參數值都相同。為什麼拿到的address可以是不同的? 更奇怪的是,位址不同,還是同樣寫到同一個檔案上頭。假如這一切都是合理的,那表示雖然這兩個位址不一定相同,其實都是對映同一個地方,表示有某種東西記錄著這個對應關係。而且,兩個program的ptr有時候會一樣,可是有時候又不一樣。唯一個可能是表示兩個process各自保有這些映對的方式。

3. 假設我只寫一個程式,但是mmap()兩次到同一個檔案上呢? 得到的兩個ptr會一樣嗎?答案是:這個兩ptr還是不一樣的,兩個不同program 跑出來的mmap()結果不同也就算了,同一個program呼叫兩次mmap()跑出來的ptr也不一樣,但又都對映著相同的檔案a.txt。因此我們又可以猜測這一個mmap()是動態的,動態去產生一種應對的方式,將傳回的ptr對應到真正的檔案a.txt去,所以同一個process可以對應好幾次,好幾次都用不同的address去對映到相同的檔案上去。

綜合上面三種現象,我們合理的懷疑系統裡面存在了一個東西,它讓每一個process可以動態地記錄address的對應關係。並且,每個process各自擁有這個table,而這些對映的關係會動態的反應在這個table當中。回想一下一個系統有誰會做這種工作,不就是MMU所擔任的重要角色嗎?每個process各自擁有自己的 page table 當他呼叫mmap()的時候,系統就動態地幫她在這個table上寫上紀錄著

[processA-pgtable]
0x00008000 ptr1 --> a.txt
0xa0008000 ptr2 --> a.txt

[processB-pgtable]
0x00008000 ptr1 --> a.txt
0xb0008000 ptr2 --> a.txt

這樣ptr1, ptr2都可以對應到相同的地方,又因為各個process又有不同table,所以有時候ptr1有可能會相同。如此一來,我們終於可以合理的解釋我們觀察mmap()為何對映出來的address會有如此的表現。原來就是MMU被加入,而且OS又被設計成每個process都會各自擁有一個page table來記錄他對記憶體如何解讀。

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

未完待續~

搜尋此網誌