ARM linux的啓動部分源代碼簡略分析

轉載至http://blog.chinaunix.net/uid-12567959-id-160966.html

 

ARM linux的啓動部分源代碼簡略分析
以友善之臂的mini2440開發板爲平臺,以較新的內核linux-2.6.32.7版本爲例,僅作說明之用。
 

當內核映像被加載到RAM之後,Bootloader的控制權被釋放。內核映像並不是可直接運行的目標代碼,而是一個壓縮過的zImage(小內核)。但是,也並非是zImage映像中的一切均被壓縮了,映像中包含未被壓縮的部分,這部分中包含解壓縮程序,解壓縮程序會解壓縮映像中被壓縮的部分。zImage使用gzip壓縮的,它不僅僅是一個壓縮文件,而且在這個文件的開頭部分內嵌有gzip解壓縮代碼。當zImage被調用時它從arch/arm/boot/compressed/head.S的start彙編例程開始執行。這個例程進行一些基本的硬件設置,並調用arch/arm/boot/compressed/misc.c中的decompress_kernel()解壓縮內核。
 

arch/arm/kernel/head.S文件是內核真正的啓動入口點,一般是由解壓縮內核的程序來調用的。首先先看下對於運行這個文件的要求:
MMU = off; D-cache = off; I-cache = 無所謂,開也可以,關也可以; r0 = 0;r1 = 機器號;r2 = atags 指針。
這段代碼是位置無關的,所以,如果以地址0xC0008000來鏈接內核,那麼就可以直接用__pa(0xc0008000)地址來調用這裏的代碼。
 

其實,在這個(Linux內核中總共有多達幾十個的以head.S命名的文件)head.S文件中的一項重要工作就是設置內核的臨時頁表,不然mmu開起來也玩不轉,但是內核怎麼知道如何映射內存呢?linux的內核將映射到虛地址0xCxxx xxxx處,但他怎麼知道在4GB的地址空間中有哪一片ram是可用的,從而可以映射過去呢?

因爲不同的系統有不通的內存映像,所以,LINUX約定,要調用內核代碼,一定要滿足上面的調用要求,以爲最初的內核代碼提供一些最重要的關於機器的信息。內核代碼開始的時候,R1存放的是系統目標平臺的代號,對於一些常見的,標準的平臺,內核已經提供了支持,只要在編譯的時候選中就行了,例如對X86平臺,內核是從物理地址1M開始映射的。
好了好了,看下面的代碼。
 

ENTRY(stext)是這個文件的入口點。最初的幾行是這樣的:
   setmode  PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9

@ ensure svc mode

@ and irqs disabled

// 設置爲SVC模式,關閉中斷和快速中斷
// 此處設定系統的工作狀態爲SVC,arm有7種狀態每種狀態
// 都有自己的堆棧,SVC爲管理模式,具有完全的權限,可以執行任意指令
// 訪問任意地址的內存
// setmode是一個宏,其定義爲:
// .macro setmode, mode, reg
// msr   cpsr_c, #\mode

// .endm

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

   bl __lookup_processor_type     @ r5=procinfo r9=cpuid

   movs  r10, r5            @ invalid processor (r5=0)?

   beq   __error_p       @ yes, error 'p'

 

這幾行是查詢處理器的類型的,我們知道arm系列有很多型號,arm7、arm9、arm11、Cortex核等等類型,這麼多型號要如何區分呢?其實,在arm的15號協處理器(其實ARM暫時也就這麼一個協處理器)中有一個只讀寄存器,存放與處理器相關信息。
 

__lookup_processor_type是arch/arm/kernel/head-common.S文件中定義的一個例程,這個head-common.S用include命令被包含在head.S文件中。其定義爲:
__lookup_processor_type:

   adr   r3, 3f

   ldmia r3, {r5 - r7}

   add   r3, r3, #8

   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)

 

這個例程接受處理器ID(保存在寄存器r9中)爲參數,查找鏈接器建立的支持的處理器表。此時此刻還不能使用__proc_info表的絕對地址,因爲這時候MMU還沒有開啓,所以此時運行的程序沒有在正確的地址空間中。所以不得不計算偏移量。若沒有找到processor ID對應的處理器,則在r5寄存器中返回返回0,否則返回一個proc_info_list結構體的指針(在物理地址空間)。proc_info_list結構體在文件中定義:
struct proc_info_list {

   unsigned int    cpu_val;

   unsigned int    cpu_mask;

   unsigned long      __cpu_mm_mmu_flags;   /* used by head.S */

   unsigned long      __cpu_io_mmu_flags;   /* used by head.S */

   unsigned long      __cpu_flush;    /* used by head.S */

   const char      *arch_name;

   const char      *elf_name;

   unsigned int    elf_hwcap;

   const char      *cpu_name;

   struct processor   *proc;

