ARM Linux啓動分析----head-armv.S內幕

Linux啓動後執行的第一個文件是arch/arm/kernel下的head-($PROCESSOR).S文件,processor代表的是該cpu的類型。ARM 6及其以後的處理器核心支持32位地址空間。這些處理器可以在26位和 32位PC的模式下操作。在26位PC模式下,R15寄存器的表現如同在以前的處理器上,代碼只能運行在地址空間的最低的64M字節空間中。在32位PC模式下,32位的R15寄存器被用做程序計數器。使用獨立的狀態寄存器來存儲處理器模式和狀態標誌。對於26位的arm處理器類型,linux用armo來表示;對於32位的arm處理器,使用armv表示。在include/linux/autoconf.h文件中通過
#define CONFIG_CPU_32   1
將處理器類型設置爲支持32位PC模式。然後在arch/arm/Makefile中通過
ifeq ($(CONFIG_CPU_32),y)
PROCESSOR = armv
TEXTADDR = 0xC0008000
LDSCRIPT = arch/arm/vmlinux-armv.lds.in
endif
設置處理器類型爲armv,這樣linux運行所執行的第一個文件就是head-armv.S。接着,Makefile定義了內核中代碼和數據所使用的虛擬地址TEXRADDR,最後,定義了鏈接器所使用的腳本文件,這個文件也是與處理器類型相關的。
在執行head-armv.S文件之前,有一點需要注意的是,bootloader已經在處理器的R1寄存器中存放了機器體系結構的類型號。由於在文件的執行過程中將要針對當前的機器體系結構設置相關的參數,如果沒有這個步驟,系統將顯示“ERROR:a”,同時停止執行。當然,也可以在head-armv.S文件的開頭添加代碼,手工對R1賦值,具體的機器類型號在arch/arm/tools/mach-types文件中。
好了,接下來我們可以開始閱讀head-armv.S文件了,看看它到底作了些什麼事情。由於篇幅的限制,對一些不是很關鍵的代碼和英文註釋予以省略,但是在每段代碼後,我會根據自己的理解給出解釋。
 
#if (TEXTADDR & 0xffff) != 0x8000
#error TEXTADDR must start at 0xXXXX8000
#endif
 
      .globl  SYMBOL_NAME(swapper_pg_dir)
      .equ    SYMBOL_NAME(swapper_pg_dir), TEXTADDR - 0x4000
 
      .macro pgtbl, reg, rambase
      adr   \reg, stext
      sub   \reg, \reg, #0x4000
      .endm
 
      .macro krnladr, rd, pgtable, rambase
      bic   \rd, \pgtable, #0x000ff000
      .endm
首先,系統確保TEXTADDR的地址是以0x8000結尾的,前面已經提到過,TEXTADDR的地址是0xC0008000,是內核所使用的虛擬地址,而我所使用的PXA255處理器上支持的SDRAM空間是從0xA0000000開始的,這就需要通過MMU進行虛擬地址到實際物理地址的轉換,也就是說將0xC0008000映射到0xA0008000。地址轉換所使用的頁表將存放在從0xA0008000網上的16K空間中,即從0xA0004000到0xA0008000這一段。因此,系統必須空出0xA0000000到0xA0008000這一段,存放頁表和其它的一些內核將使用到的數據結構。雖然上面的代碼判斷的是TEXTADDR的地址是否以0x8000結尾,但從效果上說是一樣的。
接着,代碼定義了全局變量swapper_pg_dir,它是頁表目錄項的虛擬地址。前面用SYMBOL_NAME()修飾,這是因爲在有的系統中,C編繹器對.C文件中的符號名有"_"前綴,SYMBOL_NAME()可以使彙編代碼也適應這種變化。但是在當前的Linux中,SYMBOL_NAME實際上不起任何作用。大家可以參考include/linux/linkage.h中對該修飾符的定義。
然後,代碼定義了pgtbl和krnladr兩個宏。Pgtbl宏得到的是與位置無關的頁表目錄項地址,值爲0xA000800往上16k的地址,即0xA0004000。stext所代表的也是內核的起始地址,通過arch/arm/vmlinux-armv.lds.in的鏈接腳本可以發現它在內核中的鏈接地址和TEXTADDR一致。那麼爲什麼頁表地址不是0xC0004000呢?因爲我們在定義/reg寄存器時使用的adr指令,adr指令是在當前的PC值上+/-一個標號的偏移得到的, 所以得到的地址只跟PC和標號到PC的偏移相關, 跟編譯地址無關。在MMU打開前, 代碼要是地址無關的, 會經常用到adr指令。由於當前的PC運行的地址是從0xA0008000開始的地址空間,所以最後得到的頁表地址爲0xA0004000。krnladr宏需要配合其它代碼使用,它的本意是爲了使對從0xA0000000開始的內核的地址空間的尋址不會因爲MMU的原因而被映射到其它的地址。因此需要將定義0xA0000000地址轉換的頁表項中的值的高20位定義爲0xA0000,最低的12位保存的是頁表的標誌位。由於該頁表項的索引值是由地址的最高12位所決定的,因此krnladr宏將地址的最低20位清零。在本文件中,只清空了第4--11位,是因爲有其它的代碼屏蔽了低12位的作用。如果將上面的代碼改成bic \rd, \pgtable, #0x000fffff,效果是一樣的。
 
