Hexdump clone: hex + ASCII view of any file, in C.
A small, self-contained demo written in pure C — no external libraries, just the standard library and POSIX. Part of the Corg-Labs collection of single-file C programs.
- Input is read 16 bytes at a time
- Each row prints the byte offset, then 16 hex values
- A gap splits the row into two 8-byte halves for readability
- Printable bytes are shown in an ASCII gutter, others as '.'
This tutorial walks through every stage of hexview.c, from opening the file
to printing the final ASCII gutter, explaining the data layout and rendering
choices along the way.
The program accepts either a filename argument or piped stdin.
A single conditional selects the right FILE * before any reading begins:
FILE *f = (argc > 1) ? fopen(argv[1], "rb") : stdin;
if (!f) { perror("open"); return 1; }Opening in "rb" (read-binary) mode is important: on Windows it prevents
newline translation, and on all platforms it signals that every byte — including
nulls — should be delivered unmodified.
The entire rendering model is built around one small buffer:
unsigned char buf[16];
long off = 0;
size_t r;buf holds one row's worth of data. off tracks the running byte offset
(printed as the address at the start of each row). r is the number of bytes
actually read by the current fread call — it will be less than 16 for the
final row of most files.
All work happens inside a single while loop that terminates when fread
returns zero (EOF or error):
while ((r = fread(buf, 1, 16, f)) > 0) {
/* print offset, hex columns, ASCII gutter */
off += r;
}Each iteration processes exactly one output row. off is incremented by r
(not by the fixed value 16) so that the address shown on the last row is
always accurate, even when the file length is not a multiple of 16.
The first field on every row is the file offset of buf[0], zero-padded to
eight hex digits:
printf("%08lx ", off);This matches the classic xxd/hexdump layout: the address is always exactly
10 characters wide (8 digits + 2 spaces), so columns stay aligned regardless
of file size (up to 4 GiB with a 32-bit long; larger on 64-bit platforms).
The hex section loops over all 16 column positions, printing either a two-digit hex value or three spaces for positions past the end of the buffer:
for (size_t i = 0; i < 16; i++) {
if (i < r) printf("%02x ", buf[i]); else printf(" ");
if (i == 7) putchar(' ');
}The if (i == 7) putchar(' ') inserts an extra blank after the eighth column.
This splits the 16 hex values into two visual groups of 8, making it easier to
locate a byte by eye when scanning long output.
After the hex columns, a |…|-delimited gutter shows the printable
representation of every byte in the row:
printf(" |");
for (size_t i = 0; i < r; i++)
putchar((buf[i] >= 32 && buf[i] < 127) ? buf[i] : '.');
printf("|\n");The range [32, 127) is the printable subset of ASCII: space through ~.
Bytes outside that range — control characters, high bytes from UTF-8
sequences, binary data — are replaced with . so the gutter never emits
unprintable characters that would corrupt a terminal.
After the loop, the file is closed only when it was opened from a path
argument (closing stdin is unnecessary and potentially harmful):
if (argc > 1) fclose(f);
return 0;Returning 0 signals success; the earlier return 1 after a failed fopen
is the only non-zero exit path, keeping the error-handling contract simple and
scriptable.
gcc hexview.c -o hexview
./hexview file # or: cat file | ./hexview
Pass a file as an argument, or pipe data on stdin.