   struct cpu_tlb_fns *tlb;

   struct cpu_user_fns   *user;

   struct cpu_cache_fns  *cache;

};
第一項是CPU id,將與協處理器中讀出的id作比較,其餘的字段也都是與處理器相關的信息,到下面初始化的過程中自然會用到。
 

另外,這個例程加載符地址的代碼也是挺值得我輩學習的:
   adr   r3, 3f
加載一個符號的地址,這個符號在加載語句前面(下面)定義,forward嘛,這個符號爲3,離這條語句最近的那個。在那個符號爲3的位置我們看到這樣的代碼:
   .align 2

3: .long __proc_info_begin

   .long __proc_info_end

4: .long .

   .long __arch_info_begin

   .long __arch_info_end

搜索這兩個符號的值,在文件arch/arm/kernel/vmlinux.lds.S中:
      __proc_info_begin = .;

         *(.proc.info.init)

      __proc_info_end = .;

這兩個符號分別是一種初始化的段的結束開始地址和結束地址。爲了瞭解由struct proc_info_list結構體組成的段的實際構成,我們還是得要了解一下在系統中到底都有哪些變量是聲明瞭要被放到這個段的。用關鍵字.proc.info.init來搜,全部都是arch/arm/mm/proc-*.S文件,這些都是特定於處理器的彙編語言文件,對於我們的mini2440, 自然是要看proc-arm920.S文件的,在其中可以看到這些內容:
   .section ".proc.info.init", #alloc, #execinstr

   .type __arm920_proc_info,#object

__arm920_proc_info:

   .long 0x41009200

   .long 0xff00fff0

   .long   PMD_TYPE_SECT | \

      PMD_SECT_BUFFERABLE | \

      PMD_SECT_CACHEABLE | \

      PMD_BIT4 | \

      PMD_SECT_AP_WRITE | \

      PMD_SECT_AP_READ

   .long   PMD_TYPE_SECT | \

      PMD_BIT4 | \

      PMD_SECT_AP_WRITE | \

      PMD_SECT_AP_READ

   b  __arm920_setup

   .long cpu_arch_name

   .long cpu_elf_name

   .long HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB

   .long cpu_arm920_name

   .long arm920_processor_functions

   .long v4wbi_tlb_fns

   .long v4wb_user_fns

#ifndef CONFIG_CPU_DCACHE_WRITETHROUGH

   .long arm920_cache_fns

#else

   .long v4wt_cache_fns

#endif

   .size __arm920_proc_info, . - __arm920_proc_info

 

看到這兒我們再回國頭去看__lookup_processor_type的代碼:
    ldmia r3, {r5 - r7}       

   add   r3, r3, #8

   sub   r3, r3, r7

儘管符號3處只有兩個有效值,但它加載了三個數,而第三個數,我們看到是這樣定義的:
.long .

 

__lookup_processor_type中,給r3加上8,也就是讓r3指向“.”的地址,然後用r3減r7來獲取虛擬地址與物理地址的差,這樣看來,“.”就應該是虛擬空間(編譯地址)裏那個數據的地址。
 

之後的代碼獲得__proc_info_begin和__arch_info_end這兩個符號在物理空間中的地址:
    add r5, r5, r3        @ convert virt addresses to

    add r6, r6, r3

 

然後便是在那個段中逐個的檢查struct proc_info_list結構體,以找到與我們的CPU相匹配的:
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

 

__lookup_processor_type例程會返回在文件arch/arm/mm/proc-arm920.S中定義的一個保存有與我們的處理器相關的信息的struct proc_info_list結構體的地址。
 

接下來我們繼續看stext的代碼:
   bl __lookup_machine_type    @ r5=machinfo

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

   beq   __error_a       @ yes, error 'a'

在獲得了處理器信息之後,則調用__lookup_machine_type來查找機器信息。這個例程同樣也在arch/arm/kernel/head-common.S文件中定義。這個例程的定義如下:
__lookup_machine_type:

   adr   r3, 4b

   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)

 

處理的過程和上面的__lookup_processor_type還是挺相似的。這個例程接收r1中傳進來的機器號作爲參數,然後,在一個由struct machine_desc結構體組成的段中查找和我們的機器號匹配的struct machine_desc結構體,這個結構體在arch/arm/include/asm/mach/arch.h文件中定義,用於保存機器的信息:
struct machine_desc {

   /*

    * Note! The first four elements are used

    * by assembler code in head.S, head-common.S

    */

   unsigned int    nr;      /* architecture number   */

   unsigned int    phys_io; /* start of physical io  */

   unsigned int    io_pg_offst; /* byte offset for io

                    * page tabe entry */

   const char      *name;   /* architecture name  */

   unsigned long      boot_params; /* tagged list     */

   unsigned int    video_start; /* start of video RAM */

