# 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
```

再來使用ls去列出當下路徑的檔案&資料夾
```
ls
```

就可以使用cd進入我們要的linux-5.15.135資料夾了
```
cd linux-5.15.135
```
接著進到linux-5.15.135後,建立一個mycall的資料夾
```
mkdir mycall
```

再進入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/
```

完畢後,到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);
```

>在 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
```

## 編譯和替換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

選 **Ubuntu, with Linux 5.15.135** 將 linux 5.15.135 替換上去

開機完畢後,確認版本是否正確
```
uname -rs
```

接著建立一個project
```
nano project.c
```
輸入好code以後,儲存並退出
對其進行編譯
```
gcc -o project project.c
```
執行該程式
```
./project
```
兩個Test Project部份都用一樣的方式進行建立、編譯、執行。
編譯和替換kernel到此
### 在「**範例Code測試**」的章節有這次需求的兩題程式及輸出結果
---
## 虛擬地址映射物理地址原理
首先要瞭解虛擬地址是如何映射到物理地址的
以下由這張圖解

#### 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

---
### 第二題:
觀察系統的 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

## 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;
}
```