嵌入式技术
Linux内核启动流程我们分上下两篇来理解:
上篇是板级引导阶段,一般是用汇编实现。
下篇是通用内核启动阶段,一般是C语言实现。
本文先讲解上篇,大家看到汇编不用担心看不懂,在内核启动阶段,没有特别复杂的流程,都是顺序执行,只需一句一句阅读代码即可。
如何理解板级引导阶段?Linux内核支持不同的芯片架构,比如我们熟知的ARM架构、Intel的X86架构、MPIS架构、RISC-V架构等等,可以在Linux内核源码的arch目录看到不同的芯片架构源码放在对应的文件夹内。每种芯片架构的目录下都包含boot、configs、kernel、lib、Kconfig等文件或目录。
我们以ARM为例,源码放在arch/arm目录下,如下图所示。
1、文件入口
ARM架构的Linux内核的链接脚本文件放在arch/arm/kernel/vmlinux.lds,链接脚本定义了整个内核编译之后的链接过程,决定了一个可执行程序文件的入口和各个section的存储位置。Line 21的OUTPUT_ARCH(arm)指示了该链接脚本针对的是ARM芯片架构,Line 22的ENTRY(stext)指明了内核入口为stext。因此,我们分析Linux内核启动流程,就从stext入口分析。
2、函数入口
ENTRY(stext)是Linux内核的入口函数,该函数定义在arch/arm/kernel/head.S文件。
根据代码注释,stext是Kernel startup entry point,一般地,它从解压代码中获取,这里解压代码是指按照Linux内核压缩格式提取内核可执行文件,它的启动要求如下:
3、调用safe_svcmode_maskall函数,确保安全进入SVC模式并屏蔽所有中断
4、获取处理器ID并找到对应的procinfo
主要实现函数是__lookup_processor_type。
Linux 内核将每种处理器都抽象为一个struct proc_info_list,因此可以通过process id 来找到对应的 proc_info 结构,__lookup_processor_type 函数在__lookup_processor_type_data中找到对应处理器的 proc info,将其保存到 R5 寄存器中。
5、调用函数__vet_atags 验证 atags 或设备树(dtb)的合法性
6、调用__create_page_tables创建页表
7、准备启动start_kernel
start_kernel函数是在__mmap_switched函数内调用的。
上图的line 146指示将__mmap_switched的地址赋给R13,在line 158的__enable_mmu函数内的__turn_mmu_on函数内执行R13,成功进入start_kernel.
__INIT
__mmap_switched:
mov r7, r1
mov r8, r2
mov r10, r0
adr r4, __mmap_switched_data
mov fp, #0
#if defined(CONFIG_XIP_DEFLATED_DATA)
ARM( ldr sp, [r4], #4 )
THUMB( ldr sp, [r4] )
THUMB( add r4, #4 )
bl __inflate_kernel_data @ decompress .data to RAM
teq r0, #0
bne __error
#elif defined(CONFIG_XIP_KERNEL)
ARM( ldmia r4!, {r0, r1, r2, sp} )
THUMB( ldmia r4!, {r0, r1, r2, r3} )
THUMB( mov sp, r3 )
sub r2, r2, r1
bl memcpy @ copy .data to RAM
#endif
ARM( ldmia r4!, {r0, r1, sp} )
THUMB( ldmia r4!, {r0, r1, r3} )
THUMB( mov sp, r3 )
sub r2, r1, r0
mov r1, #0
bl memset @ clear .bss
ldmia r4, {r0, r1, r2, r3}
str r9, [r0] @ Save processor ID
str r7, [r1] @ Save machine type
str r8, [r2] @ Save atags pointer
cmp r3, #0
strne r10, [r3] @ Save control register values
mov lr, #0
b start_kernel
ENDPROC(__mmap_switched)