   unsigned int    video_end;  /* end of video RAM   */

   unsigned int    reserve_lp0 :1; /* never has lp0   */

   unsigned int    reserve_lp1 :1; /* never has lp1   */

   unsigned int    reserve_lp2 :1; /* never has lp2   */

   unsigned int    soft_reboot :1; /* soft reboot     */

   void        (*fixup)(struct machine_desc *,

                 struct tag *, char **,

                 struct meminfo *);

   void        (*map_io)(void);/* IO mapping function  */

   void        (*init_irq)(void);

   struct sys_timer   *timer;     /* system tick timer  */

   void        (*init_machine)(void);

};

 

同樣這個例程也用到了同上面很相似的方式來獲得符號的地址:
   adr   r3, 4b
b代表back,即向後,這個符號爲4,緊接着我們前面看到的那個爲3的標號:
4: .long .

   .long __arch_info_begin

   .long __arch_info_end

在文件arch/arm/kernel/vmlinux.lds.S中我們可以看到段的定義:
      __arch_info_begin = .;

         *(.arch.info.init)

      __arch_info_end = .;

這兩個符號也是分別表示某種初始化的段的開始地址和結束地址。爲了找到段的填充內容,還是得要了解一下到底都有哪些struct machine_desc結構體類型變量聲明瞭要被放到這個段的。用關鍵字.arch.info.init 來搜索所有的內核源文件。在arch/arm/include/asm/mach/arch.h文件中我們看到:
#define MACHINE_START(_type,_name)      \

static const struct machine_desc __mach_desc_##_type \

 __used                     \

 __attribute__((__section__(".arch.info.init"))) = { \

   .nr      = MACH_TYPE_##_type,     \

   .name    = _name,

 

#define MACHINE_END            \

};

定義機器結構體,也就是.arch.info.init段中的內容,都是要通過兩個宏MACHINE_START和MACHINE_END來完成的啊,MACHINE_START宏定義一個truct machine_desc結構體,並初始化它的機器號字段和機器名字段,可以在arch/arm/tools/mach-types文件中看到各種平臺的機器號的定義。那接着我們來搜MACHINE_START吧,這是一個用於定義機器結構體的宏,所以可以看到這個符號好像都是在arch/arm/mach-*/mach-*.c這樣的文件中出現的,我們感興趣的應該是arch/arm/mach-s3c2440/ mach-mini2440.c文件中的這個符號:
MACHINE_START(MINI2440, "MINI2440")

   /* Maintainer: Michel Pollet */

   .phys_io = S3C2410_PA_UART,

   .io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,

   .boot_params = S3C2410_SDRAM_PA + 0x100,

   .map_io     = mini2440_map_io,

   .init_machine   = mini2440_init,

   .init_irq = s3c24xx_init_irq,

   .timer   = &s3c24xx_timer,

MACHINE_END

OK, __lookup_machine_type這個例程的我們也搞明白了。回憶一下,啓動代碼現在已經完成的工作,R10寄存器中爲指向proc_info_list結構體的指針(物理地址空間),這個結構體包含有關於我們的處理器的一些重要信息。R8寄存器中爲指向一個與我們的平臺相匹配的machine_desc結構體的指針,這個結構體中保存有一些關於我們的平臺的重要信息。
 

回來接着看arch/arm/kernel/head.S文件中的stext:
   bl __vet_atags

這個例程同樣同樣也是在arch/arm/kernel/head-common.S文件中定義:
__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)

這個例程接收機器信息(R8寄存器)爲參數,並檢測r2中傳入的ATAGS 指針的合法性。內核使用tag來作爲bootloader傳遞內核參數的方式。系統要求r2中傳進來的ATAGS指針式4字節對齊的,同時要求ATAGS列表的第一個tag是一個ATAG_CORE類型的。
 

此時R10寄存器中保存有指向CPU信息結構體的指針,R8寄存器中保存有指向機器結構體的指針,R2寄存器中保存有指向tag表的指針,R9中還保存有CPU ID信息。
 

回到arch/arm/kernel/head.S文件中的stext,之後就要進入初始化過程中比較關鍵的一步了,開始設置mmu,但首先要填充一個臨時的內核頁表,映射4m的內存,這在初始化過程中是足夠了:
bl  __create_page_tables

 

這個例程設置初始頁表,這裏只設置最起碼的數量,只要能使內核運行即可,r8  = machinfo,r9  = cpuid,r10 = procinfo,在r4寄存器中返回物理頁表地址。
__create_page_tables例程在文件arch/arm/kernel/head.S中定義:
__create_page_tables:

   pgtbl r4          @ page table address

// pgtbl是一個宏,本文件的前面部分有定義:
// .macro pgtbl, rd

// ldr   \rd, =(KERNEL_RAM_PADDR - 0x4000)

// .endm

