Step 66. The Structure of an Executable — The Blueprint Inside the Icon

Step 66. The Structure of an Executable — The Blueprint Inside the Icon

Level 1 — Programming and the Computer’s Interior | Difficulty ★★★☆☆ | Estimated time: 3 hours

Prerequisites: Steps 56–65 complete; you can compile a C program with gcc and have seen the compilation process and assembly output at least once.

  • What you need: a Linux terminal (WSL or Ubuntu) and gcc. Today’s tools — readelf, nm, objdump, strings, xxd, strip — come bundled with gcc, so no separate installation is needed.
  • Caution: today’s practice is 100% safe. We only "read" executables we made ourselves. We don’t touch system files or anyone else’s programs.

The executable we made looks like nothing but an icon from the outside. But open it up and there’s an orderly structure inside: a manual at the front, then a code region and a data region attached in sequence. There’s one reason to learn this structure — every analysis tool you’ll meet (reversing tools, debuggers) parses this structure to show you things. If you don’t know the structure, a tool’s output is an alien language; if you do, it becomes a map. Today we rummage through our own executable with six commands. Since it’s a file we made, nothing that comes out is scary.


1. Learning Objectives

By the end of this chapter, you will be able to:

  • Explain that an executable consists of "a manual (header) + purpose-specific regions (sections)"
  • Read the ELF header and section list with readelf
  • State the roles of the three sections .text, .data, and .bss
  • Extract clues from an executable with strings and nm
  • Explain what strip removes and why that makes analysis harder

2. Background Knowledge — Today’s Tools and Concepts

Today’s Tools at a Glance

Category Details
Language/environment C language + Linux terminal (verified on WSL Ubuntu, gcc 13.3.0)
Today’s commands file (check a file’s identity), xxd (view bytes as-is), readelf -h/-S (read header/sections), nm (symbol list), objdump -d/-h (machine code/section map), strings (extract strings), strip (remove symbols)
Concepts needed ELF, PE, header, sections (.text/.data/.bss), symbols, magic numbers
Today’s deliverable An executable-rummaging report — a document organizing the ID card, region map, and name tags of the file we made

2-1. ELF and PE — The File Formats of Two Worlds

Linux’s executable format is ELF (Executable and Linkable Format); Windows’s is PE (Portable Executable). Different names, same job: a promised structure holding "what this file is, where the code is, and where execution starts." Since we study on Linux, ELF is the protagonist; PE is covered only for conceptual comparison.

2-2. The Header — A File’s ID Card

At the very front of the file sits the header. Written in promised positions are facts like "I am an ELF," "for 64-bit," "for x86-64 CPUs," and "start execution at this address (the entry point)." The operating system reads this ID card and prepares for execution.

2-3. Sections — Regions by Purpose

After the header come the sections, regions divided by purpose. Remember just these three representatives:

  • .text: the region where machine code lives. It’s named "text" but it’s code — a name handed down through history.
  • .data: the region where global variables with initial values live.
  • .bss: the placeholder for global variables without initial values (which start at 0). Since it’s a reservation region saying "just hold the spot," its actual contents don’t exist in the file.

Step 60’s memory map (code/data regions) is engraved into the file as-is. A file’s sections are loaded up into memory regions when executed.

2-4. Symbols — The Name Tags

The name tags of functions and variables (like main and add) are called symbols. With symbols, analysis tools can show "this chunk of code is main" by name. A tool called strip can tear these name tags off — and then analysis becomes harder. Most programs released to the world are stripped, and that’s the first device raising the difficulty of reversing.


3. Follow Along

3-1. Preparing the Experiment File

Let’s make today’s experiment subject ourselves. The point is including a mix: a global variable with an initial value, one without, a static variable, and functions.

Input (reg.c)

#include <stdio.h>

int g_init = 42;        /* global with initial value → .data candidate */
int g_zero;             /* global without initial value → .bss candidate */
static int s_init = 7;  /* static variable → .data candidate */

int add(int a, int b) { return a + b; }

