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
未完待续