.section ".text.init",#alloc,#execinstr
     .type stext, #function
ENTRY(stext)
     mov   r12, r0
 
mov r0, #F_BIT | I_BIT | MODE_SVC @ make sure svc mode
msr cpsr_c, r0        @ and all irqs disabled
bl     __lookup_processor_type
teq r10, #0           @ invalid processor?
moveq r0, #'p'       @ yes, error 'p'
beq __error
bl     __lookup_architecture_type
teq r7, #0            @ invalid architecture?
moveq r0, #'a'       @ yes, error 'a'
beq __error
bl  __create_page_tables
adr lr, __ret         @ return address
add  pc, r10, #12         @ initialise processor
                    @ (return control reg)
接着我們進入了head-armv.S的主程序段,參考上面的代碼。首先,確保處理器進入SVC模式,屏蔽所有外部中斷。接着查詢處理器類型和機器的體系結構類型,其中任何一步發生錯誤,顯示“ERROR:p”或者“ERROR:a”。然後建立頁表目錄項。我們來看看每個子程序段具體是如何工作的。
 
__lookup_processor_type:
      adr       r5, 2f
      ldmia r5, {r7, r9, r10}
      sub   r5, r5, r10               @ convert addresses
      add       r7, r7, r5            @ to our address space
      add       r10, r9, r5
      mrc       p15, 0, r9, c0, c0   @ get processor id
1:    ldmia r10, {r5, r6, r8}    @ value, mask, mmuflags
      and       r6, r6, r9            @ mask wanted bits
      teq       r5, r6
      moveq pc, lr
      add       r10, r10, #36        @ sizeof(proc_info_list)
      cmp       r10, r7
      blt       1b
      mov       r10, #0               @ unknown processor
      mov       pc, lr
 
2:    .long __proc_info_end
      .long __proc_info_begin
      .long 2b
      .long __arch_info_begin
      .long __arch_info_end
代碼首先在R5寄存器中存放標號2所代表的相對地址,然後通過ldmia r5, {r7, r9, r10}在R7和R9中放置__proc_info_end、__proc_info_begin的鏈接地址,在R10中放置標號2的鏈接地址。通過將R5和R10中的數值相減,得到符號的鏈接地址和實際地址之間的差值,進而得到__proc_info_end、__proc_info_begin的實際地址。其實這些代碼的作用和adr __proc_info_end,adr __proc_info_begin的效果是一樣的。在MMU還沒有被打開的情況下,一般採取這種辦法來進行地址之間的映射。
這裏有一點要注意,在引用標號2的地址時,採取了2f和2b兩種不同的表示法,這是什麼原因呢?在代碼中你可以使用0--99之間的數字作爲標號,它們會被視爲臨時性的符號,可以在代碼中重複使用同一個數字作爲label。在一個分支指令(branch instruction)中“2f”指向下一個“2:”,而“2b”指向前一個“2:”,這樣就不用費心爲那些隨手而寫的跳轉和循環起名字了,省下這些名稱可以去命名那些子程序、還有那些比較關鍵的跳轉。
接着代碼通過訪問P15協處理器,得到當前的CPU的處理器ID,然後與以__proc_info_begin開始的處理器信息結構中的處理器ID相比較,相等則返回,不等則跳轉到下一個處理器信息結構繼續比較。從__proc_info_begin開始的保存處理器信息的結構的類型爲struct proc_info_list,在include/asm-arm/procinfo.h中有具體的定義。實際的各處理器信息結構的賦值在arch/arm/mm/proc-xscale.S文件的.section ".proc.info", #alloc, #execinstr語句下面。爲什麼是在.proc.info段的下面呢?這是由vmlinux-armv.lds.in文件中的代碼
__proc_info_begin = .;
   *(.proc.info)
