---
# System prepended metadata

title: Linux 作業系統 Project
tags: [Linux]

---

# Linux 作業系統 Project
## 環境 
>系統 - 架設於Oracle VM VirtualBox
>>虛擬機器：VMware Workstation Pro
 作業系統：ubuntu 22.04
 Kernel 版本：5.15.135
 記憶體：8GB

---
## 需求
建立一個自己的System Call用以取得虛擬地址映射到的物理地址
撰寫一段程式觀察COW、Demand Paging這兩者是如何運作的

## 新增systemcall.c檔 & 新增system call清單

首先，這邊使用的是Ubuntu 22.04版，所以這邊我們先到Ubuntu的網站下載iso檔
https://releases.ubuntu.com/jammy/
安裝好以後，打開虛擬機 
使用指令的方式下載Kernel Code (或偏好使用直接開瀏覽器下載也可以)
``` 
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.135.tar.xz
```
對其進行解壓縮 (位置會在/home/(username))
```
tar -xvf linux-5.15.135.tar.xz -C ~/
```
更新Compile工具確保待會的作業不會因為版本造成問題
```
sudo apt update

sudo apt install build-essential libncurses-dev libssl-dev libelf-dev bison flex -y
```
>- build-essential 提供了 GCC 編譯器、GNU make、g++ 等基本開發工具。這是構建和編譯軟體（包括內核）的基礎工具包。
>- libncurses-dev 提供開發 ncurses的Header file。用於支援內核配置工具，這些工具以文本界面顯示內核選項，便於用戶選擇和修改。
>- libssl-dev 提供 OpenSSL 庫的開發文件和頭文件。
為內核和其他系統組件提供加密功能（如 HTTPS 支援、加密模組等）。
>- libelf-dev 提供 ELF 文件的處理庫。用於解析和處理內核模組與其他二進制文件的 ELF 格式。
>- bison  GNU bison 是一個語法分析器生成器。 用於解析和處理內核代碼中複雜的語法結構。
>- flex 一個快速詞法分析器生成器。 用於內核代碼中生成詞法分析器。
>- -y 自動確認所有安裝請求，避免用戶手動輸入 "yes"。


