電子版下載http://download.csdn.net/detail/innost/4834459 linux
本節介紹Kernel啓動。此時Piggy已經將vimlinux解壓,BL將執行權限傳給了Kernel。 編程
代碼在arch/arm/kernel/head.S中。相關代碼以下: vim
//將採用C/C++註釋語句 數據結構
/* .section是GNU ASM的語法。格式以下: .section name[,"flags"[,@type]] 其中,name是必須的,flags是可選。 "ax"表示:a爲section is allocatable,x爲executable。 */ .section ".text.head", "ax" //這個ENTRY(stext)有至關的含義。在kernel/vmlinux.ld.S中,也定義了一個ENTRY。在ld //語法中,ENTRY是一個command,用來定義入口點。因此,這裏就是kernel執行的入口點函數。 ENTRY(stext) /* MSR:是ARM彙編指令,用來將數據copy到status register寄存器中。cpsr_c表示要操做 CPSR寄存器的Control標誌。 */ msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode @ and irqs disabled
CPSR全稱是Current Process Status Register,用來表示當前CPU的狀態,也可用於控制。相關控制位如圖1所示: 架構
圖1 CPSR控制位 dom
由圖1可知: ide
根據上面的代碼,首先將禁止I/F中斷,並進入Supervisor模式,也就是OS運行的模式。圖2爲ARM CPU支持的CPU模式。 函數
圖2 ARM CPU支持的運行模式 測試
另外,MSR指令操做的格式以下: ui
圖3 MSR指令格式
其中最重要的是fields,目前支持:
圖4 MSR二進制格式
直接看上面的解釋,還不是很清楚,由於設置的是MSR指令自己的內容,具體對應到CPSR呢,則可經過下面的僞語句獲得:
圖5 MSR 設置說明
從代碼可知:
msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE
//上面代碼將設置CPSR的0到第7位,恰好是控制I/F和設置CPU模式的。
設置好CPU模式後,下面的工做就是獲取CPU的信息。在ARM中,協處理(coprocessor)15中用於管理CPU信息和MMU相關的工做。CP15也是ARM中最重要的處理器,之後會常常碰到。
先看下面這條語句:
mrc p15, 0, r9, c0, c0 @ get processor id
MRC是ARM指令,用來從協處理對應的寄存器讀取信息到CPU的寄存器,對應寫協處理寄存器的指令是MCR。兩者的語法格式(注意,是操做CP15的時候)如圖6所示:
圖6 MRC操做CP15的格式說明
CP15有很重要的做用,可經過操做CP15的寄存器來控制它。如圖7所示:
圖7 CP15各個寄存器的做用
先來看此處操做的C0寄存器。
opcode2在指令中默認是0,因此將取出Main ID register的信息。
獲得的結果將怎麼使用呢?來看下一句指令:
bl __lookup_processor_type @ r5=procinfo r9=cpuid
BL是ARM中的跳轉指令,至關於調用函數吧。__lookup_processor_type用來獲得CPU信息。注意,這個函數調用的參數是R9,R9的值是從CP15 C0寄存器讀取出來的,而是是Main ID。下面看看此函數如何處理R9。
該函數在head-Common.S中定義。下面逐行分析它,這裏會碰到幾個重要的指令及其用法。
__lookup_processor_type: //adr是一條僞指令,其做用是將3f標籤的地址賦給R3。這個僞指令實際上是可拆分紅多條指令 //因爲後面的3f是相對當前PC位置而言,因此R3實際上存儲的是3f的物理地址。 adr r3, 3f//f是forward之意。標誌3在此代碼以後聲明 /* ldm是load multiple register的意思,它的做用是將[r3]對應的內存內容存儲到r5,r6, r7寄存器中。DA是Decrease After的意思。ARM彙編在這裏有4種模式,DA,IA,DB,IB等 此處的ldmda,將把3F所在的內容依序傳遞給R7,R6,R5。每傳遞一次,R3遞減4個字節。 */ ldmda r3, {r5 - r7} 上面語句執行完後: q R5=__proc_info_begin,這個值是虛地址。 q R6=__proc_info_end。 q R7=.。
以上幾個值都是虛地址。__proc_info_begin/end是ld在連接時候指定的信息。
圖8 arc/arm/kernel/vmlinx.lds.S文件
從中能夠看出,__proc_info_begin/end包含了代碼中定義在.proc.info.init段的內容。如圖9所示。
圖9 proc-V7.s定義的proc.info.init的內容
爲何是proc -v7.S文件呢,由於goldfish編譯的就是這個文件。從圖9能夠看出,其實也就是定義了一個數據結構罷了。
接着來看代碼
//r3指向3f的物理地址,r7指向虛擬地址,而如今只能訪問物理地址,因此須要找到一個offset sub r3, r3, r7 @ get offset between virt&phys add r5, r5, r3 @ convert virt addresses to add r6, r6, r3 @ physical address space
通過上面的換算,r5,r6如今都指向__proc_info_begin/end的物理地址了。
//ldmia將[r5]的內存信息存儲到r3,r4中,每完成一次傳輸,r5自動加4. 1: ldmia r5, {r3, r4} @ value, mask //下面將測試R9和mask以後的值是不是咱們想要的r3的值。根據圖9。應該是0x000f0000。 //在Main ID register中,這代表[16-19]位是都是1. and r4, r4, r9 @ mask wanted bits teq r3, r4 beq 2f //若是是咱們想要的數據,則跳轉到2f //不然跳過一個PROC_INFO_SIZE,繼續找,通常只有一個PROC_INFO結構體。 add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list) cmp r5, r6 blo 1b //若是沒找到,則設置R5寄存器爲0 mov r5, #0 @ unknown processor 2: mov pc, lr //從函數返回 ENDPROC(__lookup_processor_type) /* * 提供一個C接口的lookup_process_type函數 */ ENTRY(lookup_processor_type) stmfd sp!, {r4 - r7, r9, lr} mov r9, r0 bl __lookup_processor_type mov r0, r5 ldmfd sp!, {r4 - r7, r9, pc} ENDPROC(lookup_processor_type) .long __proc_info_begin .long __proc_info_end 3: .long . .long __arch_info_begin .long __arch_info_end
lookup_process_type其實比較簡單,這裏就再也不多說。但圖9的內容之後還要回過頭來繼續介紹。那裏將初始化CPU MMU相關的內容。
//若是r5爲空,則表示CPU信息獲取是否,調用__error_p,退出整個啓動 movs r10, r5 @ invalid processor (r5=0)? beq __error_p @ yes, error 'p'
不然,將調用__lookup_machine_type獲取機器信息。
該函數也是在head-comm.S中定義的。
__lookup_machine_type: adr r3, 3b //b是backward的意思。標誌3在此代碼以前聲明。 //r4,r5,r6分別指向 label 3,__arch_info_begin和__arch_info_end ldmia r3, {r4, r5, r6} sub r3, r3, r4 add r5, r5, r3 add r6, r6, r3 //以上將獲得__arch_info_begin/end的物理地址 1: ldr r3, [r5, #MACHINFO_TYPE] //比較r1和MACHINFO_TYPE是否是一致。注意,r1的值是BL傳遞給它的 teq r3, r1 @ matches loader number? beq 2f @ found add r5, r5, #SIZEOF_MACHINE_DESC @ next machine_desc cmp r5, r6 blo 1b mov r5, #0 @ unknown machine 2: mov pc, lr ENDPROC(__lookup_machine_type)
這裏涉及到另外一個關鍵數據結構,也就是定義在.arch.info.init段中的。如圖10所示:
圖10 .arch.info.init段
從圖10可知,這個段其實對應了一個數據結構,即machine_desc.在咱們的goldfish平臺中,它是這麼定義的:
[arch/arm/mach-goldfish/board-goldfish.c]
//還須要加上: nr = MACH_TYPE_GOLDFISH name = "Goldfish" MACHINE_START(GOLDFISH, "Goldfish") .phys_io = IO_START, .io_pg_offst = ((IO_BASE) >> 18) & 0xfffc, .boot_params = 0x00000100, .map_io = goldfish_map_io, .init_irq = goldfish_init_irq, .init_machine = goldfish_init, .timer = &goldfish_timer, MACHINE_END
完整的machine_desc定義如圖11所示:
圖11 machine_desc定義
在Goldfish中,nr爲1441。詳情可參考arch/arm/tools/machine-types.h。
另外,在BootLoader調用kernel以前,傳遞參數狀況如圖12所示:
圖12 arch/arm/boot/head.S調用kernel前傳遞參數
從圖12可知:
這部分代碼屬於BootLoader,至關複雜。之後再細說。
假設__lookup_machine_type一切正常
bl __lookup_machine_type @ r5=machinfo movs r8, r5 @ invalid machine (r5=0)? beq __error_a @ yes, error 'a'
接下來的任務就是Kernel校驗BL傳遞的啓動參數了。這部份內容和BootLoader有較大關係。
bl __vet_atags 此處的核心概念就是ATAG_CORE/END之類的,由BootLoader往Kernel傳遞參數,主要是tag結構體 在arch/arm/include/asm/setup.h中。BL傳遞的是struct tag的鏈表,該鏈表以ATAG_CORE開頭,以ATAG_NONE結尾。 #define ATAG_CORE 0x54410001 #define ATAG_NONE 0x00000000 struct tag_header { __u32 size; __u32 tag; }; struct tag { struct tag_header hdr; //首先是一個頭,根據頭部的tag來判斷下面的union是哪一個 union { struct tag_core core; struct tag_mem32 mem; struct tag_videotext videotext; struct tag_ramdisk ramdisk; struct tag_initrd initrd; struct tag_serialnr serialnr; struct tag_revision revision; struct tag_videolfb videolfb; struct tag_cmdline cmdline; struct tag_acorn acorn; struct tag_memclk memclk; } u; };
你能夠根據上面的信息自行分析__vet_atags函數。
下面的任務就是調用__create_page_tables建立page table。
bl __create_page_tables //調用__create_page_tables函數
此函數就在head.S中定義,代碼以下:
__create_page_tables: /* pgtbl是head.S中定義的一個宏,見下面的分析 */ pgtbl r4 pgtbl定義了一個宏,相關代碼以下: //TEXT_OFFSET是kernel鏡像在內存中的偏移量。通常定義爲0X8000,即32KB處 //PHYS_OFFSET:是內核鏡像在內存中的起始物理地址。上面兩者之和就是內核鏡像在機器上的 //物理地址。Goldfish平臺中,PHYS_OFFSET爲0。 //PAGE_OFFSET是Kernel鏡像在虛擬內存的起始地址,通常是3G處 #define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET) #define KERNEL_RAM_PADDR (PHYS_OFFSET + TEXT_OFFSET) .macro pgtbl, rd //此宏調用完畢後,r4的值就是0x4000,即16KB ldr \rd, =(KERNEL_RAM_PADDR - 0x4000) .endm 接着看代碼。 mov r0, r4 mov r3, #0 add r6, r0, #0x4000 //STR將寄存器的值往內存中傳送。r3爲0,故內存的值被設置爲0.每調用一次str,r0遞增4 //r0是base address,其值可自動增減。由arm address mode格式控制 1: str r3, [r0], #4 str r3, [r0], #4 str r3, [r0], #4 str r3, [r0], #4 teq r0, r6 bne 1b //此循環調用完畢後,0x4000-0x8000的內存都被設置爲0。此時r0=32KB //r10存儲的是圖9中proc_info的第三個long,也就是mmuflas,用於設置MMU參數 ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
雖然上面最後一條語句是一個簡單的ldr,但背後的內容卻至關豐富,不把它搞清楚,後面的內容將解釋不清。來看proc-V7中mm_mmuflags對應的值是什麼
.long PMD_TYPE_SECT | \ // #define PMD_TYPE_SECT (2 << 0) PMD_SECT_BUFFERABLE | \ //#define PMD_SECT_BUFFERABLE (1 << 2) PMD_SECT_CACHEABLE | \//#define PMD_SECT_CACHEABLE (1 << 3) PMD_SECT_AP_WRITE | \//#define PMD_SECT_AP_WRITE (1 << 10) PMD_SECT_AP_READ //#define PMD_SECT_AP_READ (1 << 11)
上面代碼中把對應PMD_SECT_XXX的值顯示出來,可知它無非是定義了一個32位的常量,某些位置的值爲1,某些位置的值爲0。爲何要怎麼作呢?先來看ARM MMU所支持的虛實地址轉換機制。
圖13 ARM MMU虛實地址轉換
由圖13可知:
結合圖13和前面的代碼:
另外,Domain是ARM CPU的一個重要概念,主要和權限有關。之後碰到再說。
至此,當ldr r7 xx執行完後,r7的值包含了section base address對應的[0-12]位的值。而section base address自己卻尚未賦值。
接下來的代碼就是爲了構造一個FLD的值。根據圖13,section base address應該是[20-31]位
//r6的值爲當前PC值右移20位 mov r6, pc, lsr #20 orr r3, r7, r6, lsl #20 @ flags + kernel base //此時,r3的值就是一個基於段尋址的FLD。把它存起來。位置是r4+r6<<2 str r3, [r4, r6, lsl #2]
如今r6存儲的是段尋址的基地址,須要把這個值存儲到對應表的位置,因爲在表中,每一項是4個字節,因此這裏須要乘以4,也就是lsl #2。
稍微解釋下這裏左移4的緣由:
當理解上面代碼後,下面就是把kernel虛擬地址的位置存儲到r4表中了
繼續看代碼
//當即數的計算比較難理解,網上也沒有相關說法。不過,只要知道下面這段代碼就是存儲kernel //虛擬地址到對應頁表位置便可 add r0, r4, #(KERNEL_START & 0xff000000) >> 18 str r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]! ldr r6, =(KERNEL_END - 1) add r0, r0, #4 //r0 = ro+4 add r6, r4, r6, lsr #18 //r6=r4+r6>>18 1: cmp r0, r6 add r3, r3, #1 << 20 //r3 += 1<<20,每次遞增1M //ls是condition code,表示小於等於,即只要r0<=r6,strls就會執行 strls r3, [r0], #4 bls 1b //map物理地址前1M到對應位置 add r0, r4, #PAGE_OFFSET >> 18 orr r6, r7, #(PHYS_OFFSET & 0xff000000) .if (PHYS_OFFSET & 0x00f00000) orr r6, r6, #(PHYS_OFFSET & 0x00f00000) .endif str r6, [r0] mov pc, lr ENDPROC(__create_page_tables) .ltorg
建議你們仔細體會create_page_tables這段內容。雖然之後不太可能會使用它們,但把這段代碼搞清楚仍是一個比較有意思的過程。
回到head.S,最後還剩下幾句代碼:
//將__switch_data的位置存儲到r13 ldr r13, __switch_data //獲取__enable_mmu標籤的地址,並保存到lr中 adr lr, __enable_mmu //r10存儲的是__v7_proc_info的地址,#PROCINFO_INITFUNC是一個偏移量 //執行完下條語句後,pc指向__v7_proc_info的b __v7_setup,故下面這條語句就是 //執行__v7_setup函數 add pc, r10, #PROCINFO_INITFUNC ENDPROC(stext)
__switch_data標籤以下,主要存儲了一些數據。
[head-common.S]
.type __switch_data, %object __switch_data: .long __mmap_switched .long __data_loc @ r4 .long _data @ r5 .long __bss_start @ r6 .long _end @ r7 .long processor_id @ r4 .long __machine_arch_type @ r5 .long __atags_pointer @ r6 .long cr_alignment @ r7 .long init_thread_union + THREAD_START_SP @ sp
之後再討論具體做用。
先來看
add pc, r10, #PROCINFO_INITFUNC 實際上就是執行__v7_setup函數。代碼在mm/proc-v7.S中。 adr r12, __v7_setup_stack @ the local stack stmia r12, {r0-r5, r7, r9, r11, lr} bl v7_flush_dcache_all ldmia r12, {r0-r5, r7, r9, r11, lr} mov r10, #0 dsb #ifdef CONFIG_MMU //goldfish定義了這個配置項 mcr p15, 0, r10, c8, c7, 0 @ invalidate I + D TLBs mcr p15, 0, r10, c2, c0, 2 @ TTB control register orr r4, r4, #TTB_FLAGS mcr p15, 0, r4, c2, c0, 1 @ load TTB1 mov r10, #0x1f @ domains 0, 1 = manager mcr p15, 0, r10, c3, c0, 0 @ load domain access register #endif ldr r5, =0xff0aa1a8 ldr r6, =0x40e040e0 mcr p15, 0, r5, c10, c2, 0 @ write PRRR mcr p15, 0, r6, c10, c2, 1 @ write NMRR adr r5, v7_crval ldmia r5, {r5, r6} mrc p15, 0, r0, c1, c0, 0 @ read control register bic r0, r0, r5 @ clear bits them orr r0, r0, r6 @ set them //最後一句,將lr賦值給pc。執行完後,將跳到__enable_mmu函數。 mov pc, lr @ return to head.S:__ret ENDPROC(__v7_setup)
上面代碼大可能是執行ARM v7 CPU的MMU相關設置的,而其中的彙編語句到比較簡單。這也是ARM MMU設置的核心內容。下面咱們將結合ARM CPU Rerference簡單介紹下這些設置的內容。
請務必從ARM官方網頁上下載下面兩個文檔:
如下面這個設置爲例:
mcr p15, 0, r10, c8, c7, 0
打開參考文檔DDI0344D_cortex_a8_r2p1_trm.pdf的第112頁。從這一頁開始,C15協處理器的各個寄存器的配置都有詳細的說明。如圖14所示
圖14 C8寄存器的設置
上圖中,左邊空白區域對應的是C8。可知,c8,c7,0的組合對應的是Invalidate unified TLB unlocked entries.詳細說明在page3-99。
若是在此文檔中碰到有不理解的內容,就須要參考DDI0406B_arm_architecture_reference_manual_errata_markup_10_0。該文檔會介紹一些理論知識。
篇幅緣由,我就不在這裏囉嗦。已經告訴你們如何釣魚了,請你們本身嘗試!
__v7_setup最後已經的mov pc, lr將使得CPU跳轉到__enable_mmu處,其代碼以下所示:
__enable_mmu: #ifdef CONFIG_ALIGNMENT_TRAP orr r0, r0, #CR_A #else bic r0, r0, #CR_A #endif #ifdef CONFIG_CPU_DCACHE_DISABLE bic r0, r0, #CR_C #endif #ifdef CONFIG_CPU_BPREDICT_DISABLE bic r0, r0, #CR_Z #endif #ifdef CONFIG_CPU_ICACHE_DISABLE bic r0, r0, #CR_I #endif //設置domain的權限,請參考前面的書籍瞭解DOMAIN在ARM MMU中的意義 mov r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \ domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \ domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \ domain_val(DOMAIN_IO, DOMAIN_CLIENT)) //請參考前面的方法,瞭解下面這兩條語句的實際做用 mcr p15, 0, r5, c3, c0, 0 @ load domain access register mcr p15, 0, r4, c2, c0, 0 @ load page table pointer b __turn_mmu_on //跳轉到__turn_mmu_on ENDPROC(__enable_mmu) 簡單看看__turn_mmu_on: __turn_mmu_on: mov r0, r0 //相似nop的空指令,浪費一點CPU時間,怕引發race condition發生 //c1,c0這兩個控制MMU的設置 mcr p15, 0, r0, c1, c0, 0 @ write control reg mrc p15, 0, r3, c0, c0, 0 @ read id reg mov r3, r3 mov r3, r3 //此時,MMU就正式啓動了 mov pc, r13 //r13指向__switch_data ENDPROC(__turn_mmu_on)
MMU啓動後,咱們也無需管什麼物理地址仍是虛擬地址,直接去看對應地址的代碼便可。若是您非對這個轉換過程很感興趣,建議您把那兩個參考書好好瞅瞅。
__switch_data第一個定義的就是__mmaped_switched,PC將執行這裏的指令:
__mmap_switched: adr r3, __switch_data + 4 ldmia r3!, {r4, r5, r6, r7} cmp r4, r5 @ Copy data segment if needed 1: cmpne r5, r6 ldrne fp, [r4], #4 strne fp, [r5], #4 bne 1b mov fp, #0 @ Clear BSS (and zero fp) 1: cmp r6, r7 strcc fp, [r6],#4 bcc 1b ldmia r3, {r4, r5, r6, r7, sp} str r9, [r4] @ Save processor ID str r1, [r5] @ Save machine type str r2, [r6] @ Save atags pointer bic r4, r0, #CR_A @ Clear 'A' bit stmia r7, {r0, r4} @ Save control register values
//上面我就懶得廢話了,下面這句代碼相信各位都很瞭解。執行start_kernel函數。
b start_kernel
ENDPROC(__mmap_switched)
我以爲須要說明下爲何寫這篇文章:
早在2010年7月的時候,我就看了那本鼎鼎大名的《ARM體系結構與編程》,這應該是第一本系統介紹ARM體系結構和編程的書。可是沒看懂,全是枯燥的ARM CPU設置,純教科書。
最近由於工做的緣由,想把ARM這塊從新撿起來,想起2年的痛苦,以爲應該換個思路。ARM也好,彙編也好,咱們應該關注它的目的,而不是具體它是怎麼實現的。即瞭解What to do比了解How to do更重要(僅我我的目的而言,前者重要。不過在某些追求細節的時候,後者重要。須要你本身去判斷)。根據這個思路,我選擇以Linux Kernel啓動爲分析對象,大體研究流程以下:
大概通過2周先痛苦掙扎,到後面豁然開朗的過程,後續的研究就很是很是流暢了。