linux arm32 mmu 啟動程式碼分析(組合部分)

2020-10-13 11:01:13

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 address0123
data0x440x330x220x11

big endian:

memory address0123
data0x110x220x330x44

現假設對0x11223344記憶體儲存分佈如下:

memory address0123
data0x110x220x330x44

在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_valcpu_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, r5r5 的值轉存到 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_OFFSETPAGE_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
*/

這裡 ldr r8, =PLAT_PHYS_OFFSET 指令無需多看,因為 PLAT_PHYS_OFFSET 只用在沒有 MMU 或者使用 XIP 技術的情況下,在一般情況下 CONFIG_PHYS_OFFSET 都會是未定義的宏,而編譯時通過一些 gcc 選項可以遮蔽未定義宏的報錯,使其通過編譯, 即在一般情況下 r8 的值是無意義的, PLAT_PHYS_OFFSET 的定義如下:

 /*
 * PLAT_PHYS_OFFSET is the offset (from zero) of the start of physical
 * memory.  This is used for XIP and NoMMU kernels, and on platforms that don't
 * have CONFIG_ARM_PATCH_PHYS_VIRT. Assembly code must always use
 * PLAT_PHYS_OFFSET and not PHYS_OFFSET.
 */
#define PLAT_PHYS_OFFSET	UL(CONFIG_PHYS_OFFSET)

開始執行建立頁表的函數: __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

未完待續