资讯动态

4412平台U-Boot源码结构全解析:从启动装配到链接脚本

发布时间:2026/10/17 4:17:28 来源:尧图企业网站定制
嵌入式学到uboot这一站差不多是大部分人开始怀疑人生的地方。前面还在点亮LED、跑流水灯突然丢给你几十个目录、上万行汇编和C代码告诉你这就是引导内核的钥匙。我手头这块4412开发板大家习惯叫讯为4412其实同类的Exynos 4412板子源码结构都一样配套的uboot我前前后后啃了三遍才摸清门道。这篇文章不是逐行讲代码而是给你一张源码结构地图哪些目录是主战场、哪些文件决定生死、编译配置和链接脚本怎么串起整个流程、阅读顺序怎么安排最省时间。适合正在用4412或者三星Exynos系列学习uboot的同学也适合任何被uboot源码劝退、想搞清楚bootloader结构的人。1. 为什么非啃uboot源码不可——先搞懂它在系统里的位置1.1 uboot到底干了什么活说白了一点uboot是一个引导加载程序负责在开机之后、Linux内核运行之前把硬件初始化到可用状态然后把内核镜像从存储介质里读出来跳转过去执行。用生活类比的话它像酒店的门童——客人内核到门口之前门童得先把旋转门通电、擦干净玻璃、核对预约信息然后开门请客人进去。很多人觉得uboot不就是烧录一下、敲几个命令的事吗对如果你只做应用开发确实可以这么想。但只要你需要移植内核、调试驱动、改启动参数、适配新板子马上就会发现uboot卡在中间内核起不来八成是uboot没把DDR初始化好网络ping不通先要看uboot里网卡驱动是否正常。这时候不理解uboot源码结构你就只能对着串口输出瞎猜。1.2 4412平台的启动链路Exynos 4412这颗芯片的启动过程比单片机要绕但理清楚了会非常爽。它的片内固化了一段只读代码厂商叫iROM也有人叫BL0。芯片上电后CPU先执行这段固化代码它负责初始化时钟和内存控制器的最基础部分然后从你选择的启动介质SD卡、eMMC、串口等读取下一段代码。4412的完整启动链大致是iROMBL0→ BL1 → BL2 → U-Boot → Linux内核 → 根文件系统。BL1和BL2在SD卡里有固定的存放位置iROM会按照固定偏移去读。我们做开发时一般不用管BL0但要明白一件事uboot不是从零开始运行的它进来之前CPU和DDR已经被前面的代码做了基础初始化uboot接手后要做的是更完整的板级初始化比如时钟树精调、DDR控制器详细配置、串口初始化、网卡/MMC驱动加载等。1.3 为什么要先看结构、而不是直接看某段代码我见过很多同学拿到uboot源码第一件事就是grep -r DM9000想直接看网卡驱动。结果看完几百行寄存器操作回头问这个函数是谁调用的答不上来。这就是典型的没有全局观的阅读方式。uboot源码最大的特点是分散配置 集中调用几乎所有功能都能通过头文件里的宏开关来控制编译器根据配置裁剪代码最后通过链接脚本把所有需要的文件串成一个二进制。你直接看某个驱动等于在不知道菜谱的情况下研究一盘菜的摆盘——得先知道整桌菜是怎么备齐的再看单独一道菜才有意义。2. 顶层源码结构总览——先看地图再进森林2.1 版本与源码获取4412学习板配套的uboot一般基于U-Boot 2010.03或2010.12改造你打开顶层Makefile看VERSION和PATCHLEVEL就能确认。这类老版本虽然年代久远但目录结构和现代uboot差异不算大学起来完全不过时。三星官方还有一个叫origin的参考板很多开发板厂商的uboot都是从这个板级代码改出来的所以下面提到的目录路径里的origin会反复出现。拿到源码后别急着解压先用du -sh看看整体大小用find . -maxdepth 2 -type d | sort扫一遍二级目录让脑子里先有个轮廓。我再强调一次这一步不是浪费时间是让后面的每一次grep都有方向感。2.2 顶层目录各自管什么uboot的顶层目录划分非常清晰一句话总结就是架构相关代码放在arch下板级相关代码放在board下通用逻辑放在common和lib下驱动放在drivers下命令放在common的cmd或专门的cmd目录里。目录职责4412平台重点关注程度arch/CPU架构相关ARM平台下包括start.S、cpu相关代码高board/板级代码每个开发板一个子目录极高common/核心逻辑和命令行实现高drivers/各类外设驱动串口、网卡、MMC、I2C等高include/头文件特别是include/configs下存放板级配置头文件极高lib/通用库函数字符串、显示、重定位等中net/网络协议栈tftp、nfs等中fs/文件系统支持ext4、fat等低看uboot阶段需要tools/主机工具如mkimage用于打包内核镜像低Documentation/说明文档老版本内容很少低以4412配套源码为例你真正高频进出的大概只有三个地方board/samsung/origin/、arch/arm/cpu/armv7/、include/configs/。其他目录主要起被调用的作用——配置头文件里开了一个宏编译系统就会去对应目录里把相关文件拉进来。2.3 4412相关源码具体埋在哪展开源码后建议先找这几个位置并记住它们后面所有分析都从这里展开board/samsung/origin/板级初始化、DDR配置、网卡配置、低层汇编初始化都在这里。arch/arm/cpu/armv7/CPU启动汇编、cache操作、中断控制等架构相关代码。arch/arm/include/asm/arch-exynos/Exynos芯片寄存器地址定义看驱动时会频繁引用。include/configs/origin.h整个uboot的总开关所有宏配置都集中于此。include/config.mk编译配置由顶层Makefile自动生成记录ARCH/CPU/BOARD等变量。先找齐这五个位置再来谈深入阅读。3. 关键目录与核心文件逐个拆解3.1 board/samsung/origin——板级初始化的主战场这个目录是学习板uboot里最应该先看透的。里面通常有origin.c、lowlevel_init.S、mem_setup.S、config.mk等多个文件。origin.c里定义了一堆带board_前缀的函数board_init、board_early_init_f、board_eth_init、board_mmc_init等等。这些函数是板级硬件和uboot核心逻辑之间的桥核心逻辑说我要初始化串口板级代码告诉他我的串口基地址是0x13800000波特率分频值这样算。也就是说核心里写的是做什么板级代码写的是具体怎么做。lowlevel_init.S和mem_setup.S是用汇编写的前者在CPU刚跳转到SRAM/DDR时执行负责设置栈指针、关闭看门狗、初始化关键时钟后者专门配置DDR初始化序列。这是uboot里最硬核的部分不建议初学者死磕每一个寄存器的值先看懂流程即可。你需要记住的是lowlevel_init之前的代码运行环境很简陋连栈都不一定可靠lowlevel_init之后才开始调用C函数。3.2 arch/arm/cpu/armv7——CPU启动汇编的接力棒从arch/arm/cpu/armv7/start.S开始是所有ARM平台uboot的入口。这段汇编做的事按顺序排列设置CPU为SVC模式超级管理员模式、关闭中断、关MMU和cache、设置栈指针、跳转到lowlevel_init、然后进入C代码。4412的BSP在这段流程里还会做一件事重定位relocate。简单说uboot可能一开始运行在SRAM里等DDR初始化完成后需要把自身代码从SRAM拷贝到DDR高位地址再跳过去继续跑。这个拷贝跳转的点在源码里是relocate_code。你看源码时会发现TEXT_BASE这个宏反复出现它的含义就是uboot最终在内存中运行的链接基地址我后面会专门讲。初学建议start.S不要试图一遍读懂所有指令你只需要在代码里画出当前在哪一步、下一步去哪的箭头。看懂跳转关系比看懂每条指令值多少。3.3 include/configs/origin.h——整个uboot的灵魂如果只能选一个文件精读我建议选这个。6410、4412、树莓派、任何开发板的uboot移植工作七成时间都在改这个头文件。它里面是清一色的#define宏分几类CONFIG_SYS_*系统级配置包括内存地址CONFIG_SYS_SDRAM_BASE、uboot链接地址CONFIG_SYS_TEXT_BASE、uboot在内存中的大小、堆栈大小等。CONFIG_CMD_*命令开关比如CONFIG_CMD_PING决定编译时是否包含ping命令。CONFIG_*_BASE外设基地址如DM9000网卡的寄存器地址。使能宏CONFIG_DM9000、CONFIG_MMC、CONFIG_CMD_NET这些决定驱动是否被编译进uboot。这个文件的工作机制是预处理期裁剪。你多定义一个CONFIG_CMD_PING编译系统就会把common/cmd_net.c里的ping实现编译进来你去掉它代码直接消失。所以改配置之前先全局搜索该宏在哪些地方被引用改完之后一定要重新完整编译不要只make一下就收工。3.4 drivers——驱动搜索路径的套路所有外设驱动都在drivers/下子目录按类型分drivers/net、drivers/mmc、drivers/serial、drivers/i2c、drivers/mtd等。4412网卡如果是DM9000驱动就在drivers/net/dm9000x.c。想找某个驱动的入口函数最靠谱的方法是看它对应的结构体。以dm9000为例文件里会定义一个struct eth_device并填充init、send、recv等函数指针。uboot核心代码通过调用eth_init、eth_send间接跳到这些函数。驱动开发者的工作就是填充这些函数指针。理解了函数指针表承载驱动接口这个模式再看MMC、串口、I2C驱动完全一通百通。3.5 common——命令行和核心流程所在老版本uboot2010.03/2010.12启动主流程是start_armboot定义在lib_arm/board.c里。它会按顺序调用一大串初始化函数比如board_init、env_init、serial_init、net_init等。初始化完成后进入main_loop等待用户输入uboot命令。命令的实现文件分两类一类是common/cmd_*.c比如cmd_ping.c、cmd_mmc.c另一类在common/根目录下比如cmd_bootm.c。每个命令通过U_BOOT_CMD宏把自己注册到一个特殊的段.u_boot_cmd。这个宏会生成一个结构体包含命令名、帮助信息、参数提示、处理函数指针。链接脚本会收集所有这类结构体uboot运行时就能遍历命令表、匹配用户输入、调用对应处理函数。很多初学uboot的同学想添加自定义命令问要改哪些文件答案就是新建或修改cmd_xxx.c里面用U_BOOT_CMD定义一条命令然后在include/configs/xxx.h里加上CONFIG_CMD_XXX配置即可。就这么简单但背后是段收集这个机制在起作用。4. 从编译配置到链接脚本——结构如何串起来4.1 配置过程到底发生了什么拿到uboot源码第一件事通常是make origin_config这个命令的含义是生成origin板配置。它调用顶层Makefile里的规则核心动作是执行mkconfig脚本根据boards.cfg里的配置组合生成两个关键文件include/config.mk内容类似ARCHarm、CPUarmv7、BOARDorigin、VENDORsamsung、SOCexynos供Makefile使用。include/config.h内容只有一个#include configs/origin.h。这就有意思了你改了板级配置文件比如新加一个宏下次编译时Makefile检测到依赖变化会重新处理config.h并自动展开宏但如果你改了include/configs/origin.h之外的某个被间接包含的文件有可能因为依赖关系不完整而没触发重编。所以改配置后最稳的操作是make clean后再make或者干脆删掉include/autoconf.mk这是另一个由config.h自动生成的文件再编。这里我补充一个为什么经常有人改了宏没效果的底层原因。uboot编译时真正的自动配置头文件是include/autoconf.mk它由include/config.h经预处理后生成。也就是说你直接改origin.h理论上autoconf.mk会在配置阶段重新生成。但如果你用的是IDE的增量编译或者make依赖检测漏掉了某些头文件就会导致autoconf.mk还是旧内容。明白这个链路排查配置不生效问题就有方向了。4.2 链接脚本如何把一切串起来uboot编译完成后会链接成一个u-boot文件而告诉链接器代码放哪、数据放哪、哪个段靠前的是链接脚本u-boot.lds。4412配套BSP里它一般在board/samsung/origin/下也有版本放在顶层目录你可以用find . -name u-boot.lds来找。链接脚本的核心内容类似这样实际条目更多SECTIONS { . 0x43e00000; .text : { *(.text.start) *(.text) *(.rodata) } __u_boot_cmd_start .; .u_boot_cmd : { *(.u_boot_cmd) } __u_boot_cmd_end .; __bss_start .; .bss : { *(.bss) } __bss_end .; }你看懂三个点链接脚本就没障碍了第一.表示当前地址一开始设成0x43e00000这个值就是CONFIG_SYS_TEXT_BASE它决定了uboot代码段被链接到内存的什么位置。第二.u_boot_cmd段专门收集所有U_BOOT_CMD宏生成的结构体__u_boot_cmd_start和__u_boot_cmd_end这两个符号标出命令表的边界uboot运行时就是从这个边界之间遍历命令的。第三.bss段放未初始化的全局变量链接脚本记录__bss_start和__bss_endstart.S会在早期把这段内存清零。很多人混淆链接地址和加载地址这里必须说清楚链接地址是代码中所有绝对地址计算的基准uboot里所有函数指针、全局变量地址都是按照TEXT_BASE算出来的加载地址是uboot二进制实际被放在存储介质中的位置。启动时SD卡里的uboot可能存放在某个固定偏移处但它会被拷贝到以TEXT_BASE为起点的RAM区域再运行只有这样才能保证代码里的绝对跳转不跑飞。如果拷贝地址和TEXT_BASE不一致最常见的现象就是运行到一半跳飞或者函数能跑、全局变量乱值。4.3 编译产物与镜像生成流程编译完成后源码根目录会出现u-bootELF格式带符号表调试用、u-boot.bin裸二进制烧录用、u-boot.map符号映射表查函数在内存中的位置。学习板配套的uboot还会有一个u-boot-sd.bin或者类似的打包文件里面有可能把BL1、BL2和uboot拼接在了一起这是因为4412的iROM要从SD卡固定位置读取BL1厂商在编译后期用脚本把多段镜像拼成一个文件。如果你想知道某个函数最终被放到了哪个地址看u-boot.map最直观。比如你想确认board_init的地址grep board_init u-boot.map即可。这在你调试跳转问题时是救命工具。5. 阅读源码的实战路线图——建议的阅读顺序5.1 主线优先从开机到命令行的路径我建议所有初学者按这条主线读第一遍目标不是记住每个函数而是画出调用链arch/arm/cpu/armv7/start.S汇编入口→lowlevel_init.S时钟、DDR→start_armbootC代码主流程老版本或board_init_f/board_init_r新版本拆分→main_loop命令行循环。这条线走通之后你对uboot的骨架就有感觉了。具体操作是打开源码先用grep -rn start_armboot找到C入口用grep -rn board_init_f\|main_loop顺藤摸瓜并在纸上画出函数调用树。不要小看这个纸上画树的笨办法它比任何IDE的关系图都管用因为你画的过程就是思考的过程。5.2 分支扩展命令和驱动的读法主线读完后再做三个分支练习。第一个是加一条命令比如仿照现有cmd_led.c写一个cmd_hello.c用U_BOOT_CMD注册编译进uboot后敲help看命令是否出现。这个练习能帮你彻底理解命令表的注册和收集机制。第二个是读一个驱动选dm9000网卡驱动顺着board_eth_init找到驱动初始化再把eth_send到实际寄存器操作走一遍。第三个是改一个环境变量默认值比如默认的bootcmd自动启动命令或bootargs传给内核的参数你会接触到env_default函数的实现。这三个练习做完你基本已经入门了。剩下的比如uboot引导内核的bootm流程、设备树支持如果版本够新的话可以在需要时再针对性研究。5.3 工具与技巧怎么查得又快又准阅读uboot源码的必备工具是grep和ctagsIDE选Source Insight、VSCode都行但记住一点IDE只是辅助不能替代命令行检索。我在实际工作中最常用的命令是grep -rn CONFIG_DM9000 --include*.h --include*.c .这条命令能帮我找到某个配置宏影响了哪些文件。另一个高频率操作是生成tagsctags -R .然后无论用Vim还是VSCode都能一键跳转到函数定义。这里分享一个很实用的习惯给uboot源码建一个阅读日志文件每搞懂一个函数/流程就用自己的话记下来包括函数名、调用者、关键参数、注意事项。很多人觉得记笔记耗时但uboot这种老代码你今天看懂了board_init_f明天不看笔记一定忘。笔记是你的第二大脑。6. 常见问题与排查技巧实录6.1 配置阶段报错Cant find board/origin典型的boards.cfg配置不匹配问题。检查顶层boards.cfg或Makefile里新板子配置行的字段顺序和board/samsung/origin/目录是否一致尤其注意Board名、Vendor、SOC列不能写错。老版本uboot里make xx_config会调用mkconfig脚本它按行拆分配置并生成include/config.mk如果boards.cfg里找不到对应行就会提示找不到板级目录。6.2 改了配置头文件编译后没变化这个问题踩的人最多根因我已经在前面解释过配置生效依赖于include/autoconf.mk被重新生成。新手最容易踩的坑是在include/configs/origin.h里改了宏但Makefile依赖关系没捕捉到这个头文件的变化直接make还是旧配置。解决办法make clean rm -f include/autoconf.mk make如果还不行直接搜一下include/autoconf.mk里有没有你新加的宏避免以为自己加了其实没加。6.3 串口完全没有输出uboot阶段串口没输出的原因很多按顺序排查先确认串口硬件连接和波特率常见115200再看CONFIG_SERIAL_BASE或CONFIG_S5P_UART这类宏是否和板子原理图一致然后确认lowlevel_init里时钟初始化是否完成、PLL配置是否正常最后确认TEXT_BASE和uboot实际加载地址是否匹配。如果uboot压根没跑起来通常不会有任何输出如果跑一半死掉串口可能会输出几个乱码字符或者半句话。这里分享一个经验老版本uboot的串口初始化分成两段一段在汇编阶段lowlevel_init前后通常只做最基础的UART寄存器配置真正完整初始化在serial_init。如果你发现前一段没输出优先怀疑时钟配置如果能看到uboot版本信息但停住优先怀疑DDR配置或环境变量加载有问题。6.4 下载代码后跳飞、死机跳飞最直接的原因往往是TEXT_BASE和实际加载地址不一致。我用4412开发板时就踩过这个坑SD卡烧写工具把uboot放到了0x43e00000而我编译时TEXT_BASE写成了0x43f00000结果代码执行几十秒后随机死机。排查方法就是用调试器J-Link或OpenOCD查看PC指针跑到哪去了再回头检查TEXT_BASE。6.5 网络不通、ping不通主机uboot阶段网卡问题一般集中在三处驱动初始化失败硬件复位引脚没配好、MAC地址没设置ethaddr环境变量为空、网络协议栈的IP配置不对。建议先用uboot的dhcp命令测试自动获取IP再ping主机地址。如果ping通但tftp下载老卡住检查防火墙和tftp服务端配置这已经是uboot之外的问题了。一个额外的调试技巧把uboot的编译日志保存下来编译报错时从日志尾部往前翻大部分问题会在第一个报错处暴露。不要只看最后一行错误往前多翻20行往往才是真正的坑。6.6 如何验证自己真的看懂了结构最好的验证方式是关掉源码花15分钟在白纸上默写整个启动流程从start.S入口到main_loop每一阶段大概做了什么、分别调用了哪些关键函数、哪个宏控制了哪段代码的编译。能默写出来说明结构已经内化写不出来的部分就是你下一步要回去补的地方。7. 最后分享一点实际体会带过不少人看4412的uboot源码我发现能不能啃下来跟基础关系不大跟会不会用搜索和能不能忍得住不求甚解关系很大。uboot代码量大但真正需要精读的核心文件不超过十个。第一遍读切忌逐行死磕寄存器操作先抓函数调用关系再回头看细节。我个人的习惯是先把start.S、lowlevel_init、start_armboot、main_loop、origin.h这五个文件读熟之后再看任何驱动和命令都是降维打击。还有一个很实用的小技巧给源码加注释不是浪费时间。你可以复制一份源码在里面用中文标注你理解的逻辑比如在board_init函数前写一句此处初始化网卡和MACH ID。这份注释版源码就是你以后快速检索的利器。等到你要移植新板子时直接复制板级目录改头文件里的宏和寄存器地址启动链路不用重学靠的就是当初这份积累。下一步你可以沿着uboot引导内核这条线继续深入重点研究bootm命令的处理流程和内核镜像的格式解析。那会是整个嵌入式引导链路最有意思的部分之一。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