Memory layout

Introduction

We usually think about memory addresses in relation to pointers, it is pretty obvious to anyone that since malloc allocates memory it has to have some address, rigth?

char *ptr = malloc(1024);
printf("%p\n", ptr);

But besides dynamically allocated memory even the stack allocated memory has it's own addresses range:

char *ptr = "KubaTaba1uga";
printf("%p\n", ptr);

The memory for malloc is allocated on the heap where stack allocated memory is allocated well, on stack. Memory allocated on heap is in different region of process virtual memory than stack. Beside stack and heap there are more memory regions available to Linux developer, in this article we will do the deep dive into how memory is presented on linux and how C developer can affect the memory layout. Because the subject is pretty complicated we will split our exploration into few parts:
- ELF file format
- Compilation And Linkage of ELF executable
- Default memory layout
- Memory layout customization

Without further a do, let's start.

Objdump and readelf usage

Along the article we will be using some objdump commands so here is summary for them:
objdump -t <file> - show symbols table of object file
objdump -h <file> - show sections of object file
objdump -p <file> - show program headers of object file
objdump -d <file> - show assembly of object file
readelf -a <file> - show ELF header of object file
realdef -l <file> - show detailed sections info

ELF file format

The main executable format on Linux is ELF (Executable and Linkable Format). In this part we will discuss how elf format looks like and how it works like, after this part you should be able to explain what is structure of ELF file and how looks ELF file lifetime cycle.

The ELF file comes in one of three types:
- A relocatable file that holds code and data suitable for linking with other object files to create an executable or a shared object file. Example of this file is foo.o.
- An executable file holds a program suitable for execution. Example of this file is foo.
- A shared object file that is much like relocatable file but allow for dynamic linking in runtime from multiple processes. It's better known as so file, it's example is foo.so.

In this article we focuse on creation of executable file, so for the moment third type can be disregarded.

The flow for creating an executable looks like this: ELF linking and compiling

For example purposes let's create a file foo.c and transform it into executable:

// foo.c

int populated = 1;
int not_populated;

int main(){
    populated = 7;
    not_populated = 0;

    return 0;
}

When we want to create an executable for foo.c we needs to create a relocatable file first:

❯ gcc -c foo.c -o foo.o

Relocatable file structure is composed of ELF header and sections: ELF linking view

ELF header specify metadata required to interpet the object file, for example a type of an object file (whether it is relocatable, executable or so):

❯ readelf -h foo.o
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          536 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         12
  Section header string table index: 11

While each section specify memory region of object file and it's settings:

❯ objdump -h foo.o

foo.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000001f  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000004  0000000000000000  0000000000000000  00000060  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000004  0000000000000000  0000000000000000  00000064  2**2
                  ALLOC
  3 .comment      00000020  0000000000000000  0000000000000000  00000064  2**0
                  CONTENTS, READONLY
  4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  00000084  2**0
                  CONTENTS, READONLY
  5 .eh_frame     00000038  0000000000000000  0000000000000000  00000088  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

Each section stands for some memory region, for example text spawn from 0x00 till 0x40 and has enabled CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE. Each of these settings is indirectly derived from ELF specification. For example ELF specification doesn't specify READONLY flag, but it specify SHF_WRITE flag which lack of can be interpreted as READONLY feature enabled. The settings of an section describe how it will behave during runetime. So for example .text section can't be modified, so linker can put there all string literals, hence .data does not have write protection it may not be good idea to put string literals there.

Beside program header and sections each object file has it's symbol table which specify all functions and variables which have to be allocated upfront in any of sections. Symbol table is optionall and can be stripped so it can't be reliad upon, however this is nice to know for all debugging related tasks:

❯ objdump -t foo.o

foo.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 foo.c
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 g     O .data  0000000000000004 populated
0000000000000000 g     O .bss   0000000000000004 not_populated
0000000000000000 g     F .text  000000000000001f main

The populated variable is put in .data section because of it's LOAD feature, LOAD enables loading value of the memory from object file into a memory, hence populated has assigned value 1 during compilation the runtime will want to take 1 from object file and put it into memory automatically.

