在非Linux的嵌入式開發中,本身手寫Bootloader是很正常的事。由於能夠定製本身想要的功能。好比定製本身的Bootloader通訊接口(UART、I2C、SPI),通訊協議,甚至更高級的固件備份回退等功能。可是使用ESP8266就不同了,整個芯片的程序是怎麼跑起來的都只知其一;不知其二(因此我寫了這篇文章:ESP8266架構探索-運行的起始);官方提供了Bootloader和完整的接口,可是是閉源的;官方Bootloader雖然有作固件備份,可是沒有固件回滾,等等這些問題。因此這時候rboot出現了。咱們有不少緣由不能從無到有寫一個本身的Bootloader,可是咱們能夠借鑑,知道rboot怎麼運做後,就可以經過修改,裁剪,作出本身想要的Bootloader。因此這篇文章不會花大力氣去分析rboot的特性是怎麼實現的,着重於怎麼寫一個ESP8266上最基礎的Bootloader。git
先給出rboot的github地址:https://github.com/raburton/rboot
編譯後,是像下面這樣github
. ├── 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就是咱們想要的代碼。segmentfault
rboot.c的ENTRY是call_user_start,咱們看看運行流程:(c語言版和彙編版功能同樣,看c語言代碼便於理解)api
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:app
/* 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。ui
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中。
這裏有兩個關鍵的地方:debug
編譯過程當中用到了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的設計仍是比較巧妙的。咱們下篇文章會對整個加載流程作詳細講解。