1. 项目概述从Bootloader到内核的惊险一跃搞嵌入式Linux开发特别是做BSP或者系统移植的兄弟最绕不开也最让人头疼的环节之一恐怕就是内核启动流程了。你辛辛苦苦编译好一个内核镜像通过Bootloader加载到内存结果串口一片死寂或者直接跑飞重启那种感觉真是让人抓狂。今天我就以经典的Linux 2.6.28内核和S3C6410smdk6410平台为例把内核从解压到start_kernel函数执行前的这一段“黑盒”过程掰开揉碎了讲清楚。这个过程是内核从“裸机”状态到建立起基本运行环境的关键理解它对于定位启动失败、优化启动速度、甚至进行深度定制都至关重要。很多朋友可能只关心start_kernel之后那些眼花缭乱的初始化函数但在我看来前面这段汇编和C语言混合的“奠基”阶段才是真正体现操作系统精妙和硬件交互细节的地方。它完成了从物理地址到虚拟地址的转换、处理器和机器类型的匹配、初始内存映射的建立等核心工作。接下来我会带你一步步走过__lookup_processor_type、创建页表、使能MMU直到跳转到C语言的start_kernel。我会结合ARMv6架构S3C6410核心是ARM1176JZF-S的特点和Linux 2.6.28的源码补充大量原始列表里没有的细节比如地址是如何计算的、某些操作背后的硬件原理、以及我在调试这类平台时踩过的坑和总结的技巧。无论你是正在学习内核的新手还是遇到启动问题需要排查的老鸟希望这篇近万字的深度解析都能给你带来实实在在的帮助。2. 内核启动前置条件与Bootloader的交接在深入内核自身代码之前我们必须明确一点内核不是凭空启动的它需要一个合格的“引导者”为它铺好路。这个引导者就是Bootloader比如我们熟悉的U-Boot。Bootloader和内核之间有一个非常严谨的“合同”任何一方不遵守都会导致启动失败。2.1 Bootloader的职责与“传参”约定对于ARM Linux内核Bootloader在跳转前必须满足几个硬性条件CPU模式必须处于SVC超级用户模式并且禁用中断IRQ和FIQ。这是因为内核启动初期需要完全掌控系统任何中断都可能扰乱极其脆弱的初始化过程。MMU与缓存必须关闭MMU和指令/数据缓存。内核需要从物理地址的视角来设置自己的页表开启缓存前也需要先进行无效化invalidate操作避免脏数据导致不可预知的问题。内存布局内核映像通常是zImage必须被加载到正确的物理内存位置。对于ARM这个加载地址TEXT_OFFSET偏移之后通常是0x0000800032KiB处或0x0001000064KiB处但具体取决于内核配置。更关键的是内核期望在0x00000100开始的位置找到一个有效的ATAG列表对于旧内核或DTB设备树新内核的物理地址。这是Bootloader向内核传递内存大小、命令行参数等关键信息的唯一方式。跳转地址Bootloader最后通过一条类似mov pc, #0x00008000的指令跳转到内核入口点。这个入口点并不是start_kernel而是架构相关的汇编入口在ARM上通常是arch/arm/boot/compressed/head.S或arch/arm/kernel/head.S。注意最常见的启动失败原因之一就是Bootloader传递的参数ATAG/DTB不正确或内存信息有误。我遇到过因为Bootloader中配置的RAM大小与实际硬件不符导致内核在初始化内存时访问越界而崩溃的情况。务必使用bootm或go命令正确传递参数。2.2 zImage的解压decompress_kernel的幕后工作我们常说的“内核镜像”zImage其实是一个自解压的包裹。它的结构是一小段解压程序head.S 压缩的内核代码piggy.gz。Bootloader加载并跳转到zImage后首先执行的就是解压程序。decompress_kernel()函数位于arch/arm/boot/compressed/misc.c是解压的核心。它主要做以下几件事选择解压地址计算解压后内核应该存放的最终位置。这通常是链接地址如0xC0008000这是内核虚拟地址空间的开始0x8000偏移对应的物理地址。这里有一个关键点解压程序是在物理地址空间运行的但它需要知道内核期望的虚拟地址布局。调用解压算法早期内核多用gzip它会调用gunzip()函数。这个过程会进行CRC校验确保压缩数据在加载过程中没有出错。跳转到解压后的内核解压完成后代码会跳转到解压后内核的入口点对于ARM就是arch/arm/kernel/head.S中的stext符号。至此压缩的“外壳”使命完成真正的内核开始启动。实操心得在早期调试时如果串口没有任何输出就死了一个排查思路是检查解压是否成功。可以在decompress_kernel函数中加入简单的串口打印字符如putc(D)来标记执行进度。当然这需要你重新编译内核。另一个方法是利用JTAG调试器直接查看解压目标地址的内存内容看是否变成了可识别的内核代码例如能看到“Linux version”字符串。3. 内核启动第一阶段汇编世界的奠基跳转到stext后内核开始执行最底层的、与架构紧密相关的汇编代码。这部分代码全部用汇编语言写成目的是在C语言环境堆栈、全局变量尚未建立之前为内核准备好最基础的运行环境。原始列表中的步骤1到6都在这里发生。3.1 身份验证查找处理器与机器类型内核不是万能钥匙它需要知道自己在什么样的硬件上运行。这就是__lookup_processor_type和__lookup_machine_type的作用。__lookup_processor_type内核源码中维护了一个处理器信息表proc_info_list包含了CPU ID、MMU操作函数指针、缓存操作函数等。这个函数会读取ARM协处理器CP15的c0寄存器获取当前硬件的CPU ID然后与表中的条目逐一比对。找到匹配项后会将对应的proc_info_list结构体地址存入一个寄存器通常是r5或r9供后续代码使用。如果找不到内核会挂起或尝试以最兼容的模式运行但很可能失败。为什么需要这个因为不同ARM核心如ARM9, ARM11, Cortex-A系列的缓存、MMU、协处理器操作指令可能不同。内核必须使用匹配的底层操作函数。__lookup_machine_type同样内核也有一个机器描述符表machine_desc由MACHINE_START宏定义。它描述了这块特定开发板或设备的物理内存布局、IO映射、启动参数、初始化函数等。Bootloader通过r1寄存器或者ATAG列表中的machine_arch_type告诉内核机器ID。这个函数就是根据这个ID找到对应的machine_desc。为什么需要这个即使CPU相同不同的板子内存大小、串口地址、中断控制器都可能不同。machine_desc就是这块板子的“身份证”和“说明书”。踩坑记录machine_type不匹配是移植内核时的高发问题。症状可能是内核能启动一部分但在初始化特定设备如串口时失败。确保你的内核配置CONFIG_MACH_SMDK6410和Bootloader传递的机器ID一致。在U-Boot中可以通过machid环境变量来设置。3.2 参数检查与早期内存映射__vet_atags这是一个简单的校验函数确保r2寄存器指向的ATAG列表起始于有效的ATAG_CORE标签并且列表的结束标签是ATAG_NONE。防止传入一个非法指针导致内核访问错误地址。__create_page_tables这是启动过程中最精妙也最复杂的步骤之一。此时MMU还未开启CPU访问的是物理地址。但内核代码是按虚拟地址高地址如0xC0000000编译链接的。为了在开启MMU后代码能连续执行必须提前建立一段“恒等映射”identity mapping。恒等映射将内核起始位置附近的一段物理地址例如0x00008000开始和它对应的虚拟地址0xC0008000映射到相同的物理页。这样当一条指令在0xC0008000虚拟地址执行时MMU将其翻译为0x00008000物理地址而代码实际就存放在那里。开启MMU的瞬间PC指针虽然指向虚拟地址但通过这个映射CPU依然能取到正确的下一条指令实现无缝切换。映射范围通常只映射内核代码段、数据段以及可能用于存放页表的很小一块内存区域。这是一个临时的、粗糙的映射通常使用1MB的段映射而非4KB的页映射只为保证内核能安全度过开启MMU的过渡期。更精细的内存管理会在start_kernel之后的paging_init中完成。3.3 开启MMU与切换运行环境__enable_mmu这个函数配置CP15的MMU相关控制寄存器然后使能MMU。使能后CPU发出的所有地址都将被视为虚拟地址由MMU进行翻译。__mmap_switched这是一个关键的跳板函数。在MMU开启后代码会跳转到这里。它主要完成从汇编世界到C世界的最后准备工作复制数据段将初始化数据.data段从它的加载地址可能是ROM或镜像中的只读部分复制到正确的RAM位置。清零BSS段将未初始化全局变量.bss段所在的内存区域全部清零。这是C语言标准的要求。保存处理器和机器信息将之前找到的processor_id和__machine_arch_type保存到全局变量中。设置栈指针为初始进程init_task即0号进程设置栈指针。C函数调用需要栈。最终跳转完成上述所有工作后通过b start_kernel指令正式跳入C语言的主入口函数。至此内核的汇编启动阶段圆满结束一个具备基本运行环境虚拟内存、栈、初始数据的C语言世界已经准备就绪。核心原理为什么需要这么复杂的“恒等映射”和“跳板”根本原因在于编译地址和运行地址的分离。内核代码被编译为在虚拟地址空间运行但CPU上电后执行的是物理地址。开启MMU是连接这两个世界的“开关”。__mmap_switched所做的正是在“开关”扳动之后将程序执行流和数据处理平稳地过渡到虚拟地址空间预设的位置上。4. 内核启动第二阶段start_kernel的宏大序章进入start_kernel位于init/main.c我们终于来到了熟悉的C语言领域。这个函数是一个庞大的初始化例程集合它几乎初始化了内核的所有子系统。原始列表从第7步开始列出了多达80多个初始化函数我们不可能每个都深究但我会把其中最关键、最容易出问题的部分拎出来讲透。4.1 早期架构与平台相关初始化start_kernel一开始在打印出“Linux version” banner之前会执行一系列至关重要的早期初始化setup_arch(command_line):这是平台初始化的核心。它会解析Bootloader传来的ATAG或DTB获取内存布局信息并调用平台相关的map_io函数在machine_desc中定义来建立完整的IO内存映射。对于S3C6410这个函数会初始化串口、GPIO控制器、时钟等关键外设的虚拟地址映射。如果串口在这里初始化成功我们才能看到后续的内核打印信息。setup_command_line: 保存从Bootloader传来的命令行参数字符串。setup_per_cpu_areas: 为每个可能的CPU核心分配per-cpu数据区域。即使在单核系统中这个机制也存在。build_all_zonelists: 建立内存管理区的列表ZONE_DMA, ZONE_NORMAL等这是内存分配器buddy system工作的基础。page_alloc_init: 初始化页分配器。注意事项setup_arch的失败通常意味着严重的硬件不匹配或配置错误。例如如果DTB中描述的内存节点超出了物理实际大小或者map_io中映射的串口地址错误都会导致内核“静默死亡”或输出乱码。调试时可以尝试在setup_arch函数内部的关键路径添加early_printk如果支持来定位问题点。4.2 中断、定时器与进程管理的基石在内存和基本IO就绪后内核开始构建更上层的抽象trap_init: 设置系统的异常向量表在ARM中是在__vectors_start处设置的一系列跳转指令确保发生未定义指令、数据中止、IRQ、FIQ等异常时CPU能跳转到内核正确的处理函数。init_IRQ: 初始化中断控制器。对于S3C6410会初始化其VICVectored Interrupt Controller。这里会设置每个中断的默认处理函数可能只是个占位符并将中断向量表地址写入协处理器。softirq_init: 初始化软中断机制。软中断是下半部bottom half处理的重要方式。timekeeping_init/time_init: 初始化内核时间系统。time_init会调用平台特定的函数如s3c64xx_timer_init来初始化硬件定时器设置定时器中断tick。系统tick的启动是整个内核调度和定时功能得以运转的前提。sched_init: 初始化进程调度器创建运行队列初始化init_task0号进程的调度信息。pidhash_init: 初始化进程ID的哈希表用于快速通过PID查找进程。4.3 核心子系统与内存管理的完善随着基础设施的建立更复杂的子系统开始初始化console_init:这是用户能看到内核消息的关键一步。它初始化内核的虚拟控制台tty并尝试注册和启用一个早期控制台earlycon或真正的控制台驱动。如果串口驱动在此之前已正确初始化那么从这里开始printk的消息就能输出到串口了。mem_init: 打印内存信息我们开机时看到的那一大段内存信息并释放所有未被保留的、可用的物理页给伙伴分配器管理。标志着物理内存初始化基本完成。kmem_cache_init: 初始化slab分配器。slab是内核用于高效分配小对象如task_struct,inode等的机制它的初始化必须在很多其他子系统之前完成。calibrate_delay: 著名的“BogoMIPS”校准循环。它通过一个忙等待循环来估算处理器的速度这个值会被一些驱动程序如某些老式网卡驱动用于粗略的延时计算。虽然名字有点滑稽但它是一个必要的步骤。fork_init: 根据系统内存大小设置进程结构task_struct的slab缓存和最大进程数限制。vfs_caches_init: 初始化虚拟文件系统VFS相关的缓存如dentry和inode缓存。文件系统的抽象层开始建立。4.4 最后的准备与用户空间的诞生在经历了漫长的初始化后start_kernel接近尾声proc_root_init: 挂载proc文件系统。/proc是一个反映内核状态信息的虚拟文件系统很多工具如ps,top都依赖它。check_bugs: 检查CPU是否存在某些已知的硬件缺陷bug并应用软件补丁。rest_init: 这是start_kernel调用的最后一个函数但它的工作却开启了新的篇章。它通过kernel_thread创建两个内核线程kernel_init: 这就是著名的1号进程init进程。它最终会尝试执行用户空间的/sbin/init程序从而拉起整个用户空间的服务和应用程序。kthreadd: 2号进程内核守护进程负责管理和调度其他所有内核线程。在创建完这两个线程后rest_init会调用schedule_preempt_disabled()来主动触发一次进程调度。此时0号进程init_task也就是当前正在运行的内核启动上下文会变为空闲进程idle其PID仍然是0。CPU会开始执行调度器选出的进程通常是新创建的kernel_init。至此内核的启动主体部分宣告完成。系统的控制权从内核的初始化代码逐渐移交给了init进程和调度器一个现代操作系统的基本骨架已经搭建完毕准备迎接用户空间的到来。5. 常见启动问题排查与调试技巧实录理解了流程我们更需要知道当流程中断时该怎么办。以下是我在调试S3C6410等ARM平台内核启动时总结的一些典型问题场景和排查手段。5.1 串口无任何输出这是最令人沮丧的情况。排查需要由简入繁从硬件到软件硬件与Bootloader层面检查接线与电压确认串口线TX/RX是否接反串口芯片电平3.3V/5V是否匹配。确认Bootloader状态确保Bootloader如U-Boot本身能通过串口正常输出信息。如果Bootloader都无输出问题肯定在更底层时钟、电源、复位、串口引脚复用配置。检查内核加载地址使用U-Boot的mdmemory display命令检查内核应该存放的内存地址如0x50008000处是否有数据并与编译出的zImage文件头对比确认Bootloader正确加载了内核。检查启动命令确认U-Boot的bootm或go命令参数正确特别是ATAG或DTB的地址。内核早期汇编阶段在解压代码中加“灯”如果怀疑解压失败可以修改decompress_kernel函数在开头和结尾通过写某个GPIO引脚的高低电平来指示。用示波器或万用表测量这个GPIO可以判断代码是否执行到该点。使用JTAG调试器这是最强大的工具。连接JTAG在stext入口处设置断点单步跟踪汇编代码。查看寄存器值尤其是r0,r1,r2,sp,pc检查__lookup_processor_type和__lookup_machine_type的返回值是否有效。重点观察在__enable_mmu执行前后PC指针的变化是否如预期。内核C语言初始化早期启用EARLY_PRINTK在内核配置中启用CONFIG_DEBUG_LL和CONFIG_EARLY_PRINTK。这会在printk基础设施console_init完全初始化之前使用一个最简单的、轮询方式的串口输出函数。你需要根据你的平台在arch/arm/include/debug/目录下找到或编写对应的串口调试宏。启用后setup_arch等函数中的早期printk信息就能输出极大缩小问题范围。检查machine_desc匹配确认内核编译时选择的MACHINE_START在arch/arm/mach-s3c64xx/mach-smdk6410.c中与Bootloader传递的机器ID完全一致。不一致可能导致map_io函数不被调用串口无法初始化。5.2 内核打印乱码或跑飞有输出但不对说明代码在执行但环境有问题。乱码波特率不匹配这是最常见原因。检查Bootloader设置的串口波特率与内核中early_printk或串口驱动初始化的波特率是否完全相同。ARM平台时钟树复杂确保用于串口时钟源的PLL配置正确。数据位/停止位/校验位不匹配相对少见但也要检查。内存映射错误如果串口控制器的物理地址到虚拟地址映射map_io错误写寄存器可能会覆盖到其他内存导致奇怪现象。打印部分信息后跑飞观察最后一条信息这条信息通常指向了崩溃前最后执行的初始化函数。例如如果在mem_init后崩溃可能是内存配置ATAG/DTB中的内存节点有误导致内核访问了不存在的物理内存。启用内核Panic/Oops打印确保内核配置了CONFIG_DEBUG_INFO编译带调试信息和CONFIG_KALLSYMS。这样内核发生Oops访问非法地址等时能打印出错误的调用栈backtrace直接定位到出错的函数和行号。检查中断冲突如果在init_IRQ或设备驱动初始化后跑飞可能是中断号分配冲突或中断处理函数ISR编写有误导致进入中断后无法正确返回。5.3 内核卡在“Starting kernel ...”或“Uncompressing Linux...”这通常意味着Bootloader已经把控制权交给了内核但内核的早期汇编代码执行受阻。卡在“Uncompressing Linux...”问题出在decompress_kernel。可能原因内核镜像zImage在传输或加载过程中损坏。用md5sum或sha1sum校验加载到内存的数据与原始文件是否一致。解压目标地址与内存现有内容冲突。确保解压的目标区域内核的最终运行地址没有被Bootloader或ATAG/DTB占用。卡在“Starting kernel ...”Bootloader跳转后立即卡住。可能原因跳转地址错误Bootloader跳转的地址不是内核的入口点。对于zImage跳转地址就是加载地址。对于uImage跳转地址是加载地址64字节的U-Boot头。CPU状态不满足要求回顾2.1节检查Bootloader跳转前是否关闭了MMU和缓存是否处于SVC模式。ATAG/DTB指针错误r2寄存器指向了一个非法地址导致内核在__vet_atags或后续解析时崩溃。5.4 实用调试工具与方法速查表问题现象可能原因调试工具/方法检查点完全无输出1. 硬件/串口问题2. Bootloader未加载内核3. 内核入口代码崩溃1. 万用表/示波器2. JTAG单步调试3. GPIO点灯法1. 串口引脚电压2.r0,r1,r2寄存器值3.__lookup_processor_type结果输出乱码1. 波特率不匹配2. 时钟配置错误3. 内存映射错误1. 核对各阶段波特率配置2. 检查PLL和分频寄存器3. 启用EARLY_PRINTK1. Bootloaderbaudrate变量2. 内核early_printk设置3.map_io中的UART基地址打印部分后死机1. 内存信息错误2. 特定子系统初始化失败3. 中断问题1. 分析最后一条打印2. 启用CONFIG_DEBUG_INFO3. JTAG查看崩溃地址1. ATAG/DTB内存节点2. Oops调用栈3. 中断控制器配置卡在解压或启动1. 镜像损坏2. 加载/跳转地址错3. 运行环境不符1. 校验镜像完整性2. 检查Bootloader命令3. JTAG跟踪汇编1.go/bootm参数2. MMU/缓存状态3. 解压目标地址空间调试内核启动是一个需要耐心和系统方法的过程。我的经验是永远从最简单的假设开始验证比如线是不是松了然后利用EARLY_PRINTK和JTAG这两大利器将问题范围从“整个内核”逐步缩小到“某个函数”再到“某行代码”。理解本文剖析的启动流程能让你在查看代码或反汇编时清楚地知道自己正在哪个阶段下一步该去哪里从而高效地定位问题根源。
Linux内核启动全解析:从Bootloader到start_kernel的底层原理与调试实战
1. 项目概述从Bootloader到内核的惊险一跃搞嵌入式Linux开发特别是做BSP或者系统移植的兄弟最绕不开也最让人头疼的环节之一恐怕就是内核启动流程了。你辛辛苦苦编译好一个内核镜像通过Bootloader加载到内存结果串口一片死寂或者直接跑飞重启那种感觉真是让人抓狂。今天我就以经典的Linux 2.6.28内核和S3C6410smdk6410平台为例把内核从解压到start_kernel函数执行前的这一段“黑盒”过程掰开揉碎了讲清楚。这个过程是内核从“裸机”状态到建立起基本运行环境的关键理解它对于定位启动失败、优化启动速度、甚至进行深度定制都至关重要。很多朋友可能只关心start_kernel之后那些眼花缭乱的初始化函数但在我看来前面这段汇编和C语言混合的“奠基”阶段才是真正体现操作系统精妙和硬件交互细节的地方。它完成了从物理地址到虚拟地址的转换、处理器和机器类型的匹配、初始内存映射的建立等核心工作。接下来我会带你一步步走过__lookup_processor_type、创建页表、使能MMU直到跳转到C语言的start_kernel。我会结合ARMv6架构S3C6410核心是ARM1176JZF-S的特点和Linux 2.6.28的源码补充大量原始列表里没有的细节比如地址是如何计算的、某些操作背后的硬件原理、以及我在调试这类平台时踩过的坑和总结的技巧。无论你是正在学习内核的新手还是遇到启动问题需要排查的老鸟希望这篇近万字的深度解析都能给你带来实实在在的帮助。2. 内核启动前置条件与Bootloader的交接在深入内核自身代码之前我们必须明确一点内核不是凭空启动的它需要一个合格的“引导者”为它铺好路。这个引导者就是Bootloader比如我们熟悉的U-Boot。Bootloader和内核之间有一个非常严谨的“合同”任何一方不遵守都会导致启动失败。2.1 Bootloader的职责与“传参”约定对于ARM Linux内核Bootloader在跳转前必须满足几个硬性条件CPU模式必须处于SVC超级用户模式并且禁用中断IRQ和FIQ。这是因为内核启动初期需要完全掌控系统任何中断都可能扰乱极其脆弱的初始化过程。MMU与缓存必须关闭MMU和指令/数据缓存。内核需要从物理地址的视角来设置自己的页表开启缓存前也需要先进行无效化invalidate操作避免脏数据导致不可预知的问题。内存布局内核映像通常是zImage必须被加载到正确的物理内存位置。对于ARM这个加载地址TEXT_OFFSET偏移之后通常是0x0000800032KiB处或0x0001000064KiB处但具体取决于内核配置。更关键的是内核期望在0x00000100开始的位置找到一个有效的ATAG列表对于旧内核或DTB设备树新内核的物理地址。这是Bootloader向内核传递内存大小、命令行参数等关键信息的唯一方式。跳转地址Bootloader最后通过一条类似mov pc, #0x00008000的指令跳转到内核入口点。这个入口点并不是start_kernel而是架构相关的汇编入口在ARM上通常是arch/arm/boot/compressed/head.S或arch/arm/kernel/head.S。注意最常见的启动失败原因之一就是Bootloader传递的参数ATAG/DTB不正确或内存信息有误。我遇到过因为Bootloader中配置的RAM大小与实际硬件不符导致内核在初始化内存时访问越界而崩溃的情况。务必使用bootm或go命令正确传递参数。2.2 zImage的解压decompress_kernel的幕后工作我们常说的“内核镜像”zImage其实是一个自解压的包裹。它的结构是一小段解压程序head.S 压缩的内核代码piggy.gz。Bootloader加载并跳转到zImage后首先执行的就是解压程序。decompress_kernel()函数位于arch/arm/boot/compressed/misc.c是解压的核心。它主要做以下几件事选择解压地址计算解压后内核应该存放的最终位置。这通常是链接地址如0xC0008000这是内核虚拟地址空间的开始0x8000偏移对应的物理地址。这里有一个关键点解压程序是在物理地址空间运行的但它需要知道内核期望的虚拟地址布局。调用解压算法早期内核多用gzip它会调用gunzip()函数。这个过程会进行CRC校验确保压缩数据在加载过程中没有出错。跳转到解压后的内核解压完成后代码会跳转到解压后内核的入口点对于ARM就是arch/arm/kernel/head.S中的stext符号。至此压缩的“外壳”使命完成真正的内核开始启动。实操心得在早期调试时如果串口没有任何输出就死了一个排查思路是检查解压是否成功。可以在decompress_kernel函数中加入简单的串口打印字符如putc(D)来标记执行进度。当然这需要你重新编译内核。另一个方法是利用JTAG调试器直接查看解压目标地址的内存内容看是否变成了可识别的内核代码例如能看到“Linux version”字符串。3. 内核启动第一阶段汇编世界的奠基跳转到stext后内核开始执行最底层的、与架构紧密相关的汇编代码。这部分代码全部用汇编语言写成目的是在C语言环境堆栈、全局变量尚未建立之前为内核准备好最基础的运行环境。原始列表中的步骤1到6都在这里发生。3.1 身份验证查找处理器与机器类型内核不是万能钥匙它需要知道自己在什么样的硬件上运行。这就是__lookup_processor_type和__lookup_machine_type的作用。__lookup_processor_type内核源码中维护了一个处理器信息表proc_info_list包含了CPU ID、MMU操作函数指针、缓存操作函数等。这个函数会读取ARM协处理器CP15的c0寄存器获取当前硬件的CPU ID然后与表中的条目逐一比对。找到匹配项后会将对应的proc_info_list结构体地址存入一个寄存器通常是r5或r9供后续代码使用。如果找不到内核会挂起或尝试以最兼容的模式运行但很可能失败。为什么需要这个因为不同ARM核心如ARM9, ARM11, Cortex-A系列的缓存、MMU、协处理器操作指令可能不同。内核必须使用匹配的底层操作函数。__lookup_machine_type同样内核也有一个机器描述符表machine_desc由MACHINE_START宏定义。它描述了这块特定开发板或设备的物理内存布局、IO映射、启动参数、初始化函数等。Bootloader通过r1寄存器或者ATAG列表中的machine_arch_type告诉内核机器ID。这个函数就是根据这个ID找到对应的machine_desc。为什么需要这个即使CPU相同不同的板子内存大小、串口地址、中断控制器都可能不同。machine_desc就是这块板子的“身份证”和“说明书”。踩坑记录machine_type不匹配是移植内核时的高发问题。症状可能是内核能启动一部分但在初始化特定设备如串口时失败。确保你的内核配置CONFIG_MACH_SMDK6410和Bootloader传递的机器ID一致。在U-Boot中可以通过machid环境变量来设置。3.2 参数检查与早期内存映射__vet_atags这是一个简单的校验函数确保r2寄存器指向的ATAG列表起始于有效的ATAG_CORE标签并且列表的结束标签是ATAG_NONE。防止传入一个非法指针导致内核访问错误地址。__create_page_tables这是启动过程中最精妙也最复杂的步骤之一。此时MMU还未开启CPU访问的是物理地址。但内核代码是按虚拟地址高地址如0xC0000000编译链接的。为了在开启MMU后代码能连续执行必须提前建立一段“恒等映射”identity mapping。恒等映射将内核起始位置附近的一段物理地址例如0x00008000开始和它对应的虚拟地址0xC0008000映射到相同的物理页。这样当一条指令在0xC0008000虚拟地址执行时MMU将其翻译为0x00008000物理地址而代码实际就存放在那里。开启MMU的瞬间PC指针虽然指向虚拟地址但通过这个映射CPU依然能取到正确的下一条指令实现无缝切换。映射范围通常只映射内核代码段、数据段以及可能用于存放页表的很小一块内存区域。这是一个临时的、粗糙的映射通常使用1MB的段映射而非4KB的页映射只为保证内核能安全度过开启MMU的过渡期。更精细的内存管理会在start_kernel之后的paging_init中完成。3.3 开启MMU与切换运行环境__enable_mmu这个函数配置CP15的MMU相关控制寄存器然后使能MMU。使能后CPU发出的所有地址都将被视为虚拟地址由MMU进行翻译。__mmap_switched这是一个关键的跳板函数。在MMU开启后代码会跳转到这里。它主要完成从汇编世界到C世界的最后准备工作复制数据段将初始化数据.data段从它的加载地址可能是ROM或镜像中的只读部分复制到正确的RAM位置。清零BSS段将未初始化全局变量.bss段所在的内存区域全部清零。这是C语言标准的要求。保存处理器和机器信息将之前找到的processor_id和__machine_arch_type保存到全局变量中。设置栈指针为初始进程init_task即0号进程设置栈指针。C函数调用需要栈。最终跳转完成上述所有工作后通过b start_kernel指令正式跳入C语言的主入口函数。至此内核的汇编启动阶段圆满结束一个具备基本运行环境虚拟内存、栈、初始数据的C语言世界已经准备就绪。核心原理为什么需要这么复杂的“恒等映射”和“跳板”根本原因在于编译地址和运行地址的分离。内核代码被编译为在虚拟地址空间运行但CPU上电后执行的是物理地址。开启MMU是连接这两个世界的“开关”。__mmap_switched所做的正是在“开关”扳动之后将程序执行流和数据处理平稳地过渡到虚拟地址空间预设的位置上。4. 内核启动第二阶段start_kernel的宏大序章进入start_kernel位于init/main.c我们终于来到了熟悉的C语言领域。这个函数是一个庞大的初始化例程集合它几乎初始化了内核的所有子系统。原始列表从第7步开始列出了多达80多个初始化函数我们不可能每个都深究但我会把其中最关键、最容易出问题的部分拎出来讲透。4.1 早期架构与平台相关初始化start_kernel一开始在打印出“Linux version” banner之前会执行一系列至关重要的早期初始化setup_arch(command_line):这是平台初始化的核心。它会解析Bootloader传来的ATAG或DTB获取内存布局信息并调用平台相关的map_io函数在machine_desc中定义来建立完整的IO内存映射。对于S3C6410这个函数会初始化串口、GPIO控制器、时钟等关键外设的虚拟地址映射。如果串口在这里初始化成功我们才能看到后续的内核打印信息。setup_command_line: 保存从Bootloader传来的命令行参数字符串。setup_per_cpu_areas: 为每个可能的CPU核心分配per-cpu数据区域。即使在单核系统中这个机制也存在。build_all_zonelists: 建立内存管理区的列表ZONE_DMA, ZONE_NORMAL等这是内存分配器buddy system工作的基础。page_alloc_init: 初始化页分配器。注意事项setup_arch的失败通常意味着严重的硬件不匹配或配置错误。例如如果DTB中描述的内存节点超出了物理实际大小或者map_io中映射的串口地址错误都会导致内核“静默死亡”或输出乱码。调试时可以尝试在setup_arch函数内部的关键路径添加early_printk如果支持来定位问题点。4.2 中断、定时器与进程管理的基石在内存和基本IO就绪后内核开始构建更上层的抽象trap_init: 设置系统的异常向量表在ARM中是在__vectors_start处设置的一系列跳转指令确保发生未定义指令、数据中止、IRQ、FIQ等异常时CPU能跳转到内核正确的处理函数。init_IRQ: 初始化中断控制器。对于S3C6410会初始化其VICVectored Interrupt Controller。这里会设置每个中断的默认处理函数可能只是个占位符并将中断向量表地址写入协处理器。softirq_init: 初始化软中断机制。软中断是下半部bottom half处理的重要方式。timekeeping_init/time_init: 初始化内核时间系统。time_init会调用平台特定的函数如s3c64xx_timer_init来初始化硬件定时器设置定时器中断tick。系统tick的启动是整个内核调度和定时功能得以运转的前提。sched_init: 初始化进程调度器创建运行队列初始化init_task0号进程的调度信息。pidhash_init: 初始化进程ID的哈希表用于快速通过PID查找进程。4.3 核心子系统与内存管理的完善随着基础设施的建立更复杂的子系统开始初始化console_init:这是用户能看到内核消息的关键一步。它初始化内核的虚拟控制台tty并尝试注册和启用一个早期控制台earlycon或真正的控制台驱动。如果串口驱动在此之前已正确初始化那么从这里开始printk的消息就能输出到串口了。mem_init: 打印内存信息我们开机时看到的那一大段内存信息并释放所有未被保留的、可用的物理页给伙伴分配器管理。标志着物理内存初始化基本完成。kmem_cache_init: 初始化slab分配器。slab是内核用于高效分配小对象如task_struct,inode等的机制它的初始化必须在很多其他子系统之前完成。calibrate_delay: 著名的“BogoMIPS”校准循环。它通过一个忙等待循环来估算处理器的速度这个值会被一些驱动程序如某些老式网卡驱动用于粗略的延时计算。虽然名字有点滑稽但它是一个必要的步骤。fork_init: 根据系统内存大小设置进程结构task_struct的slab缓存和最大进程数限制。vfs_caches_init: 初始化虚拟文件系统VFS相关的缓存如dentry和inode缓存。文件系统的抽象层开始建立。4.4 最后的准备与用户空间的诞生在经历了漫长的初始化后start_kernel接近尾声proc_root_init: 挂载proc文件系统。/proc是一个反映内核状态信息的虚拟文件系统很多工具如ps,top都依赖它。check_bugs: 检查CPU是否存在某些已知的硬件缺陷bug并应用软件补丁。rest_init: 这是start_kernel调用的最后一个函数但它的工作却开启了新的篇章。它通过kernel_thread创建两个内核线程kernel_init: 这就是著名的1号进程init进程。它最终会尝试执行用户空间的/sbin/init程序从而拉起整个用户空间的服务和应用程序。kthreadd: 2号进程内核守护进程负责管理和调度其他所有内核线程。在创建完这两个线程后rest_init会调用schedule_preempt_disabled()来主动触发一次进程调度。此时0号进程init_task也就是当前正在运行的内核启动上下文会变为空闲进程idle其PID仍然是0。CPU会开始执行调度器选出的进程通常是新创建的kernel_init。至此内核的启动主体部分宣告完成。系统的控制权从内核的初始化代码逐渐移交给了init进程和调度器一个现代操作系统的基本骨架已经搭建完毕准备迎接用户空间的到来。5. 常见启动问题排查与调试技巧实录理解了流程我们更需要知道当流程中断时该怎么办。以下是我在调试S3C6410等ARM平台内核启动时总结的一些典型问题场景和排查手段。5.1 串口无任何输出这是最令人沮丧的情况。排查需要由简入繁从硬件到软件硬件与Bootloader层面检查接线与电压确认串口线TX/RX是否接反串口芯片电平3.3V/5V是否匹配。确认Bootloader状态确保Bootloader如U-Boot本身能通过串口正常输出信息。如果Bootloader都无输出问题肯定在更底层时钟、电源、复位、串口引脚复用配置。检查内核加载地址使用U-Boot的mdmemory display命令检查内核应该存放的内存地址如0x50008000处是否有数据并与编译出的zImage文件头对比确认Bootloader正确加载了内核。检查启动命令确认U-Boot的bootm或go命令参数正确特别是ATAG或DTB的地址。内核早期汇编阶段在解压代码中加“灯”如果怀疑解压失败可以修改decompress_kernel函数在开头和结尾通过写某个GPIO引脚的高低电平来指示。用示波器或万用表测量这个GPIO可以判断代码是否执行到该点。使用JTAG调试器这是最强大的工具。连接JTAG在stext入口处设置断点单步跟踪汇编代码。查看寄存器值尤其是r0,r1,r2,sp,pc检查__lookup_processor_type和__lookup_machine_type的返回值是否有效。重点观察在__enable_mmu执行前后PC指针的变化是否如预期。内核C语言初始化早期启用EARLY_PRINTK在内核配置中启用CONFIG_DEBUG_LL和CONFIG_EARLY_PRINTK。这会在printk基础设施console_init完全初始化之前使用一个最简单的、轮询方式的串口输出函数。你需要根据你的平台在arch/arm/include/debug/目录下找到或编写对应的串口调试宏。启用后setup_arch等函数中的早期printk信息就能输出极大缩小问题范围。检查machine_desc匹配确认内核编译时选择的MACHINE_START在arch/arm/mach-s3c64xx/mach-smdk6410.c中与Bootloader传递的机器ID完全一致。不一致可能导致map_io函数不被调用串口无法初始化。5.2 内核打印乱码或跑飞有输出但不对说明代码在执行但环境有问题。乱码波特率不匹配这是最常见原因。检查Bootloader设置的串口波特率与内核中early_printk或串口驱动初始化的波特率是否完全相同。ARM平台时钟树复杂确保用于串口时钟源的PLL配置正确。数据位/停止位/校验位不匹配相对少见但也要检查。内存映射错误如果串口控制器的物理地址到虚拟地址映射map_io错误写寄存器可能会覆盖到其他内存导致奇怪现象。打印部分信息后跑飞观察最后一条信息这条信息通常指向了崩溃前最后执行的初始化函数。例如如果在mem_init后崩溃可能是内存配置ATAG/DTB中的内存节点有误导致内核访问了不存在的物理内存。启用内核Panic/Oops打印确保内核配置了CONFIG_DEBUG_INFO编译带调试信息和CONFIG_KALLSYMS。这样内核发生Oops访问非法地址等时能打印出错误的调用栈backtrace直接定位到出错的函数和行号。检查中断冲突如果在init_IRQ或设备驱动初始化后跑飞可能是中断号分配冲突或中断处理函数ISR编写有误导致进入中断后无法正确返回。5.3 内核卡在“Starting kernel ...”或“Uncompressing Linux...”这通常意味着Bootloader已经把控制权交给了内核但内核的早期汇编代码执行受阻。卡在“Uncompressing Linux...”问题出在decompress_kernel。可能原因内核镜像zImage在传输或加载过程中损坏。用md5sum或sha1sum校验加载到内存的数据与原始文件是否一致。解压目标地址与内存现有内容冲突。确保解压的目标区域内核的最终运行地址没有被Bootloader或ATAG/DTB占用。卡在“Starting kernel ...”Bootloader跳转后立即卡住。可能原因跳转地址错误Bootloader跳转的地址不是内核的入口点。对于zImage跳转地址就是加载地址。对于uImage跳转地址是加载地址64字节的U-Boot头。CPU状态不满足要求回顾2.1节检查Bootloader跳转前是否关闭了MMU和缓存是否处于SVC模式。ATAG/DTB指针错误r2寄存器指向了一个非法地址导致内核在__vet_atags或后续解析时崩溃。5.4 实用调试工具与方法速查表问题现象可能原因调试工具/方法检查点完全无输出1. 硬件/串口问题2. Bootloader未加载内核3. 内核入口代码崩溃1. 万用表/示波器2. JTAG单步调试3. GPIO点灯法1. 串口引脚电压2.r0,r1,r2寄存器值3.__lookup_processor_type结果输出乱码1. 波特率不匹配2. 时钟配置错误3. 内存映射错误1. 核对各阶段波特率配置2. 检查PLL和分频寄存器3. 启用EARLY_PRINTK1. Bootloaderbaudrate变量2. 内核early_printk设置3.map_io中的UART基地址打印部分后死机1. 内存信息错误2. 特定子系统初始化失败3. 中断问题1. 分析最后一条打印2. 启用CONFIG_DEBUG_INFO3. JTAG查看崩溃地址1. ATAG/DTB内存节点2. Oops调用栈3. 中断控制器配置卡在解压或启动1. 镜像损坏2. 加载/跳转地址错3. 运行环境不符1. 校验镜像完整性2. 检查Bootloader命令3. JTAG跟踪汇编1.go/bootm参数2. MMU/缓存状态3. 解压目标地址空间调试内核启动是一个需要耐心和系统方法的过程。我的经验是永远从最简单的假设开始验证比如线是不是松了然后利用EARLY_PRINTK和JTAG这两大利器将问题范围从“整个内核”逐步缩小到“某个函数”再到“某行代码”。理解本文剖析的启动流程能让你在查看代码或反汇编时清楚地知道自己正在哪个阶段下一步该去哪里从而高效地定位问题根源。