linux arm32啟動代碼分析
首先將 linux kernel 代碼編譯好以后,在目錄 arch/arm/kernel 下生成鏈接腳本檔案 vmlinux.lds (vmlinux.lds由vmlinux.lds.S編譯而來),首先分析此腳本來熟悉 linux kernel 二進制代碼分布結構,
在 vmlinux.lds.S 中
ENTRY(stext)
指明了linux內核入口,入口為stext,符號stext定義在 arch/arm/kernel/head.S 檔案中:
.arm
__HEAD
ENTRY(stext)
ARM_BE8(setend be ) @ ensure we are in BE8 mode
THUMB( adr r9, BSYM(1f) ) @ Kernel is always entered in ARM.
THUMB( bx r9 ) @ If this is a Thumb-2 kernel,
THUMB( .thumb ) @ switch to Thumb now.
THUMB(1: )
ENTRY在 include/linux/linkage.h 中定義
#ifndef ENTRY
#define ENTRY(name) \
.globl name ASM_NL \
ALIGN ASM_NL \
name:
#endif
通過代碼可以看到 ENTRY 宏只是對全域符號的匯出起到包裝作用,
下面分析ENTRY陳述句下的五條指令
1、ARM_BE8(setend be )
ARM_BE8在 arch/arm/include/asm/assembler.h 中定義
/* Select code for any configuration running in BE8 mode */
#ifdef CONFIG_CPU_ENDIAN_BE8
#define ARM_BE8(code...) code
#else
#define ARM_BE8(code...)
#endif
如果開啟 CONFIG_CPU_ENDIAN_BE8 選項,code(setend be)則會被原封不動編譯到內核中,如果沒有開啟此選項,code(setend be)就不會被編譯到內核中,
(extension) #define后的省略號的意義如下(以PDEBUG為例):
#define PDEBUG(fmt, args...) printk( KERN_DEBUG "scull: " fmt, ## args)
// example
PDEBUG("a=%d, b=%d", a, b);
// 展開后
printk( KERN_DEBUG "scull: " "a=%d, b=%d", a, b);
(1)arm BE8 mode 是什么
根據博客 https://blog.richliu.com/2010/04/08/907/arm11-be8-and-be32 所述:
BE8 和 BE32 是位元組序相關的概念,舉例:
假設需要將 0x11223344 存入記憶體中:
little endian:
| memory address | 0 | 1 | 2 | 3 |
| data | 0x44 | 0x33 | 0x22 | 0x11 |
big endian:
| memory address | 0 | 1 | 2 | 3 |
| data | 0x11 | 0x22 | 0x33 | 0x44 |
現假設對0x11223344記憶體存盤分布如下:
| memory address | 0 | 1 | 2 | 3 |
| data | 0x11 | 0x22 | 0x33 | 0x44 |
在BE32 mode下:
LDR r0, [0]
# r0 = 0x44332211
LDRB r0, [0]
# r0 = 0x00000044
LDRB r0, [3]
# r0 = 0x00000011
在BE8 mode下:
LDR r0, [0]
# r0 = 0x11223344
LDRB r0, [0]
# r0 = 0x00000011
LDRB r0, [3]
# r0 = 0x00000044
關于 byte invariant endianness 的概念可以參照博客 https://blog.csdn.net/moreaction/article/details/5280067/
(2)setend be 指令的含義
setend 指令選擇資料訪問的位元組序,在百度文庫 https://wenku.baidu.com/view/6f83c4c2951ea76e58fafab069dc5022aaea46e5.html 解釋了這個指令,在linux內核代碼中,如果開啟了 CONFIG_CPU_ENDIAN_BE8 選項,則 setend be 指令會被編譯到內核中并執行,此指令的執行代表選擇資料訪問的位元組序為 BE8 模式,BE8 模式的資料訪問效果如上文所述,
2、THUMB( adr r9, BSYM(1f) )
3、THUMB( bx r9 )
4、THUMB( .thumb )
5、THUMB(1: )
在 2\3\4\5 中,THUMB、BSYM都定義在 arch/arm/include/asm/unified.h 中:
#ifdef CONFIG_THUMB2_KERNEL
#if __GNUC__ < 4
#error Thumb-2 kernel requires gcc >= 4
#endif
/* The CPSR bit describing the instruction set (Thumb) */
#define PSR_ISETSTATE PSR_T_BIT
#define ARM(x...)
#define THUMB(x...) x
#ifdef __ASSEMBLY__
#define W(instr) instr.w
#define BSYM(sym) sym + 1
#else
#define WASM(instr) #instr ".w"
#endif
#else /* !CONFIG_THUMB2_KERNEL */
/* The CPSR bit describing the instruction set (ARM) */
#define PSR_ISETSTATE 0
#define ARM(x...) x
#define THUMB(x...)
#ifdef __ASSEMBLY__
#define W(instr) instr
#define BSYM(sym) sym
#else
#define WASM(instr) #instr
#endif
#endif /* CONFIG_THUMB2_KERNEL */
若沒有開啟 CONFIG_THUMB2_KERNEL 選項,則這四條陳述句都不會被編譯進內核中,且開啟此選項需要 gcc 版本高于 4:
#if __GNUC__ < 4
#error Thumb-2 kernel requires gcc >= 4
#endif
如果 CONFIG_THUMB2_KERNEL 選項開啟且定義了 __ASSEMBLY__,則這四條陳述句會被預編譯為:
adr r9, 1f + 1
bx r9
.thumb
1:
至于為什么需要 1f+1 原因在于分支指令 bx:
bx 為帶狀態切換的跳轉指令,其語法格式為 bx 其中 RM 只能是暫存器,并且當 RM 的bit[0]為1時,切換到 Thumb 指令,為0時切換到 ARM 指令,
以下是猜測:由于指令對齊,所以帶狀態分支跳轉地址一定為偶數,所以CPU在收到條件分支指令時不會將bit[0]當作地址的一部分,而是用來做bx的狀態選擇,并且由于地址為偶數的原因, adr r9, 1f 指令執行后,r9的bit[0]一定為0,所以需要加上1來進行狀態選擇,以達到跳轉后開始執行Thumb指令集,并且,.thumb 也告知編譯器,其下的匯編代碼需要編譯為Thumb指令,
至此分析完這五條指令,
next,如果開啟了 CONFIG_ARM_VIRT_EXT 開關,則會跳轉到 __hyp_stub_install 繼續執行,具體代碼如下:
#ifdef CONFIG_ARM_VIRT_EXT
bl __hyp_stub_install
#endif
__hyp_stub_install 定義在 arch/arm/kernel/hyp-stub.S 中,根據源代碼中的注釋解釋:
/*
* Hypervisor stub installation functions.
*
* These must be called with the MMU and D-cache off.
* They are not ABI compliant and are only intended to be called from the kernel
* entry points in head.S.
*/
目前只知道此代碼與arm虛擬化功能有關
next, linux 內核將會開始檢測所運行的硬體是否支持此內核映像,由于 arm 內核種類多,并且互相之間有無法兼容的區別,所以當開發者對內核進行開發或者使用者編譯內核時,都會在原始碼級別上給出對硬體的某些定義,就如下文所提到的 __proc_info 結構,內核在編譯期即確定了自己所支持的硬體種類,例如在 s3c2440 平臺下即會去指定支持 arm920t 架構,由于會有一些與硬體互動的操作無法互相兼容,所以內核會通過下文的一些操作來檢查硬體的屬性是否與編譯到內核里對硬體的定義資訊相互匹配,如果發現不匹配則會立刻讓內核陷入死回圈并告知啟動失敗,
檢查 processor id
safe_svcmode_maskall r9
mrc p15, 0, r9, c0, c0 @ get processor id
bl __lookup_processor_type @ r5=procinfo r9=cpuid
movs r10, r5 @ invalid processor (r5=0)?
THUMB( it eq ) @ force fixup-able long branch encoding
beq __error_p @ yes, error 'p'
safe_svcmode_maskall r9 用來確保 CPU 在SVC模式下,并且所有中斷都被屏蔽,其中 safe_svcmode_maskall 是一個宏,他定義在 arch/arm/include/asm/assembler.h 中,原始碼中的注釋對其解釋為:
/*
* Helper macro to enter SVC mode cleanly and mask interrupts. reg is
* a scratch register for the macro to overwrite.
*
* This macro is intended for forcing the CPU into SVC mode at boot time.
* you cannot return to the original mode.
*/
mrc 指令將協處理器的暫存器中數值傳送到ARM處理器的暫存器中,其中 mrc p15, 0, r9, c0, c0 用來獲取處理器id(processor id)并將此值傳遞給r9
在指令 bl __lookup_processor_type 中,__lookup_processor_type 符號定義在 arch/arm/kernel/head-common.S 中:
__lookup_processor_type:
adr r3, __lookup_processor_type_data
ldmia r3, {r4 - 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: 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: ret lr
ENDPROC(__lookup_processor_type)
在這段代碼中涉及到 __lookup_processor_type_data 定義為:
/*
* Look in <asm/procinfo.h> for information about the __proc_info structure.
*/
.align 2
.type __lookup_processor_type_data, %object
__lookup_processor_type_data:
.long .
.long __proc_info_begin
.long __proc_info_end
.size __lookup_processor_type_data, . - __lookup_processor_type_data
可以理解為在 __proc_info_begin 表示的記憶體地址與 __proc_info_end 表示的記憶體地址之間包含了一個或多個 __proc_info 結構體,這個結構體用C語言的定義如下:
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 資訊與通過 mrc 指令獲取的處理器id進行對比,并通過 r5 暫存器回傳結構的地址,或者回傳 0 代表匹配失敗,add r5, r5, #PROC_INFO_SZ 指令就是將指標指向下一個結構體,ldmia r5, {r3, r4} 指令就是將 cpu_val 與 cpu_mask 的值分別裝載到 r3 r4 中,cmp r5, r6 指令來判斷是否已經遍歷到了陣列的末尾,即已經沒有未檢查過的 __proc_info 結構體了,如果r5 != r6 則 blo 1b,若相等則給 r5 賦值為0并回傳,指令 and r4, r4, r9 與 teq r3, r4 用于檢查processor_id,如果通過檢查則 beq 2f 并 通過指令 ret lr 回傳,回傳時暫存器 r5 存盤的值為此相匹配的 __proc_info 結構的地址,
movs r10, r5 將 r5 的值轉存到 r10 中,并且由于使用 movs 指令,會影響 CPSR 暫存器的值,若 r5 為0則會使得 Z 位置1,而 beq 指令通過判斷 Z 位是否為1來決定是否跳轉,最后,如果 r5 為0,即沒有匹配的 __proc_info 則會導致 beq __error_p 執行,若執行此陳述句,最終便會跳轉到 __error 代碼段 最后系統會 halt up,其中 __error_p 與 __error 皆定義在 arch/arm/kernel/head-common.S 中,
通過查看鏈接腳本,我們可以清晰的看到 __proc_info 結構的存盤位置:
#define PROC_INFO \
. = ALIGN(4); \
VMLINUX_SYMBOL(__proc_info_begin) = .; \
*(.proc.info.init) \
VMLINUX_SYMBOL(__proc_info_end) = .;
及
.text : { /* Real text segment */
_stext = .; /* Text and read-only data */
IDMAP_TEXT
__exception_text_start = .;
*(.exception.text)
__exception_text_end = .;
IRQENTRY_TEXT
TEXT_TEXT
SCHED_TEXT
LOCK_TEXT
KPROBES_TEXT
*(.gnu.warning)
*(.glue_7)
*(.glue_7t)
. = ALIGN(4);
*(.got) /* Global offset table */
ARM_CPU_KEEP(PROC_INFO)
}
可以看到在 __proc_info_begin 與 __proc_info_end 之間的是 .proc.info.init 段,這樣就可以通過檢索 .proc.info.init 段的定義位置來找到這些 __proc_info 結構的定義位置了,通過檢索發現 .proc.info.init 段的定義位置在 arch/arm/mm/ 目錄下的多個 proc-x.S 中,下邊舉例:
在 arch/arm/mm/proc-arm9tdmi.S 中
.section ".proc.info.init", #alloc
.macro arm9tdmi_proc_info name:req, cpu_val:req, cpu_mask:req, cpu_name:req
.type __\name\()_proc_info, #object
__\name\()_proc_info:
.long \cpu_val
.long \cpu_mask
.long 0
.long 0
initfn __arm9tdmi_setup, __\name\()_proc_info
.long cpu_arch_name
.long cpu_elf_name
.long HWCAP_SWP | HWCAP_THUMB | HWCAP_26BIT
.long \cpu_name
.long arm9tdmi_processor_functions
.long 0
.long 0
.long v4_cache_fns
.size __\name\()_proc_info, . - __\name\()_proc_info
.endm
arm9tdmi_proc_info arm9tdmi, 0x41009900, 0xfff8ff00, cpu_arm9tdmi_name
arm9tdmi_proc_info p2001, 0x41029000, 0xffffffff, cpu_p2001_name
在 arch/arm/mm/proc-arm920.S 中
.section ".proc.info.init", #alloc
.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
initfn __arm920_setup, __arm920_proc_info
.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
可以看到這些結構都是通過匯編語言進行定義的,雖然在 linux 源代碼中有對 __proc_info 結構的C語言定義(如上文),但是 linux 內核代碼并沒有使用這個C語言定義的結構,由于此時系統仍然處于底層初始化階段,并沒有進行C語言執行環境初始化作業(例如初始堆疊空間等),所以無法執行C代碼,也就無法參考C語言定義的結構體,只能通過匯編語言直接定義特定記憶體位置的值來還原結構體的資料分布,
至此,linux 內核對 processor id 的檢查流程分析完畢
下邊將分析 linux 內核在內核管理方面的初始化作業,首先解釋 linux 內核對記憶體地址相關定義的幾個值:
1、PAGE_OFFSET
2、TEXT_OFFSET
3、KERNAL_RAM_VADDR
注 : 在分析頁表建立之前,必須先搞清楚 linux 內核對地址的這幾個特殊定義才行
這些值出現在 arch/arm/kernel/head.S 中:
#define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET)
#if (KERNEL_RAM_VADDR & 0xffff) != 0x8000
#error KERNEL_RAM_VADDR must start at 0xXXXX8000
#endif
其中 TEXT_OFFSET 與 PAGE_OFFSET 在鏈接腳本中已經被用來定義當前虛擬地址
#ifdef CONFIG_XIP_KERNEL
. = XIP_VIRT_ADDR(CONFIG_XIP_PHYS_ADDR);
#else
. = PAGE_OFFSET + TEXT_OFFSET;
#endif
.head.text : {
_text = .;
HEAD_TEXT
}
查看 arch/arm/kernel/head.S 所包含的頭檔案,發現了與記憶體管理相關的頭檔案 asm/memory.h ,進而在此頭檔案中找到了 PAGE_OFFSET 的定義:
/* PAGE_OFFSET - the virtual address of the start of the kernel image */
#define PAGE_OFFSET UL(CONFIG_PAGE_OFFSET)
并在 arm 架構的 defconfig 檔案中找到了 CONFIG_PAGE_OFFSET 選項(以 arch\arm\configs\imx_alientek_emmc_defconfig 為例):
CONFIG_PAGE_OFFSET=0x80000000
而展開 UL 宏:
/*
* Allow for constants defined here to be used from assembly code
* by prepending the UL suffix only with actual C code compilation.
*/
#define UL(x) _AC(x, UL) //arch/arm/include/asm/memory.h
而 TEXT_OFFSET 的定義位置比較隱蔽,通過查看 MAKEFILE 輸出,我們可以看到(或者也可以查看 cmd 檔案 arch/arm/kernel/.head.o.cmd,其中的變數 cmd_arch/arm/kernel/head.o := arm-linux-gnueabihf-gcc …):
arm-linux-gnueabihf-gcc -Wp -WD,arch/arm/kernel/.head.o.d -nostdinc -isystem /home/ace/kernels/alpha/toolchain/gcc-linaro-4.9.4 ... (省略無關緊要的部分) -D__ASSEMBLY__ -D__KERNEL__ -D__LINUX_ARM_ARCH__=6 -DTEXT_OFFSET=0x00008000 ... (省略無關緊要的部分)
通過上述命令看到通過 gcc 的 -D 選項將 TEXT_OFFSET 宏注入到代碼中,進而可以查到這個宏的值的定義位置在 arch/arm/Makefile 中:
TEXT_OFFSET := $(textofs-y)
同在一個檔案中,使用這可以通過 config 選擇 textofs-y 的值:
# Text offset. This list is sorted numerically by address in order to
# provide a means to avoid/resolve conflicts in multi-arch kernels.
textofs-y := 0x00008000
textofs-$(CONFIG_ARCH_CLPS711X) := 0x00028000
# We don't want the htc bootloader to corrupt kernel during resume
textofs-$(CONFIG_PM_H1940) := 0x00108000
# SA1111 DMA bug: we don't want the kernel to live in precious DMA-able memory
ifeq ($(CONFIG_ARCH_SA1100),y)
textofs-$(CONFIG_SA1111) := 0x00208000
endif
textofs-$(CONFIG_ARCH_MSM8X60) := 0x00208000
textofs-$(CONFIG_ARCH_MSM8960) := 0x00208000
textofs-$(CONFIG_ARCH_AXXIA) := 0x00308000
若不在 config 中開啟這些奇怪的選項,則 textofs-y 默認的值即為 0x00008000,并且通過上文查看到的 arm-linux-gcc 命令可以看到,我在此次編譯時并沒有開啟這些選項,使用了 textofs-y 的默認值 0x00008000,這樣上文提到的宏 KERNAL_RAM_VADDR 便可以計算得:PAGE_OFFSET + TEXT_OFFSET = 0x80008000,
現在已經找到了他們的定義位置及其值,下邊將分析這些值的含義
linux 內核原始碼中的注釋如下:
/*
* swapper_pg_dir is the virtual address of the initial page table.
* We place the page tables 16K below KERNEL_RAM_VADDR. Therefore, we must
* make sure that KERNEL_RAM_VADDR is correctly set. Currently, we expect
* the least significant 16 bits to be 0x8000, but we could probably
* relax this restriction to KERNEL_RAM_VADDR >= PAGE_OFFSET + 0x4000.
*/
/* PAGE_OFFSET - the virtual address of the start of the kernel image */
意思是說,PAGE_OFFSET 是內核映像的起始 虛擬地址,而 swapper_pg_dir 是初始頁表的 虛擬地址,由于我在編譯程序中并沒有打開 LPAE 開關,所以得到:
#define PG_DIR_SIZE 0x4000
進而通過:
.globl swapper_pg_dir
.equ swapper_pg_dir, KERNEL_RAM_VADDR - PG_DIR_SIZE
得到 swapper_pg_dir = 0x80008000 - 0x00004000 = 0x80004000
查看一下鏈接腳本,可以看到這么一個定義:
. = PAGE_OFFSET + TEXT_OFFSET;
.head.text : {
_text = .;
HEAD_TEXT
}
也就是說,KERNAL_RAM_VADDR 表示的地址正好是 .head.text 段的起始地址,這就需要搞清楚 .head.text 段中包含的內容是什么,鏈接腳本說,.head.text 段中包含了一個符號 _text,這個符號的值為當前地址(PAGE_OFFSET + TEXT_OFFSET = KERNAL_RAM_VADDR),我們需要記下來這個符號,可能之后會用到;然后就是 HEAD_TEXT 宏定義,必須找到 HEAD_TEXT 的定義才能知道聯結器究竟把目標檔案中的哪些段塞進了這個 .head.text 段中,在 include/asm-generic/vmlinux.lds.h 頭檔案中找到了對 HEAD_TEXT 的定義:
/* Section used for early init (in .S files) */
#define HEAD_TEXT *(.head.text)
意思就是將所有目標檔案中的 .head.text 段聚集在了一起,別忘了我們分析的前幾行指令:
.arm
__HEAD
ENTRY(stext)
ARM_BE8(setend be )
THUMB( adr r9, BSYM(1f) )
THUMB( bx r9 )
THUMB( .thumb )
THUMB(1: )
這里有一個 __HEAD 宏定義,在 include/linux/init.h 中可以找到其定義:
#define __HEAD .section ".head.text","ax"
原來目前分析的代碼都在 .head.text 段下,這樣就完全清楚了:PAGE_OFFSET(0x80000000) 是整個內核映像的起始 虛擬地址,而 KERNAL_RAM_VADDR(0x80008000) 為內核映像中早期初始化可執行代碼段的起始 虛擬地址,在這兩個地址之間有一個初始頁表的起始虛擬地址,swapper_pg_dir(0x80004000) 這樣 TEXT_OFFSET 名字的由來也清楚了: TEXT 的偏移,即對 PAGE_OFFSET 的偏移,偏移了 TEXT_OFFSET 大小后即到了 代碼段(text) ,其由 KERNAL_RAM_VADDR 進行標識,最后確認一下我們分析的記憶體分布是否正確,打開 System.map 檔案,查看一下 0x80008000 虛擬地址處究竟是不是 stext 符號:
80008000 T _text
80008000 T stext
8000808c t __create_page_tables
看來是了,目前的分析是正確的,并且 _text 符號確實被編譯到可執行檔案中,位置在 0x80008000,與 stext 符號享有同樣的地址,
但是目標檔案中不僅僅在 head.S 中定義了 .head.text ,其他目標檔案中也會包含此段啊,為什么 head.S 中的 .head.text 段在所有 .head.text 段之上呢?這是因為在鏈接腳本中,定義了:
ENTRY(stext)
這就意味著 stext 符號需要當作入口在最前面,所以 head.S 中的包含 stext 符號的 .head.text 段理所當然地充當了內核映像中的 .head.text 段之首,
當清楚了這些值的含義后,可以繼續分析 linux 內核的啟動代碼,下一步就是 建立頁表
分析到這里,需要回過頭看一下 linux 內核原始碼注釋對此時硬體狀態的定義:
/*
* This is normally called from the decompressor code. The requirements
* are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
* r1 = machine nr, r2 = atags or dtb pointer.*/
/*
* r1 = machine no, r2 = atags or dtb,
* r8 = phys_offset, r9 = cpuid, r10 = procinfo
*/
其中 r1 r2 由 bootloader(uboot grub等) 進行設定,r2 為 dtb 結構的地址,r9 是通過之前 mrc 指令讀取到的,r10 是通過 __lookup_processor_type 獲取到的 __proc_info 結構的地址,r8 由如下操作獲取(我在編譯時并沒有開啟 CONFIG_XIP_KERNEL 選項):
#ifndef CONFIG_XIP_KERNEL
adr r3, 2f
ldmia r3, {r4, r8}
sub r4, r3, r4
add r8, r8, r4
#else
ldr r8, =PLAT_PHYS_OFFSET
#endif
/*標號2如下*/
#ifndef CONFIG_XIP_KERNEL
2: .long .
.long PAGE_OFFSET
#endif
執行 adr r3 2f 獲取了標號 2 的實際地址,并將 2 下定義的兩個 .long 型的資料裝在到 r4 r8 中,其中 r4 中裝載的是 .long . 定義的資料,即當前虛擬地址(也即標號 2 在映像中的虛擬地址),而 r3 則是標號 2 的實際物理地址,執行 sub r4, r3, r4 即 r4 = r3 - r4 后,即為 映像虛擬地址與實際物理地址之間的偏移,設為 OFFSET,再執行 add r8, r8, r4 即 r8 = r8 + r4 后,r8 中的值即為 PAGE_OFFSET + OFFSET,之前提到 PAGE_OFFSET 是 linux 內核映像的起始 虛擬地址 ,這回加上計算得到的 OFFSET(虛擬地址與物理地址的偏移) 后,即得到了 linux 內核映像在物理記憶體中的實際起始地址了,即,r8 存盤的值目前是 linux 內核映像在物理記憶體中的實際起始地址,搞清楚這些暫存器中的值后,就可以開始分析初始頁表建立的程序了,
開始執行建立頁表的函式: __create_page_tables
__create_page_tables 定義在 arch/arm/kernel/head.S 中:
__create_page_tables:
pgtbl r4, r8 @ page table address
/*
* Clear the swapper page table
*/
mov r0, r4
mov r3, #0
add r6, r0, #PG_DIR_SIZE
1: str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
teq r0, r6
bne 1b
#ifdef CONFIG_ARM_LPAE
/*
* Build the PGD table (first level) to point to the PMD table. A PGD
* entry is 64-bit wide.
*/
mov r0, r4
add r3, r4, #0x1000 @ first PMD table address
orr r3, r3, #3 @ PGD block type
mov r6, #4 @ PTRS_PER_PGD
mov r7, #1 << (55 - 32) @ L_PGD_SWAPPER
1:
#ifdef CONFIG_CPU_ENDIAN_BE8
str r7, [r0], #4 @ set top PGD entry bits
str r3, [r0], #4 @ set bottom PGD entry bits
#else
str r3, [r0], #4 @ set bottom PGD entry bits
str r7, [r0], #4 @ set top PGD entry bits
#endif
add r3, r3, #0x1000 @ next PMD table
subs r6, r6, #1
bne 1b
add r4, r4, #0x1000 @ point to the PMD tables
#ifdef CONFIG_CPU_ENDIAN_BE8
add r4, r4, #4 @ we only write the bottom word
#endif
#endif
ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
/*
* Create identity mapping to cater for __enable_mmu.
* This identity mapping will be removed by paging_init().
*/
adr r0, __turn_mmu_on_loc
ldmia r0, {r3, r5, r6}
sub r0, r0, r3 @ virt->phys offset
add r5, r5, r0 @ phys __turn_mmu_on
add r6, r6, r0 @ phys __turn_mmu_on_end
mov r5, r5, lsr #SECTION_SHIFT
mov r6, r6, lsr #SECTION_SHIFT
1: orr r3, r7, r5, lsl #SECTION_SHIFT @ flags + kernel base
str r3, [r4, r5, lsl #PMD_ORDER] @ identity mapping
cmp r5, r6
addlo r5, r5, #1 @ next section
blo 1b
/*
* Map our RAM from the start to the end of the kernel .bss section.
*/
add r0, r4, #PAGE_OFFSET >> (SECTION_SHIFT - PMD_ORDER)
ldr r6, =(_end - 1)
orr r3, r8, r7
add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)
1: str r3, [r0], #1 << PMD_ORDER
add r3, r3, #1 << SECTION_SHIFT
cmp r0, r6
bls 1b
#ifdef CONFIG_XIP_KERNEL
/*
* Map the kernel image separately as it is not located in RAM.
*/
#define XIP_START XIP_VIRT_ADDR(CONFIG_XIP_PHYS_ADDR)
mov r3, pc
mov r3, r3, lsr #SECTION_SHIFT
orr r3, r7, r3, lsl #SECTION_SHIFT
add r0, r4, #(XIP_START & 0xff000000) >> (SECTION_SHIFT - PMD_ORDER)
str r3, [r0, #((XIP_START & 0x00f00000) >> SECTION_SHIFT) << PMD_ORDER]!
ldr r6, =(_edata_loc - 1)
add r0, r0, #1 << PMD_ORDER
add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)
1: cmp r0, r6
add r3, r3, #1 << SECTION_SHIFT
strls r3, [r0], #1 << PMD_ORDER
bls 1b
#endif
/*
* Then map boot params address in r2 if specified.
* We map 2 sections in case the ATAGs/DTB crosses a section boundary.
*/
mov r0, r2, lsr #SECTION_SHIFT
movs r0, r0, lsl #SECTION_SHIFT
subne r3, r0, r8
addne r3, r3, #PAGE_OFFSET
addne r3, r4, r3, lsr #(SECTION_SHIFT - PMD_ORDER)
orrne r6, r7, r0
strne r6, [r3], #1 << PMD_ORDER
addne r6, r6, #1 << SECTION_SHIFT
strne r6, [r3]
#if defined(CONFIG_ARM_LPAE) && defined(CONFIG_CPU_ENDIAN_BE8)
sub r4, r4, #4 @ Fixup page table pointer
@ for 64-bit descriptors
#endif
#ifdef CONFIG_DEBUG_LL
#if !defined(CONFIG_DEBUG_ICEDCC) && !defined(CONFIG_DEBUG_SEMIHOSTING)
/*
* Map in IO space for serial debugging.
* This allows debug messages to be output
* via a serial console before paging_init.
*/
addruart r7, r3, r0
mov r3, r3, lsr #SECTION_SHIFT
mov r3, r3, lsl #PMD_ORDER
add r0, r4, r3
mov r3, r7, lsr #SECTION_SHIFT
ldr r7, [r10, #PROCINFO_IO_MMUFLAGS] @ io_mmuflags
orr r3, r7, r3, lsl #SECTION_SHIFT
#ifdef CONFIG_ARM_LPAE
mov r7, #1 << (54 - 32) @ XN
#ifdef CONFIG_CPU_ENDIAN_BE8
str r7, [r0], #4
str r3, [r0], #4
#else
str r3, [r0], #4
str r7, [r0], #4
#endif
#else
orr r3, r3, #PMD_SECT_XN
str r3, [r0], #4
#endif
#else /* CONFIG_DEBUG_ICEDCC || CONFIG_DEBUG_SEMIHOSTING */
/* we don't need any serial debugging mappings */
ldr r7, [r10, #PROCINFO_IO_MMUFLAGS] @ io_mmuflags
#endif
#if defined(CONFIG_ARCH_NETWINDER) || defined(CONFIG_ARCH_CATS)
/*
* If we're using the NetWinder or CATS, we also need to map
* in the 16550-type serial port for the debug messages
*/
add r0, r4, #0xff000000 >> (SECTION_SHIFT - PMD_ORDER)
orr r3, r7, #0x7c000000
str r3, [r0]
#endif
#ifdef CONFIG_ARCH_RPC
/*
* Map in screen at 0x02000000 & SCREEN2_BASE
* Similar reasons here - for debug. This is
* only for Acorn RiscPC architectures.
*/
add r0, r4, #0x02000000 >> (SECTION_SHIFT - PMD_ORDER)
orr r3, r7, #0x02000000
str r3, [r0]
add r0, r4, #0xd8000000 >> (SECTION_SHIFT - PMD_ORDER)
str r3, [r0]
#endif
#endif
#ifdef CONFIG_ARM_LPAE
sub r4, r4, #0x1000 @ point to the PGD table
mov r4, r4, lsr #ARCH_PGD_SHIFT
#endif
ret lr
ENDPROC(__create_page_tables)
其中 pgtbl 宏的定義為(在 arch/arm/kernel/head.S 中):
.macro pgtbl, rd, phys
add \rd, \phys, #TEXT_OFFSET
sub \rd, \rd, #PG_DIR_SIZE
.endm
swapper_pg_dir 的定義為(在 arch/arm/kernel/head.S 中):
.globl swapper_pg_dir
.equ swapper_pg_dir, KERNEL_RAM_VADDR - PG_DIR_SIZE
PG_DIR_SIZE 的定義為(在 arch/arm/kernel/head.S 中):
#ifdef CONFIG_ARM_LPAE
/* LPAE requires an additional page for the PGD */
#define PG_DIR_SIZE 0x5000
#define PMD_ORDER 3
#else
#define PG_DIR_SIZE 0x4000
#define PMD_ORDER 2
#endif
未完待續
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/171743.html
標籤:其他
