mirror of
https://github.com/espressif/esptool.git
synced 2025-10-18 00:32:43 +08:00

Allows generating a properly checksummed and hashed binary file which contains the hash of the original ELF file inside. Useful for being able to identify the ELF file used to produce the binary.
291 lines
11 KiB
Python
Executable File
291 lines
11 KiB
Python
Executable File
#!/usr/bin/env python
|
|
import os
|
|
import os.path
|
|
import subprocess
|
|
import struct
|
|
import sys
|
|
import unittest
|
|
import hashlib
|
|
|
|
from elftools.elf.elffile import ELFFile
|
|
|
|
TEST_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "elf2image")
|
|
os.chdir(TEST_DIR)
|
|
try:
|
|
ESPTOOL_PY = os.environ["ESPTOOL_PY"]
|
|
except KeyError:
|
|
ESPTOOL_PY = os.path.join(TEST_DIR, "../..", "esptool.py")
|
|
|
|
# import the version of esptool we are testing with
|
|
sys.path.append(os.path.dirname(ESPTOOL_PY))
|
|
import esptool
|
|
|
|
def try_delete(path):
|
|
try:
|
|
os.remove(path)
|
|
except OSError:
|
|
pass
|
|
|
|
def segment_matches_section(segment, section):
|
|
""" segment is an ImageSegment from an esptool binary.
|
|
section is an elftools ELF section
|
|
|
|
Returns True if they match
|
|
"""
|
|
sh_size = (section.header.sh_size + 0x3) & ~3 # pad length of ELF sections
|
|
return (section.header.sh_addr == segment.addr
|
|
and sh_size == len(segment.data))
|
|
|
|
|
|
class BaseTestCase(unittest.TestCase):
|
|
|
|
def assertEqualHex(self, expected, actual, message=None):
|
|
try:
|
|
expected = hex(expected)
|
|
except TypeError: # if expected is character
|
|
expected = hex(ord(expected))
|
|
try:
|
|
actual = hex(actual)
|
|
except TypeError: # if actual is character
|
|
actual = hex(ord(actual))
|
|
self.assertEqual(expected, actual, message)
|
|
|
|
def assertImageContainsSection(self, image, elf, section_name):
|
|
"""
|
|
Assert an esptool binary image object contains
|
|
the data for a particular ELF section.
|
|
"""
|
|
with open(elf, "rb") as f:
|
|
e = ELFFile(f)
|
|
section = e.get_section_by_name(section_name)
|
|
self.assertTrue(section, "%s should be in the ELF" % section_name)
|
|
sh_addr = section.header.sh_addr
|
|
data = section.data()
|
|
# section contents may be smeared across multiple image segments,
|
|
# so look through each segment and remove it from ELF section 'data'
|
|
# as we find it in the image segments. When we're done 'data' should
|
|
# all be accounted for
|
|
for seg in sorted(image.segments, key=lambda s:s.addr):
|
|
print("comparing seg 0x%x sec 0x%x len 0x%x" % (seg.addr, sh_addr, len(data)))
|
|
if seg.addr == sh_addr:
|
|
overlap_len = min(len(seg.data), len(data))
|
|
self.assertEqual(data[:overlap_len], seg.data[:overlap_len],
|
|
"ELF '%s' section has mis-matching binary image data" % section_name)
|
|
sh_addr += overlap_len
|
|
data = data[overlap_len:]
|
|
|
|
# no bytes in 'data' should be left unmatched
|
|
self.assertEqual(0, len(data),
|
|
"ELF %s section '%s' has no encompassing segment(s) in binary image (image segments: %s)"
|
|
% (elf, section_name, image.segments))
|
|
|
|
def assertImageInfo(self, binpath, chip="esp8266"):
|
|
"""
|
|
Run esptool.py image_info on a binary file,
|
|
assert no red flags about contents.
|
|
"""
|
|
cmd = [ sys.executable, ESPTOOL_PY, "--chip", chip, "image_info", binpath ]
|
|
try:
|
|
output = subprocess.check_output(cmd).decode("utf-8")
|
|
print(output)
|
|
except subprocess.CalledProcessError as e:
|
|
print(e.output)
|
|
raise
|
|
self.assertFalse("invalid" in output, "Checksum calculation should be valid")
|
|
self.assertFalse("warning" in output.lower(), "Should be no warnings in image_info output")
|
|
|
|
def run_elf2image(self, chip, elf_path, version=None, extra_args=[]):
|
|
""" Run elf2image on elf_path """
|
|
cmd = [sys.executable, ESPTOOL_PY, "--chip", chip, "elf2image" ]
|
|
if version is not None:
|
|
cmd += [ "--version", str(version) ]
|
|
cmd += [ elf_path ] + extra_args
|
|
print("Executing %s" % (" ".join(cmd)))
|
|
try:
|
|
output = str(subprocess.check_output(cmd))
|
|
print(output)
|
|
self.assertFalse("warning" in output.lower(), "elf2image should not output warnings")
|
|
except subprocess.CalledProcessError as e:
|
|
print(e.output)
|
|
raise
|
|
|
|
class ESP8266V1ImageTests(BaseTestCase):
|
|
ELF="esp8266-nonosssdk20-iotdemo.elf"
|
|
BIN_LOAD="esp8266-nonosssdk20-iotdemo.elf-0x00000.bin"
|
|
BIN_IROM="esp8266-nonosssdk20-iotdemo.elf-0x10000.bin"
|
|
|
|
def setUp(self):
|
|
self.run_elf2image("esp8266", self.ELF, 1)
|
|
|
|
def tearDown(self):
|
|
try_delete(self.BIN_LOAD)
|
|
try_delete(self.BIN_IROM)
|
|
|
|
def test_irom_bin(self):
|
|
with open(self.ELF, "rb") as f:
|
|
e = ELFFile(f)
|
|
irom_section = e.get_section_by_name(".irom0.text")
|
|
self.assertEqual(irom_section.header.sh_size,
|
|
os.stat(self.BIN_IROM).st_size,
|
|
"IROM raw binary file should be same length as .irom0.text section")
|
|
|
|
def test_loaded_sections(self):
|
|
image = esptool.LoadFirmwareImage("esp8266", self.BIN_LOAD)
|
|
self.assertEqual(3, len(image.segments))
|
|
self.assertImageContainsSection(image, self.ELF, ".data")
|
|
self.assertImageContainsSection(image, self.ELF, ".text")
|
|
self.assertImageContainsSection(image, self.ELF, ".rodata")
|
|
|
|
class ESP8266V12SectionHeaderNotAtEnd(BaseTestCase):
|
|
""" Ref https://github.com/espressif/esptool/issues/197 -
|
|
this ELF image has the section header not at the end of the file """
|
|
ELF="esp8266-nonossdkv12-example.elf"
|
|
BIN=ELF+"-0x00000.bin"
|
|
|
|
def test_elf_section_header_not_at_end(self):
|
|
self.run_elf2image("esp8266", self.ELF)
|
|
image = esptool.LoadFirmwareImage("esp8266", self.BIN)
|
|
self.assertEqual(3, len(image.segments))
|
|
self.assertImageContainsSection(image, self.ELF, ".data")
|
|
self.assertImageContainsSection(image, self.ELF, ".text")
|
|
self.assertImageContainsSection(image, self.ELF, ".rodata")
|
|
|
|
def tearDown(self):
|
|
try_delete(self.BIN)
|
|
|
|
class ESP8266V2ImageTests(BaseTestCase):
|
|
|
|
def _test_elf2image(self, elfpath, binpath):
|
|
try:
|
|
self.run_elf2image("esp8266", elfpath, 2)
|
|
image = esptool.LoadFirmwareImage("esp8266", binpath)
|
|
self.assertEqual(4, len(image.segments))
|
|
self.assertImageContainsSection(image, elfpath, ".data")
|
|
self.assertImageContainsSection(image, elfpath, ".text")
|
|
self.assertImageContainsSection(image, elfpath, ".rodata")
|
|
irom_segment = image.segments[0]
|
|
self.assertEqual(0, irom_segment.addr,
|
|
"IROM segment 'load address' should be zero")
|
|
with open(elfpath, "rb") as f:
|
|
e = ELFFile(f)
|
|
sh_size = (e.get_section_by_name(".irom0.text").header.sh_size + 15) & ~15
|
|
self.assertEqual(len(irom_segment.data), sh_size, "irom segment (0x%x) should be same size (16 padded) as .irom0.text section (0x%x)" % (len(irom_segment.data), sh_size))
|
|
|
|
# check V2 CRC (for ESP8266 SDK bootloader)
|
|
with open(binpath, "rb") as f:
|
|
f.seek(-4, os.SEEK_END)
|
|
image_len = f.tell()
|
|
crc_stored = struct.unpack("<I", f.read(4))[0]
|
|
f.seek(0)
|
|
crc_calc = esptool.esp8266_crc32(f.read(image_len))
|
|
self.assertEqual(crc_stored, crc_calc)
|
|
|
|
# test imageinfo doesn't fail
|
|
self.assertImageInfo(binpath)
|
|
|
|
finally:
|
|
try_delete(binpath)
|
|
|
|
def test_nonossdkimage(self):
|
|
ELF="esp8266-nonossdkv20-at-v2.elf"
|
|
BIN="esp8266-nonossdkv20-at-v2-0x01000.bin"
|
|
self._test_elf2image(ELF, BIN)
|
|
|
|
def test_espopenrtosimage(self):
|
|
ELF="esp8266-openrtos-blink-v2.elf"
|
|
BIN="esp8266-openrtos-blink-v2-0x02000.bin"
|
|
self._test_elf2image(ELF, BIN)
|
|
|
|
class ESP32ImageTests(BaseTestCase):
|
|
def _test_elf2image(self, elfpath, binpath):
|
|
try:
|
|
self.run_elf2image("esp32", elfpath)
|
|
image = esptool.LoadFirmwareImage("esp32", binpath)
|
|
self.assertImageInfo(binpath, "esp32")
|
|
return image
|
|
finally:
|
|
try_delete(binpath)
|
|
|
|
def test_bootloader(self):
|
|
ELF="esp32-bootloader.elf"
|
|
BIN="esp32-bootloader.bin"
|
|
image = self._test_elf2image(ELF, BIN)
|
|
self.assertEqual(3, len(image.segments))
|
|
for section in [ ".iram1.text", ".iram_pool_1.text",
|
|
".dram0.rodata"]:
|
|
self.assertImageContainsSection(image, ELF, section)
|
|
|
|
def test_app_template(self):
|
|
ELF="esp32-app-template.elf"
|
|
BIN="esp32-app-template.bin"
|
|
image = self._test_elf2image(ELF, BIN)
|
|
self.assertEqual(7, len(image.segments))
|
|
# the other two segments are padding segments
|
|
for section in [ ".iram0.text", ".iram0.vectors",
|
|
".dram0.data", ".flash.rodata",
|
|
".flash.text" ]:
|
|
self.assertImageContainsSection(image, ELF, section)
|
|
|
|
def test_too_many_sections(self):
|
|
ELF="esp32-too-many-sections.elf"
|
|
BIN="esp32-too-many-sections.bin"
|
|
with self.assertRaises(subprocess.CalledProcessError) as e:
|
|
self._test_elf2image(ELF, BIN)
|
|
output = e.exception.output
|
|
self.assertIn(b"max 16", output)
|
|
self.assertIn(b"linker script", output)
|
|
|
|
class ESP8266FlashHeaderTests(BaseTestCase):
|
|
def test_2mb(self):
|
|
ELF="esp8266-nonossdkv20-at-v2.elf"
|
|
BIN="esp8266-nonossdkv20-at-v2-0x01000.bin"
|
|
try:
|
|
self.run_elf2image("esp8266", ELF, version=2, extra_args=["--flash_size", "2MB", "--flash_mode", "dio"])
|
|
with open(BIN, "rb") as f:
|
|
header = f.read(4)
|
|
print("header %r" % header)
|
|
self.assertEqualHex(0xea, header[0])
|
|
self.assertEqualHex(0x02, header[2])
|
|
self.assertEqualHex(0x30, header[3])
|
|
finally:
|
|
try_delete(BIN)
|
|
|
|
class ESP32FlashHeaderTests(BaseTestCase):
|
|
def test_16mb(self):
|
|
ELF="esp32-app-template.elf"
|
|
BIN="esp32-app-template.bin"
|
|
try:
|
|
self.run_elf2image("esp32", ELF, extra_args=["--flash_size", "16MB", "--flash_mode", "dio"])
|
|
with open(BIN, "rb") as f:
|
|
header = f.read(4)
|
|
self.assertEqualHex(0xe9, header[0])
|
|
self.assertEqualHex(0x02, header[2])
|
|
self.assertEqualHex(0x40, header[3])
|
|
finally:
|
|
try_delete(BIN)
|
|
|
|
class ELFSHA256Tests(BaseTestCase):
|
|
ELF = "esp32-app-template.elf"
|
|
BIN = "esp32-app-template.bin"
|
|
|
|
def test_binary_patched(self):
|
|
self.run_elf2image("esp32", self.ELF, extra_args=["--elf-sha256-offset", "32"])
|
|
image = esptool.LoadFirmwareImage("esp32", self.BIN)
|
|
rodata_segment = image.segments[0]
|
|
observed_sha256 = rodata_segment.data[0:32]
|
|
|
|
sha256 = hashlib.sha256()
|
|
with open(self.ELF, "rb") as f:
|
|
f.seek(0, os.SEEK_END)
|
|
size = f.tell()
|
|
f.seek(0, 0)
|
|
sha256.update(f.read(size))
|
|
expected_sha256 = sha256.digest()
|
|
|
|
self.assertSequenceEqual(expected_sha256, observed_sha256)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
print("Running image generation tests...")
|
|
unittest.main(buffer=True)
|