接著我們可以在終端使用指令：pwd 去確認我們當下的位置
```
pwd
```
![image](https://hackmd.io/_uploads/SyVxRKb4ke.png)
再來使用ls去列出當下路徑的檔案&資料夾
```
ls
```
![image](https://hackmd.io/_uploads/ByVQCFWV1e.png)
就可以使用cd進入我們要的linux-5.15.135資料夾了
```
cd linux-5.15.135
```
接著進到linux-5.15.135後，建立一個mycall的資料夾
```
mkdir mycall
```
![image](https://hackmd.io/_uploads/SJNMkcW4Je.png)

再進入mycall這個資料夾，建立一個名為my_get_physical_addresses的C語言檔案
內容參考後面附上的程式碼「my_get_physical_Address - 程式碼」

```
nano my_get_physical_addresses.c
```
程式撰寫完畢，並存檔退出後，在mycall資料夾中建立一個Makefile
```
nano Makefile
```
填入下述文字
```
obj-y := my_get_physical_addresses.o
```
接下來回到linux-5.15.135這層，找到原本的Makefile，進行修改
找到 ore-y			+= kernel/ certs/ mm/ fs/ ipc/ security/ 在其後新增 /mycall，如下圖
```
-- as if --

    core-y			+= kernel/ certs/ mm/ fs/ ipc/ security/ crypto/

-- to be --

    core-y			+= kernel/ certs/ mm/ fs/ ipc/ security/ crypto/ mycall/
```
![image](https://hackmd.io/_uploads/B1TWGqZNJx.png)

完畢後，到include/linux/syscalls.h修改內容
``` 
nano include/linux/syscalls.h
```
在結尾處加上下面這段文字，比其他人多了asmlinkage的原因是以assembly code 呼叫 C function，並且是用 stack 方式傳參數（parameter）情況下而需要
``` C
asmlinkage long sys_my_get_physical_addresses(unsigned long __user *usr_ptr);
```
![image](https://hackmd.io/_uploads/BynwgaGVyl.png)

>在 Linux 核心中，系統呼叫是由 組合語言（Assembly）實現，因此當使用 asmlinkage 修飾函數時：
>- 參數會通過 <font color=#FF6600>堆疊（Stack）</font>傳遞，而不是通過寄存器。
>- <font color=#FF6600>避免寄存器干擾</font>： 核心中的其他部分可能會使用寄存器來保存臨時數據。使用 asmlinkage 可以避免寄存器中的數據與系統呼叫的參數發生衝突。

接著打開 arch/x86/entry/syscalls/syscall_64.tbl，將我們這次寫的systemcall加入清單
```
nano arch/x86/entry/syscalls/syscall_64.tbl
```
找到x32 系統呼叫系列上方，依照編號加下去，如下圖中的位置
```
449	common	my_get_physical_addresses		sys_my_get_physical_addresses
```
![image](https://hackmd.io/_uploads/SJDU8c-Nkl.png)

## 編譯和替換kernel
使用make mrproper，清理編譯產物、刪除配置文件、恢復Kernel code到初始狀態
```
sudo make mrproper
```
### ※ 若.config有需要留著的，要記得先備份起來

**接著編譯設定檔**
將目前 Kernel Config 文件複製到當前目錄，最後生成此 kernel 的配置文件
為了避免建置大量不必要的 driver 和 kernel module，使用 localmodconfig 來節省時間
```
cd linux-5.15.135/
cp -v /boot/config-$(uname -r) .config
make localmodconfig
```

因為直接從 /boot/config-$(uname -r) 複製設定檔，所以設定檔裡面的設定的是 Debian 官方編譯 kernel 時憑證的路徑，若是直接編譯會報錯，因此這邊取消使用憑證，並將值設為空字串
```
scripts/config --disable SYSTEM_TRUSTED_KEYS
scripts/config --disable SYSTEM_REVOCATION_KEYS
scripts/config --set-str CONFIG_SYSTEM_TRUSTED_KEYS ""
scripts/config --set-str CONFIG_SYSTEM_REVOCATION_KEYS ""
```
> Debian 內核啟用了模組簽名驗證，如果模組未經受信任密鑰簽名，將無法載入，因此需要禁用

### 編譯

直接對著終端下指令
$(nproc)取決於個人Linux有多少邏輯核，不然可以直接預設給他填12
```
make -j$(nproc)
```
(預設給他填12)
```
make -j12
```
接著等待漫長的編譯時間...
### 過程中若發生Error，請參考章節 - Error 處理

準備kernel的安裝程式
```
sudo make modules_install -j12
```
>使內核能夠擴展功能（如硬體驅動程式）而無需重新編譯整個內核
>>模組的常見用途包括：
硬體驅動程式：如網路驅動、顯卡驅動。
內核功能擴展：如防火牆、虛擬化支持（KVM）。

安裝kernel
```
sudo make install -j12
```
>系統啟動時，啟動引導程序（如 GRUB）會從 /boot/ 目錄讀取內核映像並啟動。
>執行 make install 之前，內核文件僅存在於源代碼目錄下
所以需要執行 make install，讓Kernel出現在 /boot/ 中，使系統能夠辯識它。

若下載版本與原運行版本相同，則需使用新核心更新作業系統的引導程式
```
sudo update-grub
```
>配合上一步，讓等等重新啟動到GRUB時，能夠選取新Kernel

然後重開機
```
sudo reboot
```

重開機過程需不斷按F4使其進入開機選單 (我這邊是用esc)

點選Advanced options for Ubuntu
![image](https://hackmd.io/_uploads/HyONp9Z41x.png)

選 **Ubuntu, with Linux 5.15.135** 將 linux 5.15.135 替換上去
![image](https://hackmd.io/_uploads/HJD8p9ZVkx.png)

開機完畢後，確認版本是否正確
```
uname -rs
```
![image](https://hackmd.io/_uploads/BksN1o-Nkg.png)

接著建立一個project
```
nano project.c
```
輸入好code以後，儲存並退出
對其進行編譯
```
gcc -o project project.c             
```
執行該程式
```
./project
```
兩個Test Project部份都用一樣的方式進行建立、編譯、執行。
編譯和替換kernel到此

### 在「**範例Code測試**」的章節有這次需求的兩題程式及輸出結果
---
## 虛擬地址映射物理地址原理

首先要瞭解虛擬地址是如何映射到物理地址的
以下由這張圖解
![image](https://hackmd.io/_uploads/SJP-SXv4Jx.png)

#### PGD
CR3 存放了 <font color=#FF6600>Page Global Directory (PGD) 的 Base Address</font>
從虛擬地址的 47~39 位元取得 Index，用來定位 PGD 中的Entry
Entry Address = Base Address + ( Index * 8bits )

#### PUD

從 PGD Entry中獲取 Base Address，它<font color=#FF6600>指向 PUD 的起始位置</font>
使用虛擬地址的 38~30 位元作為 Index，來定位 PUD 中的Entry
Entry Address = Base Address(from PGD) + ( Index * 8bits )

#### PMD

從 PUD Entry中獲取 Base Address，它<font color=#FF6600>指向 PMD 的起始位置</font>
使用虛擬地址的 29~21 位元作為 Index，來定位 PMD 中的Entry
Entry Address = Base Address(from PUD) + ( Index * 8bits )

#### PTE 

從 PMD Entry中獲取 Base Address，它<font color=#FF6600>指向 PTE 的起始位置</font>
使用虛擬地址的 20~12 位元作為 Index，來定位 PTE 中的Entry
Entry Address = Base Address(from PMD) + ( Index * 8bits )

#### offset

最後，將Virtual Address的 11~0 bits的Offset 加到Page Frame的 Base Address 中，得到最終的Physical Address
Physical Address = Base Address (from PTE) + Offset


## 範例Code測試
### 第一題：
通過在子進程中修改變數（global_a=789），觸發COW(Copy On Write)，系統為子進程分配新的物理頁面。需要觀察變數 global_a 在修改前後的物理地址變化，從而深入理解 COW 的運作原理

fork()回傳值若不為0，代表目前為父進程執行中
fork()回傳0時，代表目前為子進程
所以依此判斷目前為父進程或是子進程


``` C
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

#define SYS_my_get_physical_addresses 449 // 替換為系統呼叫編號

int global_a = 123; // Global 

int main() {
    void *parent_use, *child_use;

    // Before fork
    printf("===========================Before Fork==================================\n");
    parent_use = (void *)syscall(SYS_my_get_physical_addresses, &global_a);
    printf("pid=%d: global variable global_a:\n", getpid());
    printf("Offset of logical address:[%p]   Physical address:[%p]\n", &global_a, parent_use);
    printf("========================================================================\n");

    if (fork()) { /* parent code */
        // Parent process
        printf("vvvvvvvvvvvvvvvvvvvvvvvvvv  After Fork by parent  vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n");
        parent_use = (void *)syscall(SYS_my_get_physical_addresses, &global_a);
        printf("pid=%d: global variable global_a:\n", getpid());
        printf("******* Offset of logical address:[%p]   Physical address:[%p]\n", &global_a, parent_use);
        printf("vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n");
        wait(NULL); // 等待子進程完成
    } else { /* child code */
        // Child process
        printf("llllllllllllllllllllllllll  After Fork by child  llllllllllllllllllllllllllllllll\n");
        child_use = (void *)syscall(SYS_my_get_physical_addresses, &global_a);
        printf("******* pid=%d: global variable global_a:\n", getpid());
        printf("******* Offset of logical address:[%p]   Physical address:[%p]\n", &global_a, child_use);
        printf("llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll\n");

        // Trigger COW
        printf("Modifying global_a to trigger COW...\n");
        global_a = 789; // 修改變數，觸發 COW

        child_use = (void *)syscall(SYS_my_get_physical_addresses, &global_a);
        printf("******* After COW in child *******\n");
        printf("******* pid=%d: global variable global_a:\n", getpid());
        printf("******* Offset of logical address:[%p]   Physical address:[%p]\n", &global_a, child_use);
        printf("=================================\n");

        // 防止過快退出，影響觀察
        sleep(1);
        exit(0);
    }
    return 0;
}
```
從下圖結果我們可以觀察到，在子進程fork出以後，嘗試更動global_a時，系統會分配新的page給他，導致子進程抓到的global_a會是0x183553010而不是原先讀到的0x194877010

![image](https://hackmd.io/_uploads/SkVt-2G4yg.png)

---

### 第二題：
觀察系統的 Demand Paging（按需載入）機制如何處理全域變數的記憶體分配。透過查詢靜態陣列 a 之中，不同元素的物理地址，觀察系統在進程啟動時和訪問記憶體時的行為差異
``` C
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

#define SYS_my_get_physical_addresses 449 // 替換為系統呼叫編號

int a[2000000]; // 定義全域靜態陣列

int main() {
    void *phy_add_0, *phy_add_last;

    // 使用 my_get_physical_addresses 查詢 a[0] 的 Physical Address
    printf("===========================Before Accessing a[0]==================================\n");
    phy_add_0 = (void *)syscall(SYS_my_get_physical_addresses, &a[0]);
    printf("Element a[0]:\n");
    printf("Offset of logical address:[%p]   Physical address:[%p]\n", &a[0], phy_add_0);
    printf("=================================================================================\n");

    // 使用 my_get_physical_addresses 查詢 a[1999999] 的 Physical Address
    printf("===========================Before Accessing a[1999999]==================================\n");
    phy_add_last = (void *)syscall(SYS_my_get_physical_addresses, &a[1999999]);
    printf("Element a[1999999]:\n");
    printf("Offset of logical address:[%p]   Physical address:[%p]\n", &a[1999999], phy_add_last);
    printf("=================================================================================\n");

    // 訪問 a[1999999]，觸發Page Fault
    printf("Accessing a[1999999]...\n");
    a[1999999] = 123;

    // 再次查詢 a[1999999] 的物理地址
    phy_add_last = (void *)syscall(SYS_my_get_physical_addresses, &a[1999999]);
    printf("\n");
    printf("Element a[%i]: Offset of logical address:[%p]   Physical address:[%p]\n", 1999999 ,&a[1999999i], phy_add_last);

    return 0;
}
```
同樣藉由下圖觀察出，a[0]的物理地址可能因為系統的預載所以已經存在了，但a[1999999]的地址一開始呼叫時，會回傳的是nil(or 0xffff...)，代表還沒被分配到Physical address

在a[1999999] = 123; 執行完後再次呼叫
就能看到a[1999999]被分配了一個地址為0x183553010

![image](https://hackmd.io/_uploads/rk4BX3zEyl.png)



    


## Error 處理
>> No rule to make target 'debian/canonical-certs.pem’

打開.config檔
```
sudo nano .config
```

找到debian/canonical-certs.pem與debian/canonical-revoked-certs.pem
刪除引號內的值
並再次執行
```
make -j12  (or)  make -j$(nproc)
```
---
>> BTF: .tmp_vmlinux.btf: pahole (pahole) is not available

打開.config檔
```
sudo nano .config
```

找到CONFIG_DEBUG_INFO_BTF，將值改為n
並再次執行
```
make -j12  

   or

make -j$(nproc)
```


## my_get_physical_Address - 程式碼
``` C
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/uaccess.h>
#include <linux/kernel.h>
#include <linux/syscalls.h>

SYSCALL_DEFINE1(my_get_physical_addresses, void *, user_va) {
    unsigned long va = (unsigned long)user_va; // 虛擬地址
    struct mm_struct *mm = current->mm;       // 獲取當前進程的記憶體描述符
    pgd_t *pgd;
    p4d_t *p4d;
    pud_t *pud;
    pmd_t *pmd;
    pte_t *pte;
    struct page *page;
    unsigned long pa = 0; // 用於存儲物理地址

    if (!mm)
        return 0;

    // 逐層查詢頁表
    pgd = pgd_offset(mm, va);
    if (pgd_none(*pgd) || pgd_bad(*pgd))
        return 0;

    p4d = p4d_offset(pgd, va);
    if (p4d_none(*p4d) || p4d_bad(*p4d))
        return 0;

    pud = pud_offset(p4d, va);
    if (pud_none(*pud) || pud_bad(*pud))
        return 0;

    pmd = pmd_offset(pud, va);
    if (pmd_none(*pmd) || pmd_bad(*pmd))
        return 0;

    pte = pte_offset_map(pmd, va);
    if (!pte || pte_none(*pte))
        return 0;

    // 如果 PTE 有效，查詢物理頁框
    if (pte_present(*pte)) {
        page = pte_page(*pte);
        if (!page)
            return 0;

        // 計算物理地址
        pa = page_to_pfn(page) << PAGE_SHIFT; // 物理頁框基址
        pa |= va & ~PAGE_MASK;               // 加上頁內偏移
    }

    return (long)pa;
}

```