The not_populated variable is put in .bss section because it doesn't have a value assigned until main execution, so we need to allocate some memory for it but we do not need to load any value to it.

Once we have relocatable file we are ready to generate executable based on it:

❯ gcc foo.o -o foo

Executable file structure is composed of ELF header, program headers and sections: ELF linking view

ELF header has the same role as in relocable object file, it specify metadata required for handling ELF file:

❯ readelf -a foo
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Position-Independent Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x1040
  Start of program headers:          64 (bytes into file)
  Start of section headers:          13976 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         14
  Size of section headers:           64 (bytes)
  Number of section headers:         30
  Section header string table index: 29

Each program header contains metadata important for loading the section into memory:

❯ objdump -p main

main:     file format elf64-x86-64

Program Header:
    PHDR off    0x0000000000000040 vaddr 0x0000000000000040 paddr 0x0000000000000040 align 2**3
         filesz 0x0000000000000310 memsz 0x0000000000000310 flags r--
  INTERP off    0x0000000000000394 vaddr 0x0000000000000394 paddr 0x0000000000000394 align 2**0
         filesz 0x000000000000001c memsz 0x000000000000001c flags r--
    LOAD off    0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**12
         filesz 0x00000000000005f0 memsz 0x00000000000005f0 flags r--
    LOAD off    0x0000000000001000 vaddr 0x0000000000001000 paddr 0x0000000000001000 align 2**12
         filesz 0x0000000000000151 memsz 0x0000000000000151 flags r-x
    LOAD off    0x0000000000002000 vaddr 0x0000000000002000 paddr 0x0000000000002000 align 2**12
         filesz 0x00000000000000fc memsz 0x00000000000000fc flags r--
    LOAD off    0x0000000000002e00 vaddr 0x0000000000003e00 paddr 0x0000000000003e00 align 2**12
         filesz 0x0000000000000214 memsz 0x0000000000000220 flags rw-
 DYNAMIC off    0x0000000000002e10 vaddr 0x0000000000003e10 paddr 0x0000000000003e10 align 2**3
         filesz 0x00000000000001b0 memsz 0x00000000000001b0 flags rw-
    NOTE off    0x0000000000000350 vaddr 0x0000000000000350 paddr 0x0000000000000350 align 2**3
         filesz 0x0000000000000020 memsz 0x0000000000000020 flags r--
    NOTE off    0x0000000000000370 vaddr 0x0000000000000370 paddr 0x0000000000000370 align 2**2
         filesz 0x0000000000000024 memsz 0x0000000000000024 flags r--
    NOTE off    0x00000000000020dc vaddr 0x00000000000020dc paddr 0x00000000000020dc align 2**2
         filesz 0x0000000000000020 memsz 0x0000000000000020 flags r--
0x6474e553 off    0x0000000000000350 vaddr 0x0000000000000350 paddr 0x0000000000000350 align 2**3
         filesz 0x0000000000000020 memsz 0x0000000000000020 flags r--
EH_FRAME off    0x0000000000002004 vaddr 0x0000000000002004 paddr 0x0000000000002004 align 2**2
         filesz 0x000000000000002c memsz 0x000000000000002c flags r--
   STACK off    0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**4
         filesz 0x0000000000000000 memsz 0x0000000000000000 flags rw-
   RELRO off    0x0000000000002e00 vaddr 0x0000000000003e00 paddr 0x0000000000003e00 align 2**0
         filesz 0x0000000000000200 memsz 0x0000000000000200 flags r--

Notice the range inside header, for first header one we have start at offset at 0x0040 and end at 0x0350=0x0040+0x0310 which we can calculate from offset and size.

We can confirm that first header ranging from 0x0040 till 0x0350 does not have any matching section:

❯ objdump -h foo

main:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .note.gnu.property 00000020  0000000000000350  0000000000000350  00000350  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .note.gnu.build-id 00000024  0000000000000370  0000000000000370  00000370  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .interp       0000001c  0000000000000394  0000000000000394  00000394  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .gnu.hash     00000024  00000000000003b0  00000000000003b0  000003b0  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .dynsym       00000090  00000000000003d8  00000000000003d8  000003d8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  5 .dynstr       00000088  0000000000000468  0000000000000468  00000468  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  6 .gnu.version  0000000c  00000000000004f0  00000000000004f0  000004f0  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  7 .gnu.version_r 00000030  0000000000000500  0000000000000500  00000500  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  8 .rela.dyn     000000c0  0000000000000530  0000000000000530  00000530  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  9 .init         00000017  0000000000001000  0000000000001000  00001000  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 10 .plt          00000010  0000000000001020  0000000000001020  00001020  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 11 .plt.got      00000008  0000000000001030  0000000000001030  00001030  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 12 .text         00000108  0000000000001040  0000000000001040  00001040  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 13 .fini         00000009  0000000000001148  0000000000001148  00001148  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 14 .rodata       00000004  0000000000002000  0000000000002000  00002000  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 15 .eh_frame_hdr 0000002c  0000000000002004  0000000000002004  00002004  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 16 .eh_frame     000000ac  0000000000002030  0000000000002030  00002030  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 17 .note.ABI-tag 00000020  00000000000020dc  00000000000020dc  000020dc  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 18 .init_array   00000008  0000000000003e00  0000000000003e00  00002e00  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 19 .fini_array   00000008  0000000000003e08  0000000000003e08  00002e08  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 20 .dynamic      000001b0  0000000000003e10  0000000000003e10  00002e10  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 21 .got          00000028  0000000000003fc0  0000000000003fc0  00002fc0  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 22 .got.plt      00000018  0000000000003fe8  0000000000003fe8  00002fe8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 23 .data         00000014  0000000000004000  0000000000004000  00003000  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 24 .bss          0000000c  0000000000004014  0000000000004014  00003014  2**2
                  ALLOC
 25 .comment      0000001f  0000000000000000  0000000000000000  00003014  2**0
                  CONTENTS, READONLY

But second one does have one match, it start is at 0x0000000000000394 and it's end is at 0x0411=0x0394 + 0x01c, this section is .interp. We are lucky because we do not need to compare all sections and program headers like this, there is already tool which can do the job for us:

❯ readelf -l  main

Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1040
There are 14 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x0000000000000310 0x0000000000000310  R      0x8
  INTERP         0x0000000000000394 0x0000000000000394 0x0000000000000394
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x00000000000005f0 0x00000000000005f0  R      0x1000
  LOAD           0x0000000000001000 0x0000000000001000 0x0000000000001000
                 0x0000000000000151 0x0000000000000151  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000002000 0x0000000000002000
                 0x00000000000000fc 0x00000000000000fc  R      0x1000
  LOAD           0x0000000000002e00 0x0000000000003e00 0x0000000000003e00
                 0x0000000000000214 0x0000000000000220  RW     0x1000
  DYNAMIC        0x0000000000002e10 0x0000000000003e10 0x0000000000003e10
                 0x00000000000001b0 0x00000000000001b0  RW     0x8
  NOTE           0x0000000000000350 0x0000000000000350 0x0000000000000350
                 0x0000000000000020 0x0000000000000020  R      0x8
  NOTE           0x0000000000000370 0x0000000000000370 0x0000000000000370
                 0x0000000000000024 0x0000000000000024  R      0x4
  NOTE           0x00000000000020dc 0x00000000000020dc 0x00000000000020dc
                 0x0000000000000020 0x0000000000000020  R      0x4
  GNU_PROPERTY   0x0000000000000350 0x0000000000000350 0x0000000000000350
                 0x0000000000000020 0x0000000000000020  R      0x8
  GNU_EH_FRAME   0x0000000000002004 0x0000000000002004 0x0000000000002004
                 0x000000000000002c 0x000000000000002c  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x0000000000002e00 0x0000000000003e00 0x0000000000003e00
                 0x0000000000000200 0x0000000000000200  R      0x1

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .note.gnu.property .note.gnu.build-id .interp .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn
   03     .init .plt .plt.got .text .fini
   04     .rodata .eh_frame_hdr .eh_frame .note.ABI-tag
   05     .init_array .fini_array .dynamic .got .got.plt .data .bss
   06     .dynamic
   07     .note.gnu.property
   08     .note.gnu.build-id
   09     .note.ABI-tag
   10     .note.gnu.property
   11     .eh_frame_hdr
   12
   13     .init_array .fini_array .dynamic .got .got.plt

