ESP8266 Bootloader开源代码解析之rboot(一)

前言

在非Linux的嵌入式开发中,自己手写Bootloader是很正常的事。因为可以定制自己想要的功能。比如定制自己的Bootloader通信接口(UART、I2C、SPI),通信协议,甚至更高级的固件备份回退等功能。但是使用ESP8266就不一样了,整个芯片的程序是怎么跑起来的都一知半解(所以我写了这篇文章:ESP8266架构探索-运行的起始);官方提供了Bootloader和完整的接口,但是是闭源的;官方Bootloader虽然有做固件备份,但是没有固件回滚,等等这些问题。所以这时候rboot出现了。我们有很多原因不能从无到有写一个自己的Bootloader,但是我们可以借鉴,知道rboot怎么运作后,就能够通过修改,裁剪,做出自己想要的Bootloader。所以这篇文章不会花大力气去分析rboot的特性是怎么实现的,着重于怎么写一个ESP8266上最基础的Bootloader。

工程目录

先给出rboot的github地址:https://github.com/raburton/rboot
编译后,是像下面这样

.
├── appcode
│   ├── rboot-api.c
│   ├── rboot-api.h
│   └── rboot-bigflash.c
├── build
│   ├── rboot.elf
│   ├── rboot-hex2a.h
│   ├── rboot.o
│   ├── rboot-stage2a.elf
│   └── rboot-stage2a.o
├── eagle.app.v6.ld
├── eagle.rom.addr.v6.ld
├── firmware
│   └── rboot.bin
├── license.txt
├── Makefile
├── rboot.c
├── rboot.h
├── rboot-private.h
├── rboot-stage2a.c
├── rboot-stage2a.ld
├── readme-api.txt
├── readme.md
├── testload1.c
└── testload2.c

其中,rboot.c rboot-stage2a.c就是我们想要的代码。

运行流程

rboot.c的ENTRY是call_user_start,我们看看运行流程:(c语言版和汇编版功能一样,看c语言代码便于理解)

void call_user_start(void) {
    uint32_t addr;
    stage2a *loader;

    addr = find_image();
    if (addr != 0) {
        loader = (stage2a*)entry_addr;      //rboot-stage2a.c中的call_user_start
        loader(addr);
    }
}

从配置参数中找到应用固件的地址,然后调用entry_addr处的函数执行。
entry_addr在rboot-hex2a.h中定义:

const uint32_t entry_addr = 0x4010fcb4;   //被rboot.c中的call_user_start里调用

const uint32_t _text_addr = 0x4010fc00;
const uint32_t _text_len = 192;
const uint8_t  _text_data[] = {
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x10, 0x00, 0x00, 0x1c, 0x4b, 0x00, 0x40, 0x12, 0xc1, 0xc0, 0xc9, 0xe1, 0x8b, 0x31, 0xcd,
  0x02, 0x0c, 0x84, 0xe9, 0xc1, 0xf9, 0xb1, 0x09, 0xf1, 0xd9, 0xd1, 0xc2, 0xcc, 0x08, 0x01, 0xf9,
  0xff, 0xc0, 0x00, 0x00, 0xf8, 0x31, 0xe2, 0x01, 0x09, 0x86, 0x10, 0x00, 0x2d, 0x0c, 0x3d, 0x01,
  0x0c, 0x84, 0x01, 0xf4, 0xff, 0xc0, 0x00, 0x00, 0x8b, 0xcc, 0x78, 0x01, 0xd8, 0x11, 0x46, 0x09,
  0x00, 0x21, 0xef, 0xff, 0x5d, 0x0d, 0xd7, 0xb2, 0x02, 0x20, 0x52, 0x20, 0x2d, 0x0c, 0x3d, 0x07,
  0x4d, 0x05, 0x59, 0x51, 0x79, 0x41, 0x01, 0xeb, 0xff, 0xc0, 0x00, 0x00, 0x58, 0x51, 0x78, 0x41,
  0x5a, 0xcc, 0x5a, 0x77, 0x50, 0xdd, 0xc0, 0x56, 0x6d, 0xfd, 0x0b, 0x6e, 0x60, 0xe0, 0x74, 0x56,
  0x9e, 0xfb, 0x08, 0xf1, 0x2d, 0x0f, 0xc8, 0xe1, 0xd8, 0xd1, 0xe8, 0xc1, 0xf8, 0xb1, 0x12, 0xc1,
  0x40, 0x0d, 0xf0, 0x00, 0xfd, 0x00, 0x05, 0xf8, 0xff, 0x0d, 0x0f, 0xa0, 0x02, 0x00, 0x0d, 0xf0,
};

