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.
| Section | Holds | Permissions | Example |
|---|---|---|---|
.text | machine code | read + execute | main |
.rodata | read-only data | read (no write) | string literals, static const tables |
.data | initialized globals / statics | read + write | static char buf[] = "hi"; |
.bss | zero-initialized globals / statics | read + write | static int counter; |
| stack | locals, return addresses | read + write | char buf[8]; inside a function |
.dataand.rodataare both in the binary; “in the binary” doesn’t mean read-only. The section decides that..bsstakes no space in the file (typeNOBITS): 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).