## Linux Project 1 [toc] **組長:** **111504512 賴詠文** 組員: 111504507 黃子銘 109401553 楊雲杰 111504008 郭奕宏 :::success ### 題目 **you need to write a new system call `void * my_get_physical_addresses(void *)` so that a process can use it to get the physical address of a virtual address of a process.** > 此系統呼叫的回傳值是 0 或位址值。 0表示目前沒有實體位址分配給邏輯位址。非零值表示作為其參數提交給系統呼叫的邏輯位址的物理位址(實際上,該位址只是邏輯位址的偏移量)。 https://staff.csie.ncu.edu.tw/hsufh/COURSES/FALL2024/linux_project_1.html ### Demo 時間 - 11/12 11:15~11:30 ### 第一次討論 - 10/19 20:30~ ### 第二次討論 - 11/1 20:00~ ### 第三次討論 - 11/9 20:00~ ::: ### Kernel 版本 `wget -P ~/ https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.137.tar.xz` :::success ## 題目 you need to write a new system call `void * my_get_physical_addresses(void *)` so that a process can use it to get the physical address of a virtual address of a process. 此系統呼叫的回傳值是 0 或位址值。 0表示目前沒有實體位址分配給邏輯位址。非零值表示作為其參數提交給系統呼叫的邏輯位址的物理位址(實際上,該位址只是邏輯位址的偏移量)。 ::: ## 前置工作 :::spoiler 下載kernel ![image](https://hackmd.io/_uploads/S1xRgy6-1x.png) ```shell= sudo apt update sudo apt install build-essential libncurses-dev libssl-dev libelf-dev bison flex -y ``` ::: ## systemcall 添加及設置 進入解壓縮完的資料夾並在內部建立一個資料夾 ```shell= #進入 linux-5.15.137 cd usr/src/linux-5.15.137/ # 建立一個資料夾 mycall mkdir mycall # 在新建立好的資料夾中建立一個system call vim mycall/my_get_physical_addresses.c # 建立Makefile vim mycall/Makefile # 將my_get_physical_addresses.o編入kernel obj-y := my_get_physical_addresses.o # 編輯原系統中的Makefile core-y += kernel/ certs/ mm/ fs/ ipc/ security/ crypto/ mycall/ # 將系統呼叫對應的函數加入到系統呼叫的標頭檔中 # 開啟檔案`include/linux/syscalls.h` vim include/linux/syscalls.h # 並在 #endif前加上 asmlinkage long sys_my_get_physical_addresses(unsigned long __user *usr_ptr); # 將syscall加入到kernel的syscall table 並開啟檔案 vim arch/x86/entry/syscalls/syscall_64.tbl # 會發現一系列 x32 系統呼叫,在x32 system call上方加入編號449的system call 449 common my_get_physical_addresses sys_my_get_physical_addresses ``` ## 編譯和替換kernel 清理kernel之前的所有配置 ```shell= sudo make mrproper ``` 編譯設定 進入 kernel 目錄中,並將目前 Kernel Config 文件複製到當前目錄,最後生成此 kernel 的配置文件 為了避免建置大量不必要的 driver 和 kernel module,使用 `localmodconfig` 來節省時間。 ```shell= cd linux-5.15.137/ cp -v /boot/config-$(uname -r) .config make localmodconfig ``` 因為我們直接從 /boot/config-$(uname -r) 複製設定檔,所以設定檔裡面的設定的是 Debian 官方當初編譯 kernel 時憑證的路徑,若是直接編譯會報錯,因此這邊取消使用憑證,並將值設為空字串 ```shell= 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 "" ``` 開始編譯 ```shell= make -j12 ``` ### Error 處理 No rule to make target 'debian/canonical-certs.pem’ ``` sudo vim .config / + enter + n(往下)搜尋 `debian/canonical-certs.pem `並直接清除此引號內的值 / + enter + n(往下)搜尋 `debian/canonical-revoked-certs.pem` 並直接清除此引號內的值 ``` BTF: .tmp_vmlinux.btf: pahole (pahole) is not available ``` sudo vim .config / + enter + n(往下)搜尋 CONFIG_DEBUG_INFO_BTF 將其值改為 n ``` ### 編譯完畢進行安裝 準備kernel的安裝程式 ```shell= sudo make modules_install -j12 ``` 安裝kernel ```shell= sudo make install -j12 ``` 重新啟動 ``` sudo update-grub sudo reboot ``` 快速地連續按F4進入開機選單,點選Advanced options for Ubuntu 檢查版本 `uname -rs` 若要看kernel環境訊息或查看printk的結果可以下`sudo dmesg`指令查看 ## 實作部分 (virtual address 轉為 physical address ) ### 背景小知識 `#ifndef` `#define` `#endif` #ifndef 的全稱是 "if not defined",用來檢查某個符號或巨集是否尚未被定義。 如果符號未定義,則編譯器會處理該條件之下的代碼。 優點:避免程式重複編譯!!! ``` #ifndef SOME_MACRO // 這段代碼會在 SOME_MACRO 尚未定義時被編譯 #define // 已編譯過會執行這段代碼 #endif ``` 在pgtable.h中 offset function trace code網址:https://elixir.bootlin.com/linux/v5.15.137/source ```cpp= // include/linux/pgtable.h line 91 #ifndef pte_offset_kernel static inline pte_t *pte_offset_kernel(pmd_t *pmd, unsigned long address) { return (pte_t *)pmd_page_vaddr(*pmd) + pte_index(address); } #define pte_offset_kernel pte_offset_kernel #endif // ... // include/linux/pgtable.h line 119 /* Find an entry in the second-level page table.. */ #ifndef pmd_offset static inline pmd_t *pmd_offset(pud_t *pud, unsigned long address) { return pud_pgtable(*pud) + pmd_index(address); } #define pmd_offset pmd_offset #endif #ifndef pud_offset // pud_t *pud:頁上級目錄(PUD,Page Upper Directory)的指針。 static inline pud_t *pud_offset(p4d_t *p4d, unsigned long address) { // p4d_pgtable(*p4d):這個會返回 P4D 指向的頁表的基礎地址。 return p4d_pgtable(*p4d) + pud_index(address); } #define pud_offset pud_offset #endif static inline pgd_t *pgd_offset_pgd(pgd_t *pgd, unsigned long address) { return (pgd + pgd_index(address)); }; ``` ### 整體架構 線性地址轉physical address ![image](https://hackmd.io/_uploads/SyrZlkpWJe.png) :::spoiler kernel內多級頁表 ![image](https://hackmd.io/_uploads/SJBVl1TZ1l.png) - GLOBAL DIR (PML4 Index): 高 9 bits,對應 PML4 索引。 - Upper DIR (PDPT Index): 接下來的 9 bits,對應上級頁目錄索引。 - Middle DIR (PD Index): 再往下的 9 bits,對應中間頁目錄索引。 - Table (PT Index): 9 bits,對應頁表的索引。 - OFFSET: 最後 12 bits,是頁框中的偏移量,表示物理頁框內的具體位置。 4 種 page table 結構 - CR3: 開始時,CR3 寄存器存儲的是 PML4 頁表的物理地址(圖中為 CR3 physical address)。PML4 是最高層的頁表。 - PML4 (Page Map Level 4): 使用虛擬地址的前 9 位作為索引(圖中的 pgd_index)來查找對應的 PML4 頁表條目(*pgd_offset)。 - PDPT (Page Directory Pointer Table): 下一層是上級頁目錄(PDPT),使用接下來的 9 位作為索引(pud_index),查找對應的頁表條目(*pud_offset)。 - PD (Page Directory): 中級頁目錄(PD)層使用接下來的 9 位作為索引(pmd_index),查找對應的條目(*pmd_offset)。 - T (Page Table): 最後的 9 位作為頁表索引(pte_index),找到具體的頁表條目(*pte_offset_kernel)。 - Offset: 最後的 12 位是頁框內的偏移量,用於定位頁框內的具體數據(Start Byte)。 ::: - 虛擬位址的最低12位元(4KB的頁大小)和實體位址的最低12位元相同。虛擬位址的4個頁表段page_index可以看做在頁表中的索引。 每一個 Process 都會有自己的 Page Table,存在它自己的 kernel space,Page table 的 Base address 會被存在 CR3 裡面,這是一個 register,又被稱為 PDBR(page directory base register),存的是實體位址 > 分頁目錄的基底位址是存放在 CR3(或稱 PDBR,page directory base register) ### Page directory and page table :::spoiler Page directory and page table architecture ![image](https://hackmd.io/_uploads/rJ5ElJ6bke.png) - P: present p=1 => in memory - R/W: =1 => can write - U/S: user/supervisor 當 U/S = 1 時,表示分頁是一個 user level 的分頁 - A(accessed)旗標:在 A = 0 時,若分頁被存取 - D(dirty)旗標:在 D = 0 時,若對分頁進行寫入動作,則處理器會把它設為 1 - PS(page size)旗標:這個旗標只在分頁目錄 entry 中有作用。當 PS = 0 時,表示這是一個 4KB 的分頁 ::: to physical address 概要 ```cppp= // 當前進程的內存描述符(current->mm)中,計算虛擬地址 vaddr 在頁全局目錄(PGD)中的條目位置。這是查找過程中的第一層。 pgd = pgd_offset(current->mm, vaddr); //通過 pmd 和 vaddr 計算頁表條目(PTE)的指針 pte = pte_offset_kernel(pmd, vaddr); //從頁表條目(pte)中提取出物理頁框地址 //PAGE_MASK 是用來過濾掉頁表條目中的非地址部分(例如標誌位)的掩碼 //pte_val(*pte) & PAGE_MASK,只保留頁表條目中的物理頁框地址部分,並將其存儲在 page_addr 中。 page_addr = pte_val(*pte) & PAGE_MASK; //vaddr 是虛擬地址,它包括了頁表查找所需的高位部分和頁框內的偏移量。 //~PAGE_MASK 是 PAGE_MASK 的反碼,將物理頁框地址的高位清除,保留低位的偏移量部分。 //vaddr & ~PAGE_MASK 是提取虛擬地址中的偏移量部分,這部分表示該地址在物理頁框中的具體位置。 page_offset = vaddr & ~PAGE_MASK; //將物理頁框地址與偏移量組合,生成最終的物理地址。 paddr = page_addr | page_offset; ``` :::info ### **轉址備註** `0x5633733901b1`,其前9個bit為 010101100 轉為10進位為172,也就是指”**下一層table的起始位置**”存於此table的第172欄 由於每一個欄位都是由8 byte組成,所以實際的offset的address為 172*8 =1376 轉為16進制為 `0x560` 相當於 010101100 左移3個bit,即 0101 0110 0000 轉為16進制也是 `0x560` 將a.的值(**pgd** )及b.的值(**pgd_index(address)**)合併: `0xffff9ce09c8e8000` + `0x560` = `0xffff9ce09c8e8560` `0xffff9ce09c8e8560` 此位置所儲存的值即為計算下一層table起始位置所需的值 在 `*pud_offset` 中的return value為 (p4d_pgtable(*p4d) + pud_index(address)) - p4d_pgtable(*p4d) 為PUD的起始位置,而 *p4d指的是`0xffff9ce09c8e8560`位置中的值 - p4d_pgtable()則是計算起始位置的方法 :::spoiler current-> mm解釋 在這段程式碼中,`current->mm` 是 Linux 核心中的一個結構體指標,指向目前執行的進程(process)的 **記憶體描述符 (memory descriptor)**。以下是詳細解釋: ### `current->mm` 的解釋 1. **`current`**: - `current` 是一個巨集,用來取得當前執行的進程的 `task_struct` 結構指標。 - `task_struct` 是 Linux 核心用來描述每個進程的資料結構,包含進程的各種信息,如進程 ID、狀態、優先級、資源限制等。 ![image](https://hackmd.io/_uploads/Sk8R_ikzke.png) 2. **`current->mm`**: - 在 `task_struct` 結構中,有一個成員變數 `mm`,它是一個指向 **`mm_struct`** 結構的指標。 - `mm_struct` 是記憶體描述符,用於描述進程的虛擬記憶體空間,包含了頁表、分段資訊、堆棧、程式段等相關的記憶體管理資訊。 - `current->mm` 可以理解為當前進程的虛擬記憶體空間的描述符。 3. **`mm_struct` 的用途**: - `mm_struct` 包含了進程記憶體相關的所有重要資訊,例如: - **頁全域目錄 (PGD)**:存放頁表層次結構的入口點。 - 在這段程式碼中,`current->mm` 被用來找到進程的頁全域目錄(PGD),從而開始虛擬地址到物理地址的轉換過程。 4. **`pgd_offset` 函數**: - `pgd_offset` 是用來計算虛擬地址 `vaddr` 在當前進程的頁全域目錄中的偏移量,並返回該地址的 `pgd_t` 型別的指標。 - 這是虛擬地址轉換成物理地址的第一步,用來獲取頁全域目錄 (PGD) 的條目。 ### 總結 在這段程式碼中,`current->mm` 是指向目前執行的進程的記憶體描述符的指標,包含該進程的所有虛擬記憶體資訊。利用 `current->mm` 可以開始從虛擬地址到物理地址的頁表查找過程,最終獲取對應的物理地址。 ::: ### 完整程式碼 ```c= // SYSCALL_DEFINE1:這個表示定義一個接收一個參數的系統調用。數字 1 表示該系統調用接受一個參數。 // 0x%lx:表示以 16 進制格式打印一個長整數(long 型別) #include<linux/syscalls.h> SYSCALL_DEFINE1(my_get_physical_addresses, void *, addr_p) { unsigned long vaddr = (unsigned long)addr_p; pgd_t *pgd; p4d_t *p4d; pud_t *pud; pmd_t *pmd; pte_t *pte; unsigned long paddr=0; unsigned long page_addr=0; unsigned long page_offset=0; pgd= pgd_offset(current->mm,vaddr); printk("pgd_val = 0x%lx\n",pgd_val(*pgd)); printk("pgd_index = %lu\n",pgd_index(vaddr)); if(pgd_none(*pgd)){ printk("not mapped in pgd\n"); } p4d=p4d_offset(pgd,vaddr); printk("p4d_val = 0x%lx\n",p4d_val(*p4d)); printk("p4d_index = %lu\n", p4d_index(vaddr)); if(p4d_none(*p4d)){ printk("no mapped in p4d\n"); return 0; } pud=pud_offset(p4d,vaddr); printk("pud_val = 0x%lx\n", pud_val(*pud)); printk("pud_index = %lu\n"), pud_index(vaddr)); if(pud_none(*pud)){ printk("no mapped in pud\n"); return 0; } pmd=pmd_offset(pud,vaddr); printk("pmd_val = 0x%lx\n",pmd_val(*pmd)); printk("pmd_index = %lu\n",pmd_index(vaddr)); if(pmd_none(*pmd)){ printk("no mapped in pmd\n"); return 0; } pte = pte_offset_kernel(pmd,vaddr); printk("pte_val = 0x%lx\n",pte_val(*pte)); printk("pte_index = %lu\n", pte_index(vaddr)); if(pte_none(*pte)){ printk("no mapped in pte\n"); return 0; } page_addr=pte_val(*pte) & PTE_PFN_MASK & PAGE_MASK; page_offset = vaddr & ~PAGE_MASK; paddr = page_addr | page_offset; printk("page_addr = %lx, page_offset = %lx\n", page_addr, page_offset); printk("vaddr = %lx, paddr = %lx\n", vaddr, paddr); return paddr; } ``` :::info ### 關於physical address 顯示 ![image](https://hackmd.io/_uploads/Hydo-16-1e.png) 可以觀察到除了`pud_val`和`pmd_val`是32bit,而`pgd_val`, `p4d_val`, `pte_val`都是64bit,而最後影響到主要是`pte_val`,因此去看`pte_val`的code ```c= // /arch/x86/include/asm/pgtable.h, #define pte_val(x) native_pte_val(x) ``` ```c= // / arch / x86 / include / asm / pgtable_types.h static inline pteval_t native_pte_val(pte_t pte) { return pte.pte; } static inline pteval_t pte_flags(pte_t pte) { return native_pte_val(pte) & PTE_FLAGS_MASK; } ``` 從上面可以發現`native_pte_val`也把`pte_flag`也取出來了 ```c #define PTE_FLAGS_MASK (~PTE_PFN_MASK) ``` 而`PTE_FLAGS_MASK`定義是`(~PTE_PFN_MASK)`,因此我們將pte_val的flag濾掉就好了`page_addr=pte_val(*pte) & PTE_PFN_MASK & PAGE_MASK;` 而`pgd_val`, `p4d_val`也都是將FLAG也取出來 ::: ## Question1 (see the effect of copy-on-write.) :::success ### copy on write `fork`時,系統會讓子進程和父進程共享相同的物理記憶體頁,而將這些頁標記為「唯讀」。當父或子進程嘗試寫入這些共享頁時,系統才會將該頁的內容複製一份(分配一個新的物理頁框),並允許進行寫入操作,這就是所謂的「寫入時複製」(Copy-on-Write)。 ### 測試程式碼解釋 After Fork,由於 `global_a`的頁還沒有被寫入,父子進程此時應該共享相同的物理地址。 子進程寫入 `global_a`(設定 `global_a` = 789),這將觸發 Copy-on-Write 機制。 - 寫入時,作業系統會分配一個新的物理頁框給子進程,然後將 global_a 的新值寫入該頁框中。 - 接著,子進程再次獲取 `global_a` 的物理地址,這次它應該和父進程的 `global_a` 物理地址不同,從而顯示出 Copy-on-Write 的效果。 ::: ### 測試程式碼 :::spoiler 測試程式碼 ```c= #include <stdio.h> #include <unistd.h> #include <sys/wait.h> void * my_get_physical_addresses(void *vaddr) { return (void*) syscall(449,vaddr); } int global_a=123; //global variable void hello(void) { printf("======================================================================================================\n"); } int main() { int loc_a; void *parent_use, *child_use; int status; printf("===========================Before Fork==================================\n"); parent_use=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*/ printf("vvvvvvvvvvvvvvvvvvvvvvvvvv After Fork by parent vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n"); parent_use=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(&status); } else { /*child code*/ printf("llllllllllllllllllllllllll After Fork by child llllllllllllllllllllllllllllllll\n"); child_use=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"); printf("____________________________________________________________________________\n"); /*----------------------- trigger CoW (Copy on Write) -----------------------------------*/ global_a=789; printf("iiiiiiiiiiiiiiiiiiiiiiiiii Test copy on write in child iiiiiiiiiiiiiiiiiiiiiiii\n"); child_use=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("iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii\n"); printf("____________________________________________________________________________\n"); sleep(1000); } } ``` ::: ### 結果 :::spoiler 結果 ![image](https://hackmd.io/_uploads/SkxsdqYnWkg.png) ::: ## Question2 (whether a loader loads all data of a process before executing it) :::success 當進程開始執行時,核心通常不會分配物理記憶體來儲存進程的所有程式碼和資料。 對於 `a[1999999]`,結果顯示物理地址為 `(nil)`示此頁面還沒有被分配物理內存。 這證明內核在進程啟動時並未立即為數組 `a` 的所有元素分配物理內存,僅在訪問` a[0] `時才分配了對應的頁面,未訪問的頁面依然未分配。 Demand paging 只有在頁面被實際訪問時,內核才會為其分配物理內存,並非所有資料在程式啟動時都已載入並分配物理記憶體,只有訪問的頁面才會被分配到物理地址 ::: ### 測試程式碼 :::spoiler code ```c= #include<stdio.h> // 使用syscall #include<unistd.h> void * my_get_physical_addresses(void *vaddr) { return (void*) syscall(449,vaddr); } int a[2000000]; int main() { int loc_a; void *phy_add; phy_add=my_get_physical_addresses(&a[0]); printf("global element a[0]:\n" ); printf("Offset of logical address:[%p] Physical address:[%p]\n", &a[0], phy_add); printf("========================================================================\n"); phy_add=my_get_physical_addresses(&a[1999999]); printf("global element a[1999999]:\n" ); printf("Offset of logical address:[%p] Physical address:[%p]\n", &a[1999999], phy_add); printf("========================================================================\n"); } ``` ::: ### 結果 :::spoiler result ![image](https://hackmd.io/_uploads/SylF5F3Z1l.png) ::: ## 參考資料 > [add to system call](https://hackmd.io/aist49C9R46-vaBIlP3LDA?view) > [學長的資料](https://satin-eyebrow-f76.notion.site/Kernel-syscall-3ec38210bb1f4d289850c549def29f9f#6f9c68a389f14319839cf04692b3e064) > [关于Linux内存寻址与页表处理的一些细节](https://www.cnblogs.com/QiQi-Robotics/p/15630380.html) > [進階版本](https://hackmd.io/@eugenechou/H1LGA9AiB#Project-1) > [gcc](https://ithelp.ithome.com.tw/articles/10257387) ## 補充資料 ### Page Table 相關數字 ```c= /* * PGDIR_SHIFT determines what a top-level page table entry can map */ #define PGDIR_SHIFT 39 #define PTRS_PER_PGD 512 #define MAX_PTRS_PER_P4D 1 #endif /* CONFIG_X86_5LEVEL */ /* * 3rd level page */ #define PUD_SHIFT 30 #define PTRS_PER_PUD 512 /* * PMD_SHIFT determines the size of the area a middle-level * page table can map */ #define PMD_SHIFT 21 #define PTRS_PER_PMD 512 /* * entries per page directory level */ #define PTRS_PER_PTE 512 #define PMD_SIZE (_AC(1, UL) << PMD_SHIFT) #define PMD_MASK (~(PMD_SIZE - 1)) #define PUD_SIZE (_AC(1, UL) << PUD_SHIFT) #define PUD_MASK (~(PUD_SIZE - 1)) #define PGDIR_SIZE (_AC(1, UL) << PGDIR_SHIFT) #define PGDIR_MASK (~(PGDIR_SIZE - 1)) ```