现在的位置: 首页 > 综合 > 正文

Linux Kernel SMP (Symmetric Multi-Processors) 開機流程解析 Part(2) Linux Kernel SMP zImage到start_kernel流程

2013年12月08日 ⁄ 综合 ⁄ 共 22114字 ⁄ 字号 评论关闭

http://loda.hala01.com/2011/07/android-%E7%AD%86%E8%A8%98-linux-kernel-smp-symmetric-multi-processors-%E9%96%8B%E6%A9%9F%E6%B5%81%E7%A8%8B%E8%A7%A3%E6%9E%90-part2-linux-kernel-smp-zimage%E5%88%B0start_kernel%E6%B5%81%E7%A8%8B/

Mmmmmm,必須承認,我把這篇文章寫的有點囉嗦,以前在Linux Kernel上的工作,沒有留下太多的筆記,抽象的概念,容易隨著下一個產品或是技術的開發,成為過往記憶的一部分,這次重新整理,希望以後回來看時,可以很快Pick-up所有的細節,所以在一些枝微末節上,會比較嘮叨.

也因此,如果你原本就對ARM與Linux Kernel原始碼有一定的基礎,可能讀起本文來會比較輕鬆些. 若是有些部分,筆者探究的太過細節,還請各位見諒.

Linux Kernel對於SMP的支援有三種組合,

1,不支援SMP的Linux Kernel

2,支援SMP的Linux Kernel (關閉SMP對於單核心UniProcessor的支援,SMP_ON_UP=n)

3,支援SMP/UP的Linux Kernel (開啟SMP對於單核心UniProcessor的支援,SMP_ON_UP=y),並在啟動時,如果偵測到為UniProcessor時,會自我修正不能在non-SMP運作的指令.

目前SMP_ON_UP選項只在ARM處理器上有.

其實,整個Linux Kernel中包括PageSet,Process ID Map與相關的資料結構,都會參考目前系統中的處理器個數,來做出對應的配置,也就是說Linux Kernel對於支援多核心的架構,已經是相當的內化(骨子裡…就是會考慮到多核心的情況.),並蘊含在許多核心模組的設計上. 也因此,在整理本文的過程中,收獲最大的也是筆者自己對於SMP架構與Linux Kernel模組的藍圖.
並希望對閱讀本文的人也能有所助益.

本文主要從zImage開始到start_kernel完畢(rest_init除外),並以Tegra平台為主要參考,由於並非所有函式都在筆者平台上被參考到,在說明中也會略過,只選擇在這平台上比較重要的部份.

由於筆者時間受限,本系列文章會分次刊登,還請見諒.

Linux Kernel Image

依據開發的需求,Linux Kernel Image可以編譯為 zImage (Compressed kernel image),Image (Uncompressed kernel image),xipImage(XIP(eXecution In Place) kernel image),uImage(U-Boot wrapped zImage)與 bootpImage( Combined
zImage and initial RAM disk).

若對Linux Kernel編譯過程有興趣,可在編譯時加上 KBUILD_VERBOSE=1,讓quiet參數為空白,可把編譯過程吐到Console中,便於觀察.

不只是ARMv32,還支援Thumb2的 Linux Kernel Image

如果所選擇的處理器是ARMv7 (也就是Cortex的架構),可以透過勾選Experimental程式碼的選項,就可把Linux Kernel以Thumb2的方式進行編譯.