int main(void) {
    int z = add(g_init, s_init);
    printf("z = %d\n", z);
    printf("g_zero = %d\n", g_zero);
    return 0;
}

Input

gcc -g -O0 reg.c -o reg
./reg

Output (verified 2026-09-09):

z = 49
g_zero = 0

How to read it: -g is the option to keep symbols and debug information alive, and -O0 means "don’t optimize," so the structure stays exactly as we wrote it. g_zero was never assigned a value, yet 0 prints — confirmation of the promise that global variables without initial values start at 0.

3-2. file and the Magic Number — A File’s True Identity

Input

file reg
xxd reg | head -2

Output (verified 2026-09-09):

reg: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, ..., with debug_info, not stripped
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000  .ELF............
00000010: 0300 3e00 0100 0000 6010 0000 0000 0000  ..>.....`.......

How to read it: file tells you a file’s true format. It answered "64-bit ELF, for x86-64, symbols not removed (not stripped)." xxd is a byte-level microscope showing the file as-is in hexadecimal. See the first four bytes, 7f 45 4c 46? 45 4c 46 is "ELF" in ASCII — every ELF file starts with this greeting, and first bytes like these are called the magic number. JPG starts with ff d8, PNG with 89 50 4e 47, and ZIP (and docx, xlsx) with 50 4b.

Why: a file’s true identity is not its extension but these first bytes. The prediction experiment below is the proof.

Predict: if you rename the file with cp reg fake.jpg and then run file fake.jpg, what will file answer? Will it trust the extension or look at the contents? (Verified answer, 2026-09-09: fake.jpg: ELF 64-bit LSB pie executable, x86-64, ... — it looks at the magic number, not the extension.) Catching files that merely renamed themselves .jpg is the use of magic-number checking.

3-3. readelf -h — Reading the ID Card

Input

readelf -h reg

Output (verified 2026-09-09, key entries):

ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Type:                              DYN (Position-Independent Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Entry point address:               0x1060

How to read it: five lines finish the identification. Magic is the greeting we saw in 3-2, Class is 64-bit, Machine is for x86-64 CPUs, and the Entry point is the address where execution begins. The DYN in Type means "a format that runs no matter which address it’s loaded at (ASLR-friendly)," and it’s the default output of modern gcc.

Predict: if you run readelf -h on a part file (.o), what will Type say? Make a part with gcc -c reg.c -o reg_part.o and check. (Verified answer, 2026-09-09: REL (Relocatable file) — a "relocatable part" whose addresses aren’t fixed yet. A different identity from the DYN finished product.)

3-4. readelf -S — Unfolding the Region Map

Input

readelf -S reg | grep -E "Nr|text|\.data|\.bss"

Output (verified 2026-09-09):

  [Nr] Name              Type             Address           Offset
  [16] .text             PROGBITS         0000000000001060  00001060
  [25] .data             PROGBITS         0000000000004000  00003000
  [26] .bss              NOBITS           0000000000004018  00003018

How to read it: the three regions are visible. .text starts at 0x1060 — the same address as the entry point from 3-3. You’ve visually confirmed the connection that execution starts in the code region. .bss has Type NOBITS, which means "no actual contents in the file, only a reserved spot." Since these variables will start at 0 anyway, there’s no need to waste file space on them.

Why: once you see the correspondence "a file’s regions = memory’s regions at runtime," the eye that views executables turns into map reading. For reference, without grep this file has as many as 37 sections (verified 2026-09-09, Number of section headers in readelf -h). Since there are many auxiliary information regions besides the ones we made, deciding what to look for and filtering is a fundamental skill.

3-5. nm — The Name Tag List and Confirming Variable Membership

Input

nm reg | grep -E " g_init| g_zero| s_init| add$| main$"

Output (verified 2026-09-09):

0000000000001149 T add
0000000000004010 D g_init
000000000000401c B g_zero
0000000000001161 T main
0000000000004014 d s_init

How to read it: nm shows the symbol (name tag) list with addresses. The letter after the address is the home region. T is .text (code), D is .data, B is .bss. As we predicted, g_init and s_init were placed in D (0x4010, 0x4014 — inside the .data region at 0x4000), and g_zero in B (0x401c — inside the .bss region at 0x4018). You’ve confirmed by number that the variables in the code went exactly into their promised regions.

3-6. objdump -d and -h — The Same Map, a Different Tool

Is the address 0x1149 that nm told us for add real? Let’s open the machine code directly and verify.

Input

objdump -d reg | sed -n "/<add>:/,/ret/p"

Output (verified 2026-09-09):

0000000000001149 <add>:
    1149:	f3 0f 1e fa          	endbr64
    114d:	55                   	push   %rbp
    ...
    115d:	01 d0                	add    %edx,%eax
    115f:	5d                   	pop    %rbp
    1160:	c3                   	ret

How to read it: objdump -d turns the machine code of the .text region back into assembly (disassembly). The starting address is exactly the 1149 nm mentioned — evidence that the two tools are looking at the same map. The two bytes 01 d0 are add %edx,%eax — the machine code of the a + b we wrote.

Input

objdump -h reg | grep -E "Idx|text|\.data|\.bss"

Output (verified 2026-09-09):

Idx Name          Size      VMA               LMA               File off  Algn
 15 .text         00000161  0000000000001060  0000000000001060  00001060  2**4
 24 .data         00000018  0000000000004000  0000000000004000  00003000  2**3
 25 .bss          00000008  0000000000004018  0000000000004018  00003018  2**2

How to read it: the same regions as readelf -S appear in a slightly different format. .text’s size is 0x161 bytes, and the VMA (the address where it will load in memory) is 0x1060, matching readelf. The tools differ, but the map is one. Cross-checking with another tool when one tool is blocked is an analyst’s habit.

3-7. strings — Rummaging the Sentences Inside a File

Input

strings reg | grep -E "z =|g_zero"
strings reg | grep -i "gcc"

Output (verified 2026-09-09):

z = %d
g_zero = %d
g_zero
g_zero
GCC: (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0

How to read it: strings picks out only "human-readable strings" from the file. The "z = %d" we wrote in printf is in there as-is. The unexpected harvest is the second one — even which compiler made it is engraved in the file. It’s information we didn’t put in, included in the debug information from the -g option.

Why: this fact matters. Strings remain raw inside executables. It means that if you hard-code a password or server address into your code, one shot of strings reveals it. This is why strings is the first command you "just run" in CTF and malware analysis.

3-8. strip — Tearing Off the Name Tags, and After

Input

cp reg reg_stripped
strip reg_stripped
nm reg_stripped
strings reg_stripped | grep "z ="
ls -l reg reg_stripped
./reg_stripped

Output (verified 2026-09-09):

nm: reg_stripped: no symbols
z = %d
-rwxr-xr-x 1 root root 17512 ... reg
-rwxr-xr-x 1 root root 14480 ... reg_stripped
z = 49
g_zero = 0

How to read it: with the name tags torn off, nm answers "no symbols." Yet the string "z = %d" is alive (symbols and strings are different goods), and it runs exactly the same. The size shrank from 17,512 bytes to 14,480 bytes. Name tags were for people and tools, not the CPU — so removing them doesn’t hinder execution.

Why: the goal is to feel firsthand why this device — "keeping the code, tearing off only the names" — makes analysis hard. Even opened with objdump, the code is the same, but the name tag "this function is main" is gone, so the analyst must read machine code and reason "this must be around main." Most of the world’s programs are in this state.


4. Missions & Exercises

Mission — An Executable-Rummaging Report

  1. Compile a C program you made yourself (including a global with an initial value, a global without one, and at least two functions) with -g -O0
  2. Copy the five ID-card lines (Magic, Class, Type, Machine, Entry) from readelf -h and explain each
  3. Find .text, .data, and .bss with readelf -S, record their addresses, and cross-check with nm which section your global variables went to
  4. Record five interesting strings found with strings. If there’s something "I didn’t put in but is visible," reason out why it’s visible
  5. Strip a copy and make a comparison table of how the outputs of the three tools nm, objdump, and strings change
  6. At the end of the report, answer: "the roles of .text, .data, and .bss, in one sentence each."

Exercises

Q1. In the readelf -S output, only .bss’s Type differs as NOBITS. Explain the difference between PROGBITS and NOBITS from the perspective of "does the file contain contents?"

Q2. You ran nm on some executable and got no symbols. Is that a malfunction? What clue does this one line of information give an analyst?

Q3. After cp reg fake.jpg, why does file fake.jpg answer "ELF executable"? Explain from the magic-number perspective.

Q4. In a stripped file, strings still shows "z = %d", but nm doesn’t show main. Explain the difference between symbols and strings using this result.


5. Model Answers & Completion Criteria

Mission Model Answer

An example skeleton of the report (figures based on the reg verified 2026-09-09 — your file’s addresses may differ):

[ID card]
Magic: 7f 45 4c 46 ...  → every ELF file's greeting (\x7f + "ELF")
Class: ELF64            → for 64-bit
Type: DYN               → ASLR-friendly format, loadable at any address
Machine: X86-64         → for x86-64 CPUs
Entry: 0x1060           → execution start address, same as the start of .text

[Region map]
.text 0x1060 / .data 0x4000 / .bss 0x4018
nm cross-check: g_init 0x4010 (D) and s_init 0x4014 (d) are inside .data,
g_zero 0x401c (B) is inside .bss

[strings harvest]
"z = %d", "g_zero = %d" (things I wrote), /lib64/ld-linux-x86-64.so.2 (a library
path needed for execution), "GCC: (Ubuntu 13.3.0...) 13.3.0" (compiler identity — -g debug info)

[strip comparison table]
         nm        objdump -d        strings
original main/add  <main>: named     z = %d visible
stripped no symbols name tags gone   z = %d unchanged

[One sentence each]
.text: the region where machine code lives. .data: the region for globals with initial values.
.bss: the placeholder for globals starting at 0 (no contents in the file).

How to verify: ① In the nm output, do my global variables’ letters (D/B) match the prediction? ② Does the program run exactly the same after stripping? ③ Does the strings harvest include at least one "string I didn’t put in"? If all three are "yes," it’s complete.

Exercise Solutions

Q1 solution. PROGBITS is a section with actual contents (bytes) inside the file; NOBITS is a section where only the spot (size and address) is written in the file, with no contents. Since .bss variables will be filled with 0 anyway, there’s no need to record zeros in the file — at runtime, the operating system provides the space in memory and fills it with zeros.

Q2 solution. Not a malfunction — information. It means the file was stripped or built without symbols from the start. For an analyst, it becomes the first clue for planning strategy: "a file where I must read machine code without name tags" (verified message, 2026-09-09: nm: reg_stripped: no symbols).

Q3 solution. Because the file command determines the format by looking at the first bytes of the file’s contents (the magic number), not the extension. reg’s first four bytes, 7f 45 4c 46, stay the same no matter how you rename it, so file pinpoints "ELF" exactly (verified 2026-09-09). The extension is a label for people; the magic number is the real ID card.

Q4 solution. Symbols are name tags saying "the code/data at this address has this name," and they’re what strip removes. Strings are content the program will print — the actual contents of a data section like .rodata — so they remain even after the name tags are torn off. What execution needs (machine code, strings) stays and only what people need (name tags) disappears — that’s strip.

Completion Criteria Checklist

  • [ ] I can explain that an executable consists of a header and sections
  • [ ] I can read the key entries of readelf -h (Magic, Class, Type, Machine, Entry)
  • [ ] I can state the roles of .text, .data, and .bss
  • [ ] I can find a symbol’s home region from nm’s letters (T/D/B)
  • [ ] I can explain the relationship between strings and strip (strings remain, name tags disappear)
  • [ ] I confirmed by experiment that file looks at the magic number, not the extension
  • [ ] Mission: I completed the executable-rummaging report

6. Common Pitfalls & Fixes

Wall 1. The output is too long

Symptom: you run readelf -S and dozens of sections pour out (37 sections in our verification too).
Cause: an executable holds many auxiliary information regions beyond what we made.
Fix: filter with grep: readelf -S reg | grep -E "text|data|bss". Don’t try to read all output; deciding what to look for and filtering is a fundamental terminal skill.

Wall 2. nm says no symbols

Symptom (verified 2026-09-09):

nm: reg_stripped: no symbols

Cause: two cases. Either a stripped file, or a file built without symbols from the start.
Fix: not a malfunction — information. "This file is in a name-tag-less state" is the first clue for setting an analysis strategy. Since you made that state yourself in 3-8, this message is now a clue, not a bewilderment.

Wall 3. Addresses differ from runtime

Symptom: readelf’s addresses (small numbers like 0x1060) differ from the addresses shown while running (big numbers like 0x55…).
Cause: when Type is DYN (position-independent executable), the base point moves on every run (ASLR — you’ll observe it directly in Step 70). The addresses in the file are interpreted as "distances from the base point."
Fix: for now, focus on "the relative arrangement within the same file." The order and relationships of sections matter more than absolute address values.

Wall 4. Using readelf on a Windows .exe

Symptom: you bring a .exe file, run readelf, and it won’t read.
Cause: Windows uses the PE format, not ELF.
Fix: the concept is the same (header + sections + symbols). Only the tools differ. Later, when you do Windows analysis, you’ll meet PE tools, but the three-layer way of thinking you learned today — "ID card + regions + name tags" — applies as-is.

Wall 5. grep patterns bring along stray text

Symptom: grep text brings along unrelated lines like .note.gnu.property besides .text.
Cause: grep fetches every line containing the letters.
Fix: use the regex a bit more precisely, like grep -E "Nr|text|\.data|\.bss". \. is an escape meaning "a real single dot." At first it’s fine if some extra lines get mixed in — filter once more with your eyes.


7. Summary

Today’s Concepts

Concept One-line description
ELF / PE Linux / Windows executable formats — the "header + sections" structure is the same
Header The ID card at the front of the file (format, bitness, CPU, entry point)
.text / .data / .bss Machine code / globals with initial values / placeholder for zero-start variables
Symbol Name tags of functions and variables — analysis is easy with them; strip can remove them
Magic number The format greeting in a file’s first bytes — ELF is 7f 45 4c 46
Entry point The address where execution starts — located inside the .text region

Today’s Commands

Command What it does
file file Check a file’s true format via its magic number
xxd file | head View a file as-is in hexadecimal
readelf -h file Read the ELF header (ID card)
readelf -S file View the section (region) list
nm file Symbol (name tag) and address list
objdump -d file Turn machine code back into assembly
objdump -h file The section map in a different format from readelf
strings file Extract readable strings from a file
strip file Tear off the symbols (name tags)

The Instinct That Matters More Than Commands

Keep an order of approach with today’s tools for when you receive an unfamiliar file. (a) Confirm identity with file — what’s the real format? (b) Check the magic number — does it match the extension? (c) Skim its mind with strings — what strings are visible? (d) View the structure with readelf. (e) Only then, heavy analysis tools. The virtue of this order is "lightest first." It’s refusing to do with a ten-hour analysis what a ten-second check can filter. An investigation always starts with the cheap question.

The security connection: today’s most practical lesson is strings. Strings remain raw in executables — even the compiler version we didn’t put in (verified 2026-09-09) — so developers don’t embed secrets in code, and analysts skim the mind with strings first. Also remember that file structure is a promise. The reason a PDF made on Windows opens on Linux is that the structure is a published promise, and someone who knows the promise can both make files directly and discern strange files. When you learn packets later, today’s way of thinking — "header + regions" — will be used as-is. An eye that reads structure, once learned, serves ten places. Analysis practice is only on files I made and files I’m permitted to analyze.


Once every box is checked, Step 66 is complete. Click the checkbox in the sidebar to save your progress.