对比entry_addr和_text_addr关系,再根据_text_addr和_text_data的名字关系(手动狗头),知道entry_addr的代码就在_text_data中。rboot-hex2a.h由rboot-stage2a.c生成的,call_user_start在rboot-stage2a.ld中定义为ENTRY:

/*  Default entry point:  */
ENTRY(call_user_start)

rboot-stage2a.c的代码:

void call_user_start(uint32_t readpos) {
    usercode* user;
    user = load_rom(readpos);
    user();
}

从flash中读取代码,然后执行。
所以整个rboot流程就是:从配置参数中找到应用固件的地址,然后调用rboot-stage2a中的函数从flash中读取代码,然后执行。
这样说下来,流程是通了,但是过程一点都经不起推敲啊。那么下面来认真分析rboot-stage2a.c的代码怎么关联到rboot.c中的。

编译分析

我们先看Makefile。

all: $(RBOOT_BUILD_BASE) $(RBOOT_FW_BASE) $(RBOOT_FW_BASE)/rboot.bin

$(RBOOT_BUILD_BASE)/rboot.o: rboot.c rboot-private.h rboot.h $(RBOOT_BUILD_BASE)/rboot-hex2a.h
    @echo "CC $<"
    $(Q) $(CC) $(CFLAGS) -I$(RBOOT_BUILD_BASE) -c $< -o $@
    
$(RBOOT_BUILD_BASE)/rboot-hex2a.h: $(RBOOT_BUILD_BASE)/rboot-stage2a.elf
    @echo "E2 $@"
    $(Q) $(ESPTOOL2) -quiet -header $< $@ .text  
    
$(RBOOT_BUILD_BASE)/rboot-stage2a.elf: $(RBOOT_BUILD_BASE)/rboot-stage2a.o
    @echo "LD $@"
    $(Q) $(LD) -Trboot-stage2a.ld $(LDFLAGS) -Wl,--start-group $^ -Wl,--end-group -o $@

顺序有所调整。但最终rboot-stage2a.c变成了rboot-hex2a.h包含在rboot.c中。
这里有两个关键的地方:

  1. rboot-stage2a.elf通过ESPTOOL2变成了rboot-hex2a.h
  2. rboot-stage2a.c最终变成了rboot-hex2a.h

编译过程中用到了esptool2,先把esptool2下载下来:https://github.com/raburton/esptool2
直奔主题,esptool2.c文件中,使用-header参数就是把elf文件变成.h头文件

    // load elf file
    elf = LoadElf(elffile);
    if (!elf) {
        goto end_function;
    }
    
    // open output file
    outfile = fopen(imagefile, "wb");
    if(outfile == NULL) {
        error("Error: Failed to open output file '%s' for writing.\r\n", imagefile);
        goto end_function;
    }

    // add entry point
    fprintf(outfile, "const uint32_t entry_addr = 0x%08x;\r\n", elf->header.e_entry);

    // add sections
    for (i = 0; i < numsec; i++) {
        // get elf section header
        sect = GetElfSection(elf, sections[i]);
        if(!sect) {
            error("Error: Section '%s' not found in elf file.\r\n", sections[i]);
            goto end_function;
        }

        // simple name fix name
        strncpy(name, sect->name, 31);
        len = strlen(name);
        for (j = 0; j < len; j++) {
            if (name[j] == '.') name[j] = '_';
        }

        // add address, length and start the data block
        debug("Adding section '%s', addr: 0x%08x, size: %d.\r\n", sections[i], sect->address, sect->size);
        fprintf(outfile, "\r\nconst uint32_t %s_addr = 0x%08x;\r\nconst uint32_t %s_len = %d;\r\nconst uint8_t  %s_data[] = {",
            name, sect->address, name, sect->size, name);

        // get elf section binary data
        bindata = GetElfSectionData(elf, sect);
        if (!bindata) {
            goto end_function;
        }

        // add the data and finish off the block
        for (j = 0; j < sect->size; j++) {
            if (j % 16 == 0) fprintf(outfile, "\r\n  0x%02x,", bindata[j]);
            else fprintf(outfile, " 0x%02x,", bindata[j]);
        }
        fprintf(outfile, "\r\n};\r\n");
        free(bindata);
        bindata = 0;
    }

然后对比rboot-hex2a.h文件,是不是这样转过来的:

const uint32_t entry_addr = 0x4010fcb4;

