Sections

  • Define how your program is organized in memory and in the object file.

  • They tell the assembler and linker where code and data go, and how they should be treated (readable, writable, executable, initialized, etc.).

  • The exact names and behavior depend on the format (ELF on Linux, PE on Windows, Mach-O on macOS), but the core concepts are consistent.

  • Linker may insert padding between sections:

    align 16
    
  • At runtime:

    • Sections in your .asm file become sections in the ELF object file, which the linker groups into segments.

    • .text  → RX memory

    • .data  → RW memory (preloaded)

    • .bss  → RW memory (zeroed)

[ code ]
[ .rodata ]
[ .data ]
[ .bss ]        ← fixed
-------------------------
[ heap ↑ ]      ← grows up
-------------------------
[ stack ↓ ]     ← grows down

_start

  • Is not just a convention. It is the default program entry symbol used by the linker (on ELF systems like Linux).

  • When you run a program:

    • Kernel loads the executable

    • Sets up stack (argc, argv, envp)

    • Jumps to the address of _start

  • No C runtime, no setup—just direct execution.

  • Is _start required?

    • By default: yes, linker expects it

    • But you can override: ld -e my_entry file.o .

.text

  • The code section.

  • Contains executable instructions

  • Typically read + execute, not writable

  • Entry point ( _start  or main ) lives here

  • Writing to .text  → crash (NX protection)

.data

  • Initialized data.

  • Stores initialized global/static variables

  • Read + write

  • Values are embedded in the binary

.bss

  • Uninitialized data.

  • “reserve space, don’t store bytes”

  • Space is reserved but not initialized.

  • Read + write

  • Does not take space in the file.

  • Loader initializes it to zero at runtime

Why it exists
  • Efficiency.

  • Instead of:

    big_array times 1000000 db 0
    
  • You write:

    big_array resb 1000000
    
  • Result:

    • File size: small

    • Memory at runtime: same size

    • Loader zeroes it automatically

.rodata

  • It is not a container with capacity like an array.

  • It is a region of the program’s memory image whose size is defined by its contents.

  • Read-only data, prevents accidental modification

  • Constants, strings (in many toolchains)

section .rodata
    pi dq 3.141592653589793
  • Writing to .rodata  → crash (read-only)

  • initialized at compile time

  • never modified at runtime

  • mapped read-only by OS

  • can be shared between processes

    • multiple programs using same library

    • OS maps same physical pages

    • This only works if memory is immutable.

  • Would it be ok to no use .rodata :

    • You can put everything in writable memory:

      • Program will still:

        • run correctly

        • produce the same results

      • Nothing requires .rodata .

      • No inherent speed penalty.

    • But you lose:

      • hardware-enforced immutability

    • .rodata  is not about correctness—it’s about enforcing intent at the hardware level

Size
  • There is no fixed or intrinsic “capacity” for .rodata.

  • Its size is determined entirely by the program and the platform constraints.

  • "whatever size the compiler/linker emits, limited by the system"

  • Real limits:

    • 32-bit systems

      • Typical virtual address space: ~2–4 GB total.

      • So .rodata  must fit within that along with everything else.

      • Large .rodata  (hundreds of MB) can become a problem.

    • 64-bit systems

      • Typical virtual address space: terabytes+.

      • .rodata  is effectively limited by:

        • available RAM

        • OS limits

      • linker/toolchain constraints

      • Practically:

        • tens or hundreds of MB → fine

        • multi-GB → possible but unusual

  • Examples:

    small tool        → a few KB
    medium app        → 100 KB – few MB
    large game/engine → 10–100+ MB (assets embedded)
    
  • .rodata  is mapped using virtual memory:

    • backed by the executable file

    • loaded on demand (paging)

  • So:

    • it does not need to fully fit in RAM at once

    • pages are loaded as accessed

Addressing Constants
  • Making constants non-addressable is a deliberate design choice, not a necessity.

  • Why it can make sense to no address constants:

    • Constants don’t need storage, if a constant is purely a value:

      • 42, true, 3.14

      • The compiler (1) can inline it, (2) encode it directly in instructions

      • Example:

        • mov eax, 42   ; no memory involved

        • No address exists → so taking one is meaningless.

    • Avoids forcing memory allocation

      • If constants are addressable:

      • &CONST

      • you must give them a location:

      • .rodata  entry

      • That means:

        • more memory usage

        • more symbols

        • less optimization freedom

    • Enables better optimization

      • Non-addressable constants allow:

        • constant folding

        • dead elimination

        • duplication without concern

      • Because:

        • no observable identity (no address)

    • Prevents identity confusion

      • If this is allowed:

        • &a == &b

      • what does it mean?

        const a = 42
        const b = 42
        
      • Should they:

        • have same address? (deduplicated)

        • have different addresses?

      • Non-addressable constants avoid this question entirely.

  • How languages deal with it:

    • value → no identity → no address

    • object → has identity → has address

    • Rust:

      const X: i32 = 42;   // no fixed address
      static X: i32 = 42;  // has address
      
      • const  → inline value

      • static  → memory location

.stack

  • Defines stack region.

  • Usually handled by OS, not manually in simple programs.

  • A limited, pre-reserved region of virtual memory

  • See Memory.md#Stack Memory  for more information .

Avoiding using the stack

  • It’s entirely possible to write assembly without ever using the stack.

  • It means

    • No push  / pop

    • No call  / ret  (since they implicitly use the stack)

      • Use jmp  instead of call

      • Manually manage return addresses in registers or fixed memory

    • No stack-based local variables

      • Use general-purpose registers (rax, rbx, etc.)

      • Or reserve global/static memory

    • No stack frames

  • It works well for:

    • Bootloaders

    • Embedded systems / bare metal

    • Tiny kernels or demos

    • Performance-critical inner loops

    • Situations with fixed control flow

  • Where it becomes painful

    • Deep function nesting

    • Recursion (basically impossible without a stack or equivalent)

    • Complex control flow

    • Interfacing with standard ABIs (they expect a stack)

  • Key limitations

    • No recursion unless you emulate a stack manually

    • Limited temporary storage (register pressure)

    • Harder to maintain and scale

    • Debugging is more manual