__proc_info_end = .;
所決定的。
該段子程序完成後,各寄存器情況如下:
R8 = 頁表目錄項的標誌位
R9 = 處理器ID
R10 = 指向當前處理器信息結構的指針
 
__lookup_architecture_type:
      adr       r4, 2b
      ldmia r4, {r2, r3, r5, r6, r7}   @ throw away r2, r3
      sub       r5, r4, r5        @ convert addresses
      add       r4, r6, r5        @ to our address space
      add       r7, r7, r5
1:    ldr       r5, [r4]          @ get machine type
      teq       r5, r1
      beq       2f
      add       r4, r4,           #SIZEOF_MACHINE_DESC
      cmp       r4, r7
      blt       1b
      mov       r7, #0            @ unknown architecture
      mov       pc, lr
2:    ldmib r4, {r5, r6, r7} @ found, get results
      mov       pc, lr
這裏開始查找機器的體系結構信息。前面已經提到過,在開始執行head-armv.S文件之前,R1中已經包含了當前的體系結構的類型號。現在所要做的,就是在__arch_info_begin開始的地址中,查找與R1中的值相匹配的機器類型信息。從__arch_info_begin開始的保存機器體系結構信息的類型爲struct machine_desc,該結構在include/asm-arm/mach/arch.h中有具體的定義。對該結構的賦值使用MACHINE_START宏,該宏的定義同樣在arch.h文件中。具體的機器體系結構的信息在arch/arm/kernel/arch.c中,當然也可以在arch/arm/mach-($machine-type)目錄下的文件中添加與你自己的機器體系相對應的代碼。這一點在進行linux移植的工作中很重要。
該段子程序完成後,各寄存器情況如下:
R5 = 內存(SDRAM)的起始物理地址
R6 = IO的起始物理地址
R7 = IO虛擬地址在頁表中的索引項
?這裏有一個疑問。將R7賦值是爲了調試的需要,使得串口可以打印調試信息,因此需要操作IO。現在,我的IO起始地址爲0x40000000,在這裏被映射到了0xFC000000。獲得UART的虛擬地址的宏addruart在/arch/arm/kernel/debug-armv.S中被定義,這個宏通過io_p2v宏得到IO的虛擬地址,該宏在include/asm-arm/arch-pxa/hardware.h中被定義,但是它將0x40000000的物理地址映射到了0xF8000000,而不是上面提到的0xFC000000,這是爲什麼呢?希望能有人解釋一下這個問題。
 
__create_page_tables:
      pgtbl  r4, r5           @ page table address
 
      /* Clear the 16K level 1 swapper page table */
      mov        r0, r4
      mov        r3, #0
      add        r2, r0, #0x4000
