這邊簡單的介紹 C 的記憶體管理機制與 Process 在 memory 中的布局與分布情形,之所以在這邊進行介紹,是為了方便理解後面的 object file 的資料區段與分布,以及了解後面介紹的記憶體分頁分配是針對於哪一個區域進行分配,sbrk()
是針對哪一個地方進行操作。接著比較 C 記憶體管理與 xv6 中 Process 的記憶體管理。
Process 記憶體分布可以看作如下圖
而上方 Kernel 的部分進行展開,並與 Process 的 virtual memory address 一起看待,則會如下圖所示
以下將討論 Process virtual memory 的部分 (這邊可以理解成 ELF 格式的 memory layout),表達的概念是 virtual memory。
又被稱為 code segment,包含許多可執行的指令,這一些指令都是 machine-language instructions,也就是 CPU 執行的指令 (編譯後可以執行的機器語言指令),由於可能多個程式會執行到相同的程式 (例如 swap 之類常常被使用到的指令),因此這個區塊設定上為共享的,多個 Process 執行時,在記憶體中有一份 code 就可以被所有 Process 利用了,可以讓此區的程式碼映射到 Process 所屬於的 address space中。這一些指令會形成最後執行檔的一部分。
通過上圖觀察可以發現到 Text segment 位於 stack 和 heap 的下方,原因在於避免因為 stack 或是 heap 的 overflow 覆蓋掉 Text segment 的內容。
除了這一層的保護,另外 Text segment 被設計成唯讀的,原因在於避免程式在執行的過程中,對 CPU 的指令進行一些不當的修改操作。
又被稱為 Data segment,Data segment 也是 Process 的 virtual address space 的一部分,裡面會包含一些全域變數以及一些已經初始化的靜態變數。
這個區塊可以拆分成兩個區塊,分別為唯讀區域以及可讀寫區域。
char *str = "hello"
, const int i = 0
。char str[] = "hello"
, 全域變數以及靜態變數也是位於此區域。又被稱為 BSS segment,BSS 為 block started by symbol 的簡稱,裡面儲存沒有經過顯式 (explicit) 初始化的變數,靜態變數 int i
或是全域變數 int a
等等會被儲存在 BSS segment 中。在這個區塊內的變數會在程式執行之前被 kernel 初始化成 0 或是 null
(將其記憶體初始化成0)。
stack segment 包含程式的 stack (為一種先進後出 FILO 的結構),通常會放在記憶體位置中高位記憶體的位置不斷的向下增長,存放 stack pointer 的暫存器 (在 RISC-V 中為 sp
暫存器) 會指向到 stack 的頂端,每一次有值 push 到 stack 中時,存有 stack pointer 的暫存器都需要去調整其指向的位置,而如果這一些值都是源於某一個 function,則這一些推入 stack 的值所形成的集合就被稱為 stack frame,在 stack frame 會包含 function 的 return address。也就是 caller 的所在記憶體地址。讓 callee 能夠順利的 return。
stack segment 會存放 function 被呼叫時所產生的資訊,像是上方提及的 return address,也就是 caller 的記憶體地址,caller 暫存器的資訊,在 function 內的區域變數,參數等等都會存放在 stack segment 中。新呼叫的 function 會在 stack segment 開闢出一塊空間存放 function 的相關資訊,在 C 語言中的 function 呼叫以及 recursive 就是使用此方式,而也因為這樣 function 之間的隔離機制,因此在其中一個 function 中對某一個變數的修改並不會影響到在其他 function 的結果。
<Journey to the Stack, Part I>
上面這張圖,是以 x86 的架構看待,可以看到 return address 存放到 ebp
+ 4 的地方,而可以從 esp
和記憶體之間的關係中看見 stack 是往記憶體低位不斷的增長。
(題外話 : 如果我們使用一些未檢查記憶體邊界的函式,如scanf
,就可以通過覆蓋掉 ebp
+ 4 的值,也就是覆蓋掉 return address,讓程式跳轉到任意我們想要跳轉的地方,這是scanf
沒有對界線進行檢查的危險之處。)
我們可以使用 function call 來驗證 stack 是否為向下生長
#include <stdio.h>
#include <stdlib.h>
void f1()
{
int c = 1;
}
void f2()
{
int d = 2;
}
int main(void)
{
f1();
f2();
return 0;
}
───────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x555555555119 <__do_global_dtors_aux+57> nop DWORD PTR [rax+0x0]
0x555555555120 <frame_dummy+0> endbr64
0x555555555124 <frame_dummy+4> jmp 0x5555555550a0 <register_tm_clones>
→ 0x555555555129 <f1+0> endbr64
0x55555555512d <f1+4> push rbp
0x55555555512e <f1+5> mov rbp, rsp
0x555555555131 <f1+8> mov DWORD PTR [rbp-0x4], 0x1
0x555555555138 <f1+15> nop
0x555555555139 <f1+16> pop rbp
───────────────────────────────────────────────────────────────────── source:test.c+5 ────
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 void f1()
→ 5 {
6 int c = 1;
7 }
8
9 void f2()
10 {
───────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "test", stopped 0x555555555129 in f1 (), reason: BREAKPOINT
─────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x555555555129 → f1()
[#1] 0x55555555515f → main()
──────────────────────────────────────────────────────────────────────────────────────────
可以看到 f1()
位於記憶體地址 0x555555555129,main()
位於 0x55555555515f, f1()
的地址低於 main()
的地址,可以知道整個 stack 是不斷往記憶體地址低位的地方不斷地生長的 (值得注意的是這裡的記憶體地址都是指虛擬記憶體地址),下圖可以很好的表示這個情況。
而如果觀察函式的呼叫順序,會發現到 d()
函式是最後被呼叫,卻是最先離開 stack 區域的,可以發現到有後進先出的特性,這也是這個區域被稱作為 stack 區域的原因。
heap 為用來配置動態記憶體空間的區段,heap 從 BSS 區段的最高位記憶體地址開始向上增長,heap segment 管理包含 malloc
, realloc
, free
等等動態記憶體,heap 的最上方如圖所示,為 program break,program break 的意義為 the program break is the first location after the end of the uninitialized data segment。program break 可以藉由brk()
,sbrk()
等 System call 來修改,而修改了 program break 就相當於修改了一個 process 的 heap 大小,在 program break 走向高位記憶體地址後,process 就可以存取這一片新的區域內的記憶體地址,而這時候實體物理記憶體分頁還沒有分配,kernel 會對 process 首次試圖存取的這一些虛擬記憶體地址分配物理記憶體分頁,也就是在 xv6 paging 篇章中提及的操作。
而對於 heap 的操作,上方提及了malloc
等函式,而實際上malloc
並非 System call,底下隱藏了一些細節部分,像是brk
等 System call,這一部分將在後續提及。
我們試著找出 stack 與 heap在虛擬記憶體中的位置,我們知道一個區域變數會在 stack 上,malloc
的區塊會在 heap 上。
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int a;
int *b = (int *)malloc(sizeof(int));
printf("%p\n", &a);
printf("%p\n", b);
return 0;
}
output
0x7fffc27839ec
0x55560761b2a0
接著使用 gdb 得到暫存器以及 stack 的相關資訊
[----------------------------------registers-----------------------------------]
RAX: 0x555555555189 (<main>: endbr64)
RBX: 0x555555555200 (<__libc_csu_init>: endbr64)
RCX: 0x555555555200 (<__libc_csu_init>: endbr64)
RDX: 0x7fffffffe528 --> 0x7fffffffe785 ("SHELL=/bin/bash")
RSI: 0x7fffffffe518 --> 0x7fffffffe763 ("/home/hank/Desktop/C_program/test")
RDI: 0x1
RBP: 0x0
RSP: 0x7fffffffe428 --> 0x7ffff7de5083 (<__libc_start_main+243>: mov edi,eax)
RIP: 0x555555555189 (<main>: endbr64)
R8 : 0x0
R9 : 0x7ffff7fe0d60 (<_dl_fini>: endbr64)
R10: 0x7ffff7ffcf68 --> 0x6ffffff0
R11: 0x202
R12: 0x5555555550a0 (<_start>: endbr64)
R13: 0x7fffffffe510 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x555555555179 <__do_global_dtors_aux+57>: nop DWORD PTR [rax+0x0]
0x555555555180 <frame_dummy>: endbr64
0x555555555184 <frame_dummy+4>: jmp 0x555555555100 <register_tm_clones>
=> 0x555555555189 <main>: endbr64
0x55555555518d <main+4>: push rbp
0x55555555518e <main+5>: mov rbp,rsp
0x555555555191 <main+8>: sub rsp,0x20
0x555555555195 <main+12>: mov rax,QWORD PTR fs:0x28
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe428 --> 0x7ffff7de5083 (<__libc_start_main+243>: mov edi,eax)
0008| 0x7fffffffe430 --> 0x100000044
0016| 0x7fffffffe438 --> 0x7fffffffe518 --> 0x7fffffffe763 ("/home/hank/Desktop/C_program/test")
0024| 0x7fffffffe440 --> 0x1f7fa97a0
0032| 0x7fffffffe448 --> 0x555555555189 (<main>: endbr64)
0040| 0x7fffffffe450 --> 0x555555555200 (<__libc_csu_init>: endbr64)
0048| 0x7fffffffe458 --> 0x3e184c726e90ef0c
0056| 0x7fffffffe460 --> 0x5555555550a0 (<_start>: endbr64)
[------------------------------------------------------------------------------]
在 x86 架構中,RBP 指向 stack 的底部,可以從上圖得知 stack 底部對應到該 process 虛擬記憶體地址的 0x0。RSP 指向 stack 的頂部,對應到 0x7fffffffe428,而 malloc
出來的記憶體地址指向到 0x55560761b2a0 ,因此我們可以得知 heap 區域是位於 stack 區域以下的。
上面這一張圖為 xv6 的 process virtual address space,在 xv6 process virtual address space 我們介紹了 trampoline 和 trapframe 大致上的功能 (我們將會在 trap 篇章中特別關注這兩個記憶體分頁區塊),我們發現在 C 的記憶體管理和上圖都存在 heap 和 user stack 的區塊,而在 C 語言中 malloc()
藏有 brk()
這樣的 System call,而在 xv6 中有 sbrk()
去改變 heap 大小 (改變 program break) 的函式,我們將在後面 page fault 中通過 lazy page allocation 來了解到 sbrk()
的使用,進而對 malloc()
行為有進一步的理解。
CS61C L05 C Memory Management
kernel-stack-and-user-space-stack
Memory Layout of C Programs