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