Skip to content

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

  1. objcopy -O srec any.elf out.s19 and cut -c1-2 out.s19 | sort | uniq -c. Which types, and does your toolchain write an S5?
  2. Take one S3 line and add up its bytes after the mark by hand. 0xFF.
  3. Change its S3 to S2 and check again. Then say what address a loader would now use.
  4. Convert one Intel HEX data record from a .hex file to an S1 by hand — count, address, data, then the other complement.
  5. Offer an .s19 and a .hex to 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.

S1080100636166C3A960
S2080100636166C3A960
S1080100636167C3A960
S9030000FC

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