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:

  1. ELF Header - Identifies the file format, target architecture, and provides offsets to other sections
  2. Section Headers - Describes each section (code, data, etc.)
  3. Sections - The actual content (machine code, data, symbols)
  4. Symbol Table - Names and addresses of functions/variables
  5. 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
Output
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
Output
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  
Output
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
Output
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 Table

Contains names and properties of all symbols (functions, variables):


# View symbol table
nm simple_test.o
Output
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)
.rel.text - Relocation Information

Tells the linker what addresses need fixing:


# View relocations
objdump -r simple_test.o
Output
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
Output
[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
Output
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    
Output
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    
Output
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    
Output
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    
Output
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    
Output
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    
Output
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    
Output
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
Output
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
Output
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()
Before Linking:
increment_counter:  at offset 0x0 in .text
counter:            at offset 0x0 in .data
buffer:             at offset 0x0 in .bss    
After 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 Initialization

If startup code doesn't zero .bss, uninitialized variables have random values.

Pitfall 3: Assuming Addresses in Object Files

Object files have relative addresses (starting at 0). Only after linking do symbols get final addresses.

Did you find what you were looking for?

Quiz

No quiz questions available for this article.