U-Boot加载内核和文件系统的具体代码实现

要深入理解 U-Boot 加载内核和文件系统的代码实现,需要从命令解析→设备交互→镜像处理→内核跳转的全链路展开,结合具体函数调用和数据流转细节。以下基于 U-Boot 2023.07 版本,以 ARM 架构 + eMMC 存储为例,

一、命令解析:从bootcmd到具体操作

U-Boot 的启动逻辑由main_loop触发,位于common/main.c

// common/main.c
void main_loop(void)
{
    // 环境变量初始化(从Flash读取或使用默认值)
    env_relocate();

    // 处理自动启动倒计时
    if (bootdelay >= 0 && !abortboot(bootdelay)) {
        // 执行bootcmd环境变量定义的命令序列
        run_command_list(getenv("bootcmd"), -1, 0);
    }

    // 进入命令行交互(若未自动启动)
    cli_loop();
}
  • abortboot函数:在倒计时期间检测按键输入,若有按键则中断自动启动,进入命令行
  • run_command_list:将bootcmd字符串(如mmc dev 0; mmc read 0x80800000 0x200 0x1000; bootz 0x80800000 - 0x81000000)按分号拆分,逐个调用run_command执行。

关键代码片段解析

以下是 run_command_list 的核心实现逻辑(简化版):

int run_command_list(const char *cmd, int len, int flag)
{
    const char *p = cmd;
    int last_ret = 0;
    int i;

    if (len < 0)
        len = strlen(cmd);  // 自动计算长度

    while (p < cmd + len) {
        // 1. 查找命令分隔符(; 或 \n)
        const char *q = p;
        while (q < cmd + len && *q != ';' && *q != '\n')
            q++;

        // 2. 提取单条命令(p 到 q 之间的字符串)
        int cmd_len = q - p;
        char *buf = malloc(cmd_len + 1);
        if (!buf)
            return -1;

        memcpy(buf, p, cmd_len);
        buf[cmd_len] = '\0';  // 字符串结尾

        // 3. 跳过命令前后的空格
        char *cmd_str = buf;
        while (*cmd_str && isspace(*cmd_str))
            cmd_str++;

        // 4. 执行单条命令(非空才执行)
        if (*cmd_str) {
            last_ret = run_command(cmd_str, flag);  // 调用单命令执行函数

            // 若为 bootd 模式且命令失败,停止执行
            if (flag & CMD_FLAG_BOOTD && last_ret != 0) {
                free(buf);
                return last_ret;
            }
        }

        // 5. 移动到下一条命令
        free(buf);
        p = q + 1;  // 跳过分隔符
    }

    return last_ret;  // 返回最后一条命令的执行结果
}

与其他函数的关联

  1. run_command
    run_command_list 依赖 run_command 执行单条命令。run_command 的功能是:

    • 解析单条命令(如 mmc read 0x80800000 0x200 0x1000);
    • 查找对应的命令处理函数(如 do_mmc);
    • 传递参数并执行,返回执行结果。
  2. autoboot_command
    在自动启动流程中,autoboot_command 调用 run_command_list 执行 bootcmd 环境变量中的命令序列,例如:

    void autoboot_command(void)
    {
        char *bootcmd = getenv("bootcmd");
        if (bootcmd)
            run_command_list(bootcmd, -1, 0);  // 执行bootcmd命令序列
    }
    

