之前貼了一些自己 trace linux kernel 的一些筆記文,筆記
式的文章難免寫得比較瑣碎,閱讀起來應該比較不容易,尤其逐
行trace,應該會有人很排斥或是不認同這樣的一種方式,之前
曾經看到一些文章甚至會明白地提醒讀者切記不要逐行研究程式
碼,要將其觀念記住才是重點。
這邊想針對這樣的論點提一些想法給大家參考,目的不是要訂出
一個好與壞,其實我覺得兩種方法並不相衝突,而是大家在學習
的階段,可以依照理解程度來做取捨。
以自己的經驗來說,以前一開始囫圇吞棗,試圖去理解書上提到
的觀念,似懂非懂的記了許多東西,但是往往人家問我:
『你能夠自己寫出一個OS或是其中一部分功能嗎?』
似乎就變得很心虛,只能告訴對方『我知道它的實作原理』,但
是說要自己要寫,好像就是少了點什麼? 好像懂,但是要怎麼真
正的寫出來,卻是不怎麼敢肯定。
面對這樣的狀況持續一段時間,讓人真正有自信能夠依樣畫葫蘆
弄出個什麼東西,卻是在花心思從很基本的instruction set
開始K和逐行逐行了解之後,才覺得似乎概念和實作有了那麼一
點連接。也由於這樣的基礎,有時候有助於資料不足的狀況下,
還能夠經由看程式碼來補足資料不足的部份,甚至可以用來印
證自己的想法。
這樣的說起來多看程式的好處多多囉?
好像也不盡然,自己的經驗是,看上老半天,一大段雖然每個字
都看得懂,但是兜起來就是不曉得他要做啥用? (看英文的時候
....恩...好像也是這樣 )有時回頭翻書,看看觀念,才會發
現這一段天書似的程式碼所隱含的意義,自然就理解了。
所以後來想想要深入kernel source的方式,似乎得要雙管齊
下,一邊看觀念,一邊找出相對應的程式碼出來,最後觀念與觀
念之間,必定有一些很細節的部份沒有被提到,玩家就得自己想
辦法將他們串起來,一旦書本上的觀念可以在實際程式上得到印
證,那這樣改天要改寫,也就遊刃有餘。
這邊想特別提到的還是閱讀指令集的重要性,對指令的熟悉,不
但對閱讀低階的程式碼有很大的幫助,還對於整個系統演進了解
更為透徹。以前會覺得了解太低階的東西用處不大,後來才覺得
這些部分有時卻影響很大。
以上無聊閒談,歡迎大家提出自己的經驗,交流一下~
2009年12月20日 星期日
2009年12月11日 星期五
Platform Driver~
當我們在重新規劃新的system or hardware platform的時候
常用的作法是拿原廠的reference design當參考
上頭component常常會換掉
舊的平台和新的平台之間
常常某些部份只是位址的更動
例如:本來GPU的base address是0x80001000換到0x80002000
hardware的功能和行為是一樣的
面對的這樣的問題,在舊一點的Linux Kernel我們通常可以用
1) dynamical insert module 的方式將新的 base address 傳給 driver
或者
2) 更改 driver 的 code 直接使用新的 0x80002000 的 base address
在新一點的Linux的kernel導入的所謂的platform driver的概念
概念上我們將某一些和platform相關的資訊放在platform的level (arch/arm/mach-xxx/)
原本的driver放在./drivers/
這樣一來只要選擇到對的platform,相對應的資訊就會pass給driver
如此一來對developer來講就變得比較直覺
driver本身只要針對不同的設定去動作
不需要因為平台改變了
就改寫driver
這樣的機制對於生產SoC的vendor相當便利
因為不同的SoC經常需要對base address做調整
不同的SoC只要從platform level傳遞變更的資訊給driver
新的平台就可以得到driver的支援
而對下游的廠商來說
他們編譯新的kernel時,只要選擇對的platform
就可以得到對的結果
由以上的介紹
我們可以得知platform driver需要透過兩個部分的支援
1) platform level - 提供硬體平台資訊,例如irq line, base address, etc。
2) driver level - 控制硬體平台和實現功能。
首先,概念上我們必須對系統註冊一個新的platform device,使用的方式就是呼叫
platform_device_register( information ); 將所有platform用到的device和相關的資
訊註冊。
接著drvier level這邊,我們會使用 platform_driver_register()將driver註冊到系統
裡頭,假如有任何platform device指定了這個driver,就可以將driver初始化,請它
來服務這個device。
明眼人應該不難發現
新的機制將hardware information和功能實作的分開了
資訊放到platform level規劃
將driver功能實作獨立出來
(其實有點像是C++導入了template的概念,將資料型別和演算法分開)
常用的作法是拿原廠的reference design當參考
上頭component常常會換掉
舊的平台和新的平台之間
常常某些部份只是位址的更動
例如:本來GPU的base address是0x80001000換到0x80002000
hardware的功能和行為是一樣的
面對的這樣的問題,在舊一點的Linux Kernel我們通常可以用
1) dynamical insert module 的方式將新的 base address 傳給 driver
或者
2) 更改 driver 的 code 直接使用新的 0x80002000 的 base address
在新一點的Linux的kernel導入的所謂的platform driver的概念
概念上我們將某一些和platform相關的資訊放在platform的level (arch/arm/mach-xxx/)
原本的driver放在./drivers/
這樣一來只要選擇到對的platform,相對應的資訊就會pass給driver
如此一來對developer來講就變得比較直覺
driver本身只要針對不同的設定去動作
不需要因為平台改變了
就改寫driver
這樣的機制對於生產SoC的vendor相當便利
因為不同的SoC經常需要對base address做調整
不同的SoC只要從platform level傳遞變更的資訊給driver
新的平台就可以得到driver的支援
而對下游的廠商來說
他們編譯新的kernel時,只要選擇對的platform
就可以得到對的結果
由以上的介紹
我們可以得知platform driver需要透過兩個部分的支援
1) platform level - 提供硬體平台資訊,例如irq line, base address, etc。
2) driver level - 控制硬體平台和實現功能。
首先,概念上我們必須對系統註冊一個新的platform device,使用的方式就是呼叫
platform_device_register( information ); 將所有platform用到的device和相關的資
訊註冊。
接著drvier level這邊,我們會使用 platform_driver_register()將driver註冊到系統
裡頭,假如有任何platform device指定了這個driver,就可以將driver初始化,請它
來服務這個device。
明眼人應該不難發現
新的機制將hardware information和功能實作的分開了
資訊放到platform level規劃
將driver功能實作獨立出來
(其實有點像是C++導入了template的概念,將資料型別和演算法分開)
2009年12月7日 星期一
Cache, Write Buffer 與Device效能~
在很早以前的CPU是很單純的,固定從某一個地方去讀取下一道指令,照著指令要它做什麼就做什麼,隨著電子電路的進步,CPU和周邊的速度出現了極大的差距,CPU常常需要花時間在等待資料讀取或是寫入的動作而不是執行真正的運算工作,因此新的CPU架構:
1) 在CPU和存取的實際位址之間加入了Cache的機制
2) 在資料寫入的過程中加入 Write Buffer 來平衡兩邊的速度差異
Cache的好處是讓CPU可以有一個快速的記憶體減少等待的時間,缺點就是這塊叫做Cache的記憶體比較小,無法容納整個RAM裡頭的資料。而Write Buffer則是用來緩衝Cache和RAM之間的速度差異,而且在較新的CPU當中,還可以設定write-combine的屬性,讓連續位址存取,可以只透過一次的讀寫就達成,大大增加效率。
只是以上這些,究竟對一個撰寫device driver的工程師有什麼好處,因為提到CPU變快變好,好像只會讓一般軟體變快,driver應該不需要特別修改,也不會比較快速。
其實這幾項變革,對撰寫driver來說是有需要特別注意的小地方,而且對某些種類的driver大大有益處。
首先是Cache,CPU會把要寫入的資料先寫在Cache,而不是直接到要寫的位址上,這樣CPU等待時間很少,可是實際上要寫driver的時候,可能因為是要對device上的控制暫存器作讀寫,卻變成被cache住,動作沒有真正及時的被執行,造成程式人員以為已經寫入,實際上是寫到cache裡頭去了。因此,撰寫driver的時候首先要注意某些存取區段需要用non-cache的屬性。若是非得要用cache,那device就得考慮到cache和device/RAM的資料同步的問題。
再來是write-combine的屬性,利用現在記憶體的特性,將連續位址存取的命令合成一次,這對處理multimedia data的hardware相當有用,效能可以提升30%以上。
以上簡單的介紹對一些撰寫driver時,因為CPU不同特性造成device driver需要修改或是效能不同的現象和簡單原理,其他其實還有像是某些連接bus的device(像是pci/agp/pcie),其實也有一些需要注意的地方,了解來龍去脈,寫出來的driver才會大大加快。
1) 在CPU和存取的實際位址之間加入了Cache的機制
2) 在資料寫入的過程中加入 Write Buffer 來平衡兩邊的速度差異
Cache的好處是讓CPU可以有一個快速的記憶體減少等待的時間,缺點就是這塊叫做Cache的記憶體比較小,無法容納整個RAM裡頭的資料。而Write Buffer則是用來緩衝Cache和RAM之間的速度差異,而且在較新的CPU當中,還可以設定write-combine的屬性,讓連續位址存取,可以只透過一次的讀寫就達成,大大增加效率。
只是以上這些,究竟對一個撰寫device driver的工程師有什麼好處,因為提到CPU變快變好,好像只會讓一般軟體變快,driver應該不需要特別修改,也不會比較快速。
其實這幾項變革,對撰寫driver來說是有需要特別注意的小地方,而且對某些種類的driver大大有益處。
首先是Cache,CPU會把要寫入的資料先寫在Cache,而不是直接到要寫的位址上,這樣CPU等待時間很少,可是實際上要寫driver的時候,可能因為是要對device上的控制暫存器作讀寫,卻變成被cache住,動作沒有真正及時的被執行,造成程式人員以為已經寫入,實際上是寫到cache裡頭去了。因此,撰寫driver的時候首先要注意某些存取區段需要用non-cache的屬性。若是非得要用cache,那device就得考慮到cache和device/RAM的資料同步的問題。
再來是write-combine的屬性,利用現在記憶體的特性,將連續位址存取的命令合成一次,這對處理multimedia data的hardware相當有用,效能可以提升30%以上。
以上簡單的介紹對一些撰寫driver時,因為CPU不同特性造成device driver需要修改或是效能不同的現象和簡單原理,其他其實還有像是某些連接bus的device(像是pci/agp/pcie),其實也有一些需要注意的地方,了解來龍去脈,寫出來的driver才會大大加快。
2009年11月26日 星期四
Before bootloader~
最近相當忙,Blog也荒廢了好一陣子,利用一點時間上來澆澆水
如同標題所提示的
一個 ARM 的系統在bootloader之前究竟有沒有事情可以做?
首先,一個系統必須先上電之後才會跑
那麼第一個跑到的程式碼是什麼?
刻板印象上,聽說CPU會跑去一個固定的位置去抓第一道指令,這個位置通常是一
個flash ROM裡頭放著bootloader,bootloader被執行之後,就會自己將kernel
載入,bla..bla..bla.....最後完成開機。(以上的印象在wince or Linux皆適用。)
那麼before bootloader不就應該什麼也沒有? 因為通電後就跑bootloader了不是嗎?
其實真實的過程並非如此單純,甚至還可以說相當兇險。
在進入到bootloader之前,大致上還可以細分成兩到三個
1) Collect Hardware Information
2) Run BootROM
3) Initialize Boot Device
1) 第一個動作是收集硬體的資訊,主要包含CPU跑多快?是哪種記憶體?跑多快?這個收集的
動作通常由硬體做,或者是固定的資料,所以通常非常快就結束。結束後,CPU這時候才
會真正被通上電源。
2) 上電之後,CPU其實不是去外部的flash ROM拿資料,而是在那內部自己本身的一個ROM
裡頭拿資料開始執行,這個ROM通常叫做BootROM。BootROM裡頭的程式就分析剛剛第一
的動作收集到的資訊,去做對應的工作,可能的工作例如
a) 設置程式執行的stack (需要用到CPU裡頭的SRAM or Cache)
b) 初始化DDR RAM(有些系統還是放在bootloader做)。
3) 一開始的初始化結束後,接著就可以初始化可能放置bootloader地方的device,例如
SATA/PCIE/外部FLASH ROM等等。這樣便可以到這幾個可能的位置去找到bootloader。
以上就是在進入到bootloader可能的動作,CPU會支援這樣的功能,原因是希望可以讓CPU
支援更多種的開機方式,例如可以用SPI Flash,或是接SATA Disk等等,這樣客戶在使用
這顆CPU的時候,可以選擇的boot device就比較多種類,甚至版子就可以省掉flash ROM
的cost直接把bootloader放到硬碟上。
以上粗略的解釋before bootloader的可能的動作,會說『可能』是因為各家做法多多少少
有些差異,但其實想達到的目的都是差不多的。
如同標題所提示的
一個 ARM 的系統在bootloader之前究竟有沒有事情可以做?
首先,一個系統必須先上電之後才會跑
那麼第一個跑到的程式碼是什麼?
刻板印象上,聽說CPU會跑去一個固定的位置去抓第一道指令,這個位置通常是一
個flash ROM裡頭放著bootloader,bootloader被執行之後,就會自己將kernel
載入,bla..bla..bla.....最後完成開機。(以上的印象在wince or Linux皆適用。)
那麼before bootloader不就應該什麼也沒有? 因為通電後就跑bootloader了不是嗎?
其實真實的過程並非如此單純,甚至還可以說相當兇險。
在進入到bootloader之前,大致上還可以細分成兩到三個
1) Collect Hardware Information
2) Run BootROM
3) Initialize Boot Device
1) 第一個動作是收集硬體的資訊,主要包含CPU跑多快?是哪種記憶體?跑多快?這個收集的
動作通常由硬體做,或者是固定的資料,所以通常非常快就結束。結束後,CPU這時候才
會真正被通上電源。
2) 上電之後,CPU其實不是去外部的flash ROM拿資料,而是在那內部自己本身的一個ROM
裡頭拿資料開始執行,這個ROM通常叫做BootROM。BootROM裡頭的程式就分析剛剛第一
的動作收集到的資訊,去做對應的工作,可能的工作例如
a) 設置程式執行的stack (需要用到CPU裡頭的SRAM or Cache)
b) 初始化DDR RAM(有些系統還是放在bootloader做)。
3) 一開始的初始化結束後,接著就可以初始化可能放置bootloader地方的device,例如
SATA/PCIE/外部FLASH ROM等等。這樣便可以到這幾個可能的位置去找到bootloader。
以上就是在進入到bootloader可能的動作,CPU會支援這樣的功能,原因是希望可以讓CPU
支援更多種的開機方式,例如可以用SPI Flash,或是接SATA Disk等等,這樣客戶在使用
這顆CPU的時候,可以選擇的boot device就比較多種類,甚至版子就可以省掉flash ROM
的cost直接把bootloader放到硬碟上。
以上粗略的解釋before bootloader的可能的動作,會說『可能』是因為各家做法多多少少
有些差異,但其實想達到的目的都是差不多的。
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日 星期五
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的程
式片段,所以就先不深入探討,有興趣的朋友可以一起討論。
有錯請指正~
話說,身為一個程式設計人員,常常遇到一些程式跑著跑著就當掉的狀況也很合理的。
例如:『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
表示我可以拿到相對應的資源這樣惡意程式很容易就得到一些記憶體上的個人資料有些地方的說明不太容易,太簡略容易造成誤解,贅述又怕不容易理解。沒表達清楚的地方歡迎提出來討論。
[本文開始]
在學校學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來記錄他對記憶體如何解讀。
寫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
未完待續~
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)