// KERNEL_RAM_PADDR在本文件的前面有定義,爲(PHYS_OFFSET + TEXT_OFFSET)

// PHYS_OFFSET在arch/arm/mach-s3c2410/include/mach/memory.h定義,
// 爲UL(0x30000000)

// 而TEXT_OFFSET在arch/arm/Makefile中定義,爲內核鏡像在內存中到內存
// 開始位置的偏移(字節),爲$(textofs-y)

// textofs-y也在文件arch/arm/Makefile中定義,
// 爲textofs-y   := 0x00008000

// r4 = 30004000爲臨時頁表的起始地址
 

// 首先即是初始化16K的頁表,高12位虛擬地址爲頁表索引,所以爲
// 4K*4 = 16K,大頁表,每一個頁表項,映射1MB虛擬地址。
// 這個地方還來了個循環展開,以優化性能。
   mov   r0, r4

   mov   r3, #0

   add   r6, r0, #0x4000

1: str   r3, [r0], #4

   str   r3, [r0], #4

   str   r3, [r0], #4

   str   r3, [r0], #4

   teq   r0, r6

   bne   1b

 

    ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags

// PROCINFO_MM_MMUFLAGS在arch/arm/kernel/asm-offsets.c文件中定義,
// 爲DEFINE(PROCINFO_MM_MMUFLAGS, 

// offsetof(struct proc_info_list, __cpu_mm_mmu_flags));

// R10寄存器保存的指針指向是我們前面找到的proc_info_list結構嘛。
 

// 爲內核的第一個MB創建一致的映射,以爲打開MMU做準備,這個映射將會被
// paging_init()移除,這裏使用程序計數器來獲得相應的段的基地址。
// 這個地方是直接映射。
   mov   r6, pc

   mov   r6, r6, lsr #20       @ start of kernel section

   orr   r3, r7, r6, lsl #20      @ flags + kernel base

   str   r3, [r4, r6, lsl #2]     @ identity mapping

 

// 接下來爲內核的直接映射區設置頁表。KERNEL_START在文件的前面定義,
// 爲KERNEL_RAM_VADDR,即內核的虛擬地址。
// 而KERNEL_RAM_VADDR在文件的前面定義,則爲(PAGE_OFFSET + TEXT_OFFSET)

// 映射完整的內核代碼段,初始化數據段。
// PAGE_OFFSET爲內核鏡像開始的虛擬地址,在
// arch/arm/include/asm/memory.h中定義。在配置內核時選定具體值,默認
// 爲0xC0000000。
// 因爲最高12位的值是頁表中的偏移地址,而第三高的四位必然爲0,
// 每個頁表項爲4字節,右移20位之後,還得再左移兩位回來,所以,這裏只// 是左移18位。
// R3寄存器在經過了上面的操作之後,實際上是變成了指向內核鏡像代碼段
// 的指針(物理地址),在這個地方,再一次爲內核鏡像的第一個MB做了映射。
// R6隨後指向了內核鏡像的尾部。R0爲頁表項指針。
// 這裏以1MB爲單位來映射內核鏡像。
   add   r0, r4,  #(KERNEL_START & 0xff000000) >> 18

   str   r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!

   ldr   r6, =(KERNEL_END - 1)

   add   r0, r0, #4

   add   r6, r4, r6, lsr #18  //得到頁表的結束物理地址
1: cmp   r0, r6

   add   r3, r3, #1 << 20

   strls r3, [r0], #4

   bls   1b

 

// 爲了使用啓動參數,將物理內存的第一MB映射到內核虛擬地址空間的
// 第一個MB,r4存放的是頁表的地址。這裏的PAGE_OFFSET的虛擬地址
// 比上面的KERNEL_START要小0x8000

   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]

// 上面的這個步驟顯得似乎有些多餘。
 

// 總結一下,這個建立臨時頁表的過程:
// 1、爲內核鏡像的第一個MB建立直接映射
// 2、爲內核鏡像完整的建立從虛擬地址到物理地址的映射
// 3、爲物理內存的第一個MB建立到內核的虛擬地址空間的第一個MB的映射。
 

// OK,內核的臨時頁表建立完畢。整個初始化臨時頁表的過程都沒有修改R8,
// R9和R10。
   mov   pc, lr

ENDPROC(__create_page_tables)

 

回到stext:
   ldr   r13, __switch_data    @ address to jump to after

                              @ mmu has been enabled

這個地方實際上是在r13中保存了另一個例程的地址。後面的分析中,遇到執行到這個例程的情況時會有詳細說明。
 

接着看stext:
   adr   lr, BSYM(__enable_mmu)      @ return (PIC) address

BSYM()是一個宏,在文件arch/arm/include/asm/unified.h中定義,爲:
#define BSYM(sym) sym

