iT邦幫忙

2022 iThome 鐵人賽

DAY 13
0

前言

這邊簡單的介紹 C 的記憶體管理機制與 Process 在 memory 中的布局與分布情形,之所以在這邊進行介紹,是為了方便理解後面的 object file 的資料區段與分布,以及了解後面介紹的記憶體分頁分配是針對於哪一個區域進行分配,sbrk()是針對哪一個地方進行操作。接著比較 C 記憶體管理與 xv6 中 Process 的記憶體管理。

Overview

Process 記憶體分布可以看作如下圖

< Process memory layout >

而上方 Kernel 的部分進行展開,並與 Process 的 virtual memory address 一起看待,則會如下圖所示

< CSAPP 3/e >

以下將討論 Process virtual memory 的部分 (這邊可以理解成 ELF 格式的 memory layout),表達的概念是 virtual memory。

Text segment

又被稱為 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 的指令進行一些不當的修改操作。

Initialized Data Segment

又被稱為 Data segment,Data segment 也是 Process 的 virtual address space 的一部分,裡面會包含一些全域變數以及一些已經初始化的靜態變數。

這個區塊可以拆分成兩個區塊,分別為唯讀區域以及可讀寫區域。

  • 唯讀區域 : 存放一些固定的常數,像是 char *str = "hello", const int i = 0
  • 可讀寫區域 : 存放一些可以隨著程式執行時改變的變數,像是 char str[] = "hello", 全域變數以及靜態變數也是位於此區域。

Uninitialized Data Segment

又被稱為 BSS segment,BSS 為 block started by symbol 的簡稱,裡面儲存沒有經過顯式 (explicit) 初始化的變數,靜態變數 int i 或是全域變數 int a 等等會被儲存在 BSS segment 中。在這個區塊內的變數會在程式執行之前被 kernel 初始化成 0 或是 null (將其記憶體初始化成0)。

stack

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 為用來配置動態記憶體空間的區段,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 與 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 區域以下的。

Memory layout of xv6 process


上面這一張圖為 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() 行為有進一步的理解。

reference

CS61C L05 C Memory Management
kernel-stack-and-user-space-stack
Memory Layout of C Programs


上一篇
Day-11 xv6 process virtual address space
下一篇
Day-13 Exception vs Interrupt, Trap overview Driver
系列文
與作業系統的第一類接觸 : 探索 xv631
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言