第三章 Bootloader阶段埋点方法:uboot日志与boot.img调试

做Android启动优化,很多人只盯着Linux内核和init进程。但真正的问题,往往从uboot就开始了。我记得刚接手一个项目时,设备每次冷启动都会随机卡死在Logo画面,折腾了快一周。最后发现是uboot阶段某个EEPROM读取超时,但在日志里根本看不出来——因为uboot默认只输出三五行信息。

核心观点:Bootloader阶段是整机启动的“第一公里”,这里的埋点虽然简陋,却能卡住最根上的问题。你至少要在uboot里埋三个关键点:入口时间、加载boot.img的起止时间、跳转到内核的时间。

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及以上的日志)。很多调试信息你写了,但生产固件里看不到。你要做两件事:

  1. 在include/configs/xxx.h里把CONFIG_LOGLEVEL调到7以上
  2. 或者在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);
}
注意:在board_f阶段,内存控制器可能还没完全初始化,如果你在此时访问boot.img所在地址,可能读出的数据全是0xFF。我曾经有一个项目,uboot的DDR训练做了3次,但第一次读取分区时内存时序没稳定,导致kernel校验失败。后来我加了判断:如果在timer_init之前访问DDR,先强制等待200μs。

实战案例:排查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启动埋点流程图(5个关键位置) ① reset入口 (start.S汇编) 埋点:串口初始化时机 ② board_init_f (板级早期初始化) 埋点:入口/出口时间、定时器校准 ③ board_late_init (板级后期初始化) 埋点:环境变量/设备树加载 ④ bootm_find_os (解析boot.img头部) 埋点:magic/load_addr/CRC DDR访问稳定性 ⑤ do_bootm_linux (跳转Linux内核) 埋点:bootargs最终值 跳转时刻时间戳 常见问题与对应埋点 • 重启到log无输出 → 检查①和②,确认串口是否在reset阶段已使能 • 内核镜像无法启动 → 检查④的CRC和load地址,以及⑤的bootargs是否完整 • 启动时间漂移 → 在①~⑤均打时间戳,对比多轮数据即可定位异常波动

说实话,很多人觉得uboot是“底层破事”,不值得花时间埋点。但我做过的几个大项目,启动问题90%的根因其实都藏在uboot阶段。你如果只把精力放在应用层,那就像修房子只刷墙皮——基础都歪了,上面再漂亮也白搭。

最后给个实用技巧:你可以用bootdelay环境变量来控制uboot暂停时间,结合串口手动输入bdi命令来查看当前环境变量。如果想更深入调试,把CONFIG_CMD_BOOTSTAGE打开,uboot启动后输入bootstage report就能拿到几十个时间节点——这是零改代码的埋点方式。

下一层就要到Linux内核阶段了。但别忘了,uboot埋点是你排查问题的第一道防线。嗯,先把今天这几个点在你的板子上跑通,后面的事就好办了。