也就是說這個語句也僅僅是把__enable_mmu例程的地址加載進lr寄存器中。爲了方便之後調用的函數返回時,直接執行__enable_mmu例程。
 

接着看stext下一句:
 ARM( add   pc, r10, #PROCINFO_INITFUNC )

ARM()也是一個宏,同樣在文件arch/arm/include/asm/unified.h中定義,當配置內核爲生成ARM鏡像,則爲:#define ARM(x...)  x

 

所以這一條語句也就是在調用一個例程。R10中保存的是procinfo結構的地址。PROCINFO_INITFUNC符號在arch/arm/kernel/asm-offsets.c文件中定義,爲:
DEFINE(PROCINFO_INITFUNC, offsetof(struct proc_info_list, __cpu_flush));

 

也就是調用結構體proc_info_list的__cpu_flush成員函數。回去查看arch/arm/mm/proc-arm920.S文件中struct proc_info_list結構體的變量的定義,可以看到這個成員爲:
   b  __arm920_setup

 

也就是說,在設置好內核臨時頁表之後調用了例程__arm920_setup,這個例程同樣在arch/arm/mm/proc-arm920.S中:
__arm920_setup:

   mov   r0, #0

   mcr   p15, 0, r0, c7, c7    @ invalidate I,D caches on v4

   mcr   p15, 0, r0, c7, c10, 4      @ drain write buffer on v4

#ifdef CONFIG_MMU

   mcr   p15, 0, r0, c8, c7    @ invalidate I,D TLBs on v4

#endif

   adr   r5, arm920_crval

   ldmia r5, {r5, r6}

   mrc   p15, 0, r0, c1, c0    @ get control register v4

   bic   r0, r0, r5

   orr   r0, r0, r6

   mov   pc, lr

這一段首先使i,d caches內容無效,然後清除write buffer,接着使TLB內容無效。接下來加載變量arm920_crval的地址,我們看到arm920_crval變量的內容爲:
rm920_crval:

   crval clear=0x00003f3f, mmuset=0x00003135, ucset=0x00001130

 

crval爲一個宏,在arch/arm/mm/proc-macros.S中定義:
   .macro crval, clear, mmuset, ucset

#ifdef CONFIG_MMU

   .word \clear

   .word \mmuset

#else

   .word \clear

   .word \ucset

#endif

   .endm

其實也就是定義兩個變量而已。之後,在r0中,得到了我們想要往協處理器相應寄存器中寫入的內容。
 

之後的 __arm920_setup返回,mov  pc, lr,即是調用例程__enable_mmu,這個例程在文件arch/arm/kernel/head.S中:
__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

   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

在這兒設置了頁目錄地址(r4寄存器中保存),然後設置domain的保護,在前面建立頁表的例程中,注意到,頁表項的控制信息,是從struct proc_info_list結構體的某字段中取的,其頁目錄項的 domain都是0,domain寄存器中的domain 0對應的是0b11,表示訪問模式爲manager,不受限制。在這裏同時也完成r0的某些位的進一步設置。
 

然後,__enable_mmu例程又調用了__turn_mmu_on,在同一個文件中定義:
__turn_mmu_on:

   mov   r0, r0

   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, r13

   mov   pc, r3

ENDPROC(__turn_mmu_on)


接下來寫控制寄存器:
mcr p15, 0, r0, c1, c0 ,0

一切設置就此生效,到此算是完成了打開d,icache和mmu的工作。
注意:arm的d cache必須和mmu一起打開,而i cache可以單獨打開。其實,cache和mmu的關係實在是緊密,每一個頁表項都有標誌標示是否是cacheable的,可以說本來就是設計一起使用的

前面有提到過,r13中存放的其實是另外一個例程的地址,其值是變量__switch_data的第一個字段,即一個函數指針的值,__switch_data變量是在arch/arm/kernel/head-common.S中定義的:
__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

前面的ldr r13 __switch_data,實際上也就是加載符號__mmap_switched的地址,實際上__mmap_switched是一個arch/arm/kernel/head-common.S中定義的例程。接着來看這個例程的定義,在arch/arm/kernel/head-common.S文件中:
__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

   b  start_kernel

ENDPROC(__mmap_switched)

 

這個例程完成如下工作:
1、使r3指向__switch_data變量的第二個字段(從1開始計數)。
2、執行了一條加載指令,也就是在r4, r5, r6, r7寄存器中分別加載4個符號__data_loc,_data, __bss_start ,_end的地址,這四個符號都是在鏈接腳本arch/arm/kernel/vmlinux.lds.S中出現的,標識了鏡像各個段的地址,我們應該不難猜出他們所代表的段。
3、如果需要的話則複製數據段(數據段和BSS段是緊鄰的)。
4、初始化BSS段,全部清零,BSS是未初始化的全局變量區域。
5、又看到一條加載指令,同樣在一組寄存器中加載借個符號的地址,r4中爲processor_id,r5中爲__machine_arch_type, r6中爲__atags_pointer, r7中爲cr_alignment ,sp中爲init_thread_union + THREAD_START_SP。
6、接着我們看到下面的幾條語句,則是用前面獲取的信息來初始化那些全局變量r9,機器號被保存到processor_id處;r1寄存器的值,機器號,被保存到變量__machine_arch_type中,其他的也一樣。
7、重新設置堆棧指針,指向init_task的堆棧。init_task是系統的第一個任務,init_task的堆棧在task structure的後8K,我們後面會看到。
8、最後就要跳到C代碼的 start_kernel。
   b  start_kernel
到此爲止,彙編部分的初始化代碼就結束了
 

O,My God.初始化代碼的彙編部分終於結束。從而進入了與體系結構無關的Linux內核部分。start_kernel()會調用一系列初始化函數來設置中斷,執行進一步的內存配置。
 

現在讓我們來回憶一下目前的系統狀態:
臨時頁表已經建立,在0X30004000處,映射了映像文件大小空間,虛地址0XC000000被映射到0X30000000。CACHE,MMU 都已經打開。堆棧用的是任務init_task的堆棧。

 

如果以爲到了c代碼可以鬆一口氣的話,就大錯特措了,linux的c也不比彙編好懂多少,相反倒掩蓋了彙編的一些和機器相關的部分,有時候更難懂。其實作 爲編寫操作系統的c代碼,只不過是彙編的另一種寫法,和機器代碼的聯繫是很緊密的。另外,這些start_kernel()中調用的C函數,每一個都具有舉足輕重的地位,它們中的許多都肩負着初始化內核中的某個子系統的重要使命,而Linux內核中每一個子系統都錯綜複雜,牽涉到各種軟件、硬件的複雜算法,所以理解起來倒真的是挺困難的。

 

start_kernel函數在 init/main.c中定義:
528 asmlinkage void __init start_kernel(void)

529 {

530   char * command_line;

531   extern struct kernel_param __start___param[], __stop___param[];

532

533   smp_setup_processor_id();

534

535   /*

536    * Need to run as early as possible, to initialize the

537    * lockdep hash:

538    */

539   lockdep_init();

540   debug_objects_early_init();

541

542   /*

543    * Set up the the initial canary ASAP:

544    */

545   boot_init_stack_canary();

546

547   cgroup_init_early();

548

549   local_irq_disable();

550   early_boot_irqs_off();

551   early_init_irq_lock_class();

552

553 /*

554  * Interrupts are still disabled. Do necessary setups, then

555  * enable them

556  */

557   lock_kernel();

558   tick_init();

559   boot_cpu_init();

560   page_address_init();

561   printk(KERN_NOTICE "%s", linux_banner);

562   setup_arch(&command_line);

563   mm_init_owner(&init_mm, &init_task);

564   setup_command_line(command_line);

565   setup_nr_cpu_ids();

566   setup_per_cpu_areas();

567   smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */

568

569   build_all_zonelists();

570   page_alloc_init();

571

572 printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line);

573   parse_early_param();

574   parse_args("Booting kernel", static_command_line,

575              __start___param, __stop___param - __start___param,

576              &unknown_bootoption);

577   /*

578    * These use large bootmem allocations and must precede

579    * kmem_cache_init()

580    */

581   pidhash_init();

582   vfs_caches_init_early();

583   sort_main_extable();

584   trap_init();

585   mm_init();

586   /*

587    * Set up the scheduler prior starting any interrupts (such as the

588    * timer interrupt). Full topology setup happens at smp_init()

589    * time - but meanwhile we still have a functioning scheduler.

590   */

591   sched_init();

592   /*

593    * Disable preemption - early bootup scheduling is extremely

594    * fragile until we cpu_idle() for the first time.

595    */

596   preempt_disable();

597   if (!irqs_disabled()) {

598        printk(KERN_WARNING "start_kernel(): bug: interrupts were "

599                            "enabled *very* early, fixing it\n");

600        local_irq_disable();

601   }

602   rcu_init();

603   radix_tree_init();

604   /* init some links before init_ISA_irqs() */

605   early_irq_init();

606   init_IRQ();

607   prio_tree_init();

608   init_timers();

609   hrtimers_init();

610   softirq_init();

611   timekeeping_init();

612   time_init();

613   profile_init();

614   if (!irqs_disabled())

615           printk(KERN_CRIT "start_kernel(): bug: interrupts were "

616                            "enabled early\n");

617   early_boot_irqs_on();

618   local_irq_enable();

619

620   /* Interrupts are enabled now so all GFP allocations are safe. */

621   gfp_allowed_mask = __GFP_BITS_MASK;

622

623   kmem_cache_init_late();

624

625   /*

626    * HACK ALERT! This is early. We're enabling the console before

627    * we've done PCI setups etc, and console_init() must be aware of

628    * this. But we do want output early, in case something goes wrong.

629    */

630   console_init();

631   if (panic_later)

632           panic(panic_later, panic_param);

633

634   lockdep_info();

635

636   /*

637    * Need to run this when irqs are enabled, because it wants

638    * to self-test [hard/soft]-irqs on/off lock inversion bugs

639    * too:

640    */

641   locking_selftest();

642

643 #ifdef CONFIG_BLK_DEV_INITRD

644   if (initrd_start && !initrd_below_start_ok &&

645   page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {

646   printk(KERN_CRIT "initrd overwritten (0x%08lx < 0x%08lx) - "

647               "disabling it.\n",

648               page_to_pfn(virt_to_page((void *)initrd_start)),

649               min_low_pfn);

650               initrd_start = 0;

651   }

652 #endif

653   page_cgroup_init();

654   enable_debug_pagealloc();

655   kmemtrace_init();

656   kmemleak_init();

657   debug_objects_mem_init();

658   idr_init_cache();

659   setup_per_cpu_pageset();

660   numa_policy_init();

661   if (late_time_init)

662   late_time_init();

663   sched_clock_init();

664   calibrate_delay();

665   pidmap_init();

666   anon_vma_init();

667 #ifdef CONFIG_X86

668   if (efi_enabled)

669           efi_enter_virtual_mode();

670 #endif

671   thread_info_cache_init();

672   cred_init();

673   fork_init(totalram_pages);

674   proc_caches_init();

675   buffer_init();

676   key_init();

677   security_init();

678   vfs_caches_init(totalram_pages);

679   signals_init();

680   /* rootfs populating might need page-writeback */

681   page_writeback_init();

682 #ifdef CONFIG_PROC_FS

683   proc_root_init();

684 #endif

685   cgroup_init();

686   cpuset_init();

687   taskstats_init_early();

688   delayacct_init();

689

690   check_bugs();

691

692   acpi_early_init(); /* before LAPIC and SMP init */

693   sfi_init_late();

694

695   ftrace_init();

696

697   /* Do the rest non-__init'ed, we're now alive */

698   rest_init();

699 }

 

接着我們來近距離的觀察一下start_kernel函數中調用的這些重量級的函數。
 

首先來看setup_arch(&command_line)函數,這個函數(對於我們的mini2440平臺來說)在arch/arm/kernel/setup.c中定義:
664 void __init setup_arch(char **cmdline_p)

665 {

666   struct tag *tags = (struct tag *)&init_tags;

667   struct machine_desc *mdesc;

668   char *from = default_command_line;

669

670   unwind_init();

671

672   setup_processor();

673   mdesc = setup_machine(machine_arch_type);

674   machine_name = mdesc->name;

675

676   if (mdesc->soft_reboot)

677          reboot_setup("s");

678

679   if (__atags_pointer)

680           tags = phys_to_virt(__atags_pointer);

681   else if (mdesc->boot_params)

682           tags = phys_to_virt(mdesc->boot_params);

683

684   /*

685    * If we have the old style parameters, convert them to

686    * a tag list.

687    */

688   if (tags->hdr.tag != ATAG_CORE)

689           convert_to_tag_list(tags);

690   if (tags->hdr.tag != ATAG_CORE)

691           tags = (struct tag *)&init_tags;

692

693   if (mdesc->fixup)

694           mdesc->fixup(mdesc, tags, &from, &meminfo);

695

696   if (tags->hdr.tag == ATAG_CORE) {

697           if (meminfo.nr_banks != 0)

698                   squash_mem_tags(tags);

699          save_atags(tags);

700          parse_tags(tags);

701   }

702

703   init_mm.start_code = (unsigned long) _text;

704   init_mm.end_code   = (unsigned long) _etext;

705   init_mm.end_data   = (unsigned long) _edata;

706   init_mm.brk        = (unsigned long) _end;

707

708   /* parse_early_param needs a boot_command_line */

709   strlcpy(boot_command_line, from, COMMAND_LINE_SIZE);

710

711   /* populate cmd_line too for later use, preserving boot_command_line */

712   strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);

713   *cmdline_p = cmd_line;

714

715   parse_early_param();

716

717   paging_init(mdesc);

718   request_standard_resources(&meminfo, mdesc);

719

720 #ifdef CONFIG_SMP

721   smp_init_cpus();

722 #endif

723

724   cpu_init();

725   tcm_init();

726

727   /*

728    * Set up various architecture-specific pointers

729    */

730   init_arch_irq = mdesc->init_irq;

731   system_timer = mdesc->timer;

732   init_machine = mdesc->init_machine;

733

734 #ifdef CONFIG_VT

735 #if defined(CONFIG_VGA_CONSOLE)

736   conswitchp = &vga_con;

737 #elif defined(CONFIG_DUMMY_CONSOLE)

738   conswitchp = &dummy_con;

739 #endif

740 #endif

741   early_trap_init();

742 }