const uint32_t _text_addr = 0x4010fc00;
const uint32_t _text_len = 192;
const uint8_t  _text_data[] = {
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x10, 0x00, 0x00, 0x1c, 0x4b, 0x00, 0x40, 0x12, 0xc1, 0xc0, 0xc9, 0xe1, 0x8b, 0x31, 0xcd,
  0x02, 0x0c, 0x84, 0xe9, 0xc1, 0xf9, 0xb1, 0x09, 0xf1, 0xd9, 0xd1, 0xc2, 0xcc, 0x08, 0x01, 0xf9,
  0xff, 0xc0, 0x00, 0x00, 0xf8, 0x31, 0xe2, 0x01, 0x09, 0x86, 0x10, 0x00, 0x2d, 0x0c, 0x3d, 0x01,
  0x0c, 0x84, 0x01, 0xf4, 0xff, 0xc0, 0x00, 0x00, 0x8b, 0xcc, 0x78, 0x01, 0xd8, 0x11, 0x46, 0x09,
  0x00, 0x21, 0xef, 0xff, 0x5d, 0x0d, 0xd7, 0xb2, 0x02, 0x20, 0x52, 0x20, 0x2d, 0x0c, 0x3d, 0x07,
  0x4d, 0x05, 0x59, 0x51, 0x79, 0x41, 0x01, 0xeb, 0xff, 0xc0, 0x00, 0x00, 0x58, 0x51, 0x78, 0x41,
  0x5a, 0xcc, 0x5a, 0x77, 0x50, 0xdd, 0xc0, 0x56, 0x6d, 0xfd, 0x0b, 0x6e, 0x60, 0xe0, 0x74, 0x56,
  0x9e, 0xfb, 0x08, 0xf1, 0x2d, 0x0f, 0xc8, 0xe1, 0xd8, 0xd1, 0xe8, 0xc1, 0xf8, 0xb1, 0x12, 0xc1,
  0x40, 0x0d, 0xf0, 0x00, 0xfd, 0x00, 0x05, 0xf8, 0xff, 0x0d, 0x0f, 0xa0, 0x02, 0x00, 0x0d, 0xf0,
};

entry_addr是elf文件中的入口地址。
之后则是.text段的地址长度内容。
但这堆16进制数字,我们也不能确定就是代码编译出来的。不怕,我们有办法。在ESP8266编译后,会生成一个eagle.dump文件,我们同样可以把rboot-stage2a.elf里的信息弄出来。
稍微修改Makefile文件

ifndef XTENSA_BINDIR
CC := xtensa-lx106-elf-gcc
LD := xtensa-lx106-elf-gcc
OBJDUMP := xtensa-lx106-elf-objdump    #加上
else
CC := $(addprefix $(XTENSA_BINDIR)/,xtensa-lx106-elf-gcc)
LD := $(addprefix $(XTENSA_BINDIR)/,xtensa-lx106-elf-gcc)
OBJDUMP := $(addprefix $(XTENSA_BINDIR)/,xtensa-lx106-elf-objdump)   #加上
endif

$(RBOOT_BUILD_BASE)/rboot-hex2a.h: $(RBOOT_BUILD_BASE)/rboot-stage2a.elf
    @echo "E2 $@"
    $(Q) $(OBJDUMP) -x -s -d $< > $(RBOOT_BUILD_BASE)/rboot-stage2a.dump   #加上
    $(Q) $(ESPTOOL2) -quiet -header $< $@ .text

重新编译,就能生成rboot-stage2a.dump文件了。

make clean
make

rboot-stage2a.dump:(注意看里面加的注释)

build/rboot-stage2a.elf:     file format elf32-xtensa-le
build/rboot-stage2a.elf
architecture: xtensa, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x4010fcb4    #和entry_addr对应

