Skip to content

Raw binary

Level: 101 → 201 · for anyone who has ever imported a firmware dump

One line: Raw Binary is the loader that accepts every file, at the lowest priority, and asks the reader for everything the other formats carry in their headers — the processor, the byte order, the width, and a base address — because a pointer in the file is only a pointer under one base and a number under every other.

What the loader reads

Nothing. A raw binary is bytes with no header: a ROM dump, a bootloader, a firmware image cut out of a flash chip, a section somebody extracted from a real executable. Every question this chapter's other formats answer in a header goes to a dialog instead:

option what it stands in for
the language, from a list e_machine, cputype, Machine — and with it the byte order and the width
Base Address e_entry, ImageBase, a mapping table: where the bytes go
File Offset and Length which bytes: a slice of the file, defaulting to all of it
Block Name a section name
Overlay whether the block shadows another

Ghidra's BinaryLoader is tiered as an UNTARGETED_LOADER, which means it is offered for every file, after any loader that recognised something, and it never has an opinion about the language.

In Python

Verified output of raw_binary_py.py — regenerated by tools/run_examples.py, never hand-typed.

1. FOUR BYTES, AND WHAT EACH READING MAKES OF THEM
------------------------------------------------------------------------
   u32 little       '<I'    1065353216
   u32 big          '>I'    32831
   i32 little       '<i'    1065353216
   f32 little       '<f'    1.0
   f32 big          '>f'    4.600602988224807e-41
   two u16 little   '<HH'   (0, 16256)
   two u16 big      '>HH'   (0, 32831)
   four bytes       '4B'    (0, 0, 128, 63)

   00 00 80 3f is 1.0 as a little-endian float and 32,831 as a big-
   endian one; 1,065,353,216 or 32,831 as an integer. Every other
   loader here has a header that picks one row. Raw Binary has a
   dropdown, and the reader picks.

2. A POINTER IS A POINTER UNDER ONE BASE
------------------------------------------------------------------------
   34 bytes: a table of four 32-bit little-endian values, then four strings
   10 00 00 08 16 00 00 08 1c 00 00 08 1f 00 00 08
   63 61 66 c3 a9 00 68 65 6c 6c 6f 00 c5 bc 00 6f 6b 00

   base 0x08000000   'café', 'hello', 'ż', 'ok'
   base 0x00000000   out of range, out of range, out of range, out of range
   base 0x08000100   out of range, out of range, out of range, out of range
   base 0x07fffffc   '�', 'o', 'k', out of range

   The same 16 bytes of table read four ways. Under the base the file
   was built for, every value lands on a string. Under base 0 they
   point 128 MB past the end; four bytes lower, each lands four bytes
   into its string. The file does not say which base is right, and
   the loader has no way to tell a pointer from a number.

3. THE DIALOG'S FIELDS ARE A SLICE AND A PLACEMENT
------------------------------------------------------------------------
   File Offset 0   Length 34  Base Address 0x08000000   -> 34 bytes at 0x8000000..0x8000021   10 00 00 08 16 00 00 08 ...
   File Offset 16  Length 20  Base Address 0x08000010   -> 18 bytes at 0x8000010..0x8000021   63 61 66 c3 a9 00 68 65 ...
   File Offset 16  Length 5   Base Address 0x00001000   -> 5 bytes at 0x1000..0x1004   63 61 66 c3 a9

   File Offset and Length cut the file; Base Address says where the
   cut goes; Block Name and Overlay name the result. Nothing is
   parsed, so nothing can be wrong, and nothing can be checked.

4. WHAT EVERY OTHER FORMAT HERE WOULD HAVE SAID
------------------------------------------------------------------------
   question         a header answers with                      Raw Binary answers with
   which end        ELF byte 5, a Mach-O magic, PE by decree   the language's, chosen from a list
   how wide         ELF byte 4, the PE optional magic          the language's
   where it loads   e_entry, ImageBase, a mapping table        Base Address, typed in
   what it is       a magic number                             nothing: every file qualifies

   Ghidra's BinaryLoader is an UNTARGETED_LOADER: it appears in the
   list for any file, last, with no opinion. It is the only loader
   whose acceptance test is empty, and the only one that cannot be
   wrong about a file, because it claims nothing about it.

The same four bytes, eight readings

Section 1 is chapter 1 restated for a loader. 00 00 80 3f is 1.0 as a little-endian float, 32,831 as a big-endian integer, 1,065,353,216 as a little-endian one, and four bytes — and nothing in the file prefers a row. Every other loader here reads a header that picks one: ELF from two bytes, Mach-O from how its magic reads, PE from a document. Raw Binary has a dropdown labelled Language, and the language's name — x86:LE:64:default, ARM:BE:32:v8 — carries the byte order and the width the file does not.

A pointer is a pointer under one base

Section 2 is what the base address is for. A table of four 32-bit values followed by four strings is a table of pointers if, and only if, the loader puts the file at the address the values assume; under any other base the same values point outside the file, or four bytes into each string, and there is no error, because to a loader a pointer and a number are the same thirty-two bits. That is the fact the other formats' headers exist to prevent: ImageBase, e_entry, a dyld mapping — each is a place in the file that says I was built for this address. A raw file says nothing, and choosing the base is the first and largest guess an analyst makes about one. Guess wrong and every cross-reference in the disassembly is wrong, quietly, which is why the option is not a detail.

What Ghidra checks

BinaryLoader returns a load spec for any provider, with no preferred language; its validateOptions checks that the base address parses in the chosen language's address space, that File Offset is inside the file and Length fits after it, and nothing else. Its name is Raw Binary. It is the only loader on the list whose acceptance test is empty, and so the only one that can never be wrong about a file, because it claims nothing about it — and the loader every hex-format file becomes once its records are placed, which is why Intel Hex and Motorola Hex offer the same list of languages.

If you are coming from Python or ABAP

Python. struct.unpack with a format string is the dialog: '<I' is a language choice and an offset into data is the file offset. There is no base address in Python because there is no memory image — the program's read_string(blob, base, pointer) supplies one by subtraction, and that subtraction is the whole of what a raw loader does with a pointer. Keep the base in one variable and pass it everywhere, because a hard-coded - 0x08000000 is a guess written down where it cannot be changed.

ABAP. (Not machine-checked — CI cannot run ABAP.) An xstring with no accompanying structure is a raw binary, and ASSIGN ... CASTING lays a type over it exactly as the language dropdown does — with the platform's byte order, which is the reading nobody chose. A field that holds an offset into the same xstring is a pointer under one base, the start of the string, and becomes a number the moment the string is copied into a longer one with a header in front. Store offsets relative to something the data carries, or store the base beside them.

Try it

  1. head -c 64 /bin/ls > raw.bin and import it into Ghidra as Raw Binary at base 0, then at 0x100000000. Compare the disassembly of the same bytes.
  2. Take a firmware image for any microcontroller and find its vector table: the first few 32-bit words are pointers, and only one base makes them land inside the file.
  3. python3 -c "import struct; print(struct.unpack('<f', bytes.fromhex('0000803f')))" and then with '>I', '<i', '<HH'.
  4. Import a .hex file and note the language list Ghidra offers. Then import the same bytes as Raw Binary and compare.
  5. Pick a file you know and set File Offset and Length to cut its header off. The loader will not object; it cannot.

See also