The ranges from program headers create a segments of memory, into which sections are mapped to. From now on we will refer to the ranges from program headers as segments.

Ok so at this moment you should have pretty well understanding of how the ELF object files look on the disk, so it's time to move on to the next subject Compilation And Linkage of ELF executable. How is it actually happening that our c src code ends up as relocatable object file? And how it is possible that multiple relocatable files can compose an executable?

Compilation And Linkage of ELF executable

Let's consider a problem, how is it happening that our foo.c ends up as foo.o with all this ELF header and sections and assembly inside?

Compiler is our main friend here and he will do all the job in producing the foo.o. The flow looks sth like this:

C src file ----> Preprocessor file -----> Assembly ------> ELF relocatable file

C src file ----> Preprocessor file -----> Assembly ------> ELF relocatable file

C src file ----> Preprocessor file -----> Assembly ------> ELF relocatable file

First we need to expand all preprocessor directives in our c file:

❯ gcc -E foo.c -o foo.i

Then translate preprocessor file into assembly:

❯ gcc -S foo.i -o foo.s

And the last step is packaging assembly into ELF format:

❯ gcc -c foo.s -o foo.o

Once we have an ELF relocatable file we can use linker to link it into ELF executable file: ELF linking

ld -o foo -I /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o -lc foo.o /usr/lib/x86_64-linux-gnu/crtn.o

The exact location of other .o files can differ from system to system, they can be find like easilly like this:

sudo find / | grep -E ".*(ld-linux-x86-64.so.2|crt1.o|crti.o|crtn.o)"

If we examine foo we will see almost the the same sections and program headers as before:

❯ readelf -l  foo