Contents of section .text:   #.text段内容和_text_data[]对应
 4010fc00 00000000 00000000 00000000 00000000  ................
 4010fc10 00000000 00000000 00000000 00000000  ................
 4010fc20 00000000 00000000 00000000 00000000  ................
 4010fc30 00100000 1c4b0040 12c1c0c9 e18b31cd  .....K.@......1.
 4010fc40 020c84e9 c1f9b109 f1d9d1c2 cc0801f9  ................
 4010fc50 ffc00000 f831e201 09861000 2d0c3d01  .....1......-.=.
 4010fc60 0c8401f4 ffc00000 8bcc7801 d8114609  ..........x...F.
 4010fc70 0021efff 5d0dd7b2 02205220 2d0c3d07  .!..].... R -.=.
 4010fc80 4d055951 794101eb ffc00000 58517841  M.YQyA......XQxA
 4010fc90 5acc5a77 50ddc056 6dfd0b6e 60e07456  Z.ZwP..Vm..n`.tV
 4010fca0 9efb08f1 2d0fc8e1 d8d1e8c1 f8b112c1  ....-...........
 4010fcb0 400df000 fd0005f8 ff0d0fa0 02000df0  @...............
 
4010fcb4 <call_user_start>:     #entry_addr执行的代码
4010fcb4:    00fd          mov.n    a15, a0          #注意这个地址
4010fcb6:    fff805            call0    4010fc38 <load_rom>
4010fcb9:    0f0d          mov.n    a0, a15
4010fcbb:    0002a0            jx    a2
4010fcbe:    f00d          ret.n 

call_user_start在rboot-stage2a.ld中定义为ENTRY:

EMORY
{
  dport0_0_seg :                        org = 0x3FF00000, len = 0x10
  dram0_0_seg :                         org = 0x3FFE8000, len = 0x14000
  iram1_0_seg :                         org = 0x4010FC00, len = 0x400
  irom0_0_seg :                         org = 0x40240000, len = 0x3C000
}

PHDRS
{
  dport0_0_phdr PT_LOAD;
  dram0_0_phdr PT_LOAD;
  dram0_0_bss_phdr PT_LOAD;
  iram1_0_phdr PT_LOAD;
  irom0_0_phdr PT_LOAD;
}


/*  Default entry point:  */
ENTRY(call_user_start)
EXTERN(_DebugExceptionVector)
EXTERN(_DoubleExceptionVector)
EXTERN(_KernelExceptionVector)
EXTERN(_NMIExceptionVector)
EXTERN(_UserExceptionVector)
PROVIDE(_memmap_vecbase_reset = 0x40000000);

到了这里,应该就能弄清楚rboot-stage2a.c是如何编译,并且如果和rboot-hex2a.h对应起来的了。
但为什么要搞那么麻烦?
因为rboot.c还有一处巧妙的地方:

    // copy the loader to top of iram
    ets_memcpy((void*)_text_addr, _text_data, _text_len);

rboot-stage2a.c通过工具转换成rboot-hex2a.h,就是为了能将这段代码加载到iram中。

小结

这篇文章主要对rboot目录进行了解,大概了解了加载流程,同时根据编译过程将重要的两个程序文件串了起来。rboot的设计还是比较巧妙的。我们下篇文章会对整个加载流程做详细讲解。

18 声望
3 粉丝
0 条评论
推荐阅读
float类型中NaN和Inf是什么
做嵌入式的同学应该知道,c语言中int的大小,是和平台有关的,有的占4字节,有的占2字节。所以我们对有期望长度的变量,很少直接用int定义,而更多用uint8_t, uint16_t, uint32_t。但到了float,我们为什么没有fl...

银翼Neal阅读 623

什么是MircoPython?
摘要:互联网玩家为了让Python这样的容易学,简单易学、社区API丰富的语言可以在嵌入式领域用上,逐渐开始了一轮Python上嵌入式的迁移,这样就有了今天的主角——MircoPython。

华为云开发者联盟1阅读 1.3k

新周期重构地产与物业数智化价值,TVP行业大使有话说
新周期下,地产与物业如何重构数智化价值、助力企业穿越周期?9 月 16 日,由腾讯云 TVP、腾讯云智慧地产、长城物业联合主办,以“后疫情时代——重构地产与物业数智化价值”为主题的 TVP 行业大使地产闭门会在深圳市...

腾讯云开发者阅读 736

封面图
京东云开发者|IoT运维 - 如何部署一套高可用K8S集群
环境准备工作配置ansible(deploy 主机执行) {代码...} {代码...} 优化主机配置关闭防火墙和selinux {代码...} 修改limit关闭交换分区 {代码...} 配置ipvs {代码...} 配置网桥转发规则 {代码...} {代码...} 配置et...

京东云开发者1阅读 305

封面图
Shifu手把手教你轻松添加驱动,接入任意物联设备
您可以在 Shifu 仓库 的 pkg/deviceshifu 文件夹下查看现在 Shifu 支持的 deviceShifu。可以看到 pkg/deviceshifu/deviceshifuhttp 会将一个使用HTTP协议的物联网设备转为 deviceShifu。

物联网Shifu阅读 644

激活海量数据价值,实现生产过程优化
在全球掀起的新一轮工业转型浪潮中,智能制造面临巨大发展机遇。得益于云计算、大数据和人工智能技术的加持,工业转型升级进入新阶段,人们逐渐意识到由数据驱动催生的新商业模式所带来的巨大价值,数据和算法模...

EMQX阅读 560

国产 ETL 工具 etl-engine 数据交换系统 轻量级 跨平台 引擎
产品概述我们不仅仅是数据的搬运工,还是数据搬运过程中加工处理的工厂。我们不仅仅适用关系型数据库中,还适配当下流行的时序数据库、消息中间件中。etl-engine的核心思想是为用户快速搭建ETL产品提供解决方案,...

weigeonlyyou阅读 440

封面图
18 声望
3 粉丝
宣传栏