典型应用场景

  1. 自动启动(bootcmd
    最常见的场景是执行 bootcmd 中的命令序列,例如:

    bootcmd=mmc dev 0; mmc read 0x80800000 0x200 0x1000; bootz 0x80800000
    
     

    run_command_list 会按分号拆分并依次执行 mmc devmmc readbootz 命令。

  2. 脚本执行
    U-Boot 支持执行脚本文件(如 .scr 格式),脚本中的多条命令通过 run_command_list 逐条执行。

  3. 命令行批量输入
    在 U-Boot 命令行中,用户可输入多条命令(用分号分隔),例如:

    => setenv bootargs console=ttyS0,115200; saveenv; reset
    
     

    这些命令会被 run_command_list 拆分并执行。

二、从 MMC 设备读取内核镜像

mmc read命令为例,代码位于cmd/mmc.c,核心是调用 MMC 驱动读取数据:

// cmd/mmc.c
int do_mmc_read(cmd_tbl_t *cmdtp, int flag, int argc, char *const argv[])
{
    struct mmc *mmc;
    ulong addr, blk, cnt;
    int dev, ret;

    // 解析参数:mmc read <内存地址> <起始块> <块数>
    dev = (argc >= 2) ? simple_strtoul(argv[1], NULL, 10) : 0;
    addr = simple_strtoul(argv[2], NULL, 16);  // 内核加载地址(如0x80800000)
    blk = simple_strtoul(argv[3], NULL, 10);   // 内核在MMC中的起始块
    cnt = simple_strtoul(argv[4], NULL, 10);   // 读取块数

    mmc = find_mmc_device(dev);  // 获取MMC设备句柄
    if (!mmc) {
        printf("MMC device %d not found\n", dev);
        return 1;
    }

    // 调用MMC驱动读取数据到内存
    ret = mmc->read(mmc, blk, cnt, (void *)addr);
    if (ret) {
        printf("MMC read failed: %d\n", ret);
        return 1;
    }

    printf("Read %lu blocks from %d:%lu to 0x%lx\n", cnt, dev, blk, addr);
    return 0;
}
  • 底层驱动mmc->read最终调用drivers/mmc/mmc.cmmc_bread,通过 SD/eMMC 协议与硬件交互:
    // drivers/mmc/mmc.c
    int mmc_bread(struct mmc *mmc, lbaint_t start, lbaint_t blkcnt, void *buffer)
    {
        struct mmc_cmd cmd;
        struct mmc_data data;
        int ret;
    
        // 初始化MMC命令(读块操作)
        cmd.cmdidx = MMC_CMD_READ_SINGLE_BLOCK;  // 单块读命令
        cmd.cmdarg = start;                      // 起始块地址
        cmd.resp_type = MMC_RSP_R1;
    
        data.src = buffer;      // 内存缓冲区(内核将被读到这里)
        data.blocks = blkcnt;   // 块数量
        data.blocksize = mmc->read_bl_len;  // 块大小(通常512字节)
        data.flags = MMC_DATA_READ;
    
        ret = mmc_send_cmd(mmc, &cmd, &data);  // 发送命令到MMC控制器
        return ret;
    }
    

三、解析内核镜像并验证

bootz命令(用于 zImage)在加载内核后需验证镜像有效性,代码位于cmd/bootm.c

// cmd/bootm.c
int do_bootz(cmd_tbl_t *cmdtp, int flag, int argc, char *const argv[])
{
    ulong kernel_addr, initrd_addr, initrd_end, fdt_addr;
    int ret;

    // 解析参数:bootz <内核地址> [initrd地址] [DTB地址]
    ret = parse_bootz_args(argc, argv, &kernel_addr, &initrd_addr,
                          &initrd_end, &fdt_addr);
    if (ret)
        return ret;

    // 验证zImage头部(魔数0x016f2818)
    if (*(uint32_t *)kernel_addr != ZIMAGE_MAGIC) {
        printf("Invalid zImage magic: 0x%08x\n", *(uint32_t *)kernel_addr);
        return 1;
    }

    // 准备启动环境(如设置DTB参数)
    ret = boot_prep(kernel_addr, initrd_addr, initrd_end, fdt_addr);
    if (ret)
        return ret;

    // 执行内核启动
    return do_bootz_exec(kernel_addr, initrd_addr, initrd_end, fdt_addr);
}

四、设备树(DTB)处理与参数传递

设备树是内核硬件配置的关键,U-Boot 需加载并传递其地址:

// cmd/bootm.c 中处理DTB
static int boot_prep(ulong kernel_addr, ulong initrd_addr,
                    ulong initrd_end, ulong fdt_addr)
{
    if (fdt_addr) {
        // 验证DTB头部(魔数0xd00dfeed)
        if (fdt_check_header((void *)fdt_addr) != 0) {
            printf("Invalid DTB at 0x%lx\n", fdt_addr);
            return 1;
        }

        // 向DTB注入bootargs参数
        char *bootargs = getenv("bootargs");
        if (bootargs && fdt_setprop_string((void *)fdt_addr, 0,
                                          "bootargs", bootargs)) {
            printf("Failed to set bootargs in DTB\n");
            return 1;
        }
    }
    return 0;
}

五、最终跳转:移交控制权给内核

跳转逻辑由汇编实现(ARM 架构位于arch/arm/lib/bootm.c),U-Boot 最终通过汇编指令跳转到内核在内存中的入口地址,严格遵循 ARM 架构的启动规范(如寄存器传递参数):

// arch/arm/lib/bootm.S
ENTRY(do_bootz_exec)
    // 栈中参数布局:[0]内核地址, [4]initrd_end, [8]initrd_start, [12]fdt_addr
    stmfd sp!, {r4-r11, lr}  // 保存寄存器现场

    // 1. 准备传递给内核的参数(ARMv7规范)
    ldr r2, [sp, #12 + 4*8]  // r2 = DTB地址(内核从这里获取硬件信息)
    mov r1, #0               // r1 = 机器码(设备树模式下为0)
    mov r0, #0               // r0 = 0(规范要求)

    // 2. 禁用中断和MMU(确保内核启动环境干净)
    mrs r3, cpsr             // 读取当前程序状态寄存器
    orr r3, r3, #0xc0        // 禁止IRQ和FIQ中断(bit7和bit6置1)
    msr cpsr_c, r3           // 写入状态寄存器

    // 3.  invalidate缓存(避免脏数据影响)
    mov r3, #0
    mcr p15, 0, r3, c7, c5, 0  // invalidate I-cache
    mcr p15, 0, r3, c7, c6, 0  // invalidate D-cache

    // 4. 跳转到内核入口(zImage的解压函数)
    ldr pc, [sp, #0 + 4*8]   // pc = 内核地址(0x80800000),完成跳转

    // 理论上不会返回,若返回则挂起
    mov r0, #0
    bl hang
ENDPROC(do_bootz_exec)


关键细节:
内核入口地址(如0x80800000)是zImage的起始地址,其内部包含解压逻辑,会将自身解压到更高的内存地址后执行真正的内核初始化。

六、文件系统挂载的间接参与

U-Boot 不直接参与文件系统加载,但通过以下方式为内核提供必要信息:

  1. bootargs环境变量
    定义在include/configs/xxx.h(板级配置文件)中:

    #define CONFIG_BOOTARGS \
        "console=ttyS0,115200 " \
        "root=/dev/mmcblk0p2 " \  // 根文件系统位于eMMC第2分区
        "rootfstype=ext4 " \      // 文件系统类型为ext4
        "rw"                       // 读写模式挂载
    
     

    内核启动后,解析bootargs中的root参数,找到文件系统位置。

  2. 设备树中的存储设备节点
    U-Boot 加载的 DTB 中包含 eMMC 控制器和分区信息,例如:

    dts

    mmc@f8007000 {  // eMMC控制器节点
        compatible = "arm,pl180";
        reg = <0xf8007000 0x1000>;
        ...
        partitions {
            compatible = "fixed-partitions";
            partition@0 { label = "boot"; reg = <0x0 0x200000>; };
            partition@200000 { label = "rootfs"; reg = <0x200000 0x10000000>; };
        };
    };
    
     

    内核通过该节点识别 eMMC 设备及分区,从而挂载rootfs分区。

全流程调用链总结

main_loop() → run_command_list("bootcmd") →
do_mmc_read() → mmc_read_blocks() → pl180_send_cmd_read() 【读取内核到内存】→
do_bootz() → bootz_verify_image() → boot_prepare_env() →
do_bootz_exec() 【汇编跳转至内核】→
内核解析DTB和bootargs → 挂载文件系统

每个环节都与硬件平台强相关(如存储设备驱动、架构特定的汇编跳转),但核心思想是 **“硬件初始化→加载镜像→传递参数→移交控制权”**。实际调试时,可通过 U-Boot 的printenvmd(内存查看)等命令验证各阶段是否正确执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值