Elf file type is EXEC (Executable file)
Entry point 0x401020
There are 12 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000002a0 0x00000000000002a0  R      0x8
  INTERP         0x00000000000002e0 0x00000000004002e0 0x00000000004002e0
                 0x000000000000002f 0x000000000000002f  R      0x1
      [Requesting program interpreter: /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x0000000000000408 0x0000000000000408  R      0x1000
  LOAD           0x0000000000001000 0x0000000000401000 0x0000000000401000
                 0x0000000000000079 0x0000000000000079  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000402000 0x0000000000402000
                 0x00000000000000c0 0x00000000000000c0  R      0x1000
  LOAD           0x0000000000002e88 0x0000000000403e88 0x0000000000403e88
                 0x0000000000000180 0x0000000000000188  RW     0x1000
  DYNAMIC        0x0000000000002e88 0x0000000000403e88 0x0000000000403e88
                 0x0000000000000150 0x0000000000000150  RW     0x8
  NOTE           0x0000000000002080 0x0000000000402080 0x0000000000402080
                 0x0000000000000020 0x0000000000000020  R      0x8
  NOTE           0x00000000000020a0 0x00000000004020a0 0x00000000004020a0
                 0x0000000000000020 0x0000000000000020  R      0x4
  GNU_PROPERTY   0x0000000000002080 0x0000000000402080 0x0000000000402080
                 0x0000000000000020 0x0000000000000020  R      0x8
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x0000000000002e88 0x0000000000403e88 0x0000000000403e88
                 0x0000000000000178 0x0000000000000178  R      0x1

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn
   03     .init .text .fini
   04     .rodata .eh_frame .note.gnu.property .note.ABI-tag
   05     .dynamic .got .got.plt .data .bss
   06     .dynamic
   07     .note.gnu.property
   08     .note.ABI-tag
   09     .note.gnu.property
   10
   11     .dynamic .got .got.plt

So how is this possible that the same src code created two different program headers and sections?

Theere is one more piece of the puzzle, the linker script. Once the compiler created the foo.o there is no difference wheter it comes from assembly and preprocessor or from src c file, but when we are linking realocable ELF files together the linker lay out sections and program headers according to a script, we didn't saw it previously using ld because ld has it's own default linking script which it uses if we won't provide our own. To display default linker script do:

ld --verbose

Most linux programs on servers and desktops uses the default linker script and creates bunch of special sections like .bss, .data, .text which are described further in ELF specification 1.2, chapter Special Sections.

But in embedded programming as well as in low-level linux apps you can find custom linker scripts which changes the default layout of the memory segments. By providing your custom linker script you can abandon special sections as well as specify your own sections, we will explore such cases in Memory layout customization chapter but for now let's focus on default memory layout provided by default linker script.

Default memory layout

Linux allows each process to have its own private address space, rather than accessing raw physical RAM directly. It is implemented by virtual memory abstraction which does correspond indirectly to physical RAM adresses: ELF memory model

Virtual memory is represented as a continuous chunk of bytes, each with a unique address, starting from a fixed base address.

We learned in previous chapter about memory segments which are composed from multiple section of ELF object file, here these segments are represented on diagram as code and data but there can be more.

Beside special data segments virtual memory contain segment for heap and stack. Heap is used for dynamically allocated memory (e.g., via malloc, calloc, or realloc) and grows upward at runtime as needed. Stack stores local variables, function parameters, return addresses, and saved registers and grows downward as functions are called.

Let's consider example which shows the virtual memory in practice. Imagine two variables a which is a const and b which is not a const, both are global variables:

// foo.c
#include <stdio.h>

const int a = 1;
int b = 2;

int main(void) {
  printf("main=%p\n", main);
  printf("a=%p\n", &a); 
  printf("b=%p\n", &b);

  return 0;
}

Compile with:

❯ gcc -no-pie foo.c -o foo 

Example output:

❯ ./foo
main=0x401126
a=0x402004
b=0x404018

If we examine symbols of the executable we see we can see that a is in .rodata and b is in .data:

❯ objdump -t foo

foo:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000              Scrt1.o
00000000000020fc l     O .note.ABI-tag  0000000000000020              __abi_tag
0000000000000000 l    df *ABS*  0000000000000000              crtstuff.c
0000000000001080 l     F .text  0000000000000000              deregister_tm_clones
00000000000010b0 l     F .text  0000000000000000              register_tm_clones
00000000000010f0 l     F .text  0000000000000000              __do_global_dtors_aux
000000000000401c l     O .bss   0000000000000001              completed.0
0000000000003dd8 l     O .fini_array    0000000000000000              __do_global_dtors_aux_fini_array_entry
0000000000001130 l     F .text  0000000000000000              frame_dummy
0000000000003dd0 l     O .init_array    0000000000000000              __frame_dummy_init_array_entry
0000000000000000 l    df *ABS*  0000000000000000              foo.c
0000000000000000 l    df *ABS*  0000000000000000              crtstuff.c
00000000000020f8 l     O .eh_frame      0000000000000000              __FRAME_END__
0000000000000000 l    df *ABS*  0000000000000000  
0000000000003de0 l     O .dynamic       0000000000000000              _DYNAMIC
0000000000002020 l       .eh_frame_hdr  0000000000000000              __GNU_EH_FRAME_HDR
0000000000003fe8 l     O .got.plt       0000000000000000              _GLOBAL_OFFSET_TABLE_
0000000000000000       F *UND*  0000000000000000              __libc_start_main@GLIBC_2.34
0000000000000000  w      *UND*  0000000000000000              _ITM_deregisterTMCloneTable
0000000000004008  w      .data  0000000000000000              data_start
0000000000004018 g     O .data  0000000000000004              b
000000000000401c g       .data  0000000000000000              _edata
00000000000011a0 g     F .fini  0000000000000000              .hidden _fini
0000000000000000       F *UND*  0000000000000000              printf@GLIBC_2.2.5
0000000000004008 g       .data  0000000000000000              __data_start
0000000000000000  w      *UND*  0000000000000000              __gmon_start__
0000000000004010 g     O .data  0000000000000000              .hidden __dso_handle
0000000000002000 g     O .rodata        0000000000000004              _IO_stdin_used
0000000000004020 g       .bss   0000000000000000              _end
0000000000001050 g     F .text  0000000000000022              _start
0000000000002004 g     O .rodata        0000000000000004              a
000000000000401c g       .bss   0000000000000000              __bss_start
0000000000001139 g     F .text  0000000000000065              main
0000000000004020 g     O .data  0000000000000000              .hidden __TMC_END__
0000000000000000  w      *UND*  0000000000000000              _ITM_registerTMCloneTable
0000000000000000  w    F *UND*  0000000000000000              __cxa_finalize@GLIBC_2.2.5
0000000000001000 g     F .init  0000000000000000              .hidden _init

.rodata is a section without a wrote permission but .data is not read only:

❯ objdump -h foo

foo:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .note.gnu.property 00000020  0000000000000350  0000000000000350  00000350  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .note.gnu.build-id 00000024  0000000000000370  0000000000000370  00000370  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .interp       0000001c  0000000000000394  0000000000000394  00000394  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .gnu.hash     00000024  00000000000003b0  00000000000003b0  000003b0  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .dynsym       000000a8  00000000000003d8  00000000000003d8  000003d8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  5 .dynstr       0000008f  0000000000000480  0000000000000480  00000480  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  6 .gnu.version  0000000e  0000000000000510  0000000000000510  00000510  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  7 .gnu.version_r 00000030  0000000000000520  0000000000000520  00000520  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  8 .rela.dyn     000000c0  0000000000000550  0000000000000550  00000550  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  9 .rela.plt     00000018  0000000000000610  0000000000000610  00000610  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 10 .init         00000017  0000000000001000  0000000000001000  00001000  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 11 .plt          00000020  0000000000001020  0000000000001020  00001020  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 12 .plt.got      00000008  0000000000001040  0000000000001040  00001040  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 13 .text         0000014e  0000000000001050  0000000000001050  00001050  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 14 .fini         00000009  00000000000011a0  00000000000011a0  000011a0  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 15 .rodata       0000001d  0000000000002000  0000000000002000  00002000  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 16 .eh_frame_hdr 0000002c  0000000000002020  0000000000002020  00002020  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 17 .eh_frame     000000ac  0000000000002050  0000000000002050  00002050  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 18 .note.ABI-tag 00000020  00000000000020fc  00000000000020fc  000020fc  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 19 .init_array   00000008  0000000000003dd0  0000000000003dd0  00002dd0  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 20 .fini_array   00000008  0000000000003dd8  0000000000003dd8  00002dd8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 21 .dynamic      000001e0  0000000000003de0  0000000000003de0  00002de0  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 22 .got          00000028  0000000000003fc0  0000000000003fc0  00002fc0  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 23 .got.plt      00000020  0000000000003fe8  0000000000003fe8  00002fe8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 24 .data         00000014  0000000000004008  0000000000004008  00003008  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 25 .bss          00000004  000000000000401c  000000000000401c  0000301c  2**0
                  ALLOC
 26 .comment      0000001f  0000000000000000  0000000000000000  0000301c  2**0
                  CONTENTS, READONLY

a is placed in .rodata section, next to the section we can see READONLY feature, which make it perfectly soutable for const variable. b is placed in .data section we do not have READONLY feature enabled there, making it better suited for modifiable variable.

So far so good, we should understand up to this point how const keyword can affect section that variable is assigned to, but let's do not forget that each section and variable will be eventually mapped to a memory segment when the program will be executed, so let's see segments that our variables end up in:

❯ readelf -l foo

Elf file type is EXEC (Executable file)
Entry point 0x401040
There are 14 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x0000000000000310 0x0000000000000310  R      0x8
  INTERP         0x0000000000000394 0x0000000000400394 0x0000000000400394
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x0000000000000500 0x0000000000000500  R      0x1000
  LOAD           0x0000000000001000 0x0000000000401000 0x0000000000401000
                 0x0000000000000195 0x0000000000000195  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000402000 0x0000000000402000
                 0x0000000000000114 0x0000000000000114  R      0x1000
  LOAD           0x0000000000002df8 0x0000000000403df8 0x0000000000403df8
                 0x0000000000000224 0x0000000000000228  RW     0x1000
  DYNAMIC        0x0000000000002e08 0x0000000000403e08 0x0000000000403e08
                 0x00000000000001d0 0x00000000000001d0  RW     0x8
  NOTE           0x0000000000000350 0x0000000000400350 0x0000000000400350
                 0x0000000000000020 0x0000000000000020  R      0x8
  NOTE           0x0000000000000370 0x0000000000400370 0x0000000000400370
                 0x0000000000000024 0x0000000000000024  R      0x4
  NOTE           0x00000000000020f4 0x00000000004020f4 0x00000000004020f4
                 0x0000000000000020 0x0000000000000020  R      0x4
  GNU_PROPERTY   0x0000000000000350 0x0000000000400350 0x0000000000400350
                 0x0000000000000020 0x0000000000000020  R      0x8
  GNU_EH_FRAME   0x0000000000002020 0x0000000000402020 0x0000000000402020
                 0x000000000000002c 0x000000000000002c  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x0000000000002df8 0x0000000000403df8 0x0000000000403df8
                 0x0000000000000208 0x0000000000000208  R      0x1

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .note.gnu.property .note.gnu.build-id .interp .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
   03     .init .plt .text .fini
   04     .rodata .eh_frame_hdr .eh_frame .note.ABI-tag
   05     .init_array .fini_array .dynamic .got .got.plt .data .bss
   06     .dynamic
   07     .note.gnu.property
   08     .note.gnu.build-id
   09     .note.ABI-tag
   10     .note.gnu.property
   11     .eh_frame_hdr
   12
   13     .init_array .fini_array .dynamic .got

a is in segment four, because .rodata is in segment four which starts at 0x00402000 and ands at 0x00402114:

  LOAD           0x0000000000002000 0x0000000000402000 0x0000000000402000
                 0x0000000000000114 0x0000000000000114  R      0x1000

b is in segment five because .data section is assigned to segment five which starts at 0x00403df8 and ands at 0x00404020:

  LOAD           0x0000000000002df8 0x0000000000403df8 0x0000000000403df8
                 0x0000000000000224 0x0000000000000228  RW     0x1000

If we look on our executable output again we can confirm our conclusions, each variable is within the expected segement's range:

main=0x401126
a=0x402004 // ends at 0x402007
b=0x404018 // ends at 0x40401B

At this point you should understand why const variable is in different virtual memory segment than not const variable and how these segments map to sections and symbols in ELF object files on disk. Ahead of us last part, where we will customize the memory layout of our program to move .data section above the .rodata section and we will change the base virtual address for these sections. Let's end up this part with nice picture wich summarize whole process: ELF compilation linkage and runtime

Memory layout customization

Once compiler produces a relocatable file we can influance where sections like .data or .rodata end up in virtual memory, first create file called script.ld:

/* script.ld */
ENTRY(_start)
SECTIONS
{
  . = 0x400000; /* typical ET_EXEC base */
  .text : { *(.text .text.*) *(.plt) }
  . = 0x500000;
  .data : { *(.data .data.*) }
  .bss  : { *(.bss .bss.*) }
  . = 0x600000;
  .rodata : { *(.rodata .rodata.*) *(COMMON) }
}

So let's compile our foo.c with new linker script:

gcc -no-pie foo.c -T script.ld -o foo

We can confirm that memory layout changed:

❯ ./foo
main=0x4000e6
a=0x600004
b=0x500010

It is possible because the program headers of executable changed:

  LOAD           0x0000000000002000 0x0000000000500000 0x0000000000500000
                 0x0000000000000228 0x0000000000000229  RW     0x1000
  LOAD           0x0000000000003000 0x0000000000600000 0x0000000000600000
                 0x00000000000002d8 0x00000000000002d8  R      0x1000

As you can see the .data section is now in segment starting at 0x00500000 when .rodata section is now in segment starting at 0x00600000.

There is much more that one can customize with linker script, if you're interested checkout the webiste that i learned from colorado.edu.

Conclusions

We walked through how C code becomes an ELF file, how sections turn into segments, and how those segments end up in process memory. We saw why const data lands in .rodata, why variables end up in .data or .bss, and how all of this maps into virtual memory at runtime.

Once you understand that, you stop treating memory as “mystery addresses” and start seeing it as a structure you can read, predict, and even shape with linker scripts.

Memory layout in C isn’t random, it’s the result of how ELF files are built, linked, and loaded.

Published:

Category: Linux

Comments

Leave a comment