Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions posts/2025-08-09-more-about-program-memory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
---
title: "More about program memory layout"
date: "2025-08-09"
show: true
tags: ["linux", "os", "low-level", "memory"]
---

### Changelog

- [2025-08-09 Sat] First version released.

## Introduction

Last month I wrote a post about program memory layout [1], that introduces the core concepts regarding how programs are structured at the computer memory when a new program instance is created, what are the sections and the layout, etc.

But I felt that the explanation there was too simplistic, so I decided to keep searching for more detailed and practical references. In essence, I was looking for something that used real-life examples. Eventually, I found a new video from a presentation at NDC [2] that fortunately talks about this same topic, but now in a deeper level. And this post is the result of my notes from this new reference.

## Process address space

At a top level, the process address space is divided into two parts, the **kernel space** and the **user space** (which contains the program that is being executed). And the switch between those two sections happens whenever our program, running at the user space makes a *system call*.

```bash
# +--------------------+
# | |
# | Kernel space |
# | |
# +--------------------+
# | |
# | User space |
# | |
# +--------------------+
```

On Linux using 64-bit processors and x86-64 architecture, by default the memory is split 50% for the kernel space and 50% for the user space. This is configurable using some flags.

And why we map both the kernel and the user program memory into a single memory space? The answer is that this brings some performance benefits:
Comment on lines +34 to +36

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

❓ Verification inconclusive

Qualify the “50/50 split” and the “configurable flags” claim.

On x86‑64 with 4‑level paging (48‑bit canonical addresses), Linux maps user space in the lower half and kernel in the upper half (effectively 50/50 of the implemented VA, not the theoretical 64 bits). With 5‑level paging (57‑bit), the split and limits differ. Calling this “configurable using some flags” is vague and may mislead; it isn’t a simple runtime toggle and depends on kernel build/CPU features.

-On Linux using 64-bit processors and x86-64 architecture, by default the memory is split 50% for the kernel space and 50% for the user space. This is configurable using some flags.
+On Linux/x86‑64, with the commonly deployed 4‑level page tables (48‑bit canonical addresses), user space occupies the lower canonical half and the kernel the upper canonical half (effectively a 50/50 split of the implemented VA width). With 5‑level paging (57‑bit), limits and layout differ. These are architecture/kernel‑configuration decisions rather than simple runtime flags.

🌐 Web query:

Confirm the user/kernel virtual address split for Linux on x86-64 with 4-level and 5-level paging and whether it’s adjustable via kernel config or runtime flags.

Error during web search


Clarify x86-64 VA split and remove vague flags claim

On Linux/x86-64, the “50/50 split” refers to equal halves of the implemented virtual address space—under 4-level paging (48-bit canonical addresses), the lower 2¹⁴⁷ bytes (~128 TiB) are for user-space and the upper 2¹⁴⁷ bytes for kernel-space. With 5-level paging (57-bit), enabled via the CONFIG_X86_5LEVEL kernel configuration, the VA space grows to 2⁵⁷ bytes (~128 PiB) and is likewise split into two 2⁵⁶-byte halves (~64 PiB each). These divisions are determined by CPU architecture and build-time settings, not by simple runtime flags. (mjmwired.net, docs.kernel.org)

Please update the text accordingly (in posts/2025-08-09-more-about-program-memory.md):

-On Linux using 64-bit processors and x86-64 architecture, by default the memory is split 50% for the kernel space and 50% for the user space. This is configurable using some flags.
+On Linux/x86-64, with the standard 4-level page tables (48-bit canonical addresses), user-space occupies the lower 2¹⁴⁷ bytes (~128 TiB) and the kernel the upper 2¹⁴⁷ bytes. With 5-level paging (57-bit), enabled via CONFIG_X86_5LEVEL, the VA space extends to 2⁵⁷ bytes (~128 PiB) and is similarly divided into 2⁵⁶-byte halves (~64 PiB each). These layouts arise from the CPU architecture and kernel build-time configuration rather than simple runtime flags.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
On Linux using 64-bit processors and x86-64 architecture, by default the memory is split 50% for the kernel space and 50% for the user space. This is configurable using some flags.
And why we map both the kernel and the user program memory into a single memory space? The answer is that this brings some performance benefits:
On Linux/x86-64, with the standard 4-level page tables (48-bit canonical addresses), user-space occupies the lower 2¹⁴⁷ bytes (~128 TiB) and the kernel the upper 2¹⁴⁷ bytes. With 5-level paging (57-bit), enabled via CONFIG_X86_5LEVEL, the VA space extends to 2⁵⁷ bytes (~128 PiB) and is similarly divided into 2⁵⁶-byte halves (~64 PiB each). These layouts arise from the CPU architecture and kernel build-time configuration rather than simple runtime flags.
And why we map both the kernel and the user program memory into a single memory space? The answer is that this brings some performance benefits:
🤖 Prompt for AI Agents
In posts/2025-08-09-more-about-program-memory.md around lines 34 to 36, replace
the vague “50% for the kernel/user” and “some flags” wording with a precise
explanation: state that on x86-64 the default split refers to equal halves of
the implemented virtual address space (under 4-level paging/48-bit canonical
addresses the lower 2^47 bytes are user and the upper 2^47 bytes are kernel;
with 5-level paging/57-bit addresses enabled via CONFIG_X86_5LEVEL the VA space
grows accordingly to 2^57 bytes and is split into two 2^56-byte halves), and
clarify that these divisions are determined by CPU architecture and kernel
build-time configuration rather than simple runtime flags; remove the original
“some flags” claim and adjust the transition sentence leading into the
performance benefits accordingly.


