Embedded Systems · Embedded Tooling
Intel HEX, Motorola SREC, and What a Programmer Actually Consumes
The record structure of the two textual image formats, how each encodes load addresses and detects corruption, and why a raw binary loses the information a programmer needs.
The output of a link is an ELF file, and almost no production programmer accepts one. What gets loaded into flash is a text file of records, each carrying a handful of bytes and the address they belong to. The conversion is not a formality: it decides which bytes are written, at what addresses, and whether a corrupted transfer is detected before the target runs.
What a raw binary throws away
objcopy -O binary produces the concatenation of every loadable section, with gaps filled by padding. The result is compact and completely ambiguous. The programmer must be told the start address separately, and there is no way to represent a gap: if the image has a section at 0x08000000 and another at 0x08020000 with nothing between, the binary either pads the gap — wasting transfer time and, worse, writing erase blocks that were meant to stay untouched — or omits it and shifts the second section down to the wrong address.
Textual formats solve this by making the address part of every record. A record names a start address, a length, and a payload, so a gap is expressed by simply not emitting records for it. This also makes partial programming possible: a bootloader region can be left alone because the file contains no records covering it.
Both formats are ASCII, so each byte of payload becomes two characters. A 32-byte record in Intel HEX occupies 75 characters including the colon, the count, the address, the type, and the checksum; the same payload in an S3 record occupies 78. That is roughly 2.4 bytes of file per byte of payload, and the ratio worsens as records get shorter. What the encoding buys is resilience: a flipped bit changes the characters, and the per-record checksum detects it. Neither format is encrypted or authenticated, so neither detects a deliberately crafted image.
Intel HEX record layout
Every line is :, a byte count, a 16-bit address, a record type, the payload, and a checksum, all as uppercase hexadecimal pairs. The count field covers the payload only. The checksum is the two’s complement of the sum of all preceding bytes, so the sum of the entire record including the checksum is zero modulo 256.
:10010000214601360121470136007EFE09D2190140
^^ count 0x10 (16 bytes)
^^^^ address 0x0100
^^ type 00 (data)
<32 hex chars of payload, 16 bytes>
^^ checksum
The count field covers the payload only, so it is 0x10 for the sixteen bytes shown; the address is the offset within the current 64 KiB window; and the checksum makes the byte sum of the whole record zero modulo 256.
Six record types exist. Type 00 is data. Type 01 is end of file, with a zero count and address. Type 02 is an extended segment address: the payload is a 16-bit segment base that is multiplied by 16 and added to every subsequent data address, which extends reach to 1 MiB. Type 04 is an extended linear address: the payload is the upper 16 bits of a 32-bit address, so subsequent data records address within a 64 KiB window and the full 4 GiB space is reachable. Type 03 and 05 are the corresponding start-address records for the 80x86 segment and linear models, carrying an initial instruction pointer that a bootloader may or may not use.
The subtlety is that the base-address records are state, not data. A parser that ignores type 02 and 04 and treats every data record address as absolute produces a plausible-looking image at the wrong addresses, and the mistake is silent because the records remain individually well-formed. A parser must apply the base to data records and must not apply it to the base records themselves.
Type 04 records may appear anywhere, not only at the start, and a large image will contain several as the address crosses each 64 KiB boundary. Two type 04 records with the same value are redundant but legal; a type 04 record with a lower value than the previous one means the file is not monotonically ordered, which is legal and which a programmer must handle by seeking rather than streaming.
S-record layout
Motorola’s format has the same skeleton with a different framing. Each line begins with S, then a record type digit, then a byte count covering the address, the data, and the checksum, then the address, then the data, then a one-byte checksum that is the ones’ complement of the sum of the count, address, and data bytes.
S10B01002146013601214701EB
^ type S1: 16-bit address, data record
^^ count 0x0B = 11 bytes (2 address + 8 data + 1 checksum)
^^^^ address 0x0100
data, 8 bytes
^^ checksum: ones' complement of the sum
The count covers the address, the data, and the checksum, but not the type digit, which is why the shortest possible record still has a count of three. Adding all bytes from the count field to the end of the line yields 0xFF for a valid record.
The types divide by address width and purpose. S0 carries a header, conventionally the file name or a module identifier, with a zero address. S1, S2, and S3 are data records with 16-, 24-, and 32-bit addresses respectively. S5 and S6 carry a count of preceding data records, which is a cross-check on truncation that Intel HEX has no equivalent for. S7, S8, and S9 terminate the file and carry a start address matching the width of the data records used. A file must not mix address widths in a way that the terminator contradicts, and a parser that assumes one width for the whole file is correct in practice because the format expects consistency.
The 24-bit S2 records exist because the format predates 32-bit microcontrollers and was extended twice. A parser that handles only S1 and S3 covers essentially every current target and silently drops data if it meets an S2.
Checksums, and what they do not cover
Both checksum schemes detect single-byte corruption and most multi-byte corruption within a record. They do not detect a missing record, a reordered record, or a truncated file that happens to end on a record boundary, because there is no global digest. S-record’s S5 count is a partial defence against truncation; Intel HEX relies on the 01 terminator, and a file that lacks one is truncated by definition even though many programming tools will load it without complaint.
Neither format carries a target identifier. Programming the wrong file into the wrong board is not detectable from the file’s contents. The common mitigation is a build-time check that a known symbol’s value or a checksum embedded in the image matches the expected part, performed by the bootloader before it jumps — a check that the flash programming sequence has to leave room for, since the header must be written in the same operation as the image it describes. That check lives in the image and costs a few dozen bytes.
Nothing in either format protects the image in transit against an attacker who can recompute a checksum, and nothing prevents a partially written image from booting. Verification after programming is the probe’s job: reading back the region and comparing is the only confirmation that the bytes on the device are the bytes in the file, and it is why flash programming through a probe takes roughly twice the time of the write alone when verification is enabled.
Conversion and the address question
The usual conversion is objcopy to binary followed by a converter, or objcopy with a target-specific output format, or the linker emitting the text format directly. The important question in every case is which sections are included. By default a converter emits loadable segments, which includes .data at its load address in flash and excludes .bss because it occupies no file space. That is correct for a programmer that writes flash and leaves RAM uninitialised, since the startup code performs both the copy and the zero fill.
It is wrong for a programmer that also initialises RAM, or for an image whose .data is meant to be loaded directly to RAM by a bootloader. In that case the section’s load address must be used, which is exactly the LMA discussed in linker script section placement. A converter that reads the virtual address instead writes initialised data into flash at a RAM address, and the image either fails to fit the region or boots with garbage globals.
The final consideration is size accounting. The text format’s overhead is proportional to record count, and record count is driven by the converter’s maximum line length rather than by the payload. A converter configured for 16-byte records produces roughly twice the file of one configured for 32-byte records, and some programmers and bootloaders impose a maximum record length. That limit is a property of the receiver, not the format, and exceeding it yields a transfer that fails partway with a checksum error that looks like corruption.
References
- Intel Hexadecimal Object File Format Specification, Revision A — Intel Corporation, 1988
- Motorola S-Record Format — Motorola / Freescale, S-record specification
- GNU Binutils: objcopy — GNU Binutils