天天看點

Android arm linux kernel啟動流程(二)

     寫這個總結的時候咱的心情是沉重的,因為還有好多東西沒弄明白。。。感歎自己的知識還是淺薄得很,前途錢途漫漫阿~~不過基本脈絡是清楚的,具體的細節隻能留在以後有時間再啃了。這裡的第二部分啟動流程指的是解壓後kernel開始執行的一部分代碼,這部分代碼和ARM體系結構是緊密聯系在一起的,是以最好是将ARM ARCHITECTURE REFERENCE MANUL仔細讀讀,尤其裡面關于控制寄存器啊,MMU方面的内容~

      前面說過解壓以後,代碼會跳到解壓完成以後的vmlinux開始執行,具體從什麼地方開始執行我們可以看看生成的vmlinux.lds(arch/arm/kernel/)這個檔案:

      OUTPUT_ARCH(arm)

ENTRY(stext)

jiffies = jiffies_64;

SECTIONS

{

. = 0x80000000 + 0x00008000;

.text.head : {

_stext = .;

_sinittext = .;

*(.text.h

      很明顯我們的vmlinx最開頭的section是.text.head,這裡我們不能看ENTRY的内容,以為這時候我們沒有作業系統,根本不知道如何來解析這裡的入口位址,我們隻能來分析他的section(不過一般來說這裡的ENTRY和我們從seciton分析的結果是一樣的),這裡的.text.head section我們很容易就能在arch/arm/kernel/head.S裡面找到,而且它裡面的第一個符号就是我們的stext:

       .section ".text.head", "ax"

ENTRY(stext)

msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode

@ and irqs disabled

mrc p15, 0, r9, c0, c0 @ get processor id

bl __lookup_processor_type @ r5=procinfo r9=cpuid

      這裡的ENTRY這個宏實際我們可以在include/linux/linkage.h裡面找到,可以看到他實際上就是聲明一個GLOBAL Symbol,後面的ENDPROC和END唯一的差別是前面的聲明了一個函數,可以在c裡面被調用。

      #ifndef ENTRY

#define ENTRY(name) /

.globl name; /

ALIGN; /

name:

#endif

#ifndef WEAK

#define WEAK(name) /

.weak name; /

name:

#endif

#ifndef END

#define END(name) /

.size name, .-name

#endif

/* If symbol 'name' is treated as a subroutine (gets called, and returns)

* then please use ENDPROC to mark 'name' as STT_FUNC for the benefit of

* static analysis tools such as stack depth analyzer.

*/

#ifndef ENDPROC

#define ENDPROC(name) /

.type name, @function; /

END(name)

#endif 

      找到了vmlinux的起始代碼我們就來進行分析了,先總體概括一下這部分代碼所完成的功能,head.S會首先檢查proc和arch以及atag的有效性,然後會建立初始化頁表,并進行CPU必要的處理以後打開MMU,并跳轉到start_kernel這個symbol開始執行後面的C代碼。這裡有很多變量都是我們進行kernel移植時需要特别注意的,下面會一一講到。

      在這裡我們首先看看這段彙編開始跑的時候的寄存器資訊,這裡的寄存器内容實際上是同bootloader跳轉到解壓代碼是一樣的,就是r1=arch  r2=atag addr。下面我們就具體來看看這個head.S跑的過程:

       msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode

@ and irqs disabled

mrc p15, 0, r9, c0, c0 @ get processor id

      首先進入SVC模式并關閉所有中斷,并從arm協處理器裡面讀到CPU ID,這裡的CPU主要是指arm架構相關的CPU型号,比如ARM9,ARM11等等。

       然後跳轉到__lookup_processor_type,這個函數定義在head-common.S裡面,這裡的bl指令會儲存目前的pc在lr裡面,最後__lookup_processor_type會從這個函數傳回,我們具體看看這個函數:      

       __lookup_processor_type:

adr r3, 3f

ldmda r3, {r5 - r7}

sub r3, r3, r7 @ get offset between virt&phys

add r5, r5, r3 @ convert virt addresses to

add r6, r6, r3 @ physical address space

1: ldmia r5, {r3, r4} @ value, mask

and r4, r4, r9 @ mask wanted bits

teq r3, r4

beq 2f

add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list)

cmp r5, r6

blo 1b

mov r5, #0 @ unknown processor

2: mov pc, lr

ENDPROC(__lookup_processor_type)

       他這裡的執行過程其實比較簡單就是在__proc_info_begin和__proc_info_end這個段裡面裡面去讀取我們注冊在裡面的proc_info_list這個結構體,這個結構體的定義在arch/arm/include/asm/procinfo.h,具體實作根據你使用的cpu的架構在arch/arm/mm/裡面找到具體的實作,這裡我們使用的ARM11是proc-v6.S,我們可以看看這個結構體:

       .section ".proc.info.init", #alloc, #execinstr

/*

* Match any ARMv6 processor core.

*/

.type __v6_proc_info, #object

__v6_proc_info:

.long 0x0007b000

.long 0x0007f000

.long PMD_TYPE_SECT | /

PMD_SECT_BUFFERABLE | /

PMD_SECT_CACHEABLE | /

PMD_SECT_AP_WRITE | /

PMD_SECT_AP_READ

.long PMD_TYPE_SECT | /

PMD_SECT_XN | /

PMD_SECT_AP_WRITE | /

PMD_SECT_AP_READ

b __v6_setup

.long cpu_arch_name

.long cpu_elf_name

.long HWCAP_SWP|HWCAP_HALF|HWCAP_THUMB|HWCAP_FAST_MULT|HWCAP_EDSP|HWCAP_JAVA

.long cpu_v6_name

.long v6_processor_functions

.long v6wbi_tlb_fns

.long v6_user_fns

.long v6_cache_fns

.size __v6_proc_info, . - __v6_proc_info 

       對着.h我們就知道各個成員變量的含義了,他這裡lookup的過程實際上是先求出這個proc_info_list的實際實體位址,并将其内容讀出,然後将其中的mask也就是我們這裡的0x007f000與寄存器與之後與0x007b00進行比較,如果一樣的話呢就校驗成功了,如果不一樣呢就會讀下一個proc_info的資訊,因為proc一般都是隻有一個的,是以這裡一般不會循環,如果檢測正确寄存器就會将正确的proc_info_list的實體位址賦給寄存器,如果檢測不到就會将寄存器值賦0,然後通過LR傳回。

         bl __lookup_machine_type @ r5=machinfo

movs r8, r5 @ invalid machine (r5=0)?

beq __error_a @ yes, error 'a' 

       檢測完proc_info_list以後就開始檢測machine_type了,這個函數的實作也在head-common.S裡面,我們看看它具體的實作:

        __lookup_machine_type:

adr r3, 3b

ldmia r3, {r4, r5, r6}

sub r3, r3, r4 @ get offset between virt&phys

add r5, r5, r3 @ convert virt addresses to

add r6, r6, r3 @ physical address space

1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type

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)

        這裡的過程基本上是同proc的檢查是一樣的,這裡主要檢查晶片的類型,比如我們現在的晶片是MSM7X27FFA,這也是一個結構體,它的頭檔案在arch/arm/include/asm/arch/arch.h裡面(machine_desc),它具體的實作根據你對晶片類型的選擇而不同,這裡我們使用的是高通的7x27,具體實作在arch/arm/mach-msm/board-msm7x27.c裡面,這些結構體最後都會注冊到_arch_info_begin和_arch_info_end段裡面,具體的大家可以看看vmlinux.lds或者system.map,這裡的lookup會根據bootloader傳過來的nr來在__arch_info裡面的相比對的類型,沒有的話就尋找下一個machin_desk結構體,直到找到相應的結構體,并會将結構體的位址指派給寄存器,如果沒有的話就會指派為0的。一般來說這裡的machine_type會有好幾個,因為不同的晶片類型可能使用的都是同一個cpu架構。

       對processor和machine的檢查完以後就會檢查atags parameter的有效性,關于這個atag具體的定義我們可以在./include/asm/setup.h裡面看到,它實際是一個結構體和一個聯合體構成的結合體,裡面的size都是以字來計算的。這裡的atags param是bootloader建立的,裡面包含了ramdisk以及其他memory配置設定的一些資訊,存儲在boot.img頭部結構體定義的位址中,具體的大家可以看咱以後對bootloader的分析~ 

       __vet_atags:

tst r2, #0x3 @ aligned?

bne 1f

ldr r5, [r2, #0] @ is first tag ATAG_CORE?

cmp r5, #ATAG_CORE_SIZE

cmpne r5, #ATAG_CORE_SIZE_EMPTY

bne 1f

ldr r5, [r2, #4]

ldr r6, =ATAG_CORE

cmp r5, r6

bne 1f

mov pc, lr @ atag pointer is ok

1: mov r2, #0

mov pc, lr

ENDPROC(__vet_atags)

       這裡對atag的檢查主要檢查其是不是以ATAG_CORE開頭,size對不對,基本沒什麼好分析的,代碼也比較好看~ 下面我們來看後面一個重頭戲,就是建立初始化頁表,說實話這段内容我沒弄清楚,它需要對ARM VIRT MMU具有相當的了解,這裡我沒有太多的時間去分析spec,隻是粗略了翻了ARM V7的manu,知道這裡建立的頁表是arm的secition頁表,完成記憶體開始1m記憶體的映射,這個頁表建立在kernel和atag paramert之間,一般是4000-8000之間~具體的代碼和過程我這裡就不貼了,大家可以看看參考的連結,看看其他大蝦的分析,我還沒怎麼看明白,等以後仔細研究ARM MMU的時候再回頭來仔細研究了,不過代碼雖然不分析,這裡有幾個重要的位址需要特别分析下~

      這幾個位址都定義在arch/arm/include/asm/memory.h,我們來稍微分析下這個頭檔案,首先它包含了arch/memory.h,我們來看看arch/arm/mach-msm/include/mach/memory.h,在這個裡面定義了#define PHYS_OFFSET     UL(0x00200000) 這個實際上是memory的實體記憶體初始位址,這個位址和我們以前在boardconfig.h裡面定義的是一緻的。然後我們再看asm/memory.h,他裡面定義了我們的memory虛拟位址的首位址#define PAGE_OFFSET     UL(CONFIG_PAGE_OFFSET)。      

      另外我們在head.S裡面看到kernel的實體或者虛拟位址的定義都有一個偏移,這個偏移又是從哪來的呢,實際我們可以從arch/arm/Makefile裡面找到:textofs-y   := 0x00008000     TEXT_OFFSET := $(textofs-y) 這樣我們再看kernel啟動時候的實體位址和連結位址,實際上它和我們前面在boardconfig.h和Makefile.boot裡面定義的都是一緻的~

      建立初始化頁表以後,會首先将__switch_data這個symbol的連結位址放在sp裡面,然後獲得__enable_mmu的實體位址,然後會跳到__proc_info_list裡面的INITFUNC執行,這個偏移是定義在arch/arm/kernel/asm-offset.c裡面,實際上就是取得__proc_info_list裡面的__cpu_flush這個函數執行。

       ldr r13, __switch_data @ address to jump to after

@ mmu has been enabled

adr lr, __enable_mmu @ return (PIC) address

add pc, r10, #PROCINFO_INITFUNC

      這個__cpu_flush在這裡就是我們proc-v6.S裡面的__v6_setup函數了,具體它的實作我就不分析了,都是對arm控制寄存器的操作,這裡轉一下它對這部分操作的注釋,看完之後就基本知道它完成的功能了。

        完成這部分關于CPU的操作以後,下面就是打開MMU了,這部分内容也沒什麼好說的,也是對arm控制寄存器的操作,打開MMU以後我們就可以使用虛拟位址了,而不需要我們自己來進行位址的重定位,ARM硬體會完成這部分的工作。打開MMU以後,會将SP的值賦給PC,這樣代碼就會跳到__switch_data來運作,這個__switch_data是一個定義在head-common.S裡面的結構體,我們實際上是跳到它地一個函數指針__mmap_switched處執行的。

        這個switch的執行過程我們隻是簡單看一下,前面的copy data_loc段以及清空.bss段就不用說了,它後面會将proc的資訊和machine的資訊儲存在__switch_data這個結構體裡面,而這個結構體将來會在start_kernel的setup_arch裡面被使用到。這個在後面的對start_kernel的詳細分析中會講到。另外這個switch還涉及到控制寄存器的一些操作,這裡我不沒仔細研究spec,不懂也就不說了~

        好啦,switch操作完成以後就會b start_kernel了~ 這樣就進入了c代碼的運作了,下一篇文章仔細研究這個start_kernel的函數~~

Ref:

http://linux.chinaunix.net/bbs/thread-1021226-1-1.html

http://blog.csdn.net/yhmhappy2006/archive/2008/08/06/2775239.aspx

http://blog.csdn.net/sustzombie/archive/2010/06/12/5667607.aspx

繼續閱讀