Step 7 · Ubuntu · Independent
Reading binaries
A compiled program is just a file. These tools let you read that file as what it is — bytes, sections, symbols, instructions — rather than as what it does. It is the fastest way to understand what compiling actually produced.
It pairs naturally with step 3, since having
a compiler gives you binaries of your own to take apart. But it does not
require it — every one of these tools works on programs already on your
machine. It is not part of the install_all.sh run.
Run this
$ curl -fsSL https://aikaryashala.com/system_setup/scripts/install_binary.sh | bash
The script finishes by compiling a small program — or falling back to
/bin/ls if you have not installed clang — and showing you its
first bytes, its size breakdown and its ELF header.
What gets installed
Reading a file as bytes
xxdThe classic hex dump.xxd -rturns hex back into binary.hexylA colourised hex viewer that groups bytes by what they are. Easiest to read.hexdumpThe traditional tool;hexdump -Cis the canonical view.odOctal dump. On every Unix ever made, so worth knowing.
Reading structure and symbols
stringsPull the readable text out of a binary.objdumpobjdump -ddisassembles: machine code back to assembly.readelfExplain the structure of a Linux executable, section by section.nmList the symbols — function and variable names — inside an object file.sizeHow much of the binary is code, initialised data, and zeroed data.
Watching a program run
valgrindFind memory leaks, and reads or writes past the end of an allocation.straceShow every system call — every time the program asks the kernel for something.ltraceThe same idea, for calls into shared libraries.
Building more than one file
makeRuns aMakefile, rebuilding only what changed.cmakeninjaFor projects that outgrow a handwritten Makefile.clang-formatclang-tidyFormat code consistently; catch bugs without running the program.
Look at the bytes
Compile anything — or just use a program you already have — and look at it:
xxd — the classic
$ xxd -l 64 hello
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
00000010: 0300 3e00 0100 0000 4010 0000 0000 0000 ..>.....@.......
00000020: 4000 0000 0000 0000 7834 0000 0000 0000 @.......x4......
00000030: 0000 0000 4000 3800 0d00 4000 1e00 1d00 ....@.8...@.....
Three columns: the offset into the file, the bytes in hexadecimal, and those same bytes as text where they happen to be printable.
Those first four bytes — 7f 45 4c 46 — are the magic number
0x7F followed by "ELF". That is how Linux recognises
an executable. Every file format has a marker like this;
file works by reading them.
xxd options you will actually use
xxd -l 64 fOnly the first 64 bytesxxd -s 512 -l 64 f64 bytes starting at offset 512xxd -b fBinary — actual ones and zeroes — instead of hexxxd -c 8 f8 bytes per line instead of 16xxd -g 1 fGroup by single bytes, not pairsxxd -r dump.txt > fReverse: rebuild the binary from a hex dump
hexyl — the readable one
$ hexyl -n 64 hello
Same information, but colour-coded by category: null bytes, ASCII text, printable punctuation and non-ASCII each get their own colour. When you are hunting for structure in an unfamiliar file, this is the one to reach for.
hexdump and od — the traditional pair
# hexdump -C is the canonical side-by-side view
$ hexdump -C -n 64 hello
# od: hex bytes with hex offsets and printable characters
$ od -A x -t x1z -N 64 hello
# od can also show other bases - here, octal
$ od -A o -t o1 -N 32 hello
od is worth knowing because it is guaranteed present on any Unix
system, however minimal — including containers where xxd is not.
Reading structure, not just bytes
# what kind of file is this?
$ file hello
# the readable text hiding inside the binary
$ strings hello | head
# how big are the code and data sections?
$ size hello
# the ELF header, explained field by field
$ readelf -h hello
# every section in the file
$ readelf -S hello
# function and variable names
$ nm hello
# disassemble main back into assembly
$ objdump -d --disassemble=main hello
# assembly interleaved with the C it came from (needs -g)
$ objdump -S hello | less
Compile the same file twice — once with -O0 and once with
-O2 — then run
objdump -d --disassemble=main on each and put the output side
by side. Seeing what the optimiser did to your loop teaches more about how
C works than any amount of reading.
Watching a program run
# find memory leaks and invalid accesses, without recompiling
$ valgrind --leak-check=full ./hello
# every system call the program makes
$ strace ./hello
# just the file-related ones
$ strace -e trace=openat,read,write ./hello
# calls into shared libraries instead
$ ltrace ./hello
strace is the tool that answers “why can't it find that file?”
— you see the exact path it tried and the error it got back.
A Makefile, once one file is not enough
CC = clang
CFLAGS = -g -O0 -Wall -Wextra -std=c17
TARGET = hello
$(TARGET): hello.c
$(CC) $(CFLAGS) -o $(TARGET) hello.c
debug: CFLAGS += -fsanitize=address
debug: clean $(TARGET)
clean:
rm -f $(TARGET)
.PHONY: debug clean
The indented lines under each rule must begin with a tab, not spaces. This is the single most common reason a Makefile fails with “missing separator”.
Then make to build, make debug for the sanitizer build, make clean to start over.
Check it worked
$ xxd -l 16 /bin/ls
$ readelf -h /bin/ls | head -6
$ objdump --version | head -1
$ valgrind --version