OS Homework1 System call 的 overhead 有多大 == ###### tags: `operating system` 姓名: 林逸驤 學號: 410235011 學系: 地環四 ## knowledge ### [ELF (Executable and Linkable Format)](https://hackmd.io/@rhythm/ry5pxN6NI) - Linux 和其他類 Unix 操作系統中常見的可執行檔格式 e.g. 可執行檔、物件檔、共用函式庫(也稱為動態函式庫,像 .so 檔案),或核心傾印檔 - 動態函式庫是 ELF 格式的重要部分,負責使程式在運行時載入並連結共享庫中的函式。 - 動態連結的步驟 : 1. 主程式第一次呼叫函式 2. 程式跳到該函數的plt元素 3. 該函數之plt使用其對應到的GOT元素,想要跳到目標函式 a. 因為第一次呼叫,尚未鏈接,GOT指到的地方一樣是函數的plt,結果又跳回該plt元素,把代表該函數的數字推到stack上 b. plt繼續執行,跳到公共plt 4. 公共plt執行一些東西 5. 公共plt呼叫動態鏈接器 6. 動態鏈接器從stack觀察主程式想使用哪個函數,更改第3步所使用的GOT元素的內容,使其指向函數的真正位置,並引導程式執行目的函數 7. 之後再次呼叫該函數時,第3步時plt使用的GOT已經存著函數的真正位址,就會直接跳到函數執行,這個動態鏈接的方式叫做Lazy Linking。 - 每次的位置都不太一樣 ![image](https://hackmd.io/_uploads/BJxNC0jAA.png) ### Stack - rip:instruction pointer,用於記錄下一個要執行的instruction - rsp:stack pointer,指向stack頂端 - rbp:base pointer,指向stack底部 <div style="text-align: center;"> <img src="https://hackmd.io/_uploads/S11MUVHA0.png" alt="image alt"> </div> ### vsyscall - 考慮到相容模式 (compatibility mode) 後,讓系統呼叫的專用指令選擇上變得更複雜 :arrow_right: ==**vsystcall**== 對 syscall 的包裝,最終仍會執行到 `sysenter` 或 `syscall` 指令。 - 原本在使用者層級用 int 0x80 或 sysenter 的指令,就變成 call 指令 - [vsyscall 的執行步驟](https://hackmd.io/@sysprog/linux-vdso#:~:text=%E6%A0%B8%E5%BF%83%E5%AE%9A%E5%9D%80%E7%A9%BA%E9%96%93%E7%9A%84%E5%9C%B0%E5%9D%80%200xffffe000%20%E6%9C%83%E4%BF%9D%E7%95%99%E4%B8%80%E5%80%8B%20vsyscall%20%E5%8D%80%E5%9F%9F%EF%BC%8C%E4%B8%A6%E9%96%8B%E6%94%BE%E5%85%B6%E8%AE%80%E5%8F%96%E5%8F%8A%E5%9F%B7%E8%A1%8C%E6%AC%8A%E9%99%90%E7%B5%A6%E4%BD%BF%E7%94%A8%E8%80%85%E5%B1%A4%E7%B4%9A%EF%BC%8C%E8%A9%B2%E9%A0%81%E9%9D%A2%E7%9A%84%E5%85%A7%E5%AE%B9%E6%98%AF%20Linux%20%E6%A0%B8%E5%BF%83%E5%9C%A8%E9%96%8B%E6%A9%9F%E6%99%82%EF%BC%8C%E6%A0%B9%E6%93%9A%E8%99%95%E7%90%86%E5%99%A8%E9%A1%9E%E5%9E%8B%E6%89%80%E6%B1%BA%E5%AE%9A%E5%A5%BD%E7%9A%84%E4%B8%80%E6%AE%B5%E7%A8%8B%E5%BC%8F%E7%A2%BC%EF%BC%8C%E7%94%A8%E4%BE%86%E8%A7%B8%E7%99%BC%E7%B3%BB%E7%B5%B1%E5%91%BC%E5%8F%AB%EF%BC%8C%E4%B8%94%E8%A9%B2%E7%A8%8B%E5%BC%8F%E7%A2%BC%E5%9C%A8%20vsyscall%20%E5%8D%80%E5%9F%9F%E4%B8%AD%E7%9A%84%E5%AF%A6%E9%9A%9B%E4%BD%8D%E7%BD%AE%EF%BC%8C%E6%9C%83%E9%80%8F%E9%81%8E%20auxiliary%20vector%20%E7%9A%84%20AT_SYSINFO%20%E9%A0%85%E7%9B%AE%EF%BC%8C%E6%8F%90%E4%BE%9B%E7%B5%A6%E6%87%89%E7%94%A8%E7%A8%8B%E5%BC%8F%EF%BC%8C%E5%9B%A0%E6%AD%A4%E6%AF%8F%E7%95%B6%E8%A6%81%E8%A7%B8%E7%99%BC%E7%B3%BB%E7%B5%B1%E5%91%BC%E5%8F%AB%E6%99%82%EF%BC%8C%E7%A8%8B%E5%BC%8F%E5%8F%AA%E8%A6%81%E5%91%BC%E5%8F%AB%E8%A9%B2%E6%AE%B5%E7%A8%8B%E5%BC%8F%E5%8D%B3%E5%8F%AF) - 缺乏符號表一類的資訊,不利於debug ([core dumb](https://hackmd.io/@c_0KKCwzQE2rsd39mpvNQQ/HkipV41zY)) - `vsyscall` 映射到使用者層級的地址是固定不變的,容易被有心人士利用,致使核心的安全問題。 ### vdso - 將 x86-32 vsyscall 頁面的內容,由一段程式碼,改變為 ELF 動態函式庫,稱為 vsyscall DSO (dynamic shared object) - 能加速的原因,避免了從kernel space 轉換到 user space 的 overhead - [vdso 的執行步驟](https://hackmd.io/@sysprog/linux-vdso#:~:text=%E7%B7%A8%E8%AD%AF%20Linux%20%E6%A0%B8%E5%BF%83%E6%99%82%EF%BC%8C%E6%9C%83%E9%A0%90%E5%85%88%E7%B7%A8%E8%AD%AF%E5%87%BA%E5%A4%9A%E5%80%8B%20DSO%20%E6%AA%94%E6%A1%88%E4%B8%A6%E5%B5%8C%E5%85%A5%E8%87%B3%20Linux%20%E6%A0%B8%E5%BF%83%E4%B8%AD%EF%BC%8C%E7%95%B6%20Linux%20%E7%B3%BB%E7%B5%B1%E5%95%9F%E5%8B%95%E4%B9%8B%E9%9A%9B%EF%BC%8C%E6%A0%B8%E5%BF%83%E5%B0%87%E5%88%A4%E6%96%B7%E8%99%95%E7%90%86%E5%99%A8%E9%A1%9E%E5%9E%8B%EF%BC%8C%E9%81%B8%E6%93%87%E9%81%A9%E7%95%B6%E7%9A%84%20DSO%20%E4%B8%A6%E8%A4%87%E8%A3%BD%E5%88%B0%20vsyscall%20%E9%A0%81%E9%9D%A2%E3%80%82%E6%96%BC%E6%98%AF%EF%BC%8C%E4%B8%80%E6%97%A6%E6%87%89%E7%94%A8%E7%A8%8B%E5%BC%8F%E5%9F%B7%E8%A1%8C%EF%BC%8C%E6%A0%B8%E5%BF%83%E5%B0%B1%E6%9C%83%E9%80%8F%E9%81%8E%20auxiliary%20vector%20%E7%9A%84%20AT_SYSINFO_EHDR%20%E9%A0%85%E7%9B%AE%EF%BC%8C%E5%B0%87%20DSO%20%E7%9A%84%E4%BD%8D%E5%9D%80%E6%8F%90%E4%BE%9B%E7%B5%A6%E6%87%89%E7%94%A8%E7%A8%8B%E5%BC%8F%E3%80%82) - Why do we use vdso? - 動態連結機制確保 `vDSO` 每次所在的地址可以不同(ASLR)。 - `vDSO` 的存在不僅可解決系統呼叫專用指令的議題,還能加速在意效率的系統呼叫。 ### ASLR (Address space layout randomization) - 一種針對[緩衝區溢出](https://www.cloudflare.com/zh-tw/learning/security/threats/buffer-overflow/)的全保護技術,通過線性區佈局的隨機化,增加攻擊者預測目的地址的難度,防止攻擊者直接定位攻擊代碼位置。 - 作業系統會隨機的產生每個section的address :question: 為什麼要有ASLR? >如果沒有ASLR,libc裡面的system()函數會被放在固定地方,所以駭客只需要透過stack overflow改掉return address,並且將system所需要的參數透過stack overflow寫入到堆疊。那麼就可以透過retrun to libc執行任意的指令。 :poop: ASLR會造成效能上的損失 >如果不使用ASLR,那麼可以將常用的函數固定在同一個地方,因此在做context switch的時候就不需要切換常用函數庫的部分 :question: ASLR 的 random 是怎麼做到的? >應用程式使用IP相對定址,因此應用程式可以放在任何記憶體位址 函數庫(libc等)放到任意位址,loader可以正確定位 mmap及brk(sbrk)分配記憶體本來就可以隨機 ### [Makefile](https://hackmd.io/@sysprog/SySTMXPvl) ``` all: target1 target2 ... target1: 目標檔1 目標檔2 <tab>gcc -o 欲建立的執行檔 目標檔1 目標檔2 target2: 目標檔1 目標檔2 <tab>gcc -o 欲建立的執行檔 目標檔1 目標檔2 clean: rm ... ``` ### [gdb](https://hackmd.io/@hsuedw/gdb_skils) - 先以 l 印出source code - 用 b 設定中斷點 - r 開始 run code - n 單行執行,撞到預先設置好的 break point - 進入 break point 對 clock_gettime disassemble ### [clocksource 是什麼](https://arush15june.github.io/posts/2020-07-12-clocks-timers-virtualization/) ## question 1 ### func 的 code ```c= // #define _POISX_C_SOURCE 199309L // #define _GNU_SOURCE #include <stdio.h> #include <time.h> int main(){ struct timespec ts1, ts2; clock_gettime(CLOCK_REALTIME, &ts1); for(int i=0; i<10000; i++){ clock_gettime(CLOCK_REALTIME, &ts2); } clock_gettime(CLOCK_REALTIME, &ts2); long delta = (ts2.tv_sec - ts1.tv_sec) * 1E9 + (ts2.tv_nsec - ts1.tv_nsec); printf("%ld\n", delta); return 0; } ``` ### syscall 的 call ```c= // #define _POISX_C_SOURCE 199309L // #define _GNU_SOURCE #include <stdio.h> #include <time.h> int main(){ struct timespec ts1, ts2; clock_gettime(CLOCK_REALTIME, &ts1); for(int i=0; i<10000; i++){ clock_gettime(CLOCK_REALTIME, &ts2); } clock_gettime(CLOCK_REALTIME, &ts2); long delta = (ts2.tv_sec - ts1.tv_sec) * 1E9 + (ts2.tv_nsec - ts1.tv_nsec); printf("%ld\n", delta); return 0; } ``` ### func gdb 反組譯 - 以下是我設定break point 在clock_gettime 上,單步執行進去的結果 - 有看到 disassamble 有看到對 stack 操作的指令 - 有看到 syscall ![Screenshot 2024-09-28 152905](https://hackmd.io/_uploads/ByL9lNrAC.png) ![image](https://hackmd.io/_uploads/BJf9uV800.png) ![image](https://hackmd.io/_uploads/BkJ6dNUAA.png) ![image](https://hackmd.io/_uploads/Bko4tNU0R.png) ![image](https://hackmd.io/_uploads/H1SuKE8CR.png) ![image](https://hackmd.io/_uploads/SyJ2YNUAC.png) ![image](https://hackmd.io/_uploads/SytZqVIC0.png) ![image](https://hackmd.io/_uploads/SkTV9480A.png) 輸入: objdump -d ./func 觀察 main ![image](https://hackmd.io/_uploads/SJbAZ950R.png) 可以見到gcc把clock_gettime 跳轉到了 clock_gettime@Plt 的位置 ![image](https://hackmd.io/_uploads/HkM2Xcc0A.png) - ==可以看到上面用gdb反組譯的結果,assembly code 有一些對 stack 操作是為動態連結,然後最終找到打包好的vsyscall,執行clock_gettime== ### Why would these happen `../sysdeps/unix/sysv/linux/clock_gettime.c: No such file or directory.`? 確定有 `glibc` ![image](https://hackmd.io/_uploads/HJ56z0j0C.png) ![image](https://hackmd.io/_uploads/B1Wwl12AA.png) 檢查 clock_gettime 的位置 ![image](https://hackmd.io/_uploads/B1OcjRsRA.png) 看看 `vdso` 的位置,在 `0x7ffff7fce000` 到 `0x7ffff7fcf000` 確實 `clock_gettime` 在 `vdso` 的空間範圍 ![image](https://hackmd.io/_uploads/Sy2CsCoRR.png) [from the man page](https://man7.org/linux/man-pages/man7/vdso.7.html) > Finding the vDSO > The base address of the vDSO (if one exists) is passed by the > kernel to each program in the initial auxiliary vector (see > getauxval(3)), via the AT_SYSINFO_EHDR tag. You must not assume the vDSO is mapped at any particular location in the user's memory map. The base address will usually be randomized at run time every time a new process image is created (at execve(2) time). This is done for security reasons, to prevent "return-to-libc" attacks. For some architectures, there is also an AT_SYSINFO tag. This is used only for locating the vsyscall entry point and is frequently omitted or set to 0 (meaning it's not available). This tag is a throwback to the initial vDSO work (see History below) and its use should be avoided. ### 覺得很奇怪為什麼 `clock_gettime` 的 assebly code 會有 syscall? 先用 strace 看了一下 ![image](https://hackmd.io/_uploads/SkRGjy2C0.png) ltrace `func` ![image](https://hackmd.io/_uploads/SkOW7Co00.png) ltrace `syscall` ![image](https://hackmd.io/_uploads/rJCzX0iRC.png) 我用 `info proc mappings` 沒有看到 vsyscall 解決方式在 `C:\Users\user` 創建一個 `.wslconfig` [參考 Microsoft 的文件](https://learn.microsoft.com/zh-tw/windows/wsl/wsl-config) ![image](https://hackmd.io/_uploads/B1uHcTjRR.png) 原本`cat /proc/self/maps` ![image](https://hackmd.io/_uploads/BkpeDc9R0.png) 加了`.config` ![image](https://hackmd.io/_uploads/r1H8jTsAR.png) [難道是GNU的BUG?](https://lists.gnu.org/archive/html/emacs-bug-tracker/2021-04/msg00198.html) 用 gdb 抓到所有的syscall ![image](https://hackmd.io/_uploads/rkRdpJh0C.png) ![image](https://hackmd.io/_uploads/S15D6J3RA.png) 簡單的看一下 `__GI__Clock_gettime()` 的 assembly code ![image](https://hackmd.io/_uploads/BJf9uV800.png) ![image](https://hackmd.io/_uploads/BkJ6dNUAA.png) 1. `vDSO` Call (at +23): At instruction `0x00007ffff7eaa0a7` <+23>, you see a dereferencing of a pointer (`mov 0x1a0(%rax),%rax`), and then at `0x00007ffff7eaa0b3 <+35>`, there's a call to the address stored in `%rax`: ```asm= callq *%rax ``` This is likely a call to the `vDSO` function for clock_gettime, which attempts to handle the request in user space. 2. Fallback to `Syscall (at +98)`: If the `vDSO` cannot handle the request (either %rax is NULL, or the result suggests it needs a fallback), the code jumps to `0x00007ffff7eaa0e8 <+88>`, where it executes the actual syscall at `0x00007ffff7eaa0f2 <+98>`: ```asm= mov $0xe4,%eax ; Load syscall number for clock_gettime (228) syscall ; Transition to kernel mode ``` The register `%eax` is loaded with the value `0xe4` (which is `228` in decimal). This is the syscall number for `clock_gettime` on x86_64 Linux. The `syscall` instruction then triggers the kernel to execute the time retrieval. 3. 看看是不是真的沒有碰到 `syscall` 設置 breakpoint 在 `__GI___clock_gettime` 的 `syscall` 中,還沒碰到 `syscall` 就跳到`clock_gettime` ![image](https://hackmd.io/_uploads/Syt6pe2RC.png) 跳到下一個 `clock_gettime` 的 `breakpoint` ![image](https://hackmd.io/_uploads/SkFYJWnCC.png) 看一下單步執行的結果,沒有碰到 `syscall` 上的 `breakpoint` ![image](https://hackmd.io/_uploads/ryhe-Z2RR.png) for 迴圈外面的 `clock_gettime` 沒又撞到 `syscall`,但裡面的有 ![image](https://hackmd.io/_uploads/SJ6jX-hA0.png) 原因 :arrow_right: 因為我外面的 `clock_gettime` 用`CLOCK_REALTIME_COARSE`,裡面的用 `CLOCK_REALTIME` ### syscall gdb 反組譯 ![image](https://hackmd.io/_uploads/HyNw4ESC0.png) ![image](https://hackmd.io/_uploads/BJOdNVBRR.png) ![image](https://hackmd.io/_uploads/S1roVVrRA.png) - 設定breakpoint在 assebly code 的 syscode上,一直狂按c大概撞到了 10 次左右 ![image](https://hackmd.io/_uploads/Hy38GZhA0.png) ## 結論 ### 在wsl2 因為我 `func` for 迴圈裡的 `clock_gettime` 失敗call了`syscall`,結果時間差別不多,也許是[因為 hyperversion 的問題](https://stackoverflow.com/questions/42622427/gettimeofday-not-using-vdso) <div style="text-align: center;"> <img src="https://hackmd.io/_uploads/S1Bch4L0A.png" alt="image alt"> </div> <div style="text-align: center;"> <img src="https://hackmd.io/_uploads/HJo1qq50C.png" alt="image alt"> </div> <div style="text-align: center;"> <img src="https://hackmd.io/_uploads/rJXbq5cR0.png" alt="image alt"> </div> ### 對clocksource操作 [看一下使用的clock資源,是 hyperv_clocksource_tsc_page](https://docs.oracle.com/en/database/oracle/oracle-database/19/ladbi/setting-clock-source-vm.html) ![image](https://hackmd.io/_uploads/S1MaoaoRA.png) 然後我把 clocksource 轉成 tsc: ![image](https://hackmd.io/_uploads/SkrdF-n0R.png) 試試看: It is useless. `func` still hits the break point I set in the clock_gettime syscall. 我想也是,根本沒有tsc的clocksource。 ![image](https://hackmd.io/_uploads/Sy4gqbhR0.png) 但 vDSO is working on wsl2!,這就很吊詭!! ![image](https://hackmd.io/_uploads/HJ6OXQ2AC.png) ### [vm中linux自動連網_Linux VM中的計時介紹](https://blog.csdn.net/cumo3681/article/details/107393067?ops_request_misc=&request_id=&biz_id=102&utm_term=hyperv_clocksource_tsc_page&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-1-107393067.142^v100^pc_search_result_base7&spm=1018.2226.3001.4187) ### [Linux kernel document](https://docs.kernel.org/virt/hyperv/clocks.html#x86-x64) 裡面有說 he Hyper-V synthetic system clock is available to user space via vDSO, and gettimeofday() and related system calls can execute entirely in user space. ### 在 VMware 嘗試 ![image](https://hackmd.io/_uploads/rk0S4M6R0.png) ![image](https://hackmd.io/_uploads/rkitNM6CC.png) ![image](https://hackmd.io/_uploads/B1N2EfTCC.png) 成功! ![image](https://hackmd.io/_uploads/BJNNNG60A.png) 明顯看到 syscall 比 func 來的慢 ![image](https://hackmd.io/_uploads/B1PCSM6C0.png) ## question 2 通常只有能在 `user space` 有效且安全處理的 system call 才會適合`vDSO`,像是不涉及改變系統狀態的「讀操作」或「查詢操作」。`vDSO` 的應用適合那些安全性需求較低、頻繁調用且只讀的系統調用,而需要修改系統狀態或涉及硬體操作的系統調用則必須在內核空間中執行,以確保系統安全。 ### 適合使用 vDSO 的 System Call 1. 時間相關的系統調用: - `clock_gettime()`: 提供高精度的時間資訊,常用於計算時間間隔。由於這是讀取系統時間,不需要特權,適合透過 vDSO 加速。 - `gettimeofday()`: 與 clock_gettime() 類似,返回當前時間(以秒和微秒計算),適合使用 vDSO。 - `time()`: 返回 Unix 紀元(1970-01-01 00:00:00 UTC)後的秒數,也是讀取系統時間,適合 vDSO。 2. CPU 和 NUMA 信息: - `getcpu()`: 獲取當前處理器和 NUMA 節點號,這是讀取 CPU 資訊的操作,不涉及安全性風險,適合 vDSO。 信號相關的系統調用: - `rt_sigreturn()`: 用於從信號處理程序返回,恢復信號處理前的上下文。這是一個低開銷操作,適合 vDSO 加速。 3. 進程或線程的時間資訊: - `clock_gettime(CLOCK_PROCESS_CPUTIME_ID)`: 獲取當前進程的 CPU 時間,不涉及改變系統狀態,適合 vDSO。 - `clock_gettime(CLOCK_THREAD_CPUTIME_ID)`: 獲取當前線程的 CPU 時間,類似於進程 CPU 時間,也適合使用 vDSO。 ### 不適合使用 vDSO 的 System Call 1. 文件系統操作: `open(), read(), write(), close()`: 這些調用涉及到文件讀寫操作,對底層硬體的訪問,需要在內核空間進行以確保系統資源安全。 2. 進程管理操作: `fork(), exec(), waitpid()`: 這些調用會創建新的進程或修改現有進程的狀態,必須在內核中處理來保護進程管理的安全性。 3. 內存管理: `mmap(), munmap(), brk()`: 這些操作涉及內存映射或修改進程的地址空間,這是非常敏感的操作,必須由內核管理。 4. 網絡和設備相關的操作: `socket(), connect(), send(), recv()`: 這些調用涉及網絡通信和設備管理,需要內核來進行安全控制,確保網絡和設備資源的正確分配和使用。 5. 系統資源配置: `settimeofday(), sethostname(), mount(), umount()`: 這些調用會修改系統的核心設置,例如時間、主機名、文件系統掛載等,具有潛在的安全風險,必須由內核進行控制。