來看一些我們比較感興趣的地方:
1、666行,struct tag指針類型的局部變量指向了默認的tag列表init_tags,該靜態變量在setup_arch()定義同文件的前面有如下定義:
636 /*

637  * This holds our defaults.

638  */

639 static struct init_tags {

640         struct tag_header hdr1;

641         struct tag_core   core;

642         struct tag_header hdr2;

643         struct tag_mem32  mem;

644         struct tag_header hdr3;

645 } init_tags __initdata = {

646         { tag_size(tag_core), ATAG_CORE },

647         { 1, PAGE_SIZE, 0xff },

648         { tag_size(tag_mem32), ATAG_MEM },

649         { MEM_SIZE, PHYS_OFFSET },

650         { 0, ATAG_NONE }

651 };

第679行檢察__atags_pointer指針的有效性,這個指針是在前面,跳轉到start_kernel函數的彙編例程最後設置的幾個變量之一,用的是R2寄存器的值。如果bootloader通過R2傳遞了tag列表的話,自然是要使用bootloader穿的進來的tag列表的。

2、第688行的字符指針類型的局部變量from指向了default_command_line靜態變量,這個變量同樣在前面有定義:
124 static char default_command_line[COMMAND_LINE_SIZE] __initdata = CONFIG_CMDLINE;