- Cheaper mode switch
- Cheaper context switch (kernel stays mapped)
- Less trashing TLB (Translation Lookaside Buffer)

In modern Linux, for security reasons, the entire kernel is usually not always mapped. If you'd like to get a better understanding of this, search for Linux KPTI (Linux Kernel Page-Table Isolation).

**Kernel space**

- Contains the OS kernel, which is called through system calls;
- How to check how many system calls a program does? Use **strace**;

```c
#include <stdio.h>

// compilation:
// clang-18 main.c -o userProgram
int main() {
printf("Hello, world!\n");
return 0;
}
```

And after compiling we can use:

```bash
# compile the program
clang-18 main.c -o userProgram

# show the syscalls performed by the process
strace ./userProgram

# and count the quantity of syscalls
strace ./userProgram 2>&1 | wc -l
# which gives me 37 syscalls performed
```
Comment on lines +62 to +72

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Count syscalls with strace’s built‑in -c rather than wc -l.

wc -l counts lines, not syscalls, and includes non‑syscall output. strace -c summarizes syscall counts and time.

-# show the syscalls performed by the process
-strace ./userProgram
-
-# and count the quantity of syscalls
-strace ./userProgram 2>&1 | wc -l
-# which gives me 37 syscalls performed
+# show the syscalls performed by the process
+strace -o /dev/stderr ./userProgram
+
+# and count/summary of syscalls
+strace -qq -c ./userProgram
+# sample output: calls, errors, total time per syscall
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```bash
# compile the program
clang-18 main.c -o userProgram
# show the syscalls performed by the process
strace ./userProgram
# and count the quantity of syscalls
strace ./userProgram 2>&1 | wc -l
# which gives me 37 syscalls performed
```
🤖 Prompt for AI Agents
In posts/2025-08-09-more-about-program-memory.md around lines 62 to 72, the
guide uses `strace ./userProgram 2>&1 | wc -l` to count syscalls which is
incorrect because wc counts lines and can include non‑syscall output; replace
that step with using strace’s built‑in summary mode (`strace -c ./userProgram`)
and update the explanatory text to say to read the syscall counts from the
strace summary output (and adjust the stated “37 syscalls” result if the summary
reports a different count).


The output of using the *strace* tool will be a big chunk of text, showing all the syscalls performed by the process. And a bunch of those are related to setting the program to execute properly.

\*Something to keep in mind is that executing syscalls is not a cheap operation, so in order to improve a program's performance we can try to minimize the quantity of those.

Copilot AI Aug 26, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The escaped asterisk should be plain text. Remove the backslash: '*Something to keep in mind...'

Suggested change
\*Something to keep in mind is that executing syscalls is not a cheap operation, so in order to improve a program's performance we can try to minimize the quantity of those.
*Something to keep in mind is that executing syscalls is not a cheap operation, so in order to improve a program's performance we can try to minimize the quantity of those.

Copilot uses AI. Check for mistakes.

## Interesting tools

During this talk multiple interesting tools were presented. At this section I add the most useful and cool.

### Check your CPU

If you want to check your CPU's architecture, you can use the program *lscpu*:

```bash
# Display information about the CPU architecture.
lscpu
```

The result presents many details of your CPU, including the total cache size discriminated by the cache level, and the address sizes, which in my case uses 48 bits.

