学习了arm内存页表的工作原理,接下来就开始咱们软件工程师的本职工作,对内核相关代码进行分析。内核代码那么复杂,该从哪里下手呢,想来想去。其实不管代码逻辑如何复杂,最终的落脚点都是在对页表项的操作上,那么内核是在什么时机会对页表项进行操作,如何操作?
对于一个页表项,抛开所有的软件复杂逻辑,操作无非就是2种吧。一是填写更新页表项,二是读取获取页表项。
MMU负责根据页表项进行虚实地址转换,因此读取获取页表项的工作是MMU硬件完成,软件是不参与的。内核代码的主体工作是来更新内存页表。页表更新的时机我的理解有4个:
(1)内核启动初期开启MMU阶段临时内存页表的建立
(2)start_kernel中完整的静态内存页表的建立
(3)内核空间中动态映射页表的建立,如vmalloc ioremap dma_alloc_coherent等
(4)用户空间访问虚拟地址时的缺页异常处理函数do_page_fault
由于内核空间的动态映射以及用户空间缺页异常处理中都涉及到了内存分配系统,放到后面再来分析,我们先来分析前2点。
并且除了第1点启动初期的临时页表是汇编中建立的外,其他3点无论其软件逻辑如何复杂,最终我们会看到都是调用了set_pte_ext来设置内存页表。
今天这篇文章先来分析临时内存页表的建立。
ARM在上电启动时MMU是关闭的,此时CPU访问的地址就是物理地址,并且在kernel之前的bootloader中也不会开启MMU,因此kernel开始运行时MMU是关闭的,这时kernel的运行地址与链接地址不一致。kernel是通过计算链接地址与运行地址的offset来对全局变量进行寻址的(对该过程感兴趣的朋友可以参考我另一篇分析内核启动过程的博文:http://blog.csdn.net/skyflying2012/article/details/41344377)。
这个痛苦的过程需要一直持续到MMU开启,当然在开启MMU之前需要将内存页表准备好,这个阶段kernel是建立了一个临时页表,完成了kernel_start到kernel_end的线性映射,这里页表映射采用的是section-mapping,内核代码在静态映射时更喜欢使用section-mapping,但是如果要映射地址不是1MB对齐,则使用page-mapping。这个在后面完整页表建立时还会有体现。
临时页表的建立以及为了完成开启MMU后地址的无缝过渡而建立的平映射,我在之前学习start_kernel之前页表建立的博文中有详细分析,地址如下:http://blog.csdn.net/skyflying2012/article/details/41447843
临时页表建立的逻辑在这篇博文中说的很详细了,今天就不说了,今天主要来分析下我对页表建立一些细节的疑问。
(1)临时页表采用section-mapping,那么在第一级页表项中除了高12位物理地址,linux对剩余20位的控制位如何填写
在start_kernel之前页表建立的博文中分析过,create_page_tables在填写页表前,会由proc_info_list结构体中获取__cpu_mm_mmu_flags,然后与物理地址相于,填写页表项,__cpu_mm_mmu_flags的值就是内核数据映射区的权限。
以公司的armv7处理器为例,proc_info_list结构体的获取与CPU ID相关,在kernel启动的__lookup_processor_type函数中进行获取,这部分的分析可以参考我的另一篇博文:http://blog.csdn.net/skyflying2012/article/details/48054417
根据CPU ID获取相应的proc_info_list结构体,armv7的结构体在./arch/arm/mm/proc-v7.S,如下:
.section ".proc.info.init", #alloc, #execinstr
/*
* Standard v7 proc info content
*/
.macro __v7_proc initfunc, mm_mmuflags = 0, io_mmuflags = 0, hwcaps = 0
ALT_SMP(.long PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \
PMD_SECT_AF | PMD_FLAGS_SMP | \mm_mmuflags)
ALT_UP(.long PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \
PMD_SECT_AF | PMD_FLAGS_UP | \mm_mmuflags)
.long PMD_TYPE_SECT | PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ | PMD_SECT_AF | \io_mmuflags
W(b) \initfunc
.long cpu_arch_name
.long cpu_elf_name
.long HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB | HWCAP_FAST_MULT | \
HWCAP_EDSP | HWCAP_TLS | \hwcaps
.long cpu_v7_name
.long v7_processor_functions
.long v7wbi_tlb_fns
.long v6_user_fns
.long v7_cache_fns
.endm
#ifndef CONFIG_ARM_LPAE
/*
* ARM Ltd. Cortex A5 processor.
*/
.type __v7_ca5mp_proc_info, #object
__v7_ca5mp_proc_info:
.long 0x410fc050
.long 0xff0ffff0
__v7_proc __v7_ca5mp_setup
.size __v7_ca5mp_proc_info, . - __v7_ca5mp_proc_info
/*
* ARM Ltd. Cortex A9 processor.
*/
.type __v7_ca9mp_proc_info, #object
__v7_ca9mp_proc_info:
.long 0x410fc090
.long 0xff0ffff0
__v7_proc __v7_ca9mp_setup
.size __v7_ca9mp_proc_info, . - __v7_ca9mp_proc_info
#endif /* CONFIG_ARM_LPAE */
/*
* ARM Ltd. Cortex A7 processor.
*/
.type __v7_ca7mp_proc_info, #object
__v7_ca7mp_proc_info:
.long 0x410fc070
.long 0xff0ffff0
__v7_proc __v7_ca7mp_setup, hwcaps = HWCAP_IDIV
.size __v7_ca7mp_proc_info, . - __v7_ca7mp_proc_info
/*
* ARM Ltd. Cortex A15 processor.
*/
.type __v7_ca15mp_proc_info, #object
__v7_ca15mp_proc_info:
.long 0x410fc0f0
.long 0xff0ffff0
__v7_proc __v7_ca15mp_setup, hwcaps = HWCAP_IDIV
.size __v7_ca15mp_proc_info, . - __v7_ca15mp_proc_info
/*
* Match any ARMv7 processor core.
*/
.type __v7_proc_info, #object
__v7_proc_info:
.long 0x000f0000 @ Required ID value
.long 0x000f0000 @ Mask for ID
__v7_proc __v7_setup
.size __v7_proc_info, . - __v7_proc_info
这段汇编初看会比较晦涩,首先是定义了宏定义__v7_proc,之后定义了一系列proc_info_list结构体,其中使用__v7_proc完成了其中部分成员的赋值。而proc_info_list的定义如下:
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;
};
结构体的第三个和第四个成员是与MMU相关的标志位,结合__v7_proc的定义,可以得出,对于armv7,__cpu_mm_mmu_flags __cpu_io_mmu_flags定义如下:
.macro __v7_proc initfunc, mm_mmuflags = 0, io_mmuflags = 0, hwcaps = 0
ALT_SMP(.long PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \
PMD_SECT_AF | PMD_FLAGS_SMP | \mm_mmuflags)
ALT_UP(.long PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \
PMD_SECT_AF | PMD_FLAGS_UP | \mm_mmuflags)
.long PMD_TYPE_SECT | PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ | PMD_SECT_AF | \io_mmuflags
ALT_SMP和ALT_UP应用于多核处理器,如果没有定义CONFIG_SMP,则ALT_SMP为空,使用ALT_UP内容,反之使用ALT_SMP内容。这里以单核处理器为例。这样最终可以得出:
__cpu_mm_mmu_flags = PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | PMD_SECT_AF | PMD_FLAGS_UP
__cpu_io_mmu_flags = PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | PMD_SECT_AF
__cpu_mm_mmu_flags用于建立内核代码的线性映射。
__cpu_io_mmu_flags则用于早期对于IO空间的映射,如我们需要一个早期的调试串口,则可以在create_page_tables中将串口寄存器空间进行映射。
__cpu_mm_mmu_flags __cpu_io_mmu_flags就是内核使用section-mapping方式时的页表属性值,一般是应用于内核启动早期临时页表。
页目录项控制位的宏定义如下,在./arch/arm/include/asm/pgtable-2level-hwdef.h中。
*
* + Level 1 descriptor (PMD)
* - common
*/
#define PMD_TYPE_MASK (_AT(pmdval_t, 3) << 0)
#define PMD_TYPE_FAULT (_AT(pmdval_t, 0) << 0)
#define PMD_TYPE_TABLE (_AT(pmdval_t, 1) << 0)
#define PMD_TYPE_SECT (_AT(pmdval_t, 2) << 0)
#define PMD_BIT4 (_AT(pmdval_t, 1) << 4)
#define PMD_DOMAIN(x) (_AT(pmdval_t, (x)) << 5)
#define PMD_PROTECTION (_AT(pmdval_t, 1) << 9) /* v5 */
/*
* - section
*/
#define PMD_SECT_BUFFERABLE (_AT(pmdval_t, 1) << 2)
#define PMD_SECT_CACHEABLE (_AT(pmdval_t, 1) << 3)
#define PMD_SECT_XN (_AT(pmdval_t, 1) << 4) /* v6 */
#define PMD_SECT_AP_WRITE (_AT(pmdval_t, 1) << 10)
#define PMD_SECT_AP_READ (_AT(pmdval_t, 1) << 11)
#define PMD_SECT_TEX(x) (_AT(pmdval_t, (x)) << 12) /* v5 */
#define PMD_SECT_APX (_AT(pmdval_t, 1) << 15) /* v6 */
#define PMD_SECT_S (_AT(pmdval_t, 1) << 16) /* v6 */
#define PMD_SECT_nG (_AT(pmdval_t, 1) << 17) /* v6 */
#define PMD_SECT_SUPER (_AT(pmdval_t, 1) << 18) /* v6 */
#define PMD_SECT_AF (_AT(pmdval_t, 0))
#define PMD_SECT_UNCACHED (_AT(pmdval_t, 0))
#define PMD_SECT_BUFFERED (PMD_SECT_BUFFERABLE)
#define PMD_SECT_WT (PMD_SECT_CACHEABLE)
#define PMD_SECT_WB (PMD_SECT_CACHEABLE | PMD_SECT_BUFFERABLE)
#define PMD_SECT_MINICACHE (PMD_SECT_TEX(1) | PMD_SECT_CACHEABLE)
#define PMD_SECT_WBWA (PMD_SECT_TEX(1) | PMD_SECT_CACHEABLE | PMD_SECT_BUFFERABLE)
#define PMD_SECT_NONSHARED_DEV (PMD_SECT_TEX(2))
结合第一篇页表硬件原理中《section-mapping的页表项位定义》一图,可以看出这些宏定义都是根据arm mmu内存页表的硬件特性进行定义的。展开这些宏定义可以得出:
__cpu_mm_mmu_flags指定了临时页表映射的内核空间是section-mapping方式,可读可写,并且是cached的。
__cpu_io_mmu_flags指定了其所映射的IO区域是section-mapping方式,可读可写,但是是uncached的。
(2)临时页表完成映射的内核空间有多大?
内核完成了对KERNEL_START到KERNEL_END的映射,到底是有多大空间。首先来看这2个宏定义。在arch/arm/kernel/head.S中
#define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET)
#define KERNEL_START KERNEL_RAM_VADDR
#define KERNEL_END _end
PAGE_OFFSET + TEXT_OFFSET一般是0xc0008000,是内核镜像的开始。_end是在arch/arm/kernel/vmlinux.ld.S中定义的内核镜像的结尾。因此KERNEL_START到KERNEL_END就是内核的所有数据。
那么就有问题了,内核所有数据就是咱们编译出来的Image吗?
答案是否定的,以我的代码为例,通过如下方法可以验证:
zk@server2:~/workplace/kernel$ size vmlinux
text data bss dec hex filename
4042010 6781556 1238960 12062526 b80f3e vmlinux
zk@server2:~/workplace/kernel$ ls -l arch/arm/boot/Image
-rwxr-xr-x 1 zk git 10831140 2016-04-06 09:44 arch/arm/boot/Image
仔细观察会发现,Image大小基本等于代码段和数据段之和。
这里我们需要知道的是bss段在objcopy时是不会被打包到二进制镜像中的,这是因为bss段中数据全为0,为了简化镜像大小,所有的裸编程序(linux bootloader)镜像中删掉bss段,而是在启动过程中根据链接脚本中定义的bss段始末地址来对bss段进行清空初始化,因此bss段肯定是要被映射的,这也就说明Image大小并不能代表我们临时页表映射的内核空间大小。
临时页表映射的大小要从链接脚本中找,arm的链接脚本是arch/arm/kernel/vmlinux.ld.S。可以看到_end定义在了bss段之后。具体的地址有两种方法可以获取。
一种是arm-readelf -s vmlinux查看各个段的布局。我的vmlinux如下:
zk@server2:~/workplace/kernel$ readelf -S vmlinux
There are 34 section headers, starting at offset 0x31e2f0c:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .head.text PROGBITS c0008000 008000 0001e0 00 AX 0 0 32
[ 2] .text PROGBITS c0008200 008200 2ee818 00 AX 0 0 64
[ 3] .rodata PROGBITS c02f7000 2f7000 0af740 00 A 0 0 64
[ 4] __bug_table PROGBITS c03a6740 3a6740 0048fc 00 A 0 0 1
[ 5] __ksymtab PROGBITS c03ab03c 3ab03c 005920 00 A 0 0 4
[ 6] __ksymtab_gpl PROGBITS c03b095c 3b095c 003590 00 A 0 0 4
[ 7] __ksymtab_strings PROGBITS c03b3eec 3b3eec 013dd2 00 A 0 0 1
[ 8] __param PROGBITS c03c7cc0 3c7cc0 000620 00 A 0 0 4
[ 9] __modver PROGBITS c03c82e0 3c82e0 000d20 00 A 0 0 4
[10] .init.text PROGBITS c03c9000 3c9000 018710 00 AX 0 0 32
[11] .exit.text PROGBITS c03e1710 3e1710 001520 00 AX 0 0 4
[12] .init.proc.info PROGBITS c03e2c30 3e2c30 000104 00 AX 0 0 1
[13] .init.arch.info PROGBITS c03e2d34 3e2d34 000040 00 A 0 0 4
[14] .init.tagtable PROGBITS c03e2d74 3e2d74 000040 00 A 0 0 4
[15] .init.pv_table PROGBITS c03e2db4 3e2db4 00054c 00 A 0 0 1
[16] .init.data PROGBITS c03e3300 3e3300 621574 00 WA 0 0 8
[17] .data PROGBITS c0a06000 a06000 056500 00 WA 0 0 64
[18] .notes NOTE c0a5c500 a5c500 000024 00 AX 0 0 4
[19] .bss NOBITS c0a5c540 a5c524 12e7b0 00 WA 0 0 64
[20] .ARM.attributes ARM_ATTRIBUTES 00000000 a5c524 00002b 00 0 0 1
[21] .comment PROGBITS 00000000 a5c54f 008ff3 00 0 0 1
[22] .debug_line PROGBITS 00000000 a65542 1ff6ba 00 0 0 1
[23] .debug_info PROGBITS 00000000 c64bfc 1fa94da 00 0 0 1
[24] .debug_abbrev PROGBITS 00000000 2c0e0d6 0f6fd5 00 0 0 1
[25] .debug_aranges PROGBITS 00000000 2d050b0 008ca8 00 0 0 8
[26] .debug_ranges PROGBITS 00000000 2d0dd58 0b88e0 00 0 0 8
[27] .debug_pubnames PROGBITS 00000000 2dc6638 030bff 00 0 0 1
[28] .debug_str PROGBITS 00000000 2df7237 0e1c50 01 MS 0 0 1
[29] .debug_frame PROGBITS 00000000 2ed8e88 08f780 00 0 0 4
[30] .debug_loc PROGBITS 00000000 2f68608 27a795 00 0 0 1
[31] .shstrtab STRTAB 00000000 31e2d9d 00016f 00 0 0 1
[32] .symtab SYMTAB 00000000 31e345c 0e7a10 10 33 50550 4
[33] .strtab STRTAB 00000000 32cae6c 09aad7 00 0 0 1
根据链接脚本定义,_end位于bss段的末尾,因此是0xc0a5c540 + 0x12e7b0 = 0xc0b8acf0
另一种计算方式更加简单,因为_end是链接脚本中定义的符号,因此在编译完成后会在System.map文件中记录他的地址。如下:
c0b8acb4 b cache_cleaner
c0b8ace0 b __key.38242
c0b8ace0 b __key.42691
c0b8ace0 b __key.42767
c0b8ace0 b __key.5625
c0b8ace0 B rpc_debug
c0b8ace4 B nfs_debug
c0b8ace8 B nfsd_debug
c0b8acec B nlm_debug
c0b8acf0 A __bss_stop
c0b8acf0 A _end
2种方式获取的_end地址一致,因此实际临时页表需要映射的空间大小有0xc0b8acf0 - 0xc0008000 = 0xb82cf0,是12070128字节,大约是11.5MB。由于section-mapping是1MB对齐,因此这里内核临时页表完成了12MB空间的线性映射。
(3)在完整页表建立之前内核的函数栈等需要临时分配的空间临时页表有映射到吗?
要弄明白这个问题,还是要从链接脚本arch/arm/kernel/vmlinux.ld.S中找。在data段中可以看到如下符号定义。
.data : AT(__data_loc) {
_data = .; /* address in memory */
_sdata = .;
/*
* first, the init task union, aligned
* to an 8192 byte boundary.
*/
INIT_TASK_DATA(THREAD_SIZE)
这里初始化了8KB的空间作为内核栈空间,kernel启动过程中在汇编初始化完成后,会清空bss段,并将该空间顶端地址交给SP栈指针。然后跳转到start_kernel执行。内核的栈指针就在这8KB空间内向下生长。这8KB空间在内核数据空间中,因此在临时页表中已经完成了映射。
也就是说内核链接时为内核栈预留了空间,因此临时页表完成的内核空间映射,已经包括了text段 data段 bss段以及内核栈部分。
这里讨论了elf文件和二进制镜像文件,让我想起了一个有意思的小问题值得记下来。
有个朋友问,如果我把helloworld编译生成的a.out后面追加一些数据,a.out还能正常运行吗?
kernel编译生成的vmlinux是elf文件,elf文件是执行链接文件,其中会记录该程序各个段的信息,便于解析。我们编写应用程序编译生成的a.out也是elf文件,a.out是可以直接在linux操作系统上运行的,这是因为linux支持对elf文件的解析运行(内核menuconfig可以选择)。
需要搞明白的是,linux系统上执行./a.out运行,其实并不是运行的a.out这个elf文件。
据我的了解,内核对于要运行的elf文件会调用load_elf_binary进行解析,首先是根据elf文件的header信息获取它需要的解释器(如ld),然后加载需要运行的各个段(如data text等)到内存中,把控制权交给解释器。
解释器会加载该程序需要动态链接库(静态链接就不运行解释器),最后解释器将控制权交给内存中的代码段入口,程序开始运行。
所以说a.out后面追加数据对程序运行没有影响,因为这部分数据没有记录在a.out的段信息中,加载到内存中的数据是a.out的代码段 数据段等,追加的数据根本不会被加载到内存中。
不过对于kernel bootloader,这些都是属于裸编 裸跑程序,虽然他们不依赖与第三方库,不需要解释器,但是并没有第三方程序对其elf进行解析。所以kernel bootloader编译时会生成elf文件,但是最后会objcopy生成纯二进制文件,加载到内存中运行的都是二进制镜像文件。
内核临时页表就分析到这里,接下来开始分析start_kernel之后内核完整页表如何一步步建立起来。