A parser for the EBL and GBL (v3 and v4) firmware image formats used by the Silicon Labs Gecko bootloader.
Images are parsed into structured tags and round-trip byte-for-byte, with anything following the end tag handed back separately rather than folded into the image.
pip install pygblThe base install is pure Python and covers parsing, serializing, building images from ELFs, and LZMA. Signing, encryption and LZ4 need optional dependencies:
pip install pygbl[crypto] # signing and encryptionpip install pygbl[lz4] # LZ4 compressionpip install pygbl[all]Calling those features without their dependency raises MissingDependencyError.
Parse an image and inspect its tags:
importpathlibfrompygblimportGBL3ApplicationInfo, parse_firmware_imagedata=pathlib.Path("ncp-uart-hw.gbl").read_bytes()
image=parse_firmware_image(data)
fortaginimage.tags:
print(tag)
print(image.get_first_tag(GBL3ApplicationInfo).version)
print(image.get_metadata()) # opaque bytes, the schema is vendor definedserialize pads up to a word boundary, as commander does. parse_firmware_image
discards anything after the end tag; deserialize hands it back:
image, trailing=GBL3Image.deserialize(data)
assertimage.serialize(block_size=1) +trailing==dataModify an image. Tags are frozen dataclasses, so dataclasses.replace works, and
regenerate_crc fixes up the end tag afterwards:
importdataclassesfrompygblimportGBL3End, GBL3Metadatamodified=dataclasses.replace(
image,
tags=[tfortinimage.tagsifnotisinstance(t, GBL3End)]
+ [GBL3Metadata(metadata=b'{"fw_type": "zigbee_ncp"}')],
).regenerate_crc()Compress, encrypt and sign, in the order the bootloader expects:
fromcryptography.hazmat.primitives.serializationimportload_pem_private_keyfrompygblimportGBL3Compressionprivate_key=load_pem_private_key(
pathlib.Path("vendor_sign.key").read_bytes(), password=None
)
key=bytes.fromhex("7F8FE53979B31BC556FCB131AFF42414")
sealed=image.compress(GBL3Compression.LZMA).encrypt(key).sign(private_key)
pathlib.Path("signed.gbl").write_bytes(sealed.serialize())The bootloader decompresses into fixed buffers, so compress uses the only LZMA
parameters it can accept. Overriding them past what it can allocate raises ValueError.
And unwrap it again:
assertsealed.verify_signature(private_key.public_key())
assertsealed.decrypt(key).decompress().serialize() ==image.serialize()Images can be built straight from a linked ELF, without commander. Program data comes
from the loadable segments (keyed by physical address, since initialized data is
stored in flash but linked at its RAM address) and the application info tag is read
from the SDK's application_properties_t struct:
frompygblimportbuild_application_gbl3, build_bootloader_gbl3withopen("zigbee_ncp.out", "rb") asf:
image=build_application_gbl3(f, metadata=b'{"fw_type": "zigbee_ncp"}')
withopen("bootloader.out", "rb") asf:
bootloader=build_bootloader_gbl3(f)Series 3 parts use GBLv4, a different format that nests tags inside a signed manifest and can bundle several updates in one file.
frompygblimportGBL4Image, GBL4MemorySectionInfo, GBL4UpdateMemorySectiondata=pathlib.Path("light-simg301.gbl4").read_bytes()
image, trailing=GBL4Image.deserialize(data)
assertimage.serialize() +trailing==data# `get_tags` searches the whole tree, at any depthforupdateinimage.get_tags(GBL4UpdateMemorySection):
print(f"{update.target_address:#010x}{update.plain_image_size} bytes")
forinfoinimage.get_tags(GBL4MemorySectionInfo):
print(info.compression_scheme, info.encryption_scheme, info.nonce.hex())Reading and writing are supported; building a v4 image from an ELF is not, the ELF helpers are GBLv3 only.
EBL images, used by older EM3xx parts, work the same way:
frompygblimportEBLEraseProgram, parse_firmware_imageimage=parse_firmware_image(pathlib.Path("ncp-uart-sw.ebl").read_bytes())
fortaginimage.get_tags(EBLEraseProgram):
print(f"{tag.address:#010x}{len(tag.data)} bytes")A GBL can contain a bootloader, an application, or both. Combined images can be split apart and recombined, which is useful because some bootloaders cannot flash a combined image in one pass:
bootloader, application=combined.split_bootloader_app()
recombined=application.combine_bootloader_app(bootloader)