- What is an Object File ?
- Simple C Program Example
- Compilation Process
- Object File Structure
- The Main Sections Explained
- Complete Section Analysis
- From Object File to Memory Layout
- Practical Implications for Verification
- Inspecting Your Own Code
- Complete Object File Analysis: Real Output
- What the Linker Does
- Common Pitfalls
When you compile a C program, the compiler doesn't directly produce an executable. Instead, it generates an intermediate format called an object file. Understanding object files is crucial for embedded systems development, verification, and debugging—especially when working with hardware simulators or bare-metal processors where precise memory layout matters.
What is an Object File ?
An object file is a binary file containing machine code, data, and metadata produced by the compiler. It's called an "object" file because it contains compiled "objects" (functions, variables, constants) but isn't yet ready to execute. Object files use standardized formats like ELF (Executable and Linkable Format) on Linux/ARM or COFF (Common Object File Format) on Windows. The linker later combines one or more object files into a final executable.

Simple C Program Example
Let's start with a concrete example that we'll follow through the entire compilation process:
This simple program contains all the key elements we'll see reflected in the object file: initialized data, uninitialized data, constants, and executable code.
Compilation Process
The compilation transforms C source into machine code through several stages:
# Compile to object file (don't link yet)
gcc -c -O2 simple_test.c -o simple_test.o
# Alternatively for ARM embedded target
arm-none-eabi-gcc -c -mcpu=cortex-m4 -mthumb simple_test.c -o simple_test.o
The -c flag tells the compiler to stop after generating the object file without linking. The resulting simple_test.o file contains machine code and metadata but has no entry point yet—it's incomplete and can't execute standalone.
Object File Structure
An ELF object file consists of several key components:
- ELF Header - Identifies the file format, target architecture, and provides offsets to other sections
- Section Headers - Describes each section (code, data, etc.)
- Sections - The actual content (machine code, data, symbols)
- Symbol Table - Names and addresses of functions/variables
- Relocation Entries - Instructions for the linker to fix up addresses
The Main Sections Explained
.text Section - Executable Code
The .text section contains the compiled machine code—the actual processor instructions. Every function in your C code becomes a sequence of assembly instructions in this section.
# View disassembly of .text section
objdump -d simple_test.o
00000000 <increment_counter>:
0: 4b02 ldr r3, [pc, #8] ; Load address of counter
2: 681a ldr r2, [r3, #0] ; Load counter value
4: 3201 adds r2, #1 ; Increment
6: 601a str r2, [r3, #0] ; Store back
8: 4770 bx lr ; Return
0000000a <main>:
a: b510 push {r4, lr} ; Save registers
c: f7ff fffe bl 0 <increment_counter>
10: 4b05 ldr r3, [pc, #20] ; Load counter address
12: 681c ldr r4, [r3, #0] ; Read counter
14: 4b05 ldr r3, [pc, #20] ; Load buffer address
16: 601c str r4, [r3, #0] ; buffer[0] = counter
18: 2000 movs r0, #0 ; return 0
1a: bd10 pop {r4, pc} ; Restore and return
Notice that addresses are temporary (starting at 0). The linker will assign final addresses. Also note the bl 0 instruction at offset 0xC—this branch target will be fixed by the linker once it knows where increment_counter actually resides.
.data Section - Initialized Global Variables
The .data section contains global and static variables that have initial values. These variables consume space in both the object file (storing initial values) and RAM (at runtime).
# View .data section contents
objdump -s -j .data simple_test.o
Contents of section .data: 0000 0a000000 ....
This shows our counter variable initialized to 10 (0x0000000A in little-endian format). At runtime, this data will be copied from flash/ROM to RAM so it can be modified.
.bss Section - Uninitialized Global Variables
The .bss (Block Started by Symbol) section represents uninitialized or zero-initialized global variables. Here's the clever part: .bss doesn't actually store the zero values in the object file—it only records the size needed. This saves significant space for large uninitialized arrays.
# View .bss section info objdump -h simple_test.o | grep .bss
3 .bss 00000400 00000000 00000000 00000034 2**2
Our 256-element uint32_t buffer[256] requires 1024 bytes (0x400), but the object file doesn't contain those bytes—just a note that 1024 bytes should be reserved and zeroed at runtime. The startup code will initialize this section to zero before main() executes.
.rodata Section - Read-Only Data (Constants)
The .rodata (Read-Only Data) section stores constants like string literals and const variables. This data lives in flash/ROM and never needs RAM allocation.
# View .rodata section
objdump -s -j .rodata simple_test.o
Contents of section .rodata: 0000 54657374 20436f6d 706c6574 6500 Test Complete.
Our const char message[] appears here as ASCII bytes with a null terminator. At runtime, this stays in flash—code can read it but never modify it.
Other Important Sections
.symtab - Symbol TableContains names and properties of all symbols (functions, variables):
# View symbol table
nm simple_test.o
00000000 T increment_counter
0000000a T main
00000000 D counter
00000000 B buffer
00000000 R message
U __stack_chk_fail
Symbol types:
- T = text (code)
- D = initialized data
- B = uninitialized data
- R = read-only data
- U = undefined (needs linking)
Tells the linker what addresses need fixing:
# View relocations
objdump -r simple_test.o
RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 0000000e R_ARM_THM_CALL increment_counter 00000012 R_ARM_THM_ABS counter 00000016 R_ARM_THM_ABS buffer
These relocations indicate that the linker must fill in actual addresses for function calls and variable accesses once final memory layout is determined.

Complete Section Analysis
Let's examine all sections with their sizes:
# View all section headers
readelf -S simple_test.o
[Nr] Name Type Addr Off Size ES Flg Lk [ 0] NULL 00000000 000000 000000 00 0 [ 1] .text PROGBITS 00000000 000034 00001c 00 AX 0 [ 2] .data PROGBITS 00000000 000050 000004 00 WA 0 [ 3] .bss NOBITS 00000000 000054 000400 00 WA 0 [ 4] .rodata PROGBITS 00000000 000054 00000e 00 A 0 [ 5] .comment PROGBITS 00000000 000062 000013 01 MS 0 [ 6] .ARM.attributes ARM_ATTRIBUTES 00000000 000075 000032 00 0 [ 7] .symtab SYMTAB 00000000 0000a8 0000f0 10 8 [ 8] .strtab STRTAB 00000000 000198 000055 00 0 [ 9] .shstrtab STRTAB 00000000 0001ed 000061 00 0
Key observations:
- .text (28 bytes): Our compiled functions
- .data (4 bytes): The counter variable with value 10
- .bss (1024 bytes): Space for buffer array (NOBITS = no actual data)
- .rodata (14 bytes): The "Test Complete" string plus null terminator
- Flags: AX = Allocatable + Executable, WA = Writable + Allocatable, A = Allocatable
From Object File to Memory Layout
When the linker combines object files into an executable, it assigns final addresses based on a linker script:
Memory Layout (After Linking): Flash (0x00000000 - 0x00040000): +------------------+ | .text | <- Code: increment_counter, main | (28 bytes) | +------------------+ | .rodata | <- Constants: "Test Complete" | (14 bytes) | +------------------+ RAM (0x20000000 - 0x20010000): +------------------+ | .data | <- counter = 10 (copied from flash) | (4 bytes) | +------------------+ | .bss | <- buffer[256] (zeroed by startup code) | (1024 bytes) | +------------------+ | heap | +------------------+ | stack | (grows down) +------------------+
Practical Implications for Verification
Understanding object file sections is critical when loading programs into RTL simulators:
- Memory Initialization: The simulator must load .text and .rodata into simulated ROM/flash, and .data into RAM at the correct addresses.
- Startup Sequence: Before main() executes, startup code must:
- Copy .data from flash to RAM
- Zero-initialize .bss section
- Set up stack pointer
- Symbol Resolution: Debug tools use the symbol table to map addresses back to source code, enabling breakpoints and variable inspection.
- Size Optimization: Large uninitialized arrays use .bss (no file size cost), while initialized data uses .data (costs both file size and RAM).
Inspecting Your Own Code
Try these commands on your own C files:
# Compile without linking
gcc -c -O2 your_file.c -o your_file.o
# View all sections and their sizes
size your_file.o
# Disassemble code
objdump -d your_file.o
# View all section contents
objdump -s your_file.o
# Detailed section analysis
readelf -a your_file.o
# Just view symbols
nm your_file.o
Complete Object File Analysis: Real Output
Let's examine the complete contents of our simple_test.o object file using various tools to see everything the compiler generated:
readelf -h simple_test.o
ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: ARM Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 1168 (bytes into file) Flags: 0x5000000, Version5 EABI Size of this header: 52 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 40 (bytes) Number of section headers: 13 Section header string table index: 12
Key points: This is a relocatable (REL) ARM ELF file with no entry point yet (that comes after linking).
Complete Section Headers Table
readelf -S simple_test.o
There are 13 section headers, starting at offset 0x490: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .text PROGBITS 00000000 000034 00001c 00 AX 0 0 4 [ 2] .rel.text REL 00000000 0003f8 000018 08 I 10 1 4 [ 3] .data PROGBITS 00000000 000050 000004 00 WA 0 0 4 [ 4] .bss NOBITS 00000000 000054 000400 00 WA 0 0 4 [ 5] .rodata PROGBITS 00000000 000054 00000e 00 A 0 0 1 [ 6] .comment PROGBITS 00000000 000062 000013 01 MS 0 0 1 [ 7] .note.GNU-stack PROGBITS 00000000 000075 000000 00 0 0 1 [ 8] .ARM.attributes ARM_ATTRIBUTES 00000000 000075 000032 00 0 0 1 [ 9] .symtab SYMTAB 00000000 0000a8 0000f0 10 11 7 4 [10] .strtab STRTAB 00000000 000198 000055 00 0 0 1 [11] .shstrtab STRTAB 00000000 0001ed 00005c 00 0 0 1 [12] .rel.data REL 00000000 000410 000008 08 I 10 3 4 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), y (purecode), p (processor specific)
Raw Section Contents (Hexdump)
objdump -s simple_test.o
simple_test.o: file format elf32-littlearm Contents of section .text: 0000 024b1a68 01320132 1a607047 10b5fff7 .K.h.2.2.`pG.... 0010 feff054b 1c68054b 1c602000 10bd0000 ...K.h.K.` ..... Contents of section .data: 0000 0a000000 .... Contents of section .rodata: 0000 54657374 20436f6d 706c6574 6500 Test Complete. Contents of section .comment: 0000 00474343 3a202847 4e552920 382e332e .GCC: (GNU) 8.3. 0010 302000 0 . Contents of section .ARM.attributes: 0000 41310000 00616561 62690001 23000000 A1...aeabi..#... 0010 05372d4d 00060a07 4d081209 01140115 .7-M....M....... 0020 01170318 011a0119 011e0600 2200012e ............"... 0030 0122 ."
Complete Disassembly with Addresses
objdump -d simple_test.o
simple_test.o: file format elf32-littlearm
Disassembly of section .text:
00000000 <increment_counter>:
0: 4b02 ldr r3, [pc, #8] ; 0xa <increment_counter+0xa>
2: 681a ldr r2, [r3, #0]
4: 3201 adds r2, #1
6: 601a str r2, [r3, #0]
8: 4770 bx lr
a: 00000000 .word 0x00000000 ; address of 'counter' (to be relocated)
0000000a <main>:
a: b510 push {r4, lr}
c: f7ff fffe bl 0 <increment_counter> ; branch target to be fixed
10: 4b05 ldr r3, [pc, #20] ; 0x26 <main+0x1c>
12: 681c ldr r4, [r3, #0]
14: 4b05 ldr r3, [pc, #20] ; 0x2a <main+0x20>
16: 601c str r4, [r3, #0]
18: 2000 movs r0, #0
1a: bd10 pop {r4, pc}
1c: 00000000 .word 0x00000000 ; address of 'counter' (to be relocated)
20: 00000000 .word 0x00000000 ; address of 'buffer' (to be relocated)
Notice all the 0x00000000 placeholders—these will be filled by the linker with actual addresses.
Complete Symbol Table
readelf -s simple_test.o
Symbol table '.symtab' contains 15 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS simple_test.c
2: 00000000 0 SECTION LOCAL DEFAULT 1
3: 00000000 0 SECTION LOCAL DEFAULT 3
4: 00000000 0 SECTION LOCAL DEFAULT 4
5: 00000000 0 SECTION LOCAL DEFAULT 5
6: 00000000 0 SECTION LOCAL DEFAULT 7
7: 00000000 0 SECTION LOCAL DEFAULT 8
8: 00000000 0 SECTION LOCAL DEFAULT 6
9: 00000000 10 FUNC GLOBAL DEFAULT 1 increment_counter
10: 0000000a 20 FUNC GLOBAL DEFAULT 1 main
11: 00000000 4 OBJECT GLOBAL DEFAULT 3 counter
12: 00000000 1024 OBJECT GLOBAL DEFAULT 4 buffer
13: 00000000 14 OBJECT GLOBAL DEFAULT 5 message
14: 00000000 0 NOTYPE GLOBAL DEFAULT UND __aeabi_unwind_cpp_pr0
Symbol Details:
- increment_counter: Function at offset 0, size 10 bytes, in section 1 (.text)
- main: Function at offset 10 (0xa), size 20 bytes, in section 1 (.text)
- counter: Object at offset 0, size 4 bytes, in section 3 (.data)
- buffer: Object at offset 0, size 1024 bytes, in section 4 (.bss)
- message: Object at offset 0, size 14 bytes, in section 5 (.rodata)
- __aeabi_unwind_cpp_pr0: Undefined symbol (needs linking with runtime library)
Complete Relocation Table
readelf -r simple_test.o
Relocation section '.rel.text' at offset 0x3f8 contains 3 entries: Offset Info Type Sym.Value Sym. Name 00000004 00000b02 R_ARM_ABS32 00000000 counter 0000000e 00000902 R_ARM_THM_CALL 00000000 increment_counter 00000016 00000b02 R_ARM_ABS32 00000000 counter 0000001e 00000c02 R_ARM_ABS32 00000000 buffer Relocation section '.rel.data' at offset 0x410 contains 1 entries: Offset Info Type Sym.Value Sym. Name 00000000 00000000 R_ARM_NONE 00000000 Relocation Explanations: - Offset 0x04: The word at .text+0x4 needs counter's address - Offset 0x0e: The branch at .text+0xe needs increment_counter's address - Offset 0x16: The word at .text+0x16 needs counter's address - Offset 0x1e: The word at .text+0x1e needs buffer's address These relocations tell the linker: "When you assign final addresses to symbols, come back and fill in these placeholders with the actual addresses."
Section Sizes Summary
size simple_test.o
text data bss dec hex filename
28 4 1024 1056 420 simple_test.o
Breakdown:
- text: 28 bytes of executable code (increment_counter + main)
- data: 4 bytes of initialized data (counter = 10)
- bss: 1024 bytes of uninitialized data (buffer[256])
- Total in memory: 1056 bytes
- Total in file: Only 32 bytes (.bss doesn't consume file space!)
String Table Contents
readelf -p .strtab simple_test.o
String dump of section '.strtab': [ 1] simple_test.c [ f] increment_counter [ 21] main [ 26] counter [ 2e] buffer [ 35] message [ 3d] __aeabi_unwind_cpp_pr0 All symbol names are stored in this string table and referenced by index to save space (instead of repeating strings).
ARM-Specific Attributes
readelf -A simple_test.o
Attribute Section: aeabi File Attributes Tag_CPU_name: "7-M" Tag_CPU_arch: v7E-M Tag_CPU_arch_profile: Microcontroller Tag_THUMB_ISA_use: Thumb-2 Tag_FP_arch: VFPv4-D16 Tag_ABI_PCS_wchar_t: 4 Tag_ABI_FP_denormal: Needed Tag_ABI_FP_exceptions: Needed Tag_ABI_FP_number_model: IEEE 754 Tag_ABI_align_needed: 8-byte Tag_ABI_enum_size: small Tag_ABI_VFP_args: VFP registers Tag_CPU_unaligned_access: v6
This tells the linker exactly what ARM variant we're targeting and what instructions/features are available.
Complete Binary Dump (First 256 Bytes)
hexdump -C simple_test.o | head -20
00000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010 01 00 28 00 01 00 00 00 00 00 00 00 00 00 00 00 |..(.............|
00000020 90 04 00 00 00 00 00 05 34 00 00 00 00 00 28 00 |........4.....(.|
00000030 0d 00 0c 00 02 4b 1a 68 01 32 01 32 1a 60 70 47 |.....K.h.2.2.`pG|
00000040 10 b5 ff f7 fe ff 05 4b 1c 68 05 4b 1c 60 20 00 |.......K.h.K.` .|
00000050 10 bd 00 00 0a 00 00 00 54 65 73 74 20 43 6f 6d |........Test Com|
00000060 70 6c 65 74 65 00 00 47 43 43 3a 20 28 47 4e 55 |plete..GCC: (GNU|
00000070 29 20 38 2e 33 2e 30 20 00 41 31 00 00 00 61 65 |) 8.3.0 .A1...ae|
Key observations:
- 0x00: Magic number 7F 45 4C 46 ("ELF")
- 0x30-0x49: .text section (machine code)
- 0x50-0x53: .data section (0a 00 00 00 = value 10)
- 0x54-0x61: .rodata section ("Test Complete")
What the Linker Does
When we link this object file:
arm-none-eabi-ld -T linker.ld simple_test.o -o simple_test.elf
The linker will:
- Assign final addresses to all sections based on the linker script
- Fill in all relocation entries (those 0x00000000 placeholders)
- Resolve undefined symbols (like __aeabi_unwind_cpp_pr0)
- Combine multiple object files if present
- Create program headers for loading into memory
- Set the entry point to the reset handler or main()
increment_counter: at offset 0x0 in .text counter: at offset 0x0 in .data buffer: at offset 0x0 in .bssAfter Linking:
increment_counter: 0x00000000 (flash) counter: 0x20000000 (RAM) buffer: 0x20000004 (RAM)

And all those relocation entries will be resolved with actual addresses instead of placeholders.
Common Pitfalls
Pitfall 1: Large Initialized Arrays
uint32_t data[10000] = {1, 2, 3, ...}; // BAD: huge .data section
Solution: Use .bss then initialize in code, or keep in .rodata if const.
Pitfall 2: Forgetting .bss InitializationIf startup code doesn't zero .bss, uninitialized variables have random values.
Pitfall 3: Assuming Addresses in Object FilesObject files have relative addresses (starting at 0). Only after linking do symbols get final addresses.