A compiled C program is an ELF file split into sections. The loader maps them into memory with different permissions, and those permissions are what the CPU enforces at runtime.

SectionHoldsPermissionsExample
.textmachine coderead + executemain
.rodataread-only dataread (no write)string literals, static const tables
.datainitialized globals / staticsread + writestatic char buf[] = "hi";
.bsszero-initialized globals / staticsread + writestatic int counter;
stacklocals, return addressesread + writechar buf[8]; inside a function
  • .data and .rodata are both in the binary; “in the binary” doesn’t mean read-only. The section decides that.
  • .bss takes no space in the file (type NOBITS): the loader just hands out zeroed memory.
  • Writing to .rodata (e.g. through a pointer to a string literal) → the page isn’t writable → SIGSEGV.

See it yourself

Sections and their flags (A = alloc, W = write, X = exec):

readelf -SW ./prog | grep -E '\.text|\.rodata|\.data |\.bss'
  [12] .text    PROGBITS ... AX
  [14] .rodata  PROGBITS ... A
  [22] .data    PROGBITS ... WA
  [23] .bss     NOBITS   ... WA

Sections are grouped into segments (readelf -lW ./prog, the LOAD lines), and segments are what actually get mapped. At runtime, compare addresses (printf("%p", (void *)ptr)) against /proc/self/maps:

aaaabfd80000-aaaabfd81000 r-xp ... /path/to/prog   <- .text + .rodata (literal was here)
aaaabfd9f000-aaaabfda0000 r--p ... /path/to/prog   <- RELRO (.got etc., made read-only after startup)
aaaabfda0000-aaaabfda1000 rw-p ... /path/to/prog   <- .data / .bss (static array was here)
ffffe15ba000-ffffe15db000 rw-p ...  [stack]         <- local array was here

Note: on this arm64 build .rodata shares the r-xp segment with .text. Other toolchains (e.g. x86-64 with -z separate-code) give it its own r--p mapping. Either way: not writable.

References: man 5 elf, man 1 readelf, man 5 proc (/proc/pid/maps).