第三章 Bootloader阶段埋点方法:uboot日志与boot.img调试
做Android启动优化,很多人只盯着Linux内核和init进程。但真正的问题,往往从uboot就开始了。我记得刚接手一个项目时,设备每次冷启动都会随机卡死在Logo画面,折腾了快一周。最后发现是uboot阶段某个EEPROM读取超时,但在日志里根本看不出来——因为uboot默认只输出三五行信息。
uboot日志埋点基础
uboot的打印其实很简单,就是printf和serial_puts。但你要注意,uboot的串口初始化分两段:最早在汇编阶段(lowlevel_init)就要打开串口,否则前面那几百个CPU时钟周期你什么也看不到。
我的习惯是在board_init_f里加第一条时间戳。为什么选这里?因为这个时候串口大概率已经能用了,而且板级初始化还没开始,可以抓住最原始的启动时间。
// 在 arch/arm/mach-xxx/board.c 或对应板级文件中
#include <common.h>
#include <timestamp.h> // uboot自带的时间戳工具
void board_init_f(ulong boot_flags)
{
// 埋点:记录uboot入口时间(毫秒精度)
ulong start_ms = get_timer(0);
printf("[UBOOT_MARK] board_init_f entry: %lu ms\n", start_ms);
// ... 原有初始化 ...
// 埋点:即将跳转到board_init_r
printf("[UBOOT_MARK] board_init_f exit, elapsed: %lu ms\n",
get_timer(start_ms));
}
这里有个坑——get_timer依赖定时器,某些低端芯片在board_init_f早期,定时器还没初始化,会输出0或者乱码。我曾经遇到过输出全是0的情况,后来在timer_init之后再加埋点才解决问题。
uboot日志输出控制
默认的uboot只会输出LOGLEVEL=4(即LINFO及以上的日志)。很多调试信息你写了,但生产固件里看不到。你要做两件事:
- 在
include/configs/xxx.h里把CONFIG_LOGLEVEL调到7以上 - 或者在board文件中临时用
debug()宏,它会在CONFIG_DEBUG打开时才输出
我比较推荐第二种方式:正式版本用printf打关键标记,调试版本开CONFIG_DEBUG打细节。这样不会污染生产日志,但出问题时可以快速切换。
// 调试版本:打开所有调试输出
#define DEBUG
#include <debug.h>
void board_late_init(void)
{
debug("[UBOOT_MARK] board_late_init: entering\n");
// ...
debug("[UBOOT_MARK] board_late_init: exit\n");
}
#define CONFIG_BOOTSTAGE开启uboot自带的计时框架。它能自动记录几十个内置事件的耗时,配合bootstage_report()直接输出启动流水线时长表。
说白了,你要避免的误区是:不要在uboot里做非阻塞IO或动辄几百字节的格式化字符串。uboot的堆栈很小,printf的参数太多可能导致栈溢出。我见过同事在调试时加了一行打印启动时间的代码,结果因为字符串过长,直接让uboot跑飞了。
boot.img调试埋点
uboot的角色是加载boot.img(包含kernel和ramdisk)到内存,然后跳过去。但这个“加载”过程可能隐藏很多问题:分区读取失败、校验超时、内存地址冲突、镜像损坏。
我们可以在common/bootm.c里找到do_bootm_states函数,它是uboot启动内核的核心入口。在这之前,加一些对boot.img头部的检查很实用:
// 在 common/bootm.c 或 bootm_command.c 中
static int bootm_find_os(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[])
{
// 埋点:读取boot.img头部信息
ulong img_addr = simple_strtoul(argv[0], NULL, 16);
printf("[BOOTIMG_MARK] image addr: 0x%08lx\n", img_addr);
image_header_t *hdr = (image_header_t *)img_addr;
printf("[BOOTIMG_MARK] magic: 0x%08x\n", hdr->ih_magic);
printf("[BOOTIMG_MARK] load addr: 0x%08x\n", ntohl(hdr->ih_load));
printf("[BOOTIMG_MARK] image size: %u bytes\n", ntohl(hdr->ih_size));
// 校验耗时
ulong t0 = get_timer(0);
if (image_check_hcrc(hdr)) {
printf("[BOOTIMG_MARK] header crc OK, took %lu ms\n", get_timer(t0));
} else {
printf("[BOOTIMG_MARK] header crc FAIL\n");
// 这里可以记录并继续,也可以halt
}
return bootm_find_os(cmdtp, flag, argc, argv);
}
实战案例:排查Logcat不输出
有一次反馈说某个平板开机后Logcat完全没内容,只显示“waiting for device”。我按照常规思路去查init.rc、logd配置,折腾半天没用。后来想到uboot阶段可能根本没把console=ttyS0正确传递给kernel,导致串口和logd都没拿到输出。
我在uboot的board_fbt_preboot里加了个埋点:
// 在bootm_process_bootargs前后
printf("[BOOTARGS_MARK] original bootargs: %s\n", env_get("bootargs"));
env_set("bootargs", "console=ttyS0,115200 loglevel=7 ...");
printf("[BOOTARGS_MARK] modified bootargs: %s\n", env_get("bootargs"));
结果发现uboot在设置bootargs之前,有一个env_import操作把环境变量覆盖成了空的。最后定位是EMC的某个shell脚本在uboot阶段错误地调用了env default。这个坑,不多埋几个打印根本不可能抓到。
Bootloader埋点流程图
下面这张图展示了uboot启动过程中我建议的5个必备埋点位置。每个位置都能帮你锁定一类典型问题:
说实话,很多人觉得uboot是“底层破事”,不值得花时间埋点。但我做过的几个大项目,启动问题90%的根因其实都藏在uboot阶段。你如果只把精力放在应用层,那就像修房子只刷墙皮——基础都歪了,上面再漂亮也白搭。
bootdelay环境变量来控制uboot暂停时间,结合串口手动输入bdi命令来查看当前环境变量。如果想更深入调试,把CONFIG_CMD_BOOTSTAGE打开,uboot启动后输入bootstage report就能拿到几十个时间节点——这是零改代码的埋点方式。
下一层就要到Linux内核阶段了。但别忘了,uboot埋点是你排查问题的第一道防线。嗯,先把今天这几个点在你的板子上跑通,后面的事就好办了。