Motorola Hex¶
Level: 201 · for anyone with a hex editor open
One line: An S-record puts the address width in its type letter — S1 16-bit, S2 24, S3 32 — counts the address and the checksum in its byte-count field where Intel HEX counts only data, and takes a one's complement where Intel HEX takes a two's; so the same five bytes of café make two records whose every field is a different number, and a wrong type letter is invisible to both of a reader's checks.
What the loader reads¶
Motorola's S-record format is the other 1970s hex-text format for shipping a memory image, and Ghidra calls it Motorola Hex. A record is the letter S, a type digit, and then pairs of hex characters:
S1 08 0100 636166C3A9 60
│ │ │ │ └── checksum: the one's complement of the sum of count, address and data
│ │ │ └────────────── data
│ │ └──────────────────── address, 2 bytes for S1, 3 for S2, 4 for S3
│ └──────────────────────── byte count of everything that follows, checksum included
└──────────────────────────── the record mark and the type
| type | what it is | address width |
|---|---|---|
S0 |
header; the data is text by convention, usually the file's name | 2 |
S1, S2, S3 |
data | 2, 3, 4 |
S5, S6 |
a count of the data records so far, in the address field | 2, 3 |
S7, S8, S9 |
termination, with the start address in the address field | 4, 3, 2 |
Every address is in the record, so a loader keeps no state between lines. That is the design difference from Intel Hex, whose 16-bit offset needs an extended-address record before it.
In Python¶
Verified output of motorola_hex_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. THE SAME FIVE BYTES, TWO FRAMES
------------------------------------------------------------------------
Motorola S1080100636166C3A960
Intel :05010000636166C3A964
field Motorola Intel
mark S1 :
count 08 = address + data + checksum 05 = data only
address 0100, width from the type letter 0100, always 16-bit
type in the letter 00, a field of its own
checksum one's complement two's complement
Every field is a different number for the same payload, and the
Intel line is one character longer because its type is a field
where Motorola's is the second character of the mark.
2. THE COUNT INCLUDES THE CHECKSUM, AND THE ADDRESS
------------------------------------------------------------------------
S1080100636166C3A960 S1: address 2 bytes count 8 = 2 + 5 + 1
S209000100636166C3A95F S2: address 3 bytes count 9 = 3 + 5 + 1
S30A00000100636166C3A95E S3: address 4 bytes count 10 = 4 + 5 + 1
Three records for the same bytes at the same address, and the
count grows with the address width the type letter chose. Intel's
count would be 05 for all three, because it counts only data.
3. ONE'S COMPLEMENT: THE RECORD SUMS TO 0xFF, NOT TO 0
------------------------------------------------------------------------
Motorola bytes after the mark 08 01 00 63 61 66 c3 a9 60 sum mod 256 = 0xff
Intel bytes after the mark 05 01 00 00 63 61 66 c3 a9 64 sum mod 256 = 0x00
Intel's checksum is the two's complement of the sum, so the whole
record sums to 0x00; Motorola's is the one's complement, so it
sums to 0xFF. A reader checks for a different constant, and a
record from one format fed to the other's check always fails --
which is the useful property, since the marks can be confused.
4. A WHOLE FILE: HEADER, DATA, COUNT, START
------------------------------------------------------------------------
S00C000068656C6C6F2E733139D4 S0 header hello.s19 ok
S30A08000000636166C3A957 S3 data, 32-bit address 63 61 66 c3 a9 ok
S3090800001000010203D8 S3 data, 32-bit address 00 01 02 03 ok
S5030002FA S5 count, 16-bit ok
S70508000000F2 S7 start, 32-bit ok
S0's data field is text by convention -- the module name -- and
Ghidra skips the line. S5 carries the number of S1/S2/S3 records
as its address; Ghidra treats it as invalid and skips it too. S7,
S8 and S9 end the file with a start address in their address
field, one letter per width, matching S3, S2 and S1.
5. WHAT A CHECKSUM DOES NOT NOTICE
------------------------------------------------------------------------
S1050100414276 ok True
S1050100424176 ok True -- the same two bytes, swapped: the same checksum
Addition does not care about order, in either complement. A byte
count and a sum catch a changed byte and a missing one; they do
not catch two bytes exchanged, in this format or in Intel's.
The same bytes, two frames¶
Section 1 puts café at 0x0100 in both formats and lines the fields up. Nothing agrees: Motorola's count is 8 and Intel's 5, because one counts the address and the checksum and the other only the data; Motorola's type is the second character of the mark and Intel's a field after the address; and the checksums differ in kind, not just in value. Section 3 is the kind: Intel's two's complement makes a record sum to 0x00, Motorola's one's complement makes it sum to 0xFF. A reader checks for one constant, so a record from the other format fails its check every time — useful, since S and : are the only other way to tell them apart, and Ghidra's recognition test accepts either mark for both loaders.
The width is in the letter¶
Section 2 writes the same five bytes at the same address as S1, S2 and S3, and the count goes 8, 9, 10 as the address grows. The kata below is the trap that makes: change the letter and nothing else, and the record still passes both checks — the count is still right, the sum is still 0xFF, because the bytes are the same bytes — while the address has swallowed the first data byte. ELF keeps its width in a byte of the header and PE in a magic; here it is in the type, per record, and the checks cannot see it.
What Ghidra checks¶
MotorolaHexLoader ↗ owns the recognition test both hex loaders use — the first non-blank line must match ^[S:][0-9a-fA-F]+$ — and then offers every language. Its parser handles S1, S2 and S3 with the address widths above, stops at S7, S8 or S9, skips S0, and treats S5 and S6 as not valid and skips those too; it checks each record's sum and reports a bad one by line number. The name it shows is Motorola Hex.
If you are coming from Python or ABAP¶
Python. bytes.fromhex(line[2:]) after the two-character mark, (sum(body) & 0xFF) == 0xFF for the check, and int.from_bytes(body[1:1 + width], 'big') for the address, with width looked up from line[1]. The program's srecord() writes one in three lines, and the whole difference from Intel HEX's writer is ~sum in place of -sum and a count that adds the address and one more.
ABAP. (Not machine-checked — CI cannot run ABAP.) One's and two's complement are the two ways the Negate and Binary Invert pages take a byte apart, and here they are two checksum conventions a byte apart: BIT-NOT of the low byte of a sum against zero minus it. A record whose type carries its width is a record whose fixed-position slicing has to read the type first — the same order of operations as reading a field's unit before its value on the MZ page.
Try it¶
objcopy -O srec any.elf out.s19andcut -c1-2 out.s19 | sort | uniq -c. Which types, and does your toolchain write anS5?- Take one
S3line and add up its bytes after the mark by hand.0xFF. - Change its
S3toS2and check again. Then say what address a loader would now use. - Convert one Intel HEX data record from a
.hexfile to anS1by hand — count, address, data, then the other complement. - Offer an
.s19and a.hexto Ghidra's importer and read the format list. Both loaders appear for both files.
Practice¶
Four records. Here are four S-records; exactly one has a wrong checksum, exactly one has a wrong address, and one of those two passes every check a reader has.
For each: the type, the address, the data bytes, and whether the count and the sum pass. Then the question: which line is wrong without either check being able to say so, and what would a loader do with it?
Answers
Verified output of motorola_hex_kata_py.py — regenerated by tools/run_examples.py, never hand-typed.
S1080100636166C3A960 S1 address 0x000100 data 63 61 66 c3 a9
ok
S2080100636166C3A960 S2 address 0x010063 data 61 66 c3 a9
ok
S1080100636167C3A960 S1 address 0x000100 data 63 61 67 c3 a9
sums to 0x00, not 0xff
S9030000FC S9 address 0x000000 data
ok
Line 1 is café at 0x0100 and correct. Line 2 is the same characters
with the type letter changed to 2: nothing else moved, but a 24-bit
address takes one more byte, so the first data byte 63 has become
the third address byte, the address reads 0x010063, and the count
still says 8 -- which is right, so only the sum can object, and
it cannot either: the bytes are the same bytes. A wrong type letter
is invisible to both checks. Line 3 has one data byte changed, 66
to 67, and the sum moves by one: caught. Line 4 is a correct S9
with a start address of zero.
See also¶
- Intel Hex — the same job with a 16-bit offset and loader state instead of a full address per record
- A record has to say what it is, how long it is, and whether it arrived — the three fields, worked on Intel HEX; the S-record has all three arranged differently
- Negate and Binary Invert — two's and one's complement, as hex-editor operations
- Which base did you mean? — every field is hex, and the file never says so
- SREC (file format) ↗ — the record types and the checksum rule, as this page checked them