1. 程式人生 > >四、內核啟動(二)

四、內核啟動(二)

clu 更新 mage onf 指針 內核編譯 try ext 內存空間

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 bufferable
3 * 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()是用於解壓內核

四、內核啟動(二)