(1) 下面是確定內核的虛擬地址、物理地址的關鍵信息, 感興趣的同學可以自己看:
vmlinux虛擬地址的確定:
內核源碼:
.config : CONFIG_PAGE_OFFSET=0xC0000000 arch/arm/include/asm/memory.h #define PAGE_OFFSET UL(CONFIG_PAGE_OFFSET) arch/arm/Makefile textofs-y := 0x00008000 TEXT_OFFSET := $(textofs-y) arch/arm/kernel/vmlinux.lds.S: . = PAGE_OFFSET + TEXT_OFFSET; // // 即0xC0000000+0x00008000 = 0xC0008000, vmlinux的虛擬地址為0xC0008000 arch/arm/kernel/head.S #define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET) // 即0xC0000000+0x00008000 = 0xC0008000
vmlinux物理地址的確定:
內核源碼:
arch/arm/mach-s3c24xx/Makefile.boot : zreladdr-y += 0x30008000 // zImage自解壓后得到vmlinux, vmlinux的存放位置 params_phys-y := 0x30000100 // tag參數的存放位置, 使用dtb時不再需要tag arch/arm/boot/Makefile: ZRELADDR := $(zreladdr-y) arch/arm/boot/Makefile: UIMAGE_LOADADDR=$(ZRELADDR) scripts/Makefile.lib: UIMAGE_ENTRYADDR ?= $(UIMAGE_LOADADDR) // 制作uImage的命令, uImage = 64字節的頭部 + zImage, 頭部信息中含有內核的入口地址(就是vmlinux的物理地址) cmd_uimage = $(CONFIG_SHELL) $(MKIMAGE) -A $(UIMAGE_ARCH) -O linux \ -C $(UIMAGE_COMPRESSION) $(UIMAGE_OPTS-y) \ -T $(UIMAGE_TYPE) \ -a $(UIMAGE_LOADADDR) -e $(UIMAGE_ENTRYADDR) \ -n $(UIMAGE_NAME) -d $(UIMAGE_IN) $(UIMAGE_OUT)
KERNEL_RAM_PADDR 0x30008000
在arm平台下,zImage.bin壓縮鏡像是由bootloader加載到物理內存,然后跳到zImage.bin里一段程序,它專門於將被壓縮的kernel解壓縮到KERNEL_RAM_PADDR開始的一段內存中,接着跳進真正的kernel去執行。該kernel的執行起點是stext函數, stext函數定義在Arch/arm/kernel/head.S,它的功能是獲取處理器類型和機器類型信息,並創建臨時的頁表,然后開啟MMU功能,並跳進第一個C語言函數start_kernel。
在分析stext函數前,先介紹此時內存的布局如下圖所示
在開發板tqs3c2440中,SDRAM連接到內存控制器的Bank6中,它的開始內存地址是0x30000000,大小為64M,即0x20000000。 ARM Linux kernel將SDRAM的開始地址定義為PHYS_OFFSET。經bootloader加載kernel並由自解壓部分代碼運行后,最終kernel被放置到KERNEL_RAM_PADDR(=PHYS_OFFSET + TEXT_OFFSET,即0x30008000)地址上的一段內存,經此放置后,kernel代碼以后均不會被移動。
在進入kernel代碼前,即bootloader和自解壓縮階段,ARM未開啟MMU功能。因此kernel啟動代碼一個重要功能是設置好相應的頁表,並開啟MMU功能。為了支持MMU功能,kernel鏡像中的所有符號,包括代碼段和數據段的符號,在鏈接時都生成了它在開啟MMU時,所在物理內存地址映射到的虛擬內存地址。
以arm kernel第一個符號(函數)stext為例,在編譯鏈接,它生成的虛擬地址是0xc0008000,而放置它的物理地址為0x30008000(還記得這是PHYS_OFFSET+TEXT_OFFSET嗎?)。實際上這個變換可以利用簡單的公式進行表示:va = pa – PHYS_OFFSET + PAGE_OFFSET。Arm linux最終的kernel空間的頁表,就是按照這個關系來建立。
之所以較早提及arm linux 的內存映射,原因是在進入kernel代碼,里面所有符號地址值為清一色的0xCXXXXXXX地址,而此時ARM未開啟MMU功能,故在執行stext函數第一條執行時,它的PC值就是stext所在的內存地址(即物理地址,0x30008000)。因此,下面有些代碼,需要使用地址無關技術(相對尋址)。
kernel建立臨時頁表
前面提及到,kernel里面的所有符號在鏈接時,都使用了虛擬地址值。在完成基本的初始化后,kernel代碼將跳到第一個C語言函數start_kernl來執行,在哪個時候,這些虛擬地址必須能夠對它所存放在真正內存位置,否則運行將為出錯。為此,CPU必須開啟MMU,但在開啟MMU前,必須為虛擬地址到物理地址的映射建立相應的面表。在開啟MMU后,kernel指並不馬上將PC值指向start_kernl,而是要做一些C語言運行期的設置,如堆棧,重定義等工作后才跳到start_kernel去執行。在此過程中,PC值還是物理地址,因此還需要為這段內存空間建立va = pa的內存映射關系。當然,本函數建立的所有頁表都會在將來paging_init銷毀再重建,這是臨時過度性的映射關系和頁表。
在介紹__create_table_pages前,先認識一個macro pgtbl,它將KERNL_RAM_PADDR – 0x4000的值賦給rd寄存器,從下面的使用中可以看它,該值是頁表在物理內存的基礎,也即頁表放在kernel開始地址下的16K的地方。
.macro pgtbl, rd
ldr \rd, =(KERNEL_RAM_PADDR - 0x4000)
.endm
地址無關
比如_lookup_processor_type
- # /* adr 是相對尋址,它的尋計算結果是將當前PC值加上3f符號與PC的偏移量,
- # * 而PC是物理地址,因此r3的結果也是3f符號的物理地址 */
- #
- # adr r3, 3f
ENTRY(stext)
kernel的鏈接腳本並不是直接提供的,而是提供了一個匯編文件vmlinux.lds.S,然后在編譯的時候再去編譯這個匯編文件得到真正的鏈接腳本vmlinux.lds。
為什么linux kernel不直接提供vmlinux.lds而要提供一個vmlinux.lds.S然后在編譯時才去動態生成vmlinux.lds呢?
.lds文件中只能寫死,不能用條件編譯。但是我們在kernel中鏈接腳本確實有條件編譯的需求(但是lds格式又不支持),於是乎kernel工作者找了個投機取巧的方法,就是把vmlinux.lds寫成一個匯編格式,然后匯編器處理的時候順便條件編譯給處理了,得到一個不需要條件編譯的vmlinux.lds。
從vmlinux.lds.S中 ENTRY(stext) 可以知道入口符號是stext,在SI中搜索這個符號,發現arch/arm/kernel/目錄下的head.S和head-nommu.S中都有。
head.S是啟用了MMU情況下的kernel啟動文件,相當於uboot中的start.S。head-nommu.S是未使用mmu情況下的kernel啟動文件。
內核啟動文件head.S(匯編階段)
內核運行的物理地址與虛擬地址(29-30)
KERNEL_RAM_VADDR(VADDR就是virtual address),這個宏定義了內核運行時的虛擬地址。值為0xC0008000
KERNEL_RAM_PADDR(PADDR就是physical address),這個宏定義內核運行時的物理地址。值為0x30008000
總結:內核運行的物理地址是0x30008000,對應的虛擬地址是0xC0008000。
內核運行硬件條件備注(59-76)
內核啟動不是無條件的,而是有一定的先決條件,這個條件由啟動內核的bootloader(我們這里就是uboot)來構建保證。
(1)內核的起始部分代碼是被解壓代碼調用的。回憶之前講zImage的時候,uboot啟動內核后實際調用運行的是zImage前面的那段未經壓縮的解壓代碼,解壓代碼運行時先將zImage后段的內核解壓開,然后再去調用運行真正的內核入口。並在開始時MMU和D-cache是關閉的,I-cache任意,並且寄存器r0,r1,r2傳的參數與uboot階段時最后的theKernel函數傳參對應。所以uboot中最后theKernel (0, machid, bd->bi_boot_params);執行內核時,運行時實際把0放入r0中,machid放入到了r1中,bd->bi_boot_params放入到了r2中。ARM的這種處理技巧剛好滿足了kernel啟動的條件和要求。
(2)kernel啟動時MMU是關閉的,因此硬件上需要的是物理地址。但是內核是一個整體(zImage)只能被連接到一個地址(不能分散加載),這個連接地址肯定是虛擬地址。因此內核運行時前段head.S中尚未開啟MMU之前的這段代碼必須是位置無關碼(pc使用的是物理地址),而且其中涉及到操作硬件寄存器等時必須使用物理地址。
(3)通過linux/arch/arm/tools/mach-types目錄中查找對應的機器碼。
(4)不要添加沒有用的代碼在這里,這里的代碼只是用來boot loader的。