1:    str        r3, [r0], #4
      str        r3, [r0], #4
      str        r3, [r0], #4
      str        r3, [r0], #4
      teq        r0, r2
      bne        1b
 
      /*
       * Create identity mapping for first MB of kernel to
       * cater for the MMU enable. This identity mapping
       * will be removed by paging_init()
       */
      krnladr r2, r4, r5         @ start of kernel
      add         r3, r8, r2         @ flags + kernel base
      str         r3, [r4, r2, lsr #18]    @ identity mapping
 
      /*
       * Now setup the pagetables for our kernel direct
       * mapped region. We round TEXTADDR down to the
       * nearest megabyte boundary.
       */
      add        r0, r4, #(TEXTADDR & 0xff000000) >> 18 @ start of kernel
      bic        r2, r3, #0x00f00000
      str        r2, [r0]         @ PAGE_OFFSET + 0MB
      add        r0, r0, #(TEXTADDR & 0x00f00000) >> 18
      str        r3, [r0], #4        @ KERNEL + 0MB
      add        r3, r3, #1 << 20
      str        r3, [r0], #4        @ KERNEL + 1MB
      add        r3, r3, #1 << 20
      str        r3, [r0], #4        @ KERNEL + 2MB
      add        r3, r3, #1 << 20
      str        r3, [r0], #4        @ KERNEL + 3MB
 
      /*
       * Ensure that the first section of RAM is present.
       * we assume that:
       * 1. the RAM is aligned to a 32MB boundary
       * 2. the kernel is executing in the same 32MB chunk
       *     as the start of RAM.
       */
      bic        r0, r0, #0x01f00000 >> 18 @ round down
      and        r2, r5, #0xfe000000    @ round down
      add        r3, r8, r2       @ flags + rambase
      str        r3, [r0]
 
      bic        r8, r8, #0x0c       @ turn off cacheable
                                  @ and bufferable bits
代碼創建頁表目錄。首先清空從0xA0004000開始的16K頁表項。然後,爲了可以訪問從0xA0000000開始的內核的1M空間,將該地址對應的頁表項賦值。接着映射從TEXTADDR開始的4M的虛擬地址空間,這需要4個頁表項。最後,由於SDRAM開始的第一MB的空間存放有啓動時的一些參數,所以也需要映射。在這裏,該映射和前面的虛擬地址的映射在地址上是相等的。
 
在創建頁表目錄完成後,代碼通過前面主程序的最後一句add pc, r10, #12跳轉到實際的CPU的設置子程序__xscale_setup。
__xscale_setup:
   mov        r0, #F_BIT|I_BIT|SVC_MODE
   msr        cpsr_c, r0
   mcr        p15, 0, ip, c7, c7, 0 @ invalidate I, D caches & BTB
   mcr     p15, 0, ip, c7, c10, 4 @ Drain Write (& Fill) Buffer
   mcr        p15, 0, ip, c8, c7, 0     @ invalidate I, D TLBs
   mcr        p15, 0, r4, c2, c0, 0     @ load page table pointer
   mov        r0,  #0x1f       @ Domains 0, 1 = client
   mcr        p15, 0, r0, c3, c0, 0 @ load domain access register
   mov        r0,  #1          @ Allow user space to access
   mcr        p15, 0, r0, c15, c1, 0    @ ... CP 0 only.
#if CACHE_WRITE_THROUGH
   mov        r0, #0x20
#else
   Mov     r0, #0x00
#endif
   mcr        p15, 0, r0, c1, c1, 0     @ set auxiliary control reg
   mrc        p15, 0, r0, c1, c0, 0     @ get control register
   bic     r0,  r0, #0x0200       @ ......R.........
   bic        r0,  r0, #0x0082       @ ........B.....A.
   orr        r0,  r0, #0x0005       @ .............C.M
   orr        r0,  r0, #0x3900       @ ..VIZ..S........
#ifdef CONFIG_XSCALE_CACHE_ERRATA
   bic        r0,  r0, #0x0004       @ see cpu_xscale_proc_init
#endif
   mov     pc,  lr
主要是操作協處理器,設置頁表目錄項基地址,對CACHE和BUFFER的控制位進行一些操作。具體大家可以看看介紹ARM編程的書。
 
.type __switch_data, %object
__switch_data: .long __mmap_switched
      .long SYMBOL_NAME(__bss_start)
      .long SYMBOL_NAME(_end)
      .long SYMBOL_NAME(processor_id)
      .long SYMBOL_NAME(__machine_arch_type)
      .long SYMBOL_NAME(cr_alignment)
      .long SYMBOL_NAME(init_task_union)+8192
 
      .type __ret, %function
__ret:   ldr   lr, __switch_data
      mcr   p15, 0, r0, c1, c0
      mov   r0, r0
      mov   r0, r0
      mov   r0, r0
      mov   pc, lr
 
      .align 5
__mmap_switched:
      adr   r3, __switch_data + 4
      ldmia r3, {r4, r5, r6, r7, r8, sp} @ r2 = compat
                                          @ sp = stack pointer
 
      mov   fp, #0                @ Clear BSS (and zero fp)
1:    cmp   r4, r5
      strcc fp, [r4],#4
      bcc   1b
 
      str   r9, [r6]              @ Save processor ID
      str   r1, [r7]              @ Save machine type
#ifdef CONFIG_ALIGNMENT_TRAP
      orr   r0, r0, #2            @ ...........A.
#endif
      bic   r2, r0, #2            @ Clear 'A' bit
      stmia r8, {r0, r2}       @ Save control register values
      b SYMBOL_NAME(start_kernel)
最後這段代碼的作用主要是在進入C函數前先做一些變量的初始化和保存工作。首先清空BSS區域,然後保存處理器ID和機器類型到各自變量地址,接着保存cr_alignment,最後跳轉到init/main.c中的start_kernel函數運行。
 
以上介紹的是head-armv.S文件的主要內容和功能,它是linux運行的第一個文件,具有非常重要的意義。很好的閱讀該文件,對於我們理解ARM處理器的工作方式有很大的幫助。同時,在許多linux系統的移植工作中,往往需要對該文件透徹的理解。
 

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