![lscpu result](/post-images/more-about-program-memory/lscpu-001.png "lscpu result")

But wait. How big is the process address space? The following table must help understand.

| 32-bit processors | 64-bit processors |
|----------------- |------------------ |

Copilot AI Aug 26, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The table separator has inconsistent dashes. Should be consistent: '|------------------|-------------------|'

Suggested change
|----------------- |------------------ |
|------------------|-------------------|

Copilot uses AI. Check for mistakes.
| 2^32 bytes = 4GB | 2^48 bytes = 256TB |
| | 2^57 bytes = 4PB |

As you can see, there are currently two possibilities for 64-bit processors, and both use less than 64 bits. Why don't we have 2^64 bytes on 64-bit processors?

Well, the explanation is related to current implementation limitations. Even though 2^64 (16 exabytes) is the theoretical limit for this architecture, 48 and 57 bits is what current implementations support for practical reasons.

### Inspect the memory layout of a Linux process

In Linux, there's this tool called *pmap*, which reports the memory map of a process.

```c
#include <stdio.h>

// compilation:
// clang-18 main.c -o userProgram
int main() {
printf("Hello, world!\n");
getchar(); // so we have time to check the memory map of the program
return 0;
}
```

And to see the memory mapping of this program:

```bash
# compile the program
clang-18 main.c -o userProgram

# see memory mappings
./userProgram & pmap $! -XX
```

Where the result, as presented by Piotr is:

![pmap result](/post-images/more-about-program-memory/pmap-001.png "pmap result")

Notice that in this table, except from the first memory address which is 64-bit size, the others are 48-bit size, as expected.

Other than that, something else to keep in mind is that, in Linux, memory addresses with 48-bits, where the most significant bit is 1 (48th bit) are associated with the kernel space. If this bit is 0, then this memory address is associated with the user space.

Comment on lines +136 to +139

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

❓ Verification inconclusive

Fix canonical-addressing explanation (bit numbering and zero‑padding).

  • pmap prints 64‑bit zero‑padded values; user addresses “look” 64‑bit even if only 48 bits are canonical.
  • On x86‑64 with 4‑level paging, bit 47 being 1 means the upper canonical half; bits 63..48 are sign‑extended to 1. Calling it “the 48th bit is 1 ⇒ kernel” is imprecise and kernel/user is a convention, not solely the bit value.
-Notice that in this table, except from the first memory address which is 64-bit size, the others are 48-bit size, as expected.
-Other than that, something else to keep in mind is that, in Linux, memory addresses with 48-bits, where the most significant bit is 1 (48th bit) are associated with the kernel space. If this bit is 0, then this memory address is associated with the user space.
+Note: pmap prints zero‑padded 64‑bit hex addresses; user addresses are still displayed as 64‑bit even though only the lower 48 bits are canonical (with 4‑level paging).
+On x86‑64 (4‑level), canonical addresses have bits 63..48 equal to bit 47. Addresses with bit 47 = 0 belong to the lower canonical half (user space on Linux), and bit 47 = 1 to the upper canonical half (where Linux maps the kernel).

🌐 Web query:

Verify canonical address rules on x86-64 (sign-extension of bit 47 for 4-level, bit 56 for 5-level) from AMD64/Intel manuals or Linux memory layout docs.

Error during web search


Clarify x86-64 canonical address explanation (bit numbering and zero-padding)

File posts/2025-08-09-more-about-program-memory.md, lines 136–139

• pmap prints full 64-bit zero-padded hex addresses; user-space pointers still look 64-bit even though only the lower 48 bits are valid (4-level paging).
• On x86-64 with 4-level paging, valid “canonical” addresses must have bits 63..48 set equal to bit 47 (sign-extension).
– bit 47 = 0 ⇒ addresses 0x0000_0000_0000_0000…0x0000_7FFF_FFFF_FFFF (lower half; user space conventionally)
– bit 47 = 1 ⇒ addresses 0xFFFF_8000_0000_0000…0xFFFF_FFFF_FFFF_FFFF (upper half; kernel space conventionally)
• Referring to “the 48th bit” is imprecise—canonicality is enforced by sign-extending bit 47 into bits 48–63, and user/kernel mapping is a Linux convention, not an inherent hardware rule.

Suggested diff:

