|
| 1 | +#!/usr/bin/env python |
| 2 | + |
| 3 | +# Convert an ESP 32 OTA partition into an ELF |
| 4 | + |
| 5 | +import os, argparse |
| 6 | +from makeelf.elf import * |
| 7 | +from esptool import * |
| 8 | +from esp32_firmware_reader import * |
| 9 | + |
| 10 | +def image_base_name(path): |
| 11 | + filename_w_ext = os.path.basename(path) |
| 12 | + filename, ext = os.path.splitext(filename_w_ext) |
| 13 | + return filename |
| 14 | + |
| 15 | +# section header flags |
| 16 | +def calcShFlg(flags): |
| 17 | + mask = 0 |
| 18 | + if 'W' in flags: |
| 19 | + mask |= SHF.SHF_WRITE |
| 20 | + if 'A' in flags: |
| 21 | + mask |= SHF.SHF_ALLOC |
| 22 | + if 'X' in flags: |
| 23 | + mask |= SHF.SHF_EXECINSTR |
| 24 | + |
| 25 | + return mask |
| 26 | + |
| 27 | +# program header flags |
| 28 | +def calcPhFlg(flags): |
| 29 | + p_flags = 0 |
| 30 | + if 'r' in flags: |
| 31 | + p_flags |= PF.PF_R |
| 32 | + if 'w' in flags: |
| 33 | + p_flags |= PF.PF_W |
| 34 | + if 'x' in flags: |
| 35 | + p_flags |= PF.PF_X |
| 36 | + return p_flags |
| 37 | + |
| 38 | +def image2elf(filename, output_file, verbose=False): |
| 39 | + image = LoadFirmwareImage('esp32', filename) |
| 40 | + section_map = { |
| 41 | + 'DROM' : '.flash.rodata', |
| 42 | + 'BYTE_ACCESSIBLE, DRAM, DMA': '.dram0.data', |
| 43 | + 'RTC_IRAM' : '.rtc.text', # TODO double-check |
| 44 | + 'IROM' : '.flash.text' |
| 45 | + } |
| 46 | + |
| 47 | + # parse image name |
| 48 | + image_name = image_base_name(filename) |
| 49 | + |
| 50 | + # iram is split into .vectors and .text |
| 51 | + # however, the .vectors section can be identified by its unique 0x400 size |
| 52 | + |
| 53 | + section_data = {} |
| 54 | + elf = ELF(e_machine=EM.EM_XTENSA, e_data=ELFDATA.ELFDATA2LSB) |
| 55 | + elf.Elf.Ehdr.e_entry = image.entrypoint |
| 56 | + |
| 57 | + # TODO bss is not accounted for |
| 58 | + # 00 .flash.rodata |
| 59 | + # 01 .dram0.data .dram0.bss |
| 60 | + # 02 .iram0.vectors .iram0.text |
| 61 | + # 03 .flash.text |
| 62 | + print_verbose(verbose, "Entrypoint " + str(hex(image.entrypoint))) |
| 63 | + |
| 64 | + section_ids = {} |
| 65 | + |
| 66 | + # map to hold pre-defined elf section attributes |
| 67 | + sect_attr_map = { |
| 68 | + '.flash.rodata' : {'ES':0x00, 'Flg':'WA', 'Lk':0, 'Inf':0, 'Al':16}, |
| 69 | + '.dram0.data' : {'ES':0x00, 'Flg':'WA', 'Lk':0, 'Inf':0, 'Al':16}, |
| 70 | + '.iram0.vectors': {'ES':0x00, 'Flg':'AX', 'Lk':0, 'Inf':0, 'Al':4}, |
| 71 | + '.iram0.text' : {'ES':0x00, 'Flg':'WAX', 'Lk':0, 'Inf':0, 'Al':4}, |
| 72 | + '.flash.text' : {'ES':0x00, 'Flg':'AX', 'Lk':0, 'Inf':0, 'Al':4} |
| 73 | + } |
| 74 | + # TODO rtc not accounted for |
| 75 | + |
| 76 | + idx = 0 |
| 77 | + ##### build out the section data ##### |
| 78 | + ###################################### |
| 79 | + for seg in sorted(image.segments, key=lambda s:s.addr): |
| 80 | + idx += 1 |
| 81 | + |
| 82 | + # name from image |
| 83 | + segment_name = ", ".join([seg_range[2] for seg_range in image.ROM_LOADER.MEMORY_MAP if seg_range[0] <= seg.addr < seg_range[1]]) |
| 84 | + |
| 85 | + # TODO when processing an image, there was an empty segment name? |
| 86 | + if segment_name == '': |
| 87 | + continue |
| 88 | + |
| 89 | + section_name = '' |
| 90 | + # handle special case |
| 91 | + if segment_name == 'IRAM': |
| 92 | + if len(seg.data) == 0x400: |
| 93 | + section_name = '.iram0.vectors' |
| 94 | + else: |
| 95 | + section_name = '.iram0.text' |
| 96 | + else: |
| 97 | + section_name = section_map[segment_name] |
| 98 | + |
| 99 | + # if we have a mapped segment <-> section |
| 100 | + # add the elf section |
| 101 | + if section_name != '': |
| 102 | + # might need to append to section (e.g. IRAM is split up due to alignment) |
| 103 | + if section_name in section_data: |
| 104 | + section_data[section_name]['data'] += seg.data |
| 105 | + else: |
| 106 | + section_data[section_name] = {'addr':seg.addr, 'data':seg.data} |
| 107 | + |
| 108 | + ##### append the sections ##### |
| 109 | + ############################### |
| 110 | + for name in section_data.keys(): |
| 111 | + data = section_data[name]['data'] |
| 112 | + addr = section_data[name]['addr'] |
| 113 | + # build the section out as much as possible |
| 114 | + # if we already know the attribute values |
| 115 | + if name in sect_attr_map: |
| 116 | + sect = sect_attr_map[name] |
| 117 | + flg = calcShFlg(sect['Flg']) |
| 118 | + section_ids[name] = elf._append_section(name, data, addr,SHT.SHT_PROGBITS, flg, sect['Lk'], sect['Inf'], sect['Al'], sect['ES']) |
| 119 | + else: |
| 120 | + section_ids[name] = elf.append_section(name, data, addr) |
| 121 | + |
| 122 | + elf.append_special_section('.strtab') |
| 123 | + elf.append_special_section('.symtab') |
| 124 | + add_elf_symbols(elf) |
| 125 | + |
| 126 | + # segment flags |
| 127 | + # TODO double check this stuff |
| 128 | + segments = { |
| 129 | + '.flash.rodata' : 'rw', |
| 130 | + '.dram0.data' : 'rw', |
| 131 | + '.iram0.vectors': 'rwx', |
| 132 | + '.flash.text' : 'rx' |
| 133 | + } |
| 134 | + |
| 135 | + # there is an initial program header that we don't want... |
| 136 | + elf.Elf.Phdr_table.pop() |
| 137 | + |
| 138 | + bytes(elf) # kind of a hack, but __bytes__() calculates offsets in elf object |
| 139 | + size_of_phdrs = len(Elf32_Phdr()) * len(segments) # to pre-calculate program header offsets |
| 140 | + |
| 141 | + ##### add the segments #### |
| 142 | + ########################### |
| 143 | + print_verbose(verbose, "\nAdding program headers") |
| 144 | + for (name, flags) in segments.items(): |
| 145 | + |
| 146 | + if (name == '.iram0.vectors'): |
| 147 | + # combine these |
| 148 | + size = len(section_data['.iram0.vectors']['data']) + len(section_data['.iram0.text']['data']) |
| 149 | + else: |
| 150 | + size = len(section_data[name]['data']) |
| 151 | + |
| 152 | + p_flags = calcPhFlg(flags) |
| 153 | + addr = section_data[name]['addr'] |
| 154 | + align = 0x1000 |
| 155 | + p_type = PT.PT_LOAD |
| 156 | + |
| 157 | + shstrtab_hdr, shstrtab = elf.get_section_by_name(name) |
| 158 | + offset = shstrtab_hdr.sh_offset + size_of_phdrs # account for new offset |
| 159 | + |
| 160 | + # build program header |
| 161 | + Phdr = Elf32_Phdr(PT.PT_LOAD, p_offset=offset, p_vaddr=addr, |
| 162 | + p_paddr=addr, p_filesz=size, p_memsz=size, |
| 163 | + p_flags=p_flags, p_align=0x1000, little=elf.little) |
| 164 | + |
| 165 | + |
| 166 | + print_verbose(verbose, name + ": " + str(Phdr)) |
| 167 | + elf.Elf.Phdr_table.append(Phdr) |
| 168 | + |
| 169 | + # write out elf file |
| 170 | + if output_file is not None: |
| 171 | + out_file = output_file |
| 172 | + else: |
| 173 | + out_file = image_name + '.elf' |
| 174 | + print("\nWriting ELF to " + out_file + "...") |
| 175 | + fd = os.open(out_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC) |
| 176 | + os.write(fd, bytes(elf)) |
| 177 | + os.close(fd) |
| 178 | + |
| 179 | +def add_elf_symbols(elf): |
| 180 | + |
| 181 | + fh = open("symbols_dump.txt", "r") |
| 182 | + lines = fh.readlines() |
| 183 | + |
| 184 | + bind_map = {"LOCAL" : STB.STB_LOCAL, "GLOBAL" : STB.STB_GLOBAL} |
| 185 | + type_map = {"NOTYPE": STT.STT_NOTYPE, "OBJECT" : STT.STT_OBJECT, "FUNC" : STT.STT_FUNC, "FILE" : STT.STT_FILE} |
| 186 | + |
| 187 | + for line in lines: |
| 188 | + line = line.split() |
| 189 | + sym_binding = line[4] |
| 190 | + sym_type = line[3] |
| 191 | + sym_size = int(line[2]) |
| 192 | + sym_val = int(line[1], 16) |
| 193 | + sym_name = line[7] |
| 194 | + # ABS |
| 195 | + elf.append_symbol(sym_name, 0xfff1, sym_val, sym_size, sym_binding=bind_map[sym_binding], sym_type=type_map[sym_type]) |
| 196 | + |
| 197 | +def flash_dump_to_elf(filename, partition): |
| 198 | + fh = open(filename, 'rb') |
| 199 | + part_table = read_partition_table(fh) |
| 200 | + fh.close() |
| 201 | + return part_table |
| 202 | + |
| 203 | +def main(): |
| 204 | + desc = 'ESP32 Firmware Image Parser Utility' |
| 205 | + arg_parser = argparse.ArgumentParser(description=desc) |
| 206 | + arg_parser.add_argument('action', choices=['show_partitions', 'dump_partition', 'create_elf'], help='Action to take') |
| 207 | + arg_parser.add_argument('input', help='Firmware image input file') |
| 208 | + arg_parser.add_argument('-output', help='Output file name') |
| 209 | + arg_parser.add_argument('-partition', help='Partition name (e.g. ota_0)') |
| 210 | + arg_parser.add_argument('-v', default=False, help='Verbose output', action='store_true') |
| 211 | + |
| 212 | + args = arg_parser.parse_args() |
| 213 | + |
| 214 | + with open(args.input, 'rb') as fh: |
| 215 | + verbose = False |
| 216 | + # read_partition_table will show the partitions if verbose |
| 217 | + if args.action == 'show_partitions' or args.v is True: |
| 218 | + verbose = True |
| 219 | + |
| 220 | + # parse that ish |
| 221 | + part_table = read_partition_table(fh, verbose) |
| 222 | + |
| 223 | + if args.action in ['dump_partition', 'create_elf']: |
| 224 | + part_name = args.partition |
| 225 | + |
| 226 | + if args.action == 'dump_partition' and args.output is not None: |
| 227 | + dump_file = args.output |
| 228 | + else: |
| 229 | + dump_file = part_name + '_out.bin' |
| 230 | + |
| 231 | + if part_name in part_table: |
| 232 | + print("Dumping partition '" + part_name + "' to " + dump_file) |
| 233 | + part = part_table[part_name] |
| 234 | + dump_bytes(fh, part['offset'], part['size'], dump_file) # dump_file will be written out |
| 235 | + |
| 236 | + if args.action == 'create_elf': |
| 237 | + # can only generate elf from 'app' partition type |
| 238 | + if part['type'] != 0: |
| 239 | + print("Uh oh... bad partition type. Can't convert to ELF") |
| 240 | + else: |
| 241 | + if args.output is None: |
| 242 | + print("Need output file name") |
| 243 | + else: |
| 244 | + # we have to load from a file |
| 245 | + output_file = args.output |
| 246 | + image2elf(dump_file, output_file, verbose) |
| 247 | + else: |
| 248 | + print("Partition '" + part_name + "' not found.") |
| 249 | + |
| 250 | +if __name__ == '__main__': |
| 251 | + main() |
0 commit comments