有關ARMv32與Thumb2效能的比較可以參考這篇在ARM工作的Richard Phelan所寫的文章Improving ARM Code Density and Performance (http://www.cs.uiuc.edu/class/fa05/cs433ug/PROCESSORS/Thumb2.pdf), 以C Code實作同樣的功能來說,編譯為Thumb2最高可以達到98%的ARM指令及效能,程式碼本身所需的記憶體空間只佔原本ARM程式碼的74%. 
對記憶體受限的嵌入式裝置,以Thumb2 16/32 bits混合的程式碼可以得到較佳的 Performance/Code Size的C/P值.

在選擇Linux Kernel選單時,只要進行如下勾選即可,

General setup  —>Prompt for development and/or incomplete code/drivers

Kernel Features  —>Compile the kernel in Thumb-2 mode

目前筆者並未驗證過這部份的代碼,僅作為有興趣的開發者參考資訊.

Linux Kernel編譯時所產生的Relocatable Object File.

當一個編譯系統比較龐大時,如果是一次要Link大量的.o或.a檔時,要解決這些Symbol Resolve會需要的記憶體與運算成本,也會對應的提高,Linux Kernel有使用GCC  relocatable output的機制,讓個別模組可以先進行 Symbol Resolve,節省最後Kernel Image產生的運算資源. 簡要說明如下

1, 編譯過程中,會透過arm-eabi-ld (GCC Linker) 搭配 “-r” 產生”relocatable output”,例如:

arm-eabi-ld -EL    -r -o drivers/tty/vt/built-in.o drivers/tty/vt/vt_ioctl.o drivers/tty/vt/vc_screen.o drivers/tty/vt/selection.o drivers/tty/vt/keyboard.o drivers/tty/vt/consolemap.o drivers/tty/vt/consolemap_deftbl.o
drivers/tty/vt/vt.o drivers/tty/vt/defkeymap.o

會把 drivers/tty/vt下的.o檔案,產生出一個在內部已經做過Symbol Resolved動作的集合Object檔案 built-in.o,透過objdump我們先檢視在目錄下的vt.o檔案中呼叫外部函式vt_ioctl,

arm-eabi-objdump -t vt.o|grep "vt_ioctl"

00000000         *UND*  00000000 vt_ioctl

由於該函式的實作不在vt.c中,因此在編譯後,.o檔案中的Symbol會被標示為 “Undefined”,再來檢視實作該函式的vt_ioctl.c產生的Object檔案,如下所示

arm-eabi-objdump -t vt_ioctl.o|grep "vt_ioctl"

vt_ioctl.o:     file format elf32-littlearm

00000000 l    df *ABS*  00000000 vt_ioctl.c

00000558 g     F .text  00001ccc vt_ioctl

可以看到該函式在vt_ioctl.c編譯後,是在text節區中,且屬性為 global,可供外部的.o檔案連結.

最後我們檢視drivers/tty/vt目錄下產生的built-in.o,

arm-eabi-objdump -t built-in.o|grep "vt_ioctl"

00000000 l    df *ABS*  00000000 vt_ioctl.c

00000558 g     F .text  00001ccc vt_ioctl

可以看到,最後產生的集合檔案built-in.o,包含了vt.o與vt_ioctl.o,且在其中這些.o之間的Symbol交互參考的問題,都已經在編譯階段被解決.

想像一下,如果一次有5000個Object檔案或是.a檔案(.a檔案,等於是Object檔案的Archive,可以分辨.o檔案的集合性,但其中所包含的.o並沒有彼此先進行Symbol Resolved,因此,所花的時間成本跟.o是一樣的.),要去做Symbol Resolved,這要建立的對應表格複雜度,跟我先把這5000檔案所在的20個目錄,針對這20個目錄先把其中包含的Object檔案做內部的Symbol
Resolved,減少要解決的Symbol個數與要建立的查表範圍,就可以顯著的加速最後要連結成Image的運算時間與記憶體成本.

參考平台Tegra2的記憶體配置

筆者以Linux Kernel 2.6.39並選擇ARM Tegra2的平台為例 (NVIDIA Tegra (ARCH_TEGRA)),關於這處理器的基本資訊為

1,兩個Cortex A9處理器

2,一個Audio/Video ARM7處理器

3,實體記憶體SDRAM定址在 0×00000000  ( AP20_BASE_PA_SDRAM)

4,OnChip 256KB SRAM定址在0×40000000 (AP20_BASE_PA_SRAM)

5,NOR Flash的定址在0xD0000000 ( AP20_BASE_PA_NOR_FLASH)

有關NVidia Tegra2的資訊可以參考http://developer.nvidia.com/tegra/taxonomy/term/36/0

有關ALT_UP對SMP到UP程式碼的修正

由於Linux Kernel SMP的實作,在ARM的架構下會有SMP與單核心共用函式實作程式碼的差異,在檔案 arch/arm/include/asm/assembler.h中,有實現ALT_SMP與ALT_UP兩個巨集,例如在程式碼中使用ALT_UP,該指令就會被加入Section .alt.smp.init 如下所示.

#define ALT_UP(instr…)                                        \

.pushsection ".alt.smp.init", "a"                       ;\

.long   9998b                                           ;\

9997:   instr                                                   ;\

.if . – 9997b != 4                                      ;\

.error "ALT_UP() content must assemble to exactly 4 bytes";\

.endif                                                  ;\

.popsection

藉此我們可以在同樣的函式中,根據單核心與SMP實作的差異,透過ALT_SMP與ALT_UP來把兩種版本的程式碼置入,以開啟SMP與SMP_ON_UP的實作來說,屬於SMP的實作,會被編譯在原本執行函式的內容中,而屬於單核心版本的實作,則會被編譯到Section .alt.smp.init下,參考如下程式碼的例子

在檔案arch/arm/mm/proc-v7.S中,

ALT_SMP(orr     r0, r0, #TTB_FLAGS_SMP)

ALT_UP(orr      r0, r0, #TTB_FLAGS_UP)

…..

cpu_resume_l1_flags:

ALT_SMP(.long PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_FLAGS_SMP)

ALT_UP(.long  PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_FLAGS_UP)

或檔案arch/arm/mm/tlb-v7.S中,

ALT_SMP(mcr     p15, 0, r0, c8, c3, 1)  @ TLB invalidate U MVA (shareable)

ALT_UP(mcr      p15, 0, r0, c8, c7, 1)  @ TLB invalidate U MVA

我們可以看到依據SMP與單核心版本的差異,實作上會在同一處程式碼中同時實現兩種版本的程式碼,並透過ALT_UP把單核心的版本在編譯階段放到Section .alt.smp.init中,並且會在每4bytes程式碼位址後,記錄對應4bytes單核心版本指令集,以便修正時參考,如下例子

0xc001858c <__smpalt_begin>:

…..

0xc0018634: c0029fd8     .word 0xc0029fd8 //4 bytes 要取代的目標記憶體位址

0xc0018638: ee080f37     mcr    15, 0, r0, cr8, cr7, {1} //4bytes UniProcessor版本指令

0xc001863c: c0029fec     .word 0xc0029fec

0xc0018640: ee07cfd5     mcr    15, 0, ip, cr7, cr5, {6}

0xc0018644: c002a00c    .word 0xc002a00c

0xc0018648: ee080f37     mcr    15, 0, r0, cr8, cr7, {1}

…..

在最後的Link階段,會把Section .alt.smp.init放在Symbol __smpalt_begin與__smpalt_end之中,因此在程式碼執行階段,就可以透過這兩個Symbol取得 Section .alt.smp.init中所包含單核心程式碼的內容與記憶體範圍.

在Linux Kernel啟動後會呼叫函式__fixup_smp,如果判斷目前是在單核心平台上,就會把在__smpalt_begin到__smpalt_end記憶體範圍的單核心程式碼依據其對應的記憶體位址,進行修正動作.

運作概念如下圖所示

從zImage開始,啟動Linux Kernel

接下來,以Linux Kernel zImage為例,簡要說明執行流程,也借此對產生的Linux Kernel Image有一個概念,有關SMP的部份,會在流程走到時,著重說明

編譯完成後,在根目錄下的vmlinux會透過如下的命令產生出來,其中有關記憶體位置與節區的配置參考檔案為 arch/arm/kernel/vmlinux.lds

arm-eabi-ld -EL  -p –no-undefined -X –build-id -o vmlinux -T arch/arm/kernel/vmlinux.lds arch/arm/kernel/head.o arch/arm/kernel/init_task.o 
init/built-in.o –start-group  usr/built-in.o  arch/arm/kernel/built-in.o  arch/arm/mm/built-in.o  arch/arm/common/built-in.o  arch/arm/mach-tegra/built-in.o  kernel/built-in.o  mm/built-in.o  fs/built-in.o  ipc/built-in.o  security/built-in.o  crypto/built-in.o 
block/built-in.o  arch/arm/lib/lib.a  lib/lib.a  arch/arm/lib/built-in.o  lib/built-in.o  drivers/built-in.o  sound/built-in.o  firmware/built-in.o  net/built-in.o –end-group .tmp_kallsyms2.o

(關於 .tmp_vmlinux1 與 .tmp_vmlinux2的產生,在此先略過)

之後,執行如下命令把ELF格式的vmlinux轉為 Binary 格式的Image

arm-eabi-objcopy -O binary -R .comment -S  vmlinux arch/arm/boot/Image

並執行如下命令把 Linux Kernel Binary Image轉為壓縮檔案

cat arch/arm/boot/compressed/../Image | gzip -f -9 > arch/arm/boot/compressed/piggy.gzip

參考arch/arm/boot/compressed/piggy.gzip.S原始碼

.section .piggydata,#alloc

.globl  input_data

input_data:

.incbin "arch/arm/boot/compressed/piggy.gzip"

.globl  input_data_end

input_data_end:

可以知道在編譯arch/arm/boot/compressed/piggy.gzip.S產生arch/arm/boot/compressed/piggy.gzip.o時,就會把壓縮後的Linux Kernel Image " arch/arm/boot/compressed/piggy.gzip",一併產生在piggy.gzip.o中的Symbol input_data與input_data_end之間.

然後,把壓縮檔跟解壓縮的部份,連結產生 compressed目錄下的vmlinux (記憶體起點為0×00000000,也就是對應到Tegra2外部記憶體的起點),執行的Link指令如下所示

arm-eabi-ld -EL    –defsym _image_size=1602596 –defsym zreladdr=0×00008000 -p –no-undefined -X -T arch/arm/boot/compressed/vmlinux.lds arch/arm/boot/compressed/head.o arch/arm/boot/compressed/piggy.gzip.o
arch/arm/boot/compressed/misc.o arch/arm/boot/compressed/decompress.o arch/arm/boot/compressed/lib1funcs.o -o arch/arm/boot/compressed/vmlinux

然後,執行如下命令把帶有壓縮後的vmlinux與解壓縮程式的ELF格式vmlinux轉為 Binary 格式的zImage

arm-eabi-objcopy -O binary -R .comment -S  arch/arm/boot/compressed/vmlinux arch/arm/boot/zImage

如此,就完成Linux Kernel Image的產生.

其中有關zImage執行的實體記憶體位址可以透過CONFIG_ZBOOT_ROM_TEXT與CONFIG_ZBOOT_ROM_BSS設定.

而Linux Kernel解壓縮的位址會在 CONFIG_ZBOOT_ROM_TEXT + 16kbytes的位址,以這例子來說就是 0×00008000. 這是在最後產生arch/arm/boot/compressd/vmlinux時,透過 “–defsym zreladdr=0×00008000” 產生zreladdr Syombol傳遞給zImage.

可以參考 boot/compressed/Makefile中

LDFLAGS_vmlinux += –defsym zreladdr=$(ZRELADDR)

而 ZRELADDR是在arch/arm/boot/Makefile 中設定的

ZRELADDR    := $(zreladdr-y)

其中,zreladdr-y會是在每個Machine對應的目錄下的Makefile.boot被定義,例如Tegra2是在檔案 arch/arm/mach-tegra/Makefile.boot中,以如下方式定義zreladdr-y

zreladdr-$(CONFIG_ARCH_TEGRA_2x_SOC)   := 0×00008000

同時,對解壓縮的Kernel Image執行的虛擬與實體記憶體對應,必須滿足如下條件

ZRELADDR == virt_to_phys(PAGE_OFFSET + TEXT_OFFSET)

因為如此,解壓縮的Linux Kernel在虛擬記憶體中的位址就必須是0xc0008000 對應到實體記憶體中的位址會是 0×00008000. 如果Kernel Space的虛擬記憶體空間有調整的話(例如從 0xc0000000調整為0×80000000,就會變成 0×80008000 ↔ 0×00008000).

Linux Kernel Image在虛擬記憶體的運作位置可以透過 xx訂定,一般而言都是給User-Space 3GB的範圍,Kernel Space為1GB的範圍.

CONFIG_PAGE_OFFSET=0xC0000000

要進一步探討Linux Kernel啟動流程,我們可以透arch/arm/boot/compressed/vmlinux.lds了解zImage的啟動流程,

1,節區.text產生的Binary Symbol有

a,<lext.1135> ("static const unsigned short lext" in lib/zlib_inflate/inftrees.c)

b,<lbase.1134> ("static const unsigned short lbase"  in lib/zlib_inflate/inftrees.c)

c,<dext.1137>  ("static const unsigned short dext" in lib/zlib_inflate/inftrees.c)

d,<dbase.1136> ("static const unsigned short dbase" in lib/zlib_inflate/inftrees.c)

e,<lenfix.1621> ("static const code lenfix" in lib/zlib_inflate/inffixed.h)

f,<distfix.1622> ("static const code distfix" in lib/zlib_inflate/inffixed.h)

….etc

2,會把壓縮後的Linux Kernel piggy.gzip 放在.text節區中Symbol <input_data> 到 <input_data_end>之間,以筆者驗證的環境來說,大約介於實體記憶體 0x00003fde-0x000bdcbe之間. (約761kbytes)=>要看總共編譯多少核心模組與程式碼範圍.

如下圖所示

3,編譯後的zImage,在會檔頭  offset: 0×00000024 bytes位置存放32-bits 的 “0x016f2818” 作為 BootLoader載入zImage時,確認zImage檔案正確與否的Magic Number. 緊接著offset: 0×00000028位置存放32-bits  zImage在實體記憶體的起始位址 (以筆者編譯Tegra2
Seaboard環境而言該值為 0×00000000.) 
再來的offset: 0x0000002c位置存放32-bits zImage在實體記憶體的結束位置 (以筆者編譯Tegra2 Seaboard環境而言該值為0x000bdcf4). 透過上述的三個值,就可以讓BootLoader驗證Linux Kernel Magic Number,並且知道要把該Kernel Image擺放到哪一段記憶體位址執行與大小.

4,啟動後,會執行arch/arm/boot/compressed/head.S 中的函式start ,可以參考UBoot中呼叫Linux Kernel Entry 的函式原型為

void    (*kernel_entry)(int zero, int arch, uint params);

也就是說, UBoot/BootLoader 呼叫zImage時,第一個參數固定為0(r0),第二個參數為處理器平台ID(r1),第三個參數為Linux Kernel啟動的Boot Argument(r2)

00000000 <start>:

0×00000000: nop                      (mov r0,r0)

0×00000004: nop                       (mov r0,r0)

0×00000008: nop                       (mov r0,r0)

0x0000000c: nop                       (mov r0,r0)

0×00000010: nop                       (mov r0,r0)

0×00000014: nop                       (mov r0,r0)

0×00000018: nop                       (mov r0,r0)

0x0000001c: nop                       (mov r0,r0)

0×00000020: b       30 <_text+0×30>

0×00000024: .word          0x016f2818

0×00000028: .word          0×00000000

0x0000002c: .word          0x000bdcf4

0×00000030: mov r7, r1

0×00000034: mov r8, r2

0×00000038: mrs   r2, CPSR

0x0000003c: tst    r2, #3 ; 0×3 (確認是否為Supervisor Mode,User Mode=b10000(0×0010) and Supervisor Mode=b10011(0×0013)).

0×00000040: bne  4c <not_angel>

0×00000044: mov r0, #23          ; 0×17 (如果在SVC Mode就執行這部份的程式碼).

0×00000048: svc   0×00123456 (ARM semihosting 會傳遞參數0×17給JTAG除錯器,並讓ARM Core停止執行,可用於ICE除錯啟動流程)

如果zImage在編譯時是載入到記憶體0×00000000的位址,則zImage的進入點為 0×00000030.  (從0×00000000開始執行,也會執行連續的 mov r0,r0 直到0×00000020再Branch到正式的函式入口.).

5,之後呼叫函式__armv7_mmu_cache_on,並透過函式setup_mmu以1MB  Section方式配置TLB (MMU虛擬記憶體機制在這並沒有作用,所設定的TLB虛擬記憶體是直接對應到4GB的範圍,所以TLB虛擬位址0xc0000000就會直接對應到實體記憶體的0xc0000000.)

000002bc <__armv7_mmu_cache_on>:

0x000002bc: mov  ip, lr

0x000002c0: mrc    15, 0, fp, cr0, cr1, {4}   //read ID_MMFR0

0x000002c4: tst      fp, #15         ; 0xf    //VMSA

0x000002c8: blne   21c <__setup_mmu>

0x000002cc: mov   r0, #0 ; 0×0

0x000002d0: mcr    15, 0, r0, cr7, cr10, {4}  //drain write buffer

0x000002d4: tst     fp, #15         ; 0xf    //VMSA

0x000002d8: mcrne          15, 0, r0, cr8, cr7, {0}    //flush I,D TLBs

0x000002dc: mrc    15, 0, r0, cr1, cr0, {0}    //read control reg

0x000002e0: orr     r0, r0, #20480         ; 0×5000    //I-cache enable, RR cache replacement

0x000002e4: orr     r0, r0, #60    ; 0x3c     //write buffer

0x000002e8: orrne r0, r0, #1      ; 0×1      //MMU enabled

0x000002ec: mvnne         r1, #0 ; 0×0

0x000002f0: mcrne           15, 0, r3, cr2, cr0, {0}    //load page table pointer //r3 from setup_mmu = page table entry.

0x000002f4: mcrne           15, 0, r1, cr3, cr0, {0}    //load domain access control

0x000002f8: mcr     15, 0, r0, cr1, cr0, {0}    //load control register

0x000002fc: mrc     15, 0, r0, cr1, cr0, {0}    //and read it back

0×00000300: mov   r0, #0 ; 0×0

0×00000304: mcr    15, 0, r0, cr7, cr5, {4}    //ISB

0×00000308: mov   pc, ip

0000021c <__setup_mmu>:

0x0000021c: sub    r3, r4, #16384         ; 0×4000    //Page directory size //(r4=Linux Kernel 解壓縮後記憶體位址 =0×00008000, r3=0×00004000)

0×00000220: bic     r3, r3, #255  ; 0xff    //Align the pointer

0×00000224: bic     r3, r3, #16128         ; 0x3f00

/* Initialise the page tables, turning on the cacheable and bufferable

* bits for the RAM area only. */

0×00000228: mov   r0, r3

0x0000022c: lsr      r9, r0, #18

0×00000230: lsl      r9, r9, #18    //start of RAM

0×00000234: add    sl, r9, #268435456 ; 0×10000000   // a reasonable RAM size

0×00000238: mov   r1, #18          ; 0×12

0x0000023c: orr     r1, r1, #3072           ; 0xc00

0×00000240: add    r2, r3, #16384         ; 0×4000

0×00000244: cmp   r1, r9    //if virt > start of RAM

0×00000248: orrcs r1, r1, #12    ; 0xc    //set cacheable, bufferable

0x0000024c: cmp   r1, sl    //if virt > end of RAM

0×00000250: biccs r1, r1, #12    ; 0xc    //clear cacheable, bufferable

0×00000254: str      r1, [r0], #4    //1:1 mapping

0×00000258: add    r1, r1, #1048576     ; 0×100000 //Use Section Size=1MB//Page Table從0×4000-0×8000,每個Entry值為4 bytes,所以會有0×1000筆Entry,也就是說0×1000 * 1MB=4GB Range=32bits處理器訂址的上限

0x0000025c: teq     r0, r2

0×00000260: bne    244 <__setup_mmu+0×28>

/* If ever we are running from Flash, then we surely want the cache

* to be enabled also for our execution instance…  We map 2MB of it

* so there is no map overlap problem for up to 1 MB compressed kernel.

* If the execution is in RAM then we would only be duplicating the above.  */

0×00000264: mov   r1, #30          ; 0x1e

0×00000268: orr     r1, r1, #3072           ; 0xc00

0x0000026c: mov   r2, pc

0×00000270: lsr      r2, r2, #20

0×00000274: orr     r1, r1, r2, lsl #20

0×00000278: add    r0, r3, r2, lsl #2

0x0000027c: str      r1, [r0], #4

0×00000280: add    r1, r1, #1048576     ; 0×100000

0×00000284: str      r1, [r0]

0×00000288: mov   pc, lr

有關TLB 1MB Section Settings如下示意圖

以配置在0×00004000-0×00008000之間TLB 1MB Section的虛擬記憶體設定來說,Linux Kernel Base Address 0xc0008000,透過TLB查尋0xc0000000對應到的實體記憶體位址,首先取虛擬記憶體bits 31-20 值0xc00 作為Table Index,得到的Translation Table Base Address為
0×00004000 + 0xc00*4 = 0×00007000,該記憶體位址的1st Level Descriptor內容為0xc0000c12,屬性欄位為  1100 0001 0010 也就是AP[2:0]=011(Privileged/User permissions 都是可讀可寫的Full Access Right.),XN=1,

用虛擬記憶體0xc0008000轉換為實體記憶體時,1st Level Descriptor的bit 31-20 為0xc0000000+Offset 0×00008000=實體記憶體0xc0008000. (這是只有經過zImage前面setup_mmu設定的結果,並非正式Linux Kernel MMU TLB的配置結果喔!!!)

雖然setup_mmu已經對MMU TLB進行配置,但還不是最後的運作狀態,會在後續執行中設定完成.

6, 之後透過ARM r4暫存器 (r4  = kernel execution address )儲存Linux Kernel所要解壓縮的目標記憶體位置,如下程式碼

#ifdef CONFIG_AUTO_ZRELADDR

@ determine final kernel image address

mov     r4, pc

and     r4, r4, #0xf8000000

add     r4, r4, #TEXT_OFFSET

#else

ldr     r4, =zreladdr

#endif

bl      cache_on

呼叫函式decompress_kernel,解壓縮Linux到目標位址

/*

* The C runtime environment should now be setup sufficiently.

* Set up some pointers, and start decompressing.

*   r4  = kernel execution address

*   r7  = architecture ID

*   r8  = atags pointer

*/

mov     r0, r4

mov     r1, sp                  @ malloc space above stack

add     r2, sp, #0×10000        @ 64k max

mov     r3, r7

bl      decompress_kernel

bl      cache_clean_flush

bl      cache_off

mov     r0, #0                  @ must be zero

mov     r1, r7                  @ restore architecture number

mov     r2, r8                  @ restore atags pointer

mov     pc, r4                  @ call kernel

函式decompress_kernel  第一個參數為Linux Kernel要解壓縮的目標記憶體位置(from r4),第二個參數為目前的Stack Point,第三個參數為目前Stack加上64kbytes,第四個參數為目前的處理器平台識別碼,之後Flush Cache並關閉Cache.

最後,從Linux Kernel解壓縮後的記憶體位置的起點(也就是r4的記憶體位置)開始執行Linux Kernel (第一個參數為0, 第二個參數為目前的處理器平台識別碼,第三個參數為BootLoader要傳遞給Linux Kernel的Boot Argument.)

整個ARM從Boot Rom到UBoot BootLoader ,到 zImage解壓縮,到啟動解壓縮後Linux Kernel的流程,簡要描述如下圖所示

結束了zImage到解壓縮後的vmlinux Kernel Image流程,接下來我們以開啟SMP選項的Linux Kernel Image為例,進一步說明啟動的流程.

繼續vmlinux Linux Kernel Image執行流程

最原始的ELF格式Linux Kernel在編譯後所在的位置為/vmlinux,透過objcopy產生的Binary檔案為 arch/arm/boot/Image.

根目錄的vmlinux執行時,會從arch/arm/kernel/head.S中的函式stext開始,stext函式會帶入三個函式參數,第一個參數固定為0,第二個參數為architecture number,第三個參數為要帶給Kernel參數在記憶體中的位址.

如下為反組譯vmlinux執行時最前端的程式碼,

0×00008000 <stext>:

0×00008000:           msr    CPSR_c, #211       ; 0xd3 //ensure svc mode and irqs disabled

0×00008004:           mrc    15, 0, r9, cr0, cr0, {0} //get processor id

0×00008008:           bl       0×00156878 <__lookup_processor_type> //r5=procinfo r9=cpuid

0x0000800c:           movs sl, r5 //invalid processor (r5=0)?

0×00008010:           beq    c01568bc <__error> //yes, error ‘p’

0×00008014:           add    r3, pc, #40   ; 0×28

0×00008018:           ldm    r3, {r4, r8}

0x0000801c:           sub    r4, r3, r4   //(PHYS_OFFSET – PAGE_OFFSET)

0×00008020:           add    r8, r8, r4 //PHYS_OFFSET

/*r1 = machine no, r2 = atags,

* r8 = phys_offset, r9 = cpuid, r10 = procinfo     */

0×00008024:           bl       0x0000810c <__vet_atags>

0×00008028:           bl       0×00008144 <__fixup_smp>//用以修正SMP Linux Kernel在單核心處理器運作的機制

0x0000802c:           bl       0x0000804c <__create_page_tables> //設定BootStrap處理器的MMU TLB分頁表

0×00008030:           ldr      sp, [pc, #8]  ; 0×00008040 <stext+0×40> //address to jump to after mmu has been enabled

0×00008034:           add    lr, pc, #0       ; 0×0  //return (PIC) address

0×00008038:           add    pc, sl, #16    ; 0×10 //Go to function __v7_proc_info (0x0019abf8+0×10 = 0x0019ac08)

0x0000803c:           b        0x0015684c <__enable_mmu>

0×00008040:           .word 0xc00081a0

0×00008044:           .word 0xc0008044

0×00008048:           .word 0xc0000000

在前面的內容有提到,透過setup_mmu所配置的MMU TLB並不是一個真正適應於Linux Kernel最終在高位址執行設定的虛擬記憶體配置,而在這階段透過函式__create_page_tables,就會把MMU TLB從下圖左側原本的配置方式,改為如右側適應於Linux Kernel配置在高位址,並且針對不存在的記憶體分頁,設定為0.

如果我們所編譯的Linux Kernel為支援SMP的版本,並且有開啟編譯選項SMP_ON_UP,在函式 __fixup_smp中 (fixup = Fix UniProcessor),會進行確認是否為SMP的核心運作在單核心處理器上,並執行必要的修正.

在ARMv7 Cortex處理器下,會進入函式__v7_proc_info + 0×10 也就是函式__v7_setup中

0x0019abf8 <__v7_proc_info>:  (in arch/arm/mm/proc-v7.S)

0x0019abf8:          .word 0x000f0000

0x0019abfc:          .word 0x000f0000

0x0019ac00:          .word 0x00011c0e

0x0019ac04:          .word 0x00000c12

0x0019ac08:          b        0x001570bc <__v7_setup>

在函式__v7_setup中會初始化TLB,Caches與MMU. 並透過CP15取得Main ID Register, 簡要說明如下

透過"MRC p15, 0, <Rd>, c0, c0, 0″ 讀取處理器Main ID Register

Cortex A8 單核心 Main

Cortex A9 單核心 Main

Cortex A9 Dual-Core Main ID=  0x412FC090 (for r2p0) ,0x412FC091 (for r2p1) or  0x412FC092 (for r2p2)

Main ID Register格式如下所示

31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
Implementor Variant Architecture Primary Part Number Revision
欄位 說明
Implementor ARM處理器為0×41
Variant 可用以表示Major Revision

Indicates the variant number, or major revision, of the processor:

0×3.

Architecture Indicates that the architecture is given in the feature registers:

0xF.

Primary Part Number Indicates the part number, Cortex-A8:

0xC08.

Revision Indicates the revision number, or minor revision, of the processor:

0×2.

函式__v7_setup運作如下所示

0x001570bc <__v7_setup>: (in arch/arm/mm/proc-v7.S)

0x001570bc:                    add    ip, pc, #140 ; 0x8c //the local stack

0x001570c0:          stm    ip, {r0, r1, r2, r3, r4, r5, r7, r9, fp, lr}

0x001570c4:          bl       0x000297ec <v7_flush_dcache_all>

0x001570c8:          ldm    ip, {r0, r1, r2, r3, r4, r5, r7, r9, fp, lr}

0x001570cc:          mrc    15, 0, r0, cr0, cr0, {0} //read main ID register

0x001570d0:                    and    sl, r0, #-16777216  ; 0xff000000 //ARM?

0x001570d4:                    teq     sl, #1090519040     ; 0×41000000

0x001570d8:                    bne    0×00157108 <__v7_setup+0x4c>

0x001570dc:                    and    r5, r0, #15728640   ; 0xf00000 //variant

0x001570e0:          and    r6, r0, #15    ; 0xf //revision

0x001570e4:          orr      r6, r6, r5, lsr #16 //combine variant and revision

0x001570e8:          ubfx   r0, r0, #4, #12 //primary part number

0x001570ec:          ldr      sl, [pc, #136]          ; 0x0015717c <__v7_setup_stack+0x2c> //Cortex-A8 primary part number

0x001570f0:          teq     r0, sl

0x001570f4:          bne    0x001570fc <__v7_setup+0×40>

0x001570f8:          b        0×00157108 <__v7_setup+0x4c>

0x001570fc:          ldr      sl, [pc, #124]          ; 0×00157180 <__v7_setup_stack+0×30>//Cortex-A9 primary part number

0×00157100:          teq     r0, sl

0×00157104:          bne    0×00157108 <__v7_setup+0x4c>

0×00157108:          mov   sl, #0 ; 0×0

0x0015710c:          dsb    sy

0×00157110:          mcr    15, 0, sl, cr8, cr7, {0} //invalidate I + D TLBs

0×00157114:          mcr    15, 0, sl, cr2, cr0, {2}  //TTB control register

0×00157118:          orr      r4, r4, #106  ; 0x6a

0x0015711c:          mcr    15, 0, r4, cr2, cr0, {1}  //load TTB1

0×00157120:          ldr      r5, [pc, #92] ; 0×00157184 <__v7_setup_stack+0×34> //PRRR

0×00157124:          ldr      r6, [pc, #92] ; 0×00157188 <__v7_setup_stack+0×38> //NMRR

0×00157128:          mcr    15, 0, r5, cr10, cr2, {0} //write PRRR

0x0015712c:          mcr    15, 0, r6, cr10, cr2, {1} //write NMRR

0×00157130:          add    r5, pc, #16   ; 0×10

0×00157134:          ldm    r5, {r5, r6}

0×00157138:          mrc    15, 0, r0, cr1, cr0, {0} //read control register

0x0015713c:          bic     r0, r0, r5 //clear bits them

0×00157140:          orr      r0, r0, r6 //set them

0×00157144:          mov   pc, lr //pc=0x0000803c,之後進入 函式 __enable_mmu

回到stext函式

0x0000803c:          b        0x0015684c <__enable_mmu>

進入函式 __enable_mmu

0x0015684c <__enable_mmu>:

0x0015684c:          orr      r0, r0, #2      ; 0×2

0×00156850:          mov   r5, #21          ; 0×15

0×00156854:          mcr    15, 0, r5, cr3, cr0, {0}

0×00156858:          mcr    15, 0, r4, cr2, cr0, {0}

0x0015685c:          b        0×00156860 <__turn_mmu_on>

進入函式 __turn_mmu_on

0×00156860 <__turn_mmu_on>:

0×00156860:          nop                         (mov r0,r0)

0×00156864:          mcr    15, 0, r0, cr1, cr0, {0}

0×00156868:          mrc    15, 0, r3, cr0, cr0, {0}

0x0015686c:          mov   r3, r3

0×00156870:          mov   r3, sp

0×00156874:          mov   pc, r3 //會呼叫0xc00081a0 //到這就是第一次執行到虛擬記憶體的位址 0xc00081a0

進入函式__mmap_switched

0xc00081a0 <__mmap_switched>:

0xc00081a0:           add    r3, pc, #64   ; 0×40

0xc00081a4:           ldm    r3!, {r4, r5, r6, r7}

0xc00081a8:           cmp   r4, r5  //Copy data segment if needed

0xc00081ac:           cmpne          r5, r6

0xc00081b0:                      ldrne  fp, [r4], #4

0xc00081b4:                      strne  fp, [r5], #4

0xc00081b8:                      bne    c00081ac <__mmap_switched+0xc>

0xc00081bc:                      mov   fp, #0 ; 0×0 //Clear BSS (and zero fp)

0xc00081c0:           cmp   r6, r7

0xc00081c4:           strcc  fp, [r6], #4

0xc00081c8:           bcc    c00081c0 <__mmap_switched+0×20>

0xc00081cc:           ldm    r3, {r4, r5, r6, r7, sp}

0xc00081d0:                      str      r9, [r4] //Save processor ID

0xc00081d4:                      str      r1, [r5]  //Save machine type

0xc00081d8:                      str      r2, [r6]  //Save atags pointer

0xc00081dc:                      bic     r4, r0, #2      ; 0×2 //Clear ‘A’ bit

0xc00081e0:           stm    r7, {r0, r4} //Save control register values 
0xc00081e4:           b        0xc00088ac <start_kernel> //正式進入Linux Kernel Starting.

函式start_kernel實作在 init/main.c中,並且start_kernel啟動主要Linux核心的初始化與包括Kernel SMP機制的初始化, 有關從Linux Kernel stext到start_kernel 執行的流程示意圖,如下所示

抱歉!评论已关闭.