-Notice that in this table, except from the first memory address which is 64-bit size, the others are 48-bit size, as expected.
-Other than that, something else to keep in mind is that, in Linux, memory addresses with 48-bits, where the most significant bit is 1 (48th bit) are associated with the kernel space. If this bit is 0, then this memory address is associated with the user space.
+Note: pmap prints zero-padded 64-bit hex addresses; user-space pointers still appear 64-bit even though only the lower 48 bits are canonical (4-level paging).
+On x86-64 with 4-level paging, canonical addresses require bits 63..48 to replicate bit 47 (sign-extension):
+  - bit 47 = 0 ⇒ 0x0000_0000_0000_0000…0x0000_7FFF_FFFF_FFFF (lower canonical half; user space by Linux convention)
+  - bit 47 = 1 ⇒ 0xFFFF_8000_0000_0000…0xFFFF_FFFF_FFFF_FFFF (upper canonical half; kernel space by Linux convention)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Notice that in this table, except from the first memory address which is 64-bit size, the others are 48-bit size, as expected.
Other than that, something else to keep in mind is that, in Linux, memory addresses with 48-bits, where the most significant bit is 1 (48th bit) are associated with the kernel space. If this bit is 0, then this memory address is associated with the user space.
Note: pmap prints zero-padded 64-bit hex addresses; user-space pointers still appear 64-bit even though only the lower 48 bits are canonical (4-level paging).
On x86-64 with 4-level paging, canonical addresses require bits 63..48 to replicate bit 47 (sign-extension):
- bit 47 = 0 ⇒ 0x0000_0000_0000_0000…0x0000_7FFF_FFFF_FFFF (lower canonical half; user space by Linux convention)
- bit 47 = 1 ⇒ 0xFFFF_8000_0000_0000…0xFFFF_FFFF_FFFF_FFFF (upper canonical half; kernel space by Linux convention)
🤖 Prompt for AI Agents
In posts/2025-08-09-more-about-program-memory.md around lines 136–139, update
the paragraph to correctly describe x86-64 canonical addresses and pmap output:
explain that pmap prints full 64-bit zero‑padded hex addresses so user pointers
appear 64-bit though only the low 48 bits (4-level paging) are meaningful;
replace the imprecise “48th bit” wording with the canonicality rule that bits
63..48 must equal bit 47 (sign‑extension), and show the two ranges: bit47=0 →
0x0000_0000_0000_0000..0x0000_7FFF_FFFF_FFFF (lower half, user space convention)
and bit47=1 → 0xFFFF_8000_0000_0000..0xFFFF_FFFF_FFFF_FFFF (upper half, kernel
space convention); also note that the user/kernel division is a Linux convention
rather than a hardware rule and that canonicality is enforced by sign‑extension
of bit47 into bits48–63.

Next, after the column that presents the memory address for where that section starts, we have a column showing the permissions for the memory location, which could be the conventional read, write and execute, and there's an additional setting that defines if the memory address can be shared with other processes or not (p is for private).

Then, we have the offset column, which is only relevant if the memory address is associated with a file, and it defines what is the offset in the file that is being mapped.

Next, the Inode in Linux specifies the file id. So, if the Inode repeats at this table it means that the same file is being mapped into the process' memory addresses, maybe changing only the offset.

Then we have the Size column, which specifies the size of the memory segment in bytes, and Rss specifies what part of this memory segment is actually in physical memory.

### Explore the ELF file

To check the sections that are inside the ELF file compiled we can use the *readelf* tool:

```bash
# check what are the sections of the ELF file:
readelf -S userProgram

# to display the program headers, which stores the information of WHAT to load
# WHERE:
readelf -l userProgram
```
...
Comment on lines +159 to +160

Copilot AI Aug 26, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code block is incomplete. The content after '...' on line 160 appears to be cut off, leaving an unfinished section before the References.

Suggested change
```
...

Copilot uses AI. Check for mistakes.

# References

- [1] - *Program memory layout*. 64J0's blog post [link](https://gaio.dev/posts/2025-07-10-program-memory-structure).
- [2] - *Demystifying Process Address Space: Heap, Stack, and Beyond - Piotr Wierciński - NDC TechTown 2024*. NDC Conferences. YouTube [link](https://youtu.be/pI-HRRh7-dU?si=KwFtDIxe5mJjx8Hq). You can check the presentation slides here: [laminowany/Slides](https://github.com/laminowany/Slides).
- [3] - *Linux Memory Management FAQ*. Hacker News thread [link](https://news.ycombinator.com/item?id=26117516). I found this link while looking for something that was not clear. It has many interesting discussions there, and this is related to this other page [landley memory faq](https://landley.net/writing/memory-faq.txt).
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading