1.1内核链接脚本分析出内核第一条指令
链接文件怎么读可以参考下面的文章
https://blog.csdn.net/zhjica/article/details/52995536
路径:arch/arm/kernel/vmlinux.lds.S 部分内容如下
OUTPUT_ARCH(arm) 输出文件的架构
ENTRY(stext) 指的是执行的第一条指令的地址,这个是定义在head.S里面
SECTIONS
{
///DISCARD/ 表示这个段内的内容不会出现在输出文件中
/DISCARD/ : {
...
}
#ifdef CONFIG_XIP_KERNEL //这里没有定义
. = XIP_VIRT_ADDR(CONFIG_XIP_PHYS_ADDR);
#else
. = PAGE_OFFSET + TEXT_OFFSET;//所以起始地址就是这个
//PAGE_OFFSET 实际上指的内核空间的起始地址,对于32位的ARM来说,如果我们定义内核空间为1个G的话,这个PAGE_OFFSET = 0xc0000000,
TEXT_OFFSET 实际上是在arc/arm/Makefile中定义的 = 0x00008000
#endif
.head.text : {
_text = .;
HEAD_TEXT
}
arc/arm/Makefile
textofs-y := 0x00008000
TEXT_OFFSET := $(textofs-y)
所以我们查看编译好的system.map的stext就是卫浴0xc0008000地址处,也就是物理地址向上32KB的地方就是内核代码段开始的地方
1.2head.s的分析
根据上面的分析内核执行的第一条代码应该是在head.S中的stext处
-
启动内核必须要满足下面的一些条件
bootloader必须保证MMU是关闭的,必须保证DATA_CASH 是关闭的
arc/arm/kernel/head.S 部分代码
/*
* Kernel startup entry point.
* ---------------------------
*
* 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.
*
* This code is mostly position independent, so if you link the kernel at
* 0xc0008000, you call this at __pa(0xc0008000).
*
* See linux/arch/arm/tools/mach-types for the complete list of machine
* numbers for r1.
*
* We're trying to keep crap to a minimum; DO NOT add any machine specific
* crap here - that's what the boot loader (or in extreme, well justified
* circumstances, zImage) is for.
*/
.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: )
#ifdef CONFIG_ARM_VIRT_EXT
bl __hyp_stub_install
#endif
#跳转到SVC模式,关闭所有中断,内核刚启动的时候有关于中断的状态还没有准备好,这个时候来中断可能会让程序崩溃
#safe_svcmode_maskall位于/arch/arm/include/asm/assembler.h中
@ ensure svc mode and all interrupts masked
safe_svcmode_maskall r9
-
接下来看一个比较重要的函数 bl __create_page_tables
内核在打开MMU的过程中会创建一个虚拟地址等于物理地址的映射用于过度过程。
__create_page_tables:
pgtbl r4, r8 @ page table address 在下面会有分析
先分析一下r4和r8分别代表什么
#ifndef CONFIG_XIP_KERNEL
adr r3, 2f r3==>运行时的pc+标号2的偏移地址 = 比如当前运行的地址是0x6000_0000 那么 r3 = 标号2的相对位置 r3 = 0x6000807c
ldmia r3, {r4, r8} r4 == > 标号2绝对位置 0xc000_807c r8 = 0xc000_0000
sub r4, r3, r4 @ (PHYS_OFFSET - PAGE_OFFSET) r4 = 0x6000_807c - 0xc0000_807c = -0x6000_0000链接地址和运行地址的偏差
add r8, r8, r4 @ PHYS_OFFSET r8 = 0xc000_0000 + (-0x6000_0000) =>0x6000_0000
#else
ldr r8, =PLAT_PHYS_OFFSET @ always constant in this case
#endif
标号2在下面定义了
#ifndef CONFIG_XIP_KERNEL
2: .long .
.long PAGE_OFFSET
#endif
/*.macro pgtbl, rd, phys
add \rd, \phys, #TEXT_OFFSET //rd =0x60000000+0x8000 32KB
sub \rd, \rd, #PG_DIR_SIZE //rd=0x60008000-0x4000 16KB
.endm
所以 r4 = 0x60004000
//对0x60008000~0x60004000这段空间进行清零
根据上面计算得到 ,这里其实就是运行时的物理地址
r8 = 0x6000_0000
接着分析 __create_page_tables
-
__create_page_tables: pgtbl r4, r8 @ page table address 在下面会有分析 * Clear the swapper page table / mov r0, r4 ==>r0 = 0x60004000 mov r3, #0 ==>r3 = 0 add r6, r0, #PG_DIR_SIZE ==>r6 = 0x60008000 1: str r3, [r0], #4 ==>0写入0x60004000 并且 r0= r0 + 4 str r3, [r0], #4 ==>0写入0x60004004 并且 r0= r0 + 4 str r3, [r0], #4 ==>0写入0x60004008 并且 r0= r0 + 4 str r3, [r0], #4 ==>0写入0x6000400c 并且 r0= r0 + 4 teq r0, r6 bne 1b //把MMU的标志位加载到R7寄存器中 ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags r7 = 0xc0e 接下来就是创建虚拟地址和物理地址相等的映射关系 /* * Create identity mapping to cater for __enable_mmu. This identity mapping will be removed by paging_init(). */ //下面这一块代码就是把打开MMU(__turn_mmu_on_loc),这一个代码段一一映射到内核空间上,物理地址==虚拟地址。内核在刚开始的时候还没有开始内存管理,是使用段的内存管理方式,ARM的段大概是1M的空间大小 首先将打开MMU那段代码的section的起始地址放到R5,结束地址放到R6 adr r0, __turn_mmu_on_loc //相对地址 r0 = 0x60008130 ldmia r0, {r3, r5, r6} r3 = __turn_mmu_on_loc链接地址 r5 = __turn_mmu_on链接地址 r6= __turn_mmu_on_end虚拟地址(连接后的地址) sub r0, r0, r3 @ virt->phys offset r0 = 相对位置 -链接位置 = 地址偏差 = -0x6000_0000 add r5, r5, r0 @ phys __turn_mmu_on r5 = r5 + __turn_mmu_on链接地址 = __turn_mmu_on物理地址 add r6, r6, r0 @ phys __turn_mmu_on_end r6 = r6 + __turn_mmu_on_end = __turn_mmu_on_end物理地址 mov r5, r5, lsr #SECTION_SHIFT r5右移1M = (这里的SECTION_SHIFT为1M大小) r5= 0x60b mov r6, r6, lsr #SECTION_SHIFT r6右移1M (1M对齐) r6= 0x60b 1: orr r3, r7, r5, lsl #SECTION_SHIFT @ flags + kernel base 获得First-level descriptor 一级页表描述符 r3 = 0x60b00c0e str r3, [r4, r5, lsl #PMD_ORDER] @ identity mapping 将0x60b00c0e写入0x6000_582C cmp r5, r6 addlo r5, r5, #1 @ next section blo 1b
下图就描述了ARMv11中虚拟地址到物理地址的转换过程。这里就是虚拟地址到物理地址一一对应的关系。首先一级页表的基地址放在0x60004000这个地方。
在内核刚开始阶段是段映射的方式,只有一级页表。下图是turn_mmu这一段代码和物理地址一一映射的分析图。
接下来是内核image代码段映射到物理地址上。
/*
* 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
下面图片就是分析内核代码虚拟地址到物理地址转换的过程
接下来是映射启动参数
/*
* 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]
映射关系如下如所示
创建完临时页表之后 会把__mmap_switched 函数指针存在r13中
ldr r13, =__mmap_switched @ address to jump to after
之后就会 b __enable_mmu
在这个函数中最后会跳转到R13,也就是__mmap_switched函数中
在这个函数中会跳转到start_kernel中。
__mmap_switched在arc/arm/kernel/head-common.S这个文件中定义
arc/arm/kernel/head-common.S
.align 2
.type __mmap_switched_data, %object
__mmap_switched_data:
.long __data_loc @ r4
.long _sdata @ r5
.long __bss_start @ r6
.long _end @ r7
.long processor_id @ r4
.long __machine_arch_type @ r5
.long __atags_pointer @ r6
#ifdef CONFIG_CPU_CP15
.long cr_alignment @ r7
#else
.long 0 @ r7
#endif
.long init_thread_union + THREAD_START_SP @ sp
.size __mmap_switched_data, . - __mmap_switched_data
__mmap_switched:
//__mmap_switched_data 把上面很多值装到相应寄存器中
adr r3, __mmap_switched_data
ldmia r3!, {r4, r5, r6, r7}
cmp r4, r5 @ Copy data segment if needed
1: cmpne r5, r6
ldrne fp, [r4], #4
strne fp, [r5], #4
bne 1b
//清零BSS段
mov fp, #0 @ Clear BSS (and zero fp)
1: cmp r6, r7
strcc fp, [r6],#4
bcc 1b
ARM( ldmia r3, {r4, r5, r6, r7, sp})
THUMB( ldmia r3, {r4, r5, r6, r7} )
THUMB( ldr sp, [r3, #16] )
str r9, [r4] @ Save processor ID
str r1, [r5] @ Save machine type
str r2, [r6] @ Save atags pointer
cmp r7, #0
strne r0, [r7] @ Save control register values
//接下来就会跳转到start_kernel函数
b start_kernel
ENDPROC(__mmap_switched)
至此程序会跳转到start_kernel处。
1.3内核为什么在一开始不用C,而是先用汇编语言再跳转至C程序
C语言的执行需要有堆栈。使用汇编语言设置C程序的堆栈,也就是SP,然后才可以跳转到C语言处执行
如果觉得对你有帮助,可以关注微信公众号 死磕linux 获取更多精彩内容。