Build a Hex Dump Tool in C
Write a C program that reads any file and displays its raw bytes in hex + ASCII format.
Introduction
30 Second Summary
Every file on your computer is just a sequence of raw bytes underneath. Most tools hide those bytes from you, but sometimes you need to see exactly what's inside a file.
In this project, you will build a command-line hex dump tool in C that reads any file and displays its raw bytes as hexadecimal values alongside their printable ASCII characters.
What You'll Build
A command-line tool similar to the Unix xxd utility that takes a filename as input and outputs formatted hexadecimal and ASCII representations of the file's contents.
By the end of this project, you'll have:
- A C source file (hexdump.c) implementing the full hex dump logic.
- A compiled binary you can run from the terminal on any file in your system.
- Hands-on experience with the C compilation workflow, file I/O, pointers, and formatted output.
- Secret Mission: Add a command-line flag to limit output to a specific number of bytes.
Are there any prerequisites?
You only need a basic understanding of what a programming language is. If you know what variables, loops, and functions are at a conceptual level, you're ready to go.
Set Up Your C Development Environment
Before you can write any C code, you need a compiler. A compiler translates human-readable C source files into machine code your computer can execute directly.
In this step, you'll install a C compiler, create a project folder, and verify everything works by compiling a tiny test program.
In this step, get ready to:
- Install a C compiler (Clang on macOS, GCC on Linux).
- Create your project directory.
- Compile and run a test program to confirm everything works.
Install a C compiler
C compilers come pre-installed on some systems, but not all. Let's check if you already have one and install it if not.
- Open a terminal. Press Cmd+Space (macOS) or Ctrl+Alt+T (Linux) to open your search bar or terminal.
- On macOS, type Terminal and press Enter to open it.
macOS
- Check if you already have the Apple command line tools by running:
clang --version
✔️ I see a version number
Your C compiler is already installed. You're good to move on to the next substep.
ⓧ Command not found
- Install Apple's Clang compiler (part of Xcode Command Line Tools) by running:
xcode-select --install
What happens next?
A popup dialog appears on your desktop asking you to confirm the installation. Click Install and wait for it to finish. This can take a few minutes depending on your internet connection.
- Once installation completes, verify it worked by running:
clang --version
You should see version information printed in your terminal.
Still seeing command not found?
Close your terminal completely and open a new one. The install updates your system path, and the old terminal session may not have picked it up yet.
help me fix my Clang installation on macOS
Linux
- Check if you already have GCC installed by running:
gcc --version
✔️ I see a version number
Your C compiler is already installed. You're good to move on to the next substep.
ⓧ Command not found
- Install GCC and essential build libraries by running:
sudo apt update && sudo apt install build-essential
What does this install?
The build-essential package installs GCC, g++, make, and the core C libraries needed for compilation.
You'll be prompted for your password. Type it in (nothing will appear on screen) and press Enter.
- Once installation completes, verify it worked by running:
gcc --version
You should see GCC version information printed in your terminal.
Still seeing command not found?
Make sure the apt install command completed without errors. Look for any permission-denied messages. If you see one, confirm you included sudo at the start of the command.
help me fix my GCC installation on Linux
Create your project directory and verify compilation works
Now that your compiler is ready, create a dedicated folder for this project and test that compilation works end to end.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir hexdump-project
cd hexdump-project
- Confirm the folder was created by running:
pwd
You should see a path ending in hexdump-project.
- Create a test file called hello.c by opening it in nano:
nano hello.c
- Type or paste the following code into the editor:
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
What does this code do?
This is the simplest possible C program. #include <stdio.h> brings in the standard input/output library so you can use printf.
The main() function is where every C program starts. return 0 signals to the operating system that the program finished successfully.
- Save the file by pressing Ctrl+O, then Enter to confirm the filename.
- Exit nano by pressing Ctrl+X.
Prefer a different editor?
You can use any text editor you're comfortable with. If you have VS Code installed, run code hello.c instead. The important thing is that the file is saved as hello.c in your hexdump-project folder.
- Compile your test program by running:
gcc -Wall -o hello hello.c
- Run the compiled binary by running:
./hello
You should see Hello, World! printed in your terminal.
Seeing an error instead?
If you see a "No such file or directory" error, make sure you're inside the hexdump-project folder. Run pwd to check your current location.
If the compiler reports a syntax error, double-check that your hello.c matches the code above exactly. Pay attention to the semicolons at the end of each statement.
help me fix my first C compilation error
✔️ Awesome, I've got everything!
Great. Double check you've saved your file and that ./hello prints Hello, World! in your terminal.
ⓧ I'd like to double check the full code
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
Understand the compilation flags
The command you just ran did three things in sequence. Each flag controls a different part of the compilation process.
What do the flags mean?
- -Wall enables all common warnings. This catches bugs like unused variables, missing return statements, and format string mismatches before they become runtime problems.
- -o hello names the output binary. Without this flag, the compiler would create a file called a.out by default.
- hello.c is the source file. The compiler reads this .c file and produces an executable binary you can run directly.
Always use -Wall
Professional C developers compile with -Wall (and often even stricter flags) by default. The warnings it produces save hours of debugging by catching mistakes at compile time instead of runtime.
Your development environment is ready. Next up, you'll create the actual hex dump tool and teach it how to read files in binary mode.
Read a File in Binary Mode
Your compiler is ready to go. Now it is time to start building the actual hex dump tool.
The first job of any hex dump program is reading raw bytes from a file. Unlike reading text, you need to open the file in binary mode so nothing gets modified on the way in. In this step, you will write the skeleton of your program: accept a filename from the user, open that file safely, and read its contents 16 bytes at a time.
In this step, get ready to:
- Accept a filename from the command line using argc and argv.
- Open a file in binary mode with error handling.
- Read the file 16 bytes at a time using a loop.
Set up main() with command-line arguments
Every C program starts with a main() function. When you want your program to accept input from the terminal (like a filename), you use a special version of main that receives argc (the count of arguments) and argv (the actual argument values).
- Create a new file called hexdump.c in your hexdump-project folder using the same method you used for hello.c in the previous step.
- Add the following code to hexdump.c:
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
int main(int argc, char *argv[]) {
/* Check that the user provided a filename */
if (argc < 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
return 1;
}
return 0;
}
What does this code do?
The three #include lines pull in parts of the C standard library. <stdio.h> gives you file and print functions. <stdlib.h> and <ctype.h> will be needed in later steps.
The if (argc < 2) check ensures the user typed a filename after the program name. If they did not, fprintf prints a helpful usage message to stderr and the program exits with code 1 (meaning failure).
- Save hexdump.c.
- Compile and run the program without providing a filename by running these commands:
gcc -Wall -o hexdump hexdump.c
./hexdump
You should see the usage message printed to your terminal, something like Usage: ./hexdump <filename>.
Seeing a compilation error?
- Check that every opening brace { has a matching closing brace }.
- Make sure #include lines are at the very top of the file with no typos in the angle-bracket names.
- Confirm you saved the file before compiling.
Ask for help: Why is my hexdump.c failing to compile with gcc?
Open the file in binary mode and handle errors
Now that your program accepts a filename, you need to actually open that file. The "rb" mode tells the system to open for reading in binary mode. This ensures raw bytes come through unmodified.
- In hexdump.c, find the line that says return 0; at the bottom of your main function.
- Replace that return 0; with the following code:
FILE *file = fopen(argv[1], "rb");
if (file == NULL) {
perror("Error opening file");
return 1;
}
fclose(file);
return 0;
What does this code do?
The fopen(argv[1], "rb") call opens the file the user specified. The "rb" flag means "read, binary." This is important for a hex dump because you want every byte exactly as it is on disk.
If the file does not exist or you lack permission, fopen returns NULL. The perror() function prints a descriptive error message (like "No such file or directory") to help the user understand what went wrong. Finally, fclose(file) releases the file when you are done.
- Save hexdump.c.
You will not see a change yet. The next chunk adds the read loop that actually processes the file's bytes.
Read 16 bytes at a time using fread
Hex dump tools display data in rows of 16 bytes. You will use fread to grab exactly 16 bytes at a time from the file, storing them in a buffer until you are ready to format them as hex output.
- In hexdump.c, find the line fclose(file); near the bottom.
- Add the following code directly above fclose(file);:
unsigned char buffer[16];
unsigned int offset = 0;
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, 16, file)) > 0) {
/* output coming soon */
}
What does this code do?
The unsigned char buffer[16] creates an array that holds 16 bytes. Using unsigned char ensures byte values range from 0 to 255 (no negative numbers).
The fread(buffer, 1, 16, file) call reads up to 16 bytes from the file into your buffer. It returns the number of bytes actually read. On the last chunk of a file, this might be less than 16. When it returns 0, you have reached the end of the file and the loop stops.
The offset variable will track your position in the file. You will use it in the next step to print byte offsets alongside the hex values.
- Save hexdump.c.
- Compile and run the program on its own source file by running:
gcc -Wall -o hexdump hexdump.c
./hexdump hexdump.c
The program should return to your terminal prompt with no output and no errors. This means it successfully opened hexdump.c, read through it byte by byte, and closed it cleanly.
- Test error handling by running the program on a file that does not exist:
./hexdump fakefile.txt
You should see an error message like Error opening file: No such file or directory.
Seeing a warning about unused variables?
The -Wall flag may warn about offset being unused. This is safe to ignore for now. You will use it in the next step when you add hex output formatting.
If you see a different error, check that your while loop has matching parentheses and that the comment inside uses /* */ style.
Ask for help: My hexdump program is not compiling after adding the fread loop.
✔️ Awesome, I've got everything!
Great. Double check you have saved hexdump.c and that the program compiles cleanly.
ⓧ I'd like to double check the full code
Here is the complete hexdump.c file at this point:
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
return 1;
}
FILE *file = fopen(argv[1], "rb");
if (file == NULL) {
perror("Error opening file");
return 1;
}
unsigned char buffer[16];
unsigned int offset = 0;
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, 16, file)) > 0) {
/* output coming soon */
}
fclose(file);
return 0;
}
Your program can now read any file on your system byte by byte. Next up, you will format those raw bytes into a proper hex dump with offsets and aligned columns.
Display Hex Output with Byte Offsets
Your hex dump tool can open and read files, but it produces no visible output yet. The whole point of a hex dump is to see the raw bytes.
In this step, you'll build the formatted output that displays each byte as a hexadecimal value. Each row will start with a byte offset so you always know your position in the file.
In this step, get ready to:
- Create a helper function that formats one row of hex output.
- Print byte offsets and hex values with visual grouping every 8 bytes.
- Compile and verify your output against the built-in xxd utility.
Track and print the byte offset
Every hex dump shows byte offsets on the left side of each row. The offset tells you exactly how far into the file each row starts, counted in bytes from zero.
You'll wrap all the row-formatting logic into a dedicated print_hex_row() helper function. This keeps main() clean and focused on reading the file.
- In hexdump.c, add the following function above your main() function (below the #include lines):
void print_hex_row(unsigned int offset, unsigned char *buffer, size_t bytes_read) {
// Print the byte offset as an 8-digit hex number
printf("%08x ", offset);
// Always loop 16 positions to keep columns aligned
for (size_t i = 0; i < 16; i++) {
if (i < bytes_read) {
// Print the actual byte value in hex
printf("%02x ", buffer[i]);
} else {
// Pad with spaces for short rows
printf(" ");
}
if (i == 7) {
// Extra space after byte 8 for visual grouping
printf(" ");
}
}
}
What does this code do?
This function formats a single row of hex dump output. It takes three parameters: the current byte offset, a buffer holding the bytes, and how many bytes were actually read.
- %08x prints the offset as an 8-character hexadecimal number, padded with leading zeros. This tells you where each row starts in the file.
- The loop always iterates 16 times (not just bytes_read times). If a row has fewer than 16 bytes, the else branch prints three spaces to maintain column alignment.
- %02x prints each byte as a two-digit hex value (e.g., 0a for a newline character).
- The if (i == 7) check adds an extra space between the 8th and 9th byte. This visual grouping makes it easier to count bytes at a glance.
- Save hexdump.c.
- You won't see a change yet. The function exists but nothing calls it. The next substep hooks it up.
Seeing a compiler error about implicit declaration?
- Make sure print_hex_row() is placed above main() in your file. C reads top to bottom, so a function must be defined before it is used.
- Check that the function signature matches exactly: void print_hex_row(unsigned int offset, unsigned char *buffer, size_t bytes_read).
Still stuck? Help me fix a compiler error in my print_hex_row function
Print each byte as a two-digit hex value
The helper function is ready. Now you need to call it from your read loop and advance the offset after each row.
- In hexdump.c, find the while loop inside main(). Locate this line:
/* output coming soon */
- Replace that comment with these two lines:
print_hex_row(offset, buffer, bytes_read);
offset += bytes_read;
What's happening here?
- print_hex_row(offset, buffer, bytes_read) passes the current file position, the bytes just read, and the count to your formatting function.
- offset += bytes_read advances the offset by however many bytes were read. Each row represents 16 bytes (except the last row, which may be shorter).
- Save hexdump.c.
Seeing garbage output or a crash?
- Double-check that you removed the entire /* output coming soon */ comment. Leftover characters can cause syntax errors.
- Make sure the two new lines are inside the while loop's curly braces, not after them.
Need a hand? Help me debug my while loop in hexdump.c
Compile, run, and verify against xxd
Time to see your hex dump in action. You'll compile the updated code and run it against your own source file.
- Compile your updated code by running:
gcc -Wall -o hexdump hexdump.c
- Run your hex dump tool on its own source file:
./hexdump hexdump.c
You should see rows of output with an 8-digit hex offset on the left, followed by hex byte values. There's an extra gap between the 8th and 9th byte on each row.
- Compare your output against the built-in xxd utility by running:
xxd hexdump.c | head
Your offset and hex columns should look similar to xxd's output. The main difference is that xxd also shows an ASCII column on the right. You'll add that in the next step.
Seeing no output at all?
- Check that you removed the /* output coming soon */ comment and replaced it with the two function-call lines.
- Make sure you saved the file before compiling. The compiler reads from disk, not from your editor's unsaved buffer.
- Verify that print_hex_row() is defined above main() and not inside it.
Still not seeing output? Help me figure out why my hexdump produces no output
✔️ Awesome, I've got everything!
Great. Double check you've saved hexdump.c and that your output shows hex values with offsets.
ⓧ I'd like to double check the full code
Here's what your complete hexdump.c should look like at this point:
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
void print_hex_row(unsigned int offset, unsigned char *buffer, size_t bytes_read) {
printf("%08x ", offset);
for (size_t i = 0; i < 16; i++) {
if (i < bytes_read) {
printf("%02x ", buffer[i]);
} else {
printf(" ");
}
if (i == 7) {
printf(" ");
}
}
}
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
return 1;
}
FILE *file = fopen(argv[1], "rb");
if (file == NULL) {
perror("Error opening file");
return 1;
}
unsigned char buffer[16];
unsigned int offset = 0;
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, 16, file)) > 0) {
print_hex_row(offset, buffer, bytes_read);
offset += bytes_read;
}
fclose(file);
return 0;
}
Your tool now reveals the raw hex values hiding inside any file. Next up, you'll add the ASCII column so you can also see the human-readable characters alongside the hex.
Add the ASCII Column
Your hex dump tool already prints byte offsets and hex values for every row. That is useful, but professional hex dump tools like xxd also show the ASCII representation of each byte alongside the hex.
This ASCII column makes it easy to spot readable text inside binary files. In this step, you will add that column and complete your hex dump tool.
In this step, get ready to:
- Print the ASCII representation of each byte after the hex values.
- Handle partial rows so the last line of output stays aligned.
- Compile, test on multiple files, and compare output with xxd.
Print the ASCII representation after hex values
Each row of hex dump output ends with an ASCII column enclosed in pipe characters. Printable characters appear as themselves. Non-printable bytes (control characters, null bytes, anything above 127) appear as a dot.
- In hexdump.c, find the closing } of the print_hex_row() function.
- Add the following code just before that closing brace, after the for loop that prints hex values:
printf(" |");
for (size_t i = 0; i < bytes_read; i++) {
if (isprint(buffer[i])) {
printf("%c", buffer[i]);
} else {
printf(".");
}
}
printf("|\n");
What does this code do?
This block adds the ASCII column to each row of output.
- printf(" |") prints a separator between the hex values and the ASCII column.
- The for loop iterates through bytes_read (not 16). This means partial rows only print characters for actual bytes, not for the padding spaces.
- isprint() from <ctype.h> returns true for printable ASCII characters (letters, digits, punctuation). Everything else gets a dot.
- printf("|\n") closes the ASCII column and moves to the next line.
Why does this handle partial rows correctly?
The last row of a file often has fewer than 16 bytes. The hex padding you added in the previous step (printing spaces for missing bytes) keeps the hex column aligned. The ASCII loop uses bytes_read as its limit, so it only prints characters for actual data. No extra code needed for the final row.
- Save hexdump.c.
Seeing garbled output or misaligned columns?
- Make sure you added the ASCII code after the hex-printing for loop but before the closing brace of print_hex_row().
- Check that the ASCII loop uses bytes_read as its upper bound, not 16.
- Verify the \n is inside the final printf("|\n") call. Without it, all rows print on one line.
Ask for help debugging your ASCII column.
Compile and run the final tests
- Compile your updated program by running:
gcc -Wall -o hexdump hexdump.c
- Test the tool on its own source code by running:
./hexdump hexdump.c
You should see three columns: hex offsets on the left, hex byte values in the middle, and an ASCII column on the right enclosed in pipe characters. Readable C code appears as normal text in the ASCII column.
- Test on a binary file by running:
./hexdump hexdump
The compiled binary contains non-printable bytes. You should see far more dots in the ASCII column compared to the source file test.
- Test error handling by running the tool on a file that does not exist:
./hexdump nonexistent.txt
You should see the error message from perror() instead of a crash.
- Compare your output against the built-in xxd tool by running:
xxd hexdump.c | head -20
Your output follows the same general format: offset on the left, hex in the middle, ASCII on the right. The spacing differs slightly (xxd groups bytes in pairs), but the structure is the same.
Getting a compiler error?
- Check for missing semicolons at the end of each printf call.
- Ensure #include <ctype.h> is at the top of your file. Without it, the compiler will not recognize isprint().
- Make sure the braces in print_hex_row() are balanced. You should have one opening brace for the function and one closing brace at the end.
Ask for help with your compiler error.
✔️ Awesome, I've got everything!
Great. Double check you have saved hexdump.c and that your compiled binary produces output with all three columns.
ⓧ I'd like to double check the full code
Here is the complete hexdump.c file. Compare yours against this:
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
void print_hex_row(unsigned int offset, unsigned char *buffer, size_t bytes_read) {
printf("%08x ", offset);
for (size_t i = 0; i < 16; i++) {
if (i < bytes_read) {
printf("%02x ", buffer[i]);
} else {
printf(" ");
}
if (i == 7) {
printf(" ");
}
}
printf(" |");
for (size_t i = 0; i < bytes_read; i++) {
if (isprint(buffer[i])) {
printf("%c", buffer[i]);
} else {
printf(".");
}
}
printf("|\n");
}
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
return 1;
}
FILE *file = fopen(argv[1], "rb");
if (file == NULL) {
perror("Error opening file");
return 1;
}
unsigned char buffer[16];
unsigned int offset = 0;
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, 16, file)) > 0) {
print_hex_row(offset, buffer, bytes_read);
offset += bytes_read;
}
fclose(file);
return 0;
}
Secret mission
Add a Byte Limit Flag
Your hex dump tool currently reads entire files from start to finish. Real-world binary files can be massive. What if you only want to peek at the first few bytes? Add a -n flag that limits output to a specific number of bytes.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- hexdump-project folder on your Desktop (contains hello.c, hexdump.c, compiled hello and hexdump binaries).
Keep everything running
No action needed. Choose this if you plan to keep experimenting with your hex dump tool or want to extend it further.
- Your hexdump-project folder stays on your Desktop. You can run ./hexdump on any file at any time.
- Your C compiler (Clang on macOS, GCC on Linux) remains installed. You will need it for any future C projects.
Pause - I'll come back to this later
There are no running processes or services to pause. Your project is just files on disk. They take up minimal space and cost nothing to keep.
- Leave the hexdump-project folder in place. When you come back, open a terminal, cd into the folder, and pick up where you left off.
Delete - I don't want to use this again
Remove all project files and start fresh. Your C compiler stays installed for future use.
- Delete the project folder by running this command in your terminal:
rm -rf ~/Desktop/hexdump-project
- Confirm the folder is gone by running:
ls ~/Desktop | grep hexdump
- You should see no output. The folder and all its contents (source files and compiled binaries) are permanently deleted.
Nice Work!
Nice Work!
You just built a working hex dump tool from scratch in C. You can now inspect the raw bytes of any file on your system, just like the professionals do when debugging firmware or binary protocols.
You've learned how to:
- Write, compile, and run a C program using the gcc toolchain, turning source code into an executable binary.
- Read files in binary mode using fopen, fread, and fclose, processing raw bytes through arrays and pointers.
- Format output with printf hex specifiers and use command-line arguments (argc/argv) to accept user input from the terminal.
- Secret Mission: Added a -n byte limit flag with argument parsing using strtol().
Ready to quiz yourself?