Binary data stored in hex-files is in widespread use especially in embedded systems applications.
objutils gives you programmatic access to a wide array of formats and offers a practical API
to work with such data.
Get the latest version from Github
pip install objutilsFor development (editable install):
pip install -e .Or with Poetry:
poetry install- Python >= 3.9
- Read ELF files (including symbols) and extract loadable sections.
- Inspect PE/COFF files and symbols (optional PDB support).
- Merge multiple HEX files into one (
oj-hex-merge). - Split HEX files into separate sections (
oj-hex-split). - Typed access (scalars, arrays, strings) to binary data.
ElfParsernow supportsin_memory=True: use a transient:memory:SQLite database instead of persisting a .prgdb file – ideal for read-only analysis, CI pipelines and unit tests.- New
oj-hex-mergeandoj-hex-splitCLI tools for combining and partitioning HEX files. - New
oj-dwarf-importCLI replacesdwarfer.py/cu_info.pyfor DWARF imports, CU listing, summaries, and attribute traversal. - Faster DWARF imports: batched ORM writes, quiet flag propagation, and safe DWARF expression evaluation keep large ELF files stable.
- Exception handling and typing tightened across DWARF/ELF/PECOFF modules and public APIs to surface real errors without masking them.
ElfParser.close()releases SQLite/mmap handles; examples/tests now close parsers to avoid database locks.- S-Record metadata stays minimal and numeric (no data records mixed in); Mostec/Tek SREC roundtrips now reproduce expected files.
- Fortran-ordered ndarray reads/writes honor legacy column-major expectations; hexdump empty rows keep canonical spacing.
objutils supports a bunch of HEX formats...
- codec / format name
- ihex (Intel HEX)
- shf (S Hexdump (rfc4194))
- srec (Motorola S-Records)
- titxt (Texas Instruments Text)
- codec / format name
- ash (ASCII Space Hex)
- cosmac (RCA Cosmac)
- emon52 (Elektor EMON52)
- etek (Tektronix Extended Hexadecimal)
- fpc (Four Packed Code)
- mostec (MOS Technology)
- rca (RCA)
- sig (Signetics)
- tek (Tektronix Hexadecimal)
codec is the first parameter to dump() / load() functions, e.g.:
img=objutils.load("ihex", "myHexFile.hex") # Load an Intel HEX file...objutils.dump("srec", "mySRecFile.srec", img) # and save it as S-Records.If you are interested, what objutils provides to you out-of-the-box, refer to Scripts documentation.
In any case, you should work through the following tutorial:
First import all classes and functions used in this tutorial.
fromobjutilsimportImage, Section, dump, dumps, load, loadsEverything starts with hello world...
sec0=Section(start_address=0x1000, data="Hello HEX world!")The constructor parameters to Section reflect what they are about: A continuous area of memory with an start address.
data is not necessarily a string, array.array**s, **byte, bytearray will also do, or from an internal point of view: everything that is convertible to bytearray could be used.
Note: start_address and data are positional arguments, so there is no need to use them as keywords (just for the sake of illustration).
Now let's inspect our section.
sec0.hexdump()
0000100048656c6c6f2048455820776f726c6421|HelloHEXworld!|---------------16bytes---------------hexdump() gives us, what in the world of hackers is known as a canonical hexdump.
HEX files usually consist of more than one section, so let's create another one.
sec1=Section(0x2000, range(1, 17))
sec1.hexdump()
000020000102030405060708090a0b0c0d0e0f10|................|---------------16bytes---------------Now, let's glue together our sections.
img0=Image([sec0, sec1])
print(img0)
Section(address=0X00001000, length=16, data=b'Hello HEX world!')
Section(address=0X00002000, length=16, data=b'\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10')Images are obviously a container for sections, and they are always involved if you are interacting with disk based HEX files.
dump("srec", "example0.srec", img0)The resulting file could be inspected from command line.
$ cat example0.srec
S113100048656C6C6F2048455820776F726C64217A
S11320000102030405060708090A0B0C0D0E0F1044And loaded again...
img1=load("srec", "example0.srec")
print(img1)
Section(address=0X00001000, length=16, data=b'Hello HEX world!')
Section(address=0X00002000, length=16, data=b'\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10')Or split it into its sections...
images=img1.split()
fori, imginenumerate(images):
dump("srec", f"part_{i}.srec", img)This leads to the conversion idiom.
img1=load("srec", "example0.srec")
dump("ihex", "example0.hex", img1)Note: the formats above listed as historical are for one good reason historical: they are only 16bit wide, so if you want to convert, say a srec file for a 32bit MCU to them, you're out of luck.
OK, we're starting another session.
sec0=Section(0x100, range(1, 9))
sec1=Section(0x108, range(9, 17))
img0=Image([sec0, sec1])
print(img0)
Section(address=0X00000100, length=16, data=b'\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10')
img0.hexdump()
Section#0000-------------000001000102030405060708090a0b0c0d0e0f10|................|---------------16bytes---------------Two sections with consecutive address ranges concatenated to one, this may or may not what you are expected.
For this reason Image has a join parameter.
sec0=Section(0x100, range(1, 9))
sec1=Section(0x108, range(9, 17))
img0=Image([sec0, sec1], join=False)
print(img0)
Section(address=0X00000100, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')
Section(address=0X00000108, length=8, data=b'\t\n\x0b\x0c\r\x0e\x0f\x10')
img0.hexdump()
Section#0000-------------000001000102030405060708|........ |---------------8bytes---------------Section#0001-------------00000108090a0b0c0d0e0f10|........ |---------------8bytes---------------One feature that sets objutils apart from other libraries of this breed is typified access.
We are starting with a new image.
img0=Image([Section(0x1000, bytes(64))])
print(img0)
Section(address=0X00001000, length=64, data=b'\x00\x00\x00\x00\x00\x00\x00...00\x00\x00\x00\x00\x00\x00\x00')We are now writing a string to our image.
img0=Image([Section(0x1000, bytes(64))])
img0.write(0x1010, [0xff])
img0.hexdump()
Section#0000-------------0000100000000000000000000000000000000000|................|00001010ff000000000000000000000000000000|................|0000102000000000000000000000000000000000|................|0000103000000000000000000000000000000000|................|img0.write_string(0x1000, "Hello HEX world!")
img0.hexdump()
Section#0000-------------0000100048656c6c6f2048455820776f726c6421|HelloHEXworld!|0000101000000000000000000000000000000000|................|*0000103000000000000000000000000000000000|................|---------------64bytes---------------Notice the difference? In our Section example above, the string passed as a data parameter was just a bunch of bytes, but now it is a "real" C-string (there is a opposite function, read_string, that scans for a terminating NULL character).
Use write() and read() functions, if you want to access plain bytes.
But there is also support for numerical types.
img0=Image([Section(0x1000, bytes(64))])
img0.write_numeric(0x1000, 0x10203040, "uint32_be")
img0.write_numeric(0x1004, 0x50607080, "uint32_le")
img0.hexdump()
Section#0000-------------0000100010203040807060500000000000000000|. 0@.p`P........|0000101000000000000000000000000000000000|................|*0000103000000000000000000000000000000000|................|---------------64bytes---------------The folling types are supported:
- uint8
- int8
- uint16
- int16
- uint32
- int32
- uint64
- int64
- float32
- float64
In any case, endianess suffixes _be or _le are required.
For ASAM workflows there are dedicated helpers with explicit byte-order names:
img0=Image([Section(0x1000, bytes(64))])
img0.write_asam_numeric(0x1000, 0x11223344, "ULONG", "MSB_FIRST")
img0.write_asam_numeric(0x1004, 0x11223344, "ULONG", "MSB_FIRST_MSW_LAST")
img0.write_asam_numeric(0x1008, 0x11223344, "ULONG", "MSB_LAST_MSW_FIRST")
print(hex(img0.read_asam_numeric(0x1000, "ULONG", "MSB_FIRST")))
print(hex(img0.read_asam_numeric(0x1004, "ULONG", "MSB_FIRST_MSW_LAST")))
print(hex(img0.read_asam_numeric(0x1008, "ULONG", "MSB_LAST_MSW_FIRST")))
# All reads print 0x11223344 again.Supported ASAM byte orders:
- MSB_FIRST
- MSB_LAST
- MSB_FIRST_MSW_LAST (word-swap)
- MSB_LAST_MSW_FIRST (word-swap)
- LITTLE_ENDIAN (legacy alias for MSB_LAST)
- BIG_ENDIAN (legacy alias for MSB_FIRST)
Supported ASAM numeric datatypes:
- UBYTE, SBYTE
- UWORD, SWORD
- ULONG, SLONG
- A_UINT64, A_INT64
- FLOAT16_IEEE, FLOAT32_IEEE, FLOAT64_IEEE
ASAM string helpers are available, too:
img0.write_asam_string(0x1020, "MOTOR", "ASCII")
img0.write_asam_string(0x1030, "Drehzahl", "UTF8")
print(img0.read_asam_string(0x1020, "ASCII"))
print(img0.read_asam_string(0x1030, "UTF8"))Supported ASAM string datatypes:
- ASCII
- UTF8
- UTF16
- UTF32
Arrays are also supported.
img0=Image([Section(0x1000, bytes(64))])
img0.write_numeric_array(0x1000, [0x1000, 0x2000, 0x3000, 0x4000, 0x5000, 0x6000, 0x7000, 0x8000], "uint16_le")
img0.hexdump()
Section#0000-------------0000100000100020003000400050006000700080|... .0.@.P.`.p..|0000101000000000000000000000000000000000|................|*0000103000000000000000000000000000000000|................|---------------64bytes---------------This concludes our tutorial for now, but there is more stuff to follow...
For full documentation, including installation, tutorials and PDF documents, please see Readthedocs
Please use the GitHub issue tracker to submit bugs or request features
Here is an overview of some of the classic hex-file formats.
- Christoph Schueler - Initial work and project lead.
This project is licensed under the GNU General Public License v2.0
If you contribute code to this project, you are implicitly allowing your code to be distributed under the GNU General Public License v2.0. You are also implicitly verifying that all code is your original work.
