DEV_semaphore/.platformio/packages/tool-esptoolpy/esptool/bin_image.py

1240 lines
45 KiB
Python

# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
#
# SPDX-License-Identifier: GPL-2.0-or-later
import binascii
import copy
import hashlib
import io
import os
import re
import struct
from .loader import ESPLoader
from .targets import (
ESP32C2ROM,
ESP32C3ROM,
ESP32C6BETAROM,
ESP32C6ROM,
ESP32H2BETA1ROM,
ESP32H2BETA2ROM,
ESP32H2ROM,
ESP32ROM,
ESP32S2ROM,
ESP32S3BETA2ROM,
ESP32S3ROM,
ESP8266ROM,
)
from .util import FatalError, byte, pad_to
def align_file_position(f, size):
"""Align the position in the file to the next block of specified size"""
align = (size - 1) - (f.tell() % size)
f.seek(align, 1)
def LoadFirmwareImage(chip, image_file):
"""
Load a firmware image. Can be for any supported SoC.
ESP8266 images will be examined to determine if they are original ROM firmware
images (ESP8266ROMFirmwareImage) or "v2" OTA bootloader images.
Returns a BaseFirmwareImage subclass, either ESP8266ROMFirmwareImage (v1)
or ESP8266V2FirmwareImage (v2).
"""
def select_image_class(f, chip):
chip = re.sub(r"[-()]", "", chip.lower())
if chip != "esp8266":
return {
"esp32": ESP32FirmwareImage,
"esp32s2": ESP32S2FirmwareImage,
"esp32s3beta2": ESP32S3BETA2FirmwareImage,
"esp32s3": ESP32S3FirmwareImage,
"esp32c3": ESP32C3FirmwareImage,
"esp32c6beta": ESP32C6BETAFirmwareImage,
"esp32h2beta1": ESP32H2BETA1FirmwareImage,
"esp32h2beta2": ESP32H2BETA2FirmwareImage,
"esp32c2": ESP32C2FirmwareImage,
"esp32c6": ESP32C6FirmwareImage,
"esp32h2": ESP32H2FirmwareImage,
}[chip](f)
else: # Otherwise, ESP8266 so look at magic to determine the image type
magic = ord(f.read(1))
f.seek(0)
if magic == ESPLoader.ESP_IMAGE_MAGIC:
return ESP8266ROMFirmwareImage(f)
elif magic == ESP8266V2FirmwareImage.IMAGE_V2_MAGIC:
return ESP8266V2FirmwareImage(f)
else:
raise FatalError("Invalid image magic number: %d" % magic)
if isinstance(image_file, str):
with open(image_file, "rb") as f:
return select_image_class(f, chip)
return select_image_class(image_file, chip)
class ImageSegment(object):
"""Wrapper class for a segment in an ESP image
(very similar to a section in an ELFImage also)"""
def __init__(self, addr, data, file_offs=None):
self.addr = addr
self.data = data
self.file_offs = file_offs
self.include_in_checksum = True
if self.addr != 0:
self.pad_to_alignment(
4
) # pad all "real" ImageSegments 4 byte aligned length
def copy_with_new_addr(self, new_addr):
"""Return a new ImageSegment with same data, but mapped at
a new address."""
return ImageSegment(new_addr, self.data, 0)
def split_image(self, split_len):
"""Return a new ImageSegment which splits "split_len" bytes
from the beginning of the data. Remaining bytes are kept in
this segment object (and the start address is adjusted to match.)"""
result = copy.copy(self)
result.data = self.data[:split_len]
self.data = self.data[split_len:]
self.addr += split_len
self.file_offs = None
result.file_offs = None
return result
def __repr__(self):
r = "len 0x%05x load 0x%08x" % (len(self.data), self.addr)
if self.file_offs is not None:
r += " file_offs 0x%08x" % (self.file_offs)
return r
def get_memory_type(self, image):
"""
Return a list describing the memory type(s) that is covered by this
segment's start address.
"""
return [
map_range[2]
for map_range in image.ROM_LOADER.MEMORY_MAP
if map_range[0] <= self.addr < map_range[1]
]
def pad_to_alignment(self, alignment):
self.data = pad_to(self.data, alignment, b"\x00")
class ELFSection(ImageSegment):
"""Wrapper class for a section in an ELF image, has a section
name as well as the common properties of an ImageSegment."""
def __init__(self, name, addr, data):
super(ELFSection, self).__init__(addr, data)
self.name = name.decode("utf-8")
def __repr__(self):
return "%s %s" % (self.name, super(ELFSection, self).__repr__())
class BaseFirmwareImage(object):
SEG_HEADER_LEN = 8
SHA256_DIGEST_LEN = 32
""" Base class with common firmware image functions """
def __init__(self):
self.segments = []
self.entrypoint = 0
self.elf_sha256 = None
self.elf_sha256_offset = 0
self.pad_to_size = 0
def load_common_header(self, load_file, expected_magic):
(
magic,
segments,
self.flash_mode,
self.flash_size_freq,
self.entrypoint,
) = struct.unpack("<BBBBI", load_file.read(8))
if magic != expected_magic:
raise FatalError("Invalid firmware image magic=0x%x" % (magic))
return segments
def verify(self):
if len(self.segments) > 16:
raise FatalError(
"Invalid segment count %d (max 16). "
"Usually this indicates a linker script problem." % len(self.segments)
)
def load_segment(self, f, is_irom_segment=False):
"""Load the next segment from the image file"""
file_offs = f.tell()
(offset, size) = struct.unpack("<II", f.read(8))
self.warn_if_unusual_segment(offset, size, is_irom_segment)
segment_data = f.read(size)
if len(segment_data) < size:
raise FatalError(
"End of file reading segment 0x%x, length %d (actual length %d)"
% (offset, size, len(segment_data))
)
segment = ImageSegment(offset, segment_data, file_offs)
self.segments.append(segment)
return segment
def warn_if_unusual_segment(self, offset, size, is_irom_segment):
if not is_irom_segment:
if offset > 0x40200000 or offset < 0x3FFE0000 or size > 65536:
print("WARNING: Suspicious segment 0x%x, length %d" % (offset, size))
def maybe_patch_segment_data(self, f, segment_data):
"""
If SHA256 digest of the ELF file needs to be inserted into this segment, do so.
Returns segment data.
"""
segment_len = len(segment_data)
file_pos = f.tell() # file_pos is position in the .bin file
if (
self.elf_sha256_offset >= file_pos
and self.elf_sha256_offset < file_pos + segment_len
):
# SHA256 digest needs to be patched into this binary segment,
# calculate offset of the digest inside the binary segment.
patch_offset = self.elf_sha256_offset - file_pos
# Sanity checks
if (
patch_offset < self.SEG_HEADER_LEN
or patch_offset + self.SHA256_DIGEST_LEN > segment_len
):
raise FatalError(
"Cannot place SHA256 digest on segment boundary"
"(elf_sha256_offset=%d, file_pos=%d, segment_size=%d)"
% (self.elf_sha256_offset, file_pos, segment_len)
)
# offset relative to the data part
patch_offset -= self.SEG_HEADER_LEN
if (
segment_data[patch_offset : patch_offset + self.SHA256_DIGEST_LEN]
!= b"\x00" * self.SHA256_DIGEST_LEN
):
raise FatalError(
"Contents of segment at SHA256 digest offset 0x%x are not all zero."
" Refusing to overwrite." % self.elf_sha256_offset
)
assert len(self.elf_sha256) == self.SHA256_DIGEST_LEN
segment_data = (
segment_data[0:patch_offset]
+ self.elf_sha256
+ segment_data[patch_offset + self.SHA256_DIGEST_LEN :]
)
return segment_data
def save_segment(self, f, segment, checksum=None):
"""
Save the next segment to the image file,
return next checksum value if provided
"""
segment_data = self.maybe_patch_segment_data(f, segment.data)
f.write(struct.pack("<II", segment.addr, len(segment_data)))
f.write(segment_data)
if checksum is not None:
return ESPLoader.checksum(segment_data, checksum)
def read_checksum(self, f):
"""Return ESPLoader checksum from end of just-read image"""
# Skip the padding. The checksum is stored in the last byte so that the
# file is a multiple of 16 bytes.
align_file_position(f, 16)
return ord(f.read(1))
def calculate_checksum(self):
"""
Calculate checksum of loaded image, based on segments in
segment array.
"""
checksum = ESPLoader.ESP_CHECKSUM_MAGIC
for seg in self.segments:
if seg.include_in_checksum:
checksum = ESPLoader.checksum(seg.data, checksum)
return checksum
def append_checksum(self, f, checksum):
"""Append ESPLoader checksum to the just-written image"""
align_file_position(f, 16)
f.write(struct.pack(b"B", checksum))
def write_common_header(self, f, segments):
f.write(
struct.pack(
"<BBBBI",
ESPLoader.ESP_IMAGE_MAGIC,
len(segments),
self.flash_mode,
self.flash_size_freq,
self.entrypoint,
)
)
def is_irom_addr(self, addr):
"""
Returns True if an address starts in the irom region.
Valid for ESP8266 only.
"""
return ESP8266ROM.IROM_MAP_START <= addr < ESP8266ROM.IROM_MAP_END
def get_irom_segment(self):
irom_segments = [s for s in self.segments if self.is_irom_addr(s.addr)]
if len(irom_segments) > 0:
if len(irom_segments) != 1:
raise FatalError(
"Found %d segments that could be irom0. Bad ELF file?"
% len(irom_segments)
)
return irom_segments[0]
return None
def get_non_irom_segments(self):
irom_segment = self.get_irom_segment()
return [s for s in self.segments if s != irom_segment]
def merge_adjacent_segments(self):
if not self.segments:
return # nothing to merge
segments = []
# The easiest way to merge the sections is the browse them backward.
for i in range(len(self.segments) - 1, 0, -1):
# elem is the previous section, the one `next_elem` may need to be
# merged in
elem = self.segments[i - 1]
next_elem = self.segments[i]
if all(
(
elem.get_memory_type(self) == next_elem.get_memory_type(self),
elem.include_in_checksum == next_elem.include_in_checksum,
next_elem.addr == elem.addr + len(elem.data),
)
):
# Merge any segment that ends where the next one starts,
# without spanning memory types
#
# (don't 'pad' any gaps here as they may be excluded from the image
# due to 'noinit' or other reasons.)
elem.data += next_elem.data
else:
# The section next_elem cannot be merged into the previous one,
# which means it needs to be part of the final segments.
# As we are browsing the list backward, the elements need to be
# inserted at the beginning of the final list.
segments.insert(0, next_elem)
# The first segment will always be here as it cannot be merged into any
# "previous" section.
segments.insert(0, self.segments[0])
# note: we could sort segments here as well, but the ordering of segments is
# sometimes important for other reasons (like embedded ELF SHA-256),
# so we assume that the linker script will have produced any adjacent sections
# in linear order in the ELF, anyhow.
self.segments = segments
def set_mmu_page_size(self, size):
"""
If supported, this should be overridden by the chip-specific class.
Gets called in elf2image.
"""
print(
"WARNING: Changing MMU page size is not supported on {}! "
"Defaulting to 64KB.".format(self.ROM_LOADER.CHIP_NAME)
)
class ESP8266ROMFirmwareImage(BaseFirmwareImage):
"""'Version 1' firmware image, segments loaded directly by the ROM bootloader."""
ROM_LOADER = ESP8266ROM
def __init__(self, load_file=None):
super(ESP8266ROMFirmwareImage, self).__init__()
self.flash_mode = 0
self.flash_size_freq = 0
self.version = 1
if load_file is not None:
segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC)
for _ in range(segments):
self.load_segment(load_file)
self.checksum = self.read_checksum(load_file)
self.verify()
def default_output_name(self, input_file):
"""Derive a default output name from the ELF name."""
return input_file + "-"
def save(self, basename):
"""Save a set of V1 images for flashing. Parameter is a base filename."""
# IROM data goes in its own plain binary file
irom_segment = self.get_irom_segment()
if irom_segment is not None:
with open(
"%s0x%05x.bin"
% (basename, irom_segment.addr - ESP8266ROM.IROM_MAP_START),
"wb",
) as f:
f.write(irom_segment.data)
# everything but IROM goes at 0x00000 in an image file
normal_segments = self.get_non_irom_segments()
with open("%s0x00000.bin" % basename, "wb") as f:
self.write_common_header(f, normal_segments)
checksum = ESPLoader.ESP_CHECKSUM_MAGIC
for segment in normal_segments:
checksum = self.save_segment(f, segment, checksum)
self.append_checksum(f, checksum)
ESP8266ROM.BOOTLOADER_IMAGE = ESP8266ROMFirmwareImage
class ESP8266V2FirmwareImage(BaseFirmwareImage):
"""'Version 2' firmware image, segments loaded by software bootloader stub
(ie Espressif bootloader or rboot)
"""
ROM_LOADER = ESP8266ROM
# First byte of the "v2" application image
IMAGE_V2_MAGIC = 0xEA
# First 'segment' value in a "v2" application image,
# appears to be a constant version value?
IMAGE_V2_SEGMENT = 4
def __init__(self, load_file=None):
super(ESP8266V2FirmwareImage, self).__init__()
self.version = 2
if load_file is not None:
segments = self.load_common_header(load_file, self.IMAGE_V2_MAGIC)
if segments != self.IMAGE_V2_SEGMENT:
# segment count is not really segment count here,
# but we expect to see '4'
print(
'Warning: V2 header has unexpected "segment" count %d (usually 4)'
% segments
)
# irom segment comes before the second header
#
# the file is saved in the image with a zero load address
# in the header, so we need to calculate a load address
irom_segment = self.load_segment(load_file, True)
# for actual mapped addr, add ESP8266ROM.IROM_MAP_START + flashing_addr + 8
irom_segment.addr = 0
irom_segment.include_in_checksum = False
first_flash_mode = self.flash_mode
first_flash_size_freq = self.flash_size_freq
first_entrypoint = self.entrypoint
# load the second header
segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC)
if first_flash_mode != self.flash_mode:
print(
"WARNING: Flash mode value in first header (0x%02x) disagrees "
"with second (0x%02x). Using second value."
% (first_flash_mode, self.flash_mode)
)
if first_flash_size_freq != self.flash_size_freq:
print(
"WARNING: Flash size/freq value in first header (0x%02x) disagrees "
"with second (0x%02x). Using second value."
% (first_flash_size_freq, self.flash_size_freq)
)
if first_entrypoint != self.entrypoint:
print(
"WARNING: Entrypoint address in first header (0x%08x) disagrees "
"with second header (0x%08x). Using second value."
% (first_entrypoint, self.entrypoint)
)
# load all the usual segments
for _ in range(segments):
self.load_segment(load_file)
self.checksum = self.read_checksum(load_file)
self.verify()
def default_output_name(self, input_file):
"""Derive a default output name from the ELF name."""
irom_segment = self.get_irom_segment()
if irom_segment is not None:
irom_offs = irom_segment.addr - ESP8266ROM.IROM_MAP_START
else:
irom_offs = 0
return "%s-0x%05x.bin" % (
os.path.splitext(input_file)[0],
irom_offs & ~(ESPLoader.FLASH_SECTOR_SIZE - 1),
)
def save(self, filename):
with open(filename, "wb") as f:
# Save first header for irom0 segment
f.write(
struct.pack(
b"<BBBBI",
self.IMAGE_V2_MAGIC,
self.IMAGE_V2_SEGMENT,
self.flash_mode,
self.flash_size_freq,
self.entrypoint,
)
)
irom_segment = self.get_irom_segment()
if irom_segment is not None:
# save irom0 segment, make sure it has load addr 0 in the file
irom_segment = irom_segment.copy_with_new_addr(0)
irom_segment.pad_to_alignment(
16
) # irom_segment must end on a 16 byte boundary
self.save_segment(f, irom_segment)
# second header, matches V1 header and contains loadable segments
normal_segments = self.get_non_irom_segments()
self.write_common_header(f, normal_segments)
checksum = ESPLoader.ESP_CHECKSUM_MAGIC
for segment in normal_segments:
checksum = self.save_segment(f, segment, checksum)
self.append_checksum(f, checksum)
# calculate a crc32 of entire file and append
# (algorithm used by recent 8266 SDK bootloaders)
with open(filename, "rb") as f:
crc = esp8266_crc32(f.read())
with open(filename, "ab") as f:
f.write(struct.pack(b"<I", crc))
def esp8266_crc32(data):
"""
CRC32 algorithm used by 8266 SDK bootloader (and gen_appbin.py).
"""
crc = binascii.crc32(data, 0) & 0xFFFFFFFF
if crc & 0x80000000:
return crc ^ 0xFFFFFFFF
else:
return crc + 1
class ESP32FirmwareImage(BaseFirmwareImage):
"""ESP32 firmware image is very similar to V1 ESP8266 image,
except with an additional 16 byte reserved header at top of image,
and because of new flash mapping capabilities the flash-mapped regions
can be placed in the normal image (just @ 64kB padded offsets).
"""
ROM_LOADER = ESP32ROM
# ROM bootloader will read the wp_pin field if SPI flash
# pins are remapped via flash. IDF actually enables QIO only
# from software bootloader, so this can be ignored. But needs
# to be set to this value so ROM bootloader will skip it.
WP_PIN_DISABLED = 0xEE
EXTENDED_HEADER_STRUCT_FMT = "<BBBBHBHH" + ("B" * 4) + "B"
IROM_ALIGN = 65536
def __init__(self, load_file=None, append_digest=True):
super(ESP32FirmwareImage, self).__init__()
self.secure_pad = None
self.flash_mode = 0
self.flash_size_freq = 0
self.version = 1
self.wp_pin = self.WP_PIN_DISABLED
# SPI pin drive levels
self.clk_drv = 0
self.q_drv = 0
self.d_drv = 0
self.cs_drv = 0
self.hd_drv = 0
self.wp_drv = 0
self.chip_id = 0
self.min_rev = 0
self.min_rev_full = 0
self.max_rev_full = 0
self.append_digest = append_digest
if load_file is not None:
start = load_file.tell()
segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC)
self.load_extended_header(load_file)
for _ in range(segments):
self.load_segment(load_file)
self.checksum = self.read_checksum(load_file)
if self.append_digest:
end = load_file.tell()
self.stored_digest = load_file.read(32)
load_file.seek(start)
calc_digest = hashlib.sha256()
calc_digest.update(load_file.read(end - start))
self.calc_digest = calc_digest.digest() # TODO: decide what to do here?
self.verify()
def is_flash_addr(self, addr):
return (
self.ROM_LOADER.IROM_MAP_START <= addr < self.ROM_LOADER.IROM_MAP_END
) or (self.ROM_LOADER.DROM_MAP_START <= addr < self.ROM_LOADER.DROM_MAP_END)
def default_output_name(self, input_file):
"""Derive a default output name from the ELF name."""
return "%s.bin" % (os.path.splitext(input_file)[0])
def warn_if_unusual_segment(self, offset, size, is_irom_segment):
pass # TODO: add warnings for wrong ESP32 segment offset/size combinations
def save(self, filename):
total_segments = 0
with io.BytesIO() as f: # write file to memory first
self.write_common_header(f, self.segments)
# first 4 bytes of header are read by ROM bootloader for SPI
# config, but currently unused
self.save_extended_header(f)
checksum = ESPLoader.ESP_CHECKSUM_MAGIC
# split segments into flash-mapped vs ram-loaded,
# and take copies so we can mutate them
flash_segments = [
copy.deepcopy(s)
for s in sorted(self.segments, key=lambda s: s.addr)
if self.is_flash_addr(s.addr)
]
ram_segments = [
copy.deepcopy(s)
for s in sorted(self.segments, key=lambda s: s.addr)
if not self.is_flash_addr(s.addr)
]
# Patch to support 761 union bus memmap // TODO: ESPTOOL-512
# move ".flash.appdesc" segment to the top of the flash segment
for segment in flash_segments:
if segment.name == ".flash.appdesc":
flash_segments.remove(segment)
flash_segments.insert(0, segment)
break
# check for multiple ELF sections that are mapped in the same
# flash mapping region. This is usually a sign of a broken linker script,
# but if you have a legitimate use case then let us know
if len(flash_segments) > 0:
last_addr = flash_segments[0].addr
for segment in flash_segments[1:]:
if segment.addr // self.IROM_ALIGN == last_addr // self.IROM_ALIGN:
raise FatalError(
"Segment loaded at 0x%08x lands in same 64KB flash mapping "
"as segment loaded at 0x%08x. Can't generate binary. "
"Suggest changing linker script or ELF to merge sections."
% (segment.addr, last_addr)
)
last_addr = segment.addr
def get_alignment_data_needed(segment):
# Actual alignment (in data bytes) required for a segment header:
# positioned so that after we write the next 8 byte header,
# file_offs % IROM_ALIGN == segment.addr % IROM_ALIGN
#
# (this is because the segment's vaddr may not be IROM_ALIGNed,
# more likely is aligned IROM_ALIGN+0x18
# to account for the binary file header
align_past = (segment.addr % self.IROM_ALIGN) - self.SEG_HEADER_LEN
pad_len = (self.IROM_ALIGN - (f.tell() % self.IROM_ALIGN)) + align_past
if pad_len == 0 or pad_len == self.IROM_ALIGN:
return 0 # already aligned
# subtract SEG_HEADER_LEN a second time,
# as the padding block has a header as well
pad_len -= self.SEG_HEADER_LEN
if pad_len < 0:
pad_len += self.IROM_ALIGN
return pad_len
# try to fit each flash segment on a 64kB aligned boundary
# by padding with parts of the non-flash segments...
while len(flash_segments) > 0:
segment = flash_segments[0]
pad_len = get_alignment_data_needed(segment)
if pad_len > 0: # need to pad
if len(ram_segments) > 0 and pad_len > self.SEG_HEADER_LEN:
pad_segment = ram_segments[0].split_image(pad_len)
if len(ram_segments[0].data) == 0:
ram_segments.pop(0)
else:
pad_segment = ImageSegment(0, b"\x00" * pad_len, f.tell())
checksum = self.save_segment(f, pad_segment, checksum)
total_segments += 1
else:
# write the flash segment
assert (
f.tell() + 8
) % self.IROM_ALIGN == segment.addr % self.IROM_ALIGN
checksum = self.save_flash_segment(f, segment, checksum)
flash_segments.pop(0)
total_segments += 1
# flash segments all written, so write any remaining RAM segments
for segment in ram_segments:
checksum = self.save_segment(f, segment, checksum)
total_segments += 1
if self.secure_pad:
# pad the image so that after signing it will end on a a 64KB boundary.
# This ensures all mapped flash content will be verified.
if not self.append_digest:
raise FatalError(
"secure_pad only applies if a SHA-256 digest "
"is also appended to the image"
)
align_past = (f.tell() + self.SEG_HEADER_LEN) % self.IROM_ALIGN
# 16 byte aligned checksum
# (force the alignment to simplify calculations)
checksum_space = 16
if self.secure_pad == "1":
# after checksum: SHA-256 digest +
# (to be added by signing process) version,
# signature + 12 trailing bytes due to alignment
space_after_checksum = 32 + 4 + 64 + 12
elif self.secure_pad == "2": # Secure Boot V2
# after checksum: SHA-256 digest +
# signature sector,
# but we place signature sector after the 64KB boundary
space_after_checksum = 32
pad_len = (
self.IROM_ALIGN - align_past - checksum_space - space_after_checksum
) % self.IROM_ALIGN
pad_segment = ImageSegment(0, b"\x00" * pad_len, f.tell())
checksum = self.save_segment(f, pad_segment, checksum)
total_segments += 1
# done writing segments
self.append_checksum(f, checksum)
image_length = f.tell()
if self.secure_pad:
assert ((image_length + space_after_checksum) % self.IROM_ALIGN) == 0
# kinda hacky: go back to the initial header and write the new segment count
# that includes padding segments. This header is not checksummed
f.seek(1)
f.write(bytes([total_segments]))
if self.append_digest:
# calculate the SHA256 of the whole file and append it
f.seek(0)
digest = hashlib.sha256()
digest.update(f.read(image_length))
f.write(digest.digest())
if self.pad_to_size:
image_length = f.tell()
if image_length % self.pad_to_size != 0:
pad_by = self.pad_to_size - (image_length % self.pad_to_size)
f.write(b"\xff" * pad_by)
with open(filename, "wb") as real_file:
real_file.write(f.getvalue())
def save_flash_segment(self, f, segment, checksum=None):
"""
Save the next segment to the image file, return next checksum value if provided
"""
segment_end_pos = f.tell() + len(segment.data) + self.SEG_HEADER_LEN
segment_len_remainder = segment_end_pos % self.IROM_ALIGN
if segment_len_remainder < 0x24:
# Work around a bug in ESP-IDF 2nd stage bootloader, that it didn't map the
# last MMU page, if an IROM/DROM segment was < 0x24 bytes
# over the page boundary.
segment.data += b"\x00" * (0x24 - segment_len_remainder)
return self.save_segment(f, segment, checksum)
def load_extended_header(self, load_file):
def split_byte(n):
return (n & 0x0F, (n >> 4) & 0x0F)
fields = list(
struct.unpack(self.EXTENDED_HEADER_STRUCT_FMT, load_file.read(16))
)
self.wp_pin = fields[0]
# SPI pin drive stengths are two per byte
self.clk_drv, self.q_drv = split_byte(fields[1])
self.d_drv, self.cs_drv = split_byte(fields[2])
self.hd_drv, self.wp_drv = split_byte(fields[3])
self.chip_id = fields[4]
if self.chip_id != self.ROM_LOADER.IMAGE_CHIP_ID:
print(
(
"Unexpected chip id in image. Expected %d but value was %d. "
"Is this image for a different chip model?"
)
% (self.ROM_LOADER.IMAGE_CHIP_ID, self.chip_id)
)
self.min_rev = fields[5]
self.min_rev_full = fields[6]
self.max_rev_full = fields[7]
# reserved fields in the middle should all be zero
if any(f for f in fields[8:-1] if f != 0):
print(
"Warning: some reserved header fields have non-zero values. "
"This image may be from a newer esptool.py?"
)
append_digest = fields[-1] # last byte is append_digest
if append_digest in [0, 1]:
self.append_digest = append_digest == 1
else:
raise RuntimeError(
"Invalid value for append_digest field (0x%02x). Should be 0 or 1.",
append_digest,
)
def save_extended_header(self, save_file):
def join_byte(ln, hn):
return (ln & 0x0F) + ((hn & 0x0F) << 4)
append_digest = 1 if self.append_digest else 0
fields = [
self.wp_pin,
join_byte(self.clk_drv, self.q_drv),
join_byte(self.d_drv, self.cs_drv),
join_byte(self.hd_drv, self.wp_drv),
self.ROM_LOADER.IMAGE_CHIP_ID,
self.min_rev,
self.min_rev_full,
self.max_rev_full,
]
fields += [0] * 4 # padding
fields += [append_digest]
packed = struct.pack(self.EXTENDED_HEADER_STRUCT_FMT, *fields)
save_file.write(packed)
class ESP8266V3FirmwareImage(ESP32FirmwareImage):
"""ESP8266 V3 firmware image is very similar to ESP32 image"""
EXTENDED_HEADER_STRUCT_FMT = "B" * 16
def is_flash_addr(self, addr):
return addr > ESP8266ROM.IROM_MAP_START
def save(self, filename):
total_segments = 0
with io.BytesIO() as f: # write file to memory first
self.write_common_header(f, self.segments)
checksum = ESPLoader.ESP_CHECKSUM_MAGIC
# split segments into flash-mapped vs ram-loaded,
# and take copies so we can mutate them
flash_segments = [
copy.deepcopy(s)
for s in sorted(self.segments, key=lambda s: s.addr)
if self.is_flash_addr(s.addr) and len(s.data)
]
ram_segments = [
copy.deepcopy(s)
for s in sorted(self.segments, key=lambda s: s.addr)
if not self.is_flash_addr(s.addr) and len(s.data)
]
# check for multiple ELF sections that are mapped in the same
# flash mapping region. This is usually a sign of a broken linker script,
# but if you have a legitimate use case then let us know
if len(flash_segments) > 0:
last_addr = flash_segments[0].addr
for segment in flash_segments[1:]:
if segment.addr // self.IROM_ALIGN == last_addr // self.IROM_ALIGN:
raise FatalError(
"Segment loaded at 0x%08x lands in same 64KB flash mapping "
"as segment loaded at 0x%08x. Can't generate binary. "
"Suggest changing linker script or ELF to merge sections."
% (segment.addr, last_addr)
)
last_addr = segment.addr
# try to fit each flash segment on a 64kB aligned boundary
# by padding with parts of the non-flash segments...
while len(flash_segments) > 0:
segment = flash_segments[0]
# remove 8 bytes empty data for insert segment header
if segment.name == ".flash.rodata":
segment.data = segment.data[8:]
# write the flash segment
checksum = self.save_segment(f, segment, checksum)
flash_segments.pop(0)
total_segments += 1
# flash segments all written, so write any remaining RAM segments
for segment in ram_segments:
checksum = self.save_segment(f, segment, checksum)
total_segments += 1
# done writing segments
self.append_checksum(f, checksum)
image_length = f.tell()
# kinda hacky: go back to the initial header and write the new segment count
# that includes padding segments. This header is not checksummed
f.seek(1)
f.write(bytes([total_segments]))
if self.append_digest:
# calculate the SHA256 of the whole file and append it
f.seek(0)
digest = hashlib.sha256()
digest.update(f.read(image_length))
f.write(digest.digest())
with open(filename, "wb") as real_file:
real_file.write(f.getvalue())
def load_extended_header(self, load_file):
def split_byte(n):
return (n & 0x0F, (n >> 4) & 0x0F)
fields = list(
struct.unpack(self.EXTENDED_HEADER_STRUCT_FMT, load_file.read(16))
)
self.wp_pin = fields[0]
# SPI pin drive stengths are two per byte
self.clk_drv, self.q_drv = split_byte(fields[1])
self.d_drv, self.cs_drv = split_byte(fields[2])
self.hd_drv, self.wp_drv = split_byte(fields[3])
if fields[15] in [0, 1]:
self.append_digest = fields[15] == 1
else:
raise RuntimeError(
"Invalid value for append_digest field (0x%02x). Should be 0 or 1.",
fields[15],
)
# remaining fields in the middle should all be zero
if any(f for f in fields[4:15] if f != 0):
print(
"Warning: some reserved header fields have non-zero values. "
"This image may be from a newer esptool.py?"
)
ESP32ROM.BOOTLOADER_IMAGE = ESP32FirmwareImage
class ESP32S2FirmwareImage(ESP32FirmwareImage):
"""ESP32S2 Firmware Image almost exactly the same as ESP32FirmwareImage"""
ROM_LOADER = ESP32S2ROM
ESP32S2ROM.BOOTLOADER_IMAGE = ESP32S2FirmwareImage
class ESP32S3BETA2FirmwareImage(ESP32FirmwareImage):
"""ESP32S3 Firmware Image almost exactly the same as ESP32FirmwareImage"""
ROM_LOADER = ESP32S3BETA2ROM
ESP32S3BETA2ROM.BOOTLOADER_IMAGE = ESP32S3BETA2FirmwareImage
class ESP32S3FirmwareImage(ESP32FirmwareImage):
"""ESP32S3 Firmware Image almost exactly the same as ESP32FirmwareImage"""
ROM_LOADER = ESP32S3ROM
ESP32S3ROM.BOOTLOADER_IMAGE = ESP32S3FirmwareImage
class ESP32C3FirmwareImage(ESP32FirmwareImage):
"""ESP32C3 Firmware Image almost exactly the same as ESP32FirmwareImage"""
ROM_LOADER = ESP32C3ROM
ESP32C3ROM.BOOTLOADER_IMAGE = ESP32C3FirmwareImage
class ESP32C6BETAFirmwareImage(ESP32FirmwareImage):
"""ESP32C6 Firmware Image almost exactly the same as ESP32FirmwareImage"""
ROM_LOADER = ESP32C6BETAROM
ESP32C6BETAROM.BOOTLOADER_IMAGE = ESP32C6BETAFirmwareImage
class ESP32H2BETA1FirmwareImage(ESP32FirmwareImage):
"""ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage"""
ROM_LOADER = ESP32H2BETA1ROM
ESP32H2BETA1ROM.BOOTLOADER_IMAGE = ESP32H2BETA1FirmwareImage
class ESP32H2BETA2FirmwareImage(ESP32FirmwareImage):
"""ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage"""
ROM_LOADER = ESP32H2BETA2ROM
ESP32H2BETA2ROM.BOOTLOADER_IMAGE = ESP32H2BETA2FirmwareImage
class ESP32C2FirmwareImage(ESP32FirmwareImage):
"""ESP32C2 Firmware Image almost exactly the same as ESP32FirmwareImage"""
ROM_LOADER = ESP32C2ROM
def set_mmu_page_size(self, size):
if size not in [16384, 32768, 65536]:
raise FatalError(
"{} bytes is not a valid ESP32-C2 page size, "
"select from 64KB, 32KB, 16KB.".format(size)
)
self.IROM_ALIGN = size
ESP32C2ROM.BOOTLOADER_IMAGE = ESP32C2FirmwareImage
class ESP32C6FirmwareImage(ESP32FirmwareImage):
"""ESP32C6 Firmware Image almost exactly the same as ESP32FirmwareImage"""
ROM_LOADER = ESP32C6ROM
def set_mmu_page_size(self, size):
if size not in [8192, 16384, 32768, 65536]:
raise FatalError(
"{} bytes is not a valid ESP32-C6 page size, "
"select from 64KB, 32KB, 16KB, 8KB.".format(size)
)
self.IROM_ALIGN = size
ESP32C6ROM.BOOTLOADER_IMAGE = ESP32C6FirmwareImage
class ESP32H2FirmwareImage(ESP32C6FirmwareImage):
"""ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage"""
ROM_LOADER = ESP32H2ROM
ESP32H2ROM.BOOTLOADER_IMAGE = ESP32H2FirmwareImage
class ELFFile(object):
SEC_TYPE_PROGBITS = 0x01
SEC_TYPE_STRTAB = 0x03
SEC_TYPE_INITARRAY = 0x0E
SEC_TYPE_FINIARRAY = 0x0F
PROG_SEC_TYPES = (SEC_TYPE_PROGBITS, SEC_TYPE_INITARRAY, SEC_TYPE_FINIARRAY)
LEN_SEC_HEADER = 0x28
SEG_TYPE_LOAD = 0x01
LEN_SEG_HEADER = 0x20
def __init__(self, name):
# Load sections from the ELF file
self.name = name
with open(self.name, "rb") as f:
self._read_elf_file(f)
def get_section(self, section_name):
for s in self.sections:
if s.name == section_name:
return s
raise ValueError("No section %s in ELF file" % section_name)
def _read_elf_file(self, f):
# read the ELF file header
LEN_FILE_HEADER = 0x34
try:
(
ident,
_type,
machine,
_version,
self.entrypoint,
_phoff,
shoff,
_flags,
_ehsize,
_phentsize,
_phnum,
shentsize,
shnum,
shstrndx,
) = struct.unpack("<16sHHLLLLLHHHHHH", f.read(LEN_FILE_HEADER))
except struct.error as e:
raise FatalError(
"Failed to read a valid ELF header from %s: %s" % (self.name, e)
)
if byte(ident, 0) != 0x7F or ident[1:4] != b"ELF":
raise FatalError("%s has invalid ELF magic header" % self.name)
if machine not in [0x5E, 0xF3]:
raise FatalError(
"%s does not appear to be an Xtensa or an RISCV ELF file. "
"e_machine=%04x" % (self.name, machine)
)
if shentsize != self.LEN_SEC_HEADER:
raise FatalError(
"%s has unexpected section header entry size 0x%x (not 0x%x)"
% (self.name, shentsize, self.LEN_SEC_HEADER)
)
if shnum == 0:
raise FatalError("%s has 0 section headers" % (self.name))
self._read_sections(f, shoff, shnum, shstrndx)
self._read_segments(f, _phoff, _phnum, shstrndx)
def _read_sections(self, f, section_header_offs, section_header_count, shstrndx):
f.seek(section_header_offs)
len_bytes = section_header_count * self.LEN_SEC_HEADER
section_header = f.read(len_bytes)
if len(section_header) == 0:
raise FatalError(
"No section header found at offset %04x in ELF file."
% section_header_offs
)
if len(section_header) != (len_bytes):
raise FatalError(
"Only read 0x%x bytes from section header (expected 0x%x.) "
"Truncated ELF file?" % (len(section_header), len_bytes)
)
# walk through the section header and extract all sections
section_header_offsets = range(0, len(section_header), self.LEN_SEC_HEADER)
def read_section_header(offs):
name_offs, sec_type, _flags, lma, sec_offs, size = struct.unpack_from(
"<LLLLLL", section_header[offs:]
)
return (name_offs, sec_type, lma, size, sec_offs)
all_sections = [read_section_header(offs) for offs in section_header_offsets]
prog_sections = [s for s in all_sections if s[1] in ELFFile.PROG_SEC_TYPES]
# search for the string table section
if not (shstrndx * self.LEN_SEC_HEADER) in section_header_offsets:
raise FatalError("ELF file has no STRTAB section at shstrndx %d" % shstrndx)
_, sec_type, _, sec_size, sec_offs = read_section_header(
shstrndx * self.LEN_SEC_HEADER
)
if sec_type != ELFFile.SEC_TYPE_STRTAB:
print(
"WARNING: ELF file has incorrect STRTAB section type 0x%02x" % sec_type
)
f.seek(sec_offs)
string_table = f.read(sec_size)
# build the real list of ELFSections by reading the actual section names from
# the string table section, and actual data for each section
# from the ELF file itself
def lookup_string(offs):
raw = string_table[offs:]
return raw[: raw.index(b"\x00")]
def read_data(offs, size):
f.seek(offs)
return f.read(size)
prog_sections = [
ELFSection(lookup_string(n_offs), lma, read_data(offs, size))
for (n_offs, _type, lma, size, offs) in prog_sections
if lma != 0 and size > 0
]
self.sections = prog_sections
def _read_segments(self, f, segment_header_offs, segment_header_count, shstrndx):
f.seek(segment_header_offs)
len_bytes = segment_header_count * self.LEN_SEG_HEADER
segment_header = f.read(len_bytes)
if len(segment_header) == 0:
raise FatalError(
"No segment header found at offset %04x in ELF file."
% segment_header_offs
)
if len(segment_header) != (len_bytes):
raise FatalError(
"Only read 0x%x bytes from segment header (expected 0x%x.) "
"Truncated ELF file?" % (len(segment_header), len_bytes)
)
# walk through the segment header and extract all segments
segment_header_offsets = range(0, len(segment_header), self.LEN_SEG_HEADER)
def read_segment_header(offs):
(
seg_type,
seg_offs,
_vaddr,
lma,
size,
_memsize,
_flags,
_align,
) = struct.unpack_from("<LLLLLLLL", segment_header[offs:])
return (seg_type, lma, size, seg_offs)
all_segments = [read_segment_header(offs) for offs in segment_header_offsets]
prog_segments = [s for s in all_segments if s[0] == ELFFile.SEG_TYPE_LOAD]
def read_data(offs, size):
f.seek(offs)
return f.read(size)
prog_segments = [
ELFSection(b"PHDR", lma, read_data(offs, size))
for (_type, lma, size, offs) in prog_segments
if lma != 0 and size > 0
]
self.segments = prog_segments
def sha256(self):
# return SHA256 hash of the input ELF file
sha256 = hashlib.sha256()
with open(self.name, "rb") as f:
sha256.update(f.read())
return sha256.digest()