四、內核啟動(二)
4.1 MMU設置續
上一節分析到調用 __armv4_mmu_cache_on,執行如下,這裏我們要分析 set_mmu 函數
4.1.1 __setup_mmu
前文已經分析過在內核最終運行地址r4下面有16KB的空間(我環境中是0x00004000~0x00008000),這就是用來存放頁表的,但是現在要建立的頁表在內核真正啟動後會被銷毀,只是用於零時存放。同時這裏將要建立的頁表映射關系是1:1映射(即虛擬地址 == 物理地址)。
首先開始執行的 給頁表留出空間,將頁表的起始地址保存到 R3 中,R4 中保存的內容是 ZRELADDR,然後對齊頁表,經過兩次 bit 指令, R3 中的值的低 14 位均為0,實際上是對齊到了 16KB 邊界。
1 __setup_mmu: sub r3, r4, #16384 @ Page directory size 2 bic r3, r3, #0xff @ Align the pointer 3 bic r3, r3, #0x3f00
-
常數 #16384 的由來:
-
32 位的 RAM 系統,尋址空間為 4GB,此處每一個頁表項代表 1MB,則需要 4096 個頁表項。同時,每一個頁表項的大小為 4Byte,那麽就需要 4096 * 4Byte = 16384Byte = 16KB 的空間。
-
此時頁表項的每項對應 1MB 的內存空間,其格式為 段(section)頁表項 ,如下圖所示:
圖中 bit4 XN 為不可執行位, bit3 C 為 cacheable,bit2 B 為 bufferable
註: L1頁表項的格式有 4 種,分別為 Fault 頁表項、Section 頁表項、Page Table 頁表項和 Supersection 頁表項。詳細內容參考 ARM 手冊
繼續向下執行:
1 /* 2 * Initialise the page tables, turning on the cacheable and bufferable3 * bits for the RAM area only. 4 */ 5 mov r0, r3 6 mov r9, r0, lsr #18 7 mov r9, r9, lsl #18 @ start of RAM 8 add r10, r9, #0x10000000 @ a reasonable RAM size
註釋說的很清楚,初始化頁表,打開 cacheable 和 bufferable 位
對物理RAM空間建立cache和buffer。然後通過將r0(r3)中的地址值右移18位再左移18位(即清零r3中地址的低18位),得到物理RAM空間的“初始地址”(其實是估計值)並保存到r9(0x00000000)中去,然後將該地址加上256MB的大小作為物理RAM的“結束地址”(也是估計值)並保存到r10(0x10000000)中去。這裏的另一個隱含意思也就是最多映射256MB大小的空間
R0 = R3; R9 保存的實際是 R3 的高 14 位的內容,低 18 位全為0,對齊到了 256MB 的邊界; R10 = R9 + 256MB.
比較 R1 和 R9,若 R1 >= R9,則繼續比較 R10 和 R1,之後 R1 = 0xC02,用於設置MMU區域表項的低12位狀態位
1 mov r1, #0x12 @ XN|U + section mapping 2 orr r1, r1, #3 << 10 @ AP=11 3 add r2, r3, #16384
繼續向下執行:
1 1: cmp r1, r9 @ if virt > start of RAM 2 cmphs r10, r1 @ && end of RAM > virt 3 bic r1, r1, #0x1c @ clear XN|U + C + B 4 orrlo r1, r1, #0x10 @ Set XN|U for non-RAM 5 orrhs r1, r1, r6 @ set RAM section settings 6 str r1, [r0], #4 @ 1:1 mapping 7 add r1, r1, #1048576 8 teq r0, r2 9 bne 1b
接著r1比較r9和r10以設置MMU區域表項狀態位:(其中r6中的值在前面__armv4_mmu_cache_on中賦值)
(1) r1 > r9 && r1 <r10 (r1的值在物理RAM地址範圍內):
設置RAM表項的C+B 位來開啟cache和buffer,同時清除XN表示可執行code
(2) r1 < r9 || r1 > r10(r1的值在物理RAM地址範圍外):
設置RAM表項的XN位並清除C+B位來關閉cache和buffer,不可執行code
在設置完狀態為後就要寫入頁表的相應地址中去了,然後將頁表的地址+4(指向下一個表項),物理地址空間+1M設置下一項(下一個需要映射物理地址的基地址),直到填完所有的4096表項。設置完後頁表項與映射關系如下:
如果代碼不是運行在RAM中而是運行在FLASH中的,則映射2MB代碼,如果運行在RAM中,則這部分代碼重復前面的工作。
1 /* 2 * If ever we are running from Flash, then we surely want the cache 3 * to be enabled also for our execution instance... We map 2MB of it 4 * so there is no map overlap problem for up to 1 MB compressed kernel. 5 * If the execution is in RAM then we would only be duplicating the above. 6 */ 7 orr r1, r6, #0x04 @ ensure B is set for this 8 orr r1, r1, #3 << 10 9 mov r2, pc 10 mov r2, r2, lsr #20 11 orr r1, r1, r2, lsl #20 12 add r0, r3, r2, lsl #2 13 str r1, [r0], #4 14 add r1, r1, #1048576 15 str r1, [r0] 16 mov pc, lr 17 ENDPROC(__setup_mmu)
至此,cache on 分析結束,回到主幹上繼續執行,執行 restart 標簽中的語句,繼續是在not_angel中
4.2 restart
1 restart: adr r0, LC0 2 ldmia r0, {r1, r2, r3, r6, r10, r11, r12} 3 ldr sp, [r0, #28]
通過前面LC0地址表的內容可見,這裏r0中的內容就是編譯時決定的LC0的實際運行地址(特別註意不是鏈接地址),然後調用ldmia命令依次將LC0地址表處定義的各個地址加載到r1、r2、r3、r6、r10、r11、r12和SP寄存器中去。執行之後各個寄存器中保存內容的意義如下:
(1) r0:LC0標簽處的運行地址
(2) r1:LC0標簽處的鏈接地址
(3) r2:__bss_start處的鏈接地址
(4) r3:_ednd處的鏈接地址(即程序結束位置)
(5) r6:_edata處的鏈接地址(即數據段結束位置)
(6) r10:壓縮後內核數據大小位置
(7) r11:GOT表的啟示鏈接地址
(8) r12:GOT表的結束鏈接地址
(9) sp:棧空間結束地址
在獲取了LC0的鏈接地址和運行地址後,就可以通過計算這兩者之間的差值來判斷當前運行的地址是否就是編譯時的鏈接地址。
1 /* 2 * We might be running at a different address. We need 3 * to fix up various pointers. 4 */ 5 sub r0, r0, r1 @ calculate the delta offset 6 add r6, r6, r0 @ _edata 7 add r10, r10, r0 @ inflated kernel size location
將運行地址和鏈接地址的偏移保存到r0寄存器中,然後更新r6和r10中的地址,將其轉換為實際的運行地址。
1 /* 2 * The kernel build system appends the size of the 3 * decompressed kernel at the end of the compressed data 4 * in little-endian form. 5 */ 6 ldrb r9, [r10, #0] 7 ldrb lr, [r10, #1] 8 orr r9, r9, lr, lsl #8 9 ldrb lr, [r10, #2] 10 ldrb r10, [r10, #3] 11 orr r9, r9, lr, lsl #16 12 orr r9, r9, r10, lsl #24
註釋中說明了,內核編譯系統在壓縮內核時會在末尾處以小端模式附上未壓縮的內核大小,這部分代碼的作用就是將該值計算出來並保存到r9寄存器中去
1 #ifndef CONFIG_ZBOOT_ROM 2 /* malloc space is above the relocated stack (64k max) */ 3 add sp, sp, r0 4 add r10, sp, #0x10000 5 #else 6 /* 7 * With ZBOOT_ROM the bss/stack is non relocatable, 8 * but someone could still run this code from RAM, 9 * in which case our reference is _edata. 10 */ 11 mov r10, r6 12 #endif
這裏將鏡像的結束地址保存到r10中去,我這裏並沒有定義ZBOOT_ROM(如果定義了ZBOOT_ROM則bss和stack是非可重定位的),這裏將r10設置為sp結束地址上64kb處(這64kB空間是用來作為堆空間的)。
接下來內核如果配置為支持設備樹(DTB)會做一些特別的工作,我這裏沒有配置(#ifdef CONFIG_ARM_APPENDED_DTB),所以先跳過。
1 /* 2 * Check to see if we will overwrite ourselves. 3 * r4 = final kernel address (possibly with LSB set) 4 * r9 = size of decompressed image 5 * r10 = end of this image, including bss/stack/malloc space if non XIP 6 * We basically want: 7 * r4 - 16k page directory >= r10 -> OK 8 * r4 + image length <= address of wont_overwrite -> OK 9 * Note: the possible LSB in r4 is harmless here. 10 */ 11 add r10, r10, #16384 12 cmp r4, r10 13 bhs wont_overwrite 14 add r10, r4, r9 15 adr r9, wont_overwrite 16 cmp r10, r9 17 bls wont_overwrite
這部分代碼用來分析當前代碼是否會和最後的解壓部分重疊,如果有重疊則需要執行代碼搬移。首先比較內核解壓地址r4-16Kb(這裏是0x00004000,包括16KB的內核頁表存放位置)和r10,如果r4 – 16kB >= r10,則無需搬移,否則繼續計算解壓後的內核末尾地址是否在當前運行地址之前,如果是則同樣無需搬移,不然的話就需要進行搬移了。
總結一下可能的3種情況:
(1) 內核起始地址– 16kB >= 當前鏡像結束地址:無需搬移
(2) 內核結束地址 <= wont_overwrite運行地址:無需搬移
(3) 內核起始地址– 16kB < 當前鏡像結束地址 && 內核結束地址 > wont_overwrite運行地址:需要搬移
仔細分析一下,這裏內核真正運行的地址是0x00004000,而現在代碼的運行地址顯然已經在該地址之後了反匯編發現wont_overwrite的運行地址是0x00008000+0x00000168),而且內核解壓後的空間必然會覆蓋掉這裏(內核解壓後的大小大於0x00000168),所以這裏會執行代碼搬移。
1 /* 2 * Relocate ourselves past the end of the decompressed kernel. 3 * r6 = _edata 4 * r10 = end of the decompressed kernel 5 * Because we always copy ahead, we need to do it from the end and go 6 * backward in case the source and destination overlap. 7 */ 8 /* 9 * Bump to the next 256-byte boundary with the size of 10 * the relocation code added. This avoids overwriting 11 * ourself when the offset is small. 12 */ 13 add r10, r10, #((reloc_code_end - restart + 256) & ~255) 14 bic r10, r10, #255 15 16 /* Get start of code we want to copy and align it down. */ 17 adr r5, restart 18 bic r5, r5, #31
從這裏開始會將鏡像搬移到解壓的內核地址之後,首先將解壓後的內核結束地址進行擴展,擴展大小為代碼段的大小(reloc_code_end定義在head.s的最後)保存到r10中,即搬運目的起始地址,然後r5保存了restart的起始地址,並進行對齊,即搬運的原起始地址。反匯編查看這裏擴展的大小為0x800。
1 sub r9, r6, r5 @ size to copy 2 add r9, r9, #31 @ rounded up to a multiple 3 bic r9, r9, #31 @ ... of 32 bytes 4 add r6, r9, r5 5 add r9, r9, r10 6 7 1: ldmdb r6!, {r0 - r3, r10 - r12, lr} 8 cmp r6, r5 9 stmdb r9!, {r0 - r3, r10 - r12, lr} 10 bhi 1b 11 12 /* Preserve offset to relocated code. */ 13 sub r6, r9, r6 14 15 #ifndef CONFIG_ZBOOT_ROM 16 /* cache_clean_flush may use the stack, so relocate it */ 17 add sp, sp, r6 18 #endif 19 20 bl cache_clean_flush 21 22 badr r0, restart 23 add r0, r0, r6 24 mov pc, r0
這裏首先計算出需要搬運的大小保存到r9中,搬運的原結束地址到r6中,搬運的目的結束地址到r9中。註意這裏只搬運代碼段和數據段,並不包含bss、棧和堆空間。
接下來開始執行代碼搬移,這裏是從後往前搬移,一直到r6 == r5結束,然後r6中保存了搬移前後的偏移,並重定向棧指針(cache_clean_flush可能會使用到棧)。
之後調用調用cache_clean_flush清楚緩存,然後將PC的值設置為搬運後restart的新地址,然後重新從restart開始執行。這次由於進行了代碼搬移,所以會在檢查自覆蓋時進入wont_overwrite處執行。
1 wont_overwrite: 2 /* 3 * If delta is zero, we are running at the address we were linked at. 4 * r0 = delta 5 * r2 = BSS start 6 * r3 = BSS end 7 * r4 = kernel execution address (possibly with LSB set) 8 * r5 = appended dtb size (0 if not present) 9 * r7 = architecture ID 10 * r8 = atags pointer 11 * r11 = GOT start 12 * r12 = GOT end 13 * sp = stack pointer 14 */ 15 orrs r1, r0, r5 16 beq not_relocated 17 18 add r11, r11, r0 19 add r12, r12, r0
這裏的註釋列出了現有所有寄存器值得含義,如果r0為0則說明當前運行的地址就是鏈接地址,無需進行重定位,跳轉到not_relocated執行,但是這裏運行的地址已經被移動到內核解壓地址之後,顯然不會是鏈接地址0x00000168(反匯編代碼中得到),所以這裏需要重新修改GOT表中的變量地址來實現重定位。
1 add r11, r11, r0 2 add r12, r12, r0 3 4 #ifndef CONFIG_ZBOOT_ROM 5 /* 6 * If we‘re running fully PIC === CONFIG_ZBOOT_ROM = n, 7 * we need to fix up pointers into the BSS region. 8 * Note that the stack pointer has already been fixed up. 9 */ 10 add r2, r2, r0 11 add r3, r3, r0 12 13 /* 14 * Relocate all entries in the GOT table. 15 * Bump bss entries to _edata + dtb size 16 */ 17 1: ldr r1, [r11, #0] @ relocate entries in the GOT 18 add r1, r1, r0 @ This fixes up C references 19 cmp r1, r2 @ if entry >= bss_start && 20 cmphs r3, r1 @ bss_end > entry 21 addhi r1, r1, r5 @ entry += dtb size 22 str r1, [r11], #4 @ next entry 23 cmp r11, r12 24 blo 1b 25 26 /* bump our bss pointers too */ 27 add r2, r2, r5 28 add r3, r3, r5
更新GOT表的運行起始地址到r11和結束地址到r12中去,然後同樣更新BSS段的運行地址(需要修正BSS段的指針)。然後進入“1”標簽中開始執行重定位。
通過r1獲取GOT表中的一項,然後對這一項的地址進行修正,如果修正後的地址 < BSS段的起始地址,或者在BSS段之中則再加上DTB的大小(如果不支持DTB則r5的值為0),然後再將值寫回GOT表中去。如此循環執行直到遍歷完GOT表。
在重定位完成後,繼續執行not_relocated部分代碼,這裏循環清零BSS段。
1 not_relocated: mov r0, #0 2 1: str r0, [r2], #4 @ clear bss 3 str r0, [r2], #4 4 str r0, [r2], #4 5 str r0, [r2], #4 6 cmp r2, r3 7 blo 1b
這裏檢測r4中的最低位,如果已經置位則說明在前面執行restart前並沒有執行cache_on來打開緩存(見前文),這裏補執行。
1 /* 2 * Did we skip the cache setup earlier? 3 * That is indicated by the LSB in r4. 4 * Do it now if so. 5 */ 6 tst r4, #1 7 bic r4, r4, #1 8 blne cache_on
1 /* 2 * The C runtime environment should now be setup sufficiently. 3 * Set up some pointers, and start decompressing. 4 * r4 = kernel execution address 5 * r7 = architecture ID 6 * r8 = atags pointer 7 */ 8 mov r0, r4 9 mov r1, sp @ malloc space above stack 10 add r2, sp, #0x10000 @ 64k max 11 mov r3, r7 12 bl decompress_kernel 13 bl cache_clean_flush 14 bl cache_off 15 mov r1, r7 @ restore architecture number 16 mov r2, r8 @ restore atags pointer
到此為止,C語言的執行環境已經準備就緒,設置一些指針就可以開始解壓內核了(這裏的內核解壓部分是使用C代碼寫的)。
跳到 decompress_kernel 跳轉到內核C代碼運行。
這裏r0~r3的4個寄存器是decompress_kernel()函數傳參用的,r0傳入內核解壓後的目的地址,r1傳入堆空間的起始地址,r2傳入堆空間的結束地址,r3傳入機器碼,然後就開始調用decompress_clean_flush()函數執行內核
decompress_kernel()是用於解壓內核
四、內核啟動(二)