-
Notifications
You must be signed in to change notification settings - Fork 0
Add new post: More about program memory #431
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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: | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Count syscalls with strace’s built‑in
-# 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| 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. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
| \*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
AI
Aug 26, 2025
There was a problem hiding this comment.
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: '|------------------|-------------------|'
| |----------------- |------------------ | | |
| |------------------|-------------------| |
There was a problem hiding this comment.
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.
| 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.
Copilot
AI
Aug 26, 2025
There was a problem hiding this comment.
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.
| ``` | |
| ... |
There was a problem hiding this comment.
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.
🌐 Web query:
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):
📝 Committable suggestion
🤖 Prompt for AI Agents