傳遞給內核的命令行參數,是可以在內核配置的時候設置的。
3、第673行以machine_arch_type爲參數調用了setup_machine()函數,而這個函數的定義爲:
 static struct machine_desc * __init setup_machine(unsigned int nr)

 {

         struct machine_desc *list;

         /*

          * locate machine in the list of supported machines.

          */

         list = lookup_machine_type(nr);

         if (!list) {

                 printk("Machine configuration botched (nr %d), "

                        " unable to continue.\n", nr);

                 while (1);

         }

         printk("Machine: %s\n", list->name);

         return list;

 }

在arch/arm/kernel/head-common.S文件中,我們看到了一個對於__lookup_machine_type例程的封裝的可被C語言程序調用的彙編語言編寫的函數lookup_machine_type(),接收機器號,查表,然後返回匹配的struct machine_desc結構體的指針。在這裏,對於我們的mini2440,返回的自然是arch/arm/mach-s3c2440/ mach-mini2440.c文件中定義的結構體了:
MACHINE_START(MINI2440, "MINI2440")
   /* Maintainer: Michel Pollet */
   .phys_io = S3C2410_PA_UART,
   .io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
   .boot_params = S3C2410_SDRAM_PA + 0x100,
   .map_io     = mini2440_map_io,
   .init_machine   = mini2440_init,
   .init_irq = s3c24xx_init_irq,
   .timer   = &s3c24xx_timer,
MACHINE_END

然後,machine_desc結構體的name成員的值被賦給全局變量machine_name。
第681行,若bootloader沒有傳遞tag列表給內核,則檢測machine_desc結構體的boot_params字段,看看特定的平臺是否傳遞了標記列表。
第730、731、732行分別將machine_desc結構體的init_irq、timer和init_machine成員值賦給了三個全局變量init_arch_irq、system_timer和init_machine,即是設置特定體系結構的指針。初始化的後面階段自然會用到。
 

start_kernel()函數調用同文件下的rest_init(void)函數,rest_init(void)函數調用 kernel_thread()函數以啓動第一個核心線程,該線程執行kernel_init()函數,而原執行序列會調用cpu_idle(),等待調度。
作爲核心線程的kernel_init()函數繼續完成一些設置,並在最後調用同文件下的init_post()函數,而該函數掛在根文件系統,打開/dev/console設備,重定向stdin、stdout和stderr到控制檯。之後,它搜索文件系統中的init程序(也可以由“init=”命令行參數指定init程序),並使用run_init_process()函數執行init程序。(事實上,run_init_process()函數又調用了kernel_execve()來實際執行程序)。搜索init程序的順序爲/sbin/init、/etc/init、/bin/init、和/bin/sh。在嵌入式系統中,多數情況下,可以給內核傳入一個簡單的shell腳本來啓動必需的嵌入式應用程序。
至此,漫長的Linux內核引導和啓動過程就結束了,而kernel_init()對應的由rest_init(void)函數創建的第一個線程也進入用戶模式。
 

參考文獻:
arm 嵌入式LINUX啓動過程:
http://blog.ednchina.com/yujiebaomei/4153/message.aspx

http://www.cnblogs.com/bluepointcq/articles/490954.html

Linux設備驅動開發詳解,宋寶華

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章