--- title: System Call (系統呼叫) - 從零開始的開源地下城 tags: Linux, Linux讀書會, Kernel, 從零開始的開源地下城, COMBO-tw description: 介紹與Linux Kernel相關基本知識 lang: zh-Hant GA: G-2QY5YFX2BV --- # System Call (系統呼叫) ###### tags: `Linux` ## 目錄 [TOC] ## 簡介 ### 什麼是 System Call 根據[維基百科](https://zh.wikipedia.org/zh-tw/系統調用),系統呼叫 (system call,簡稱為 syscall),是指運行在 user space 的程式向作業系統核心請求需要更高權限運行的服務。系統呼叫提供 user space 和作業系統之間的介面。 :::info 簡單來說,system call 是 process 和 OS 之間的介面,當使用者程式需要 OS 的服務時,使用者程式便去呼叫 system call ::: System call 的種類大致分為 * Process Control * 對 process 做控制,如 * process 的啟動與停止 * 給予 process 記憶體空間 * 讀取或設定 process 的屬性 (attributes) * 讓 process 作等待 * e.g. `fork()`, `wait4()` * File Management * 對檔案做控制,如 * 檔案的建立與刪除 * 檔案的開啟與關閉 * 檔案的讀取與寫入 * 讀取或設定檔案的屬性 (attributes) * e.g. `open()`, `read()`, `write()`, `close()` * Device Management * 電腦系統各項硬體資源與周邊設備均為 device,當 process 在執行時會去要求取得資源或存取資料,如 * Device 的要求與釋放 * Device 資料的存取 * 讀取或設定 device 的屬性 (attributes) * 邏輯連接或斷開 (Logically attach / detach) * e.g. `ioctl()` * Information Maintenance * 對系統資料的更新與維護,如 * 設定日期與時間 * 存取系統資料 * 存取 process、file、device 的屬性 (attributes) * e.g. `getpid()`, `getppid()`, `getpgid()`, `setpgid()` * Communication * 當裝置以連線連通,執行訊息傳遞等 I/O 存取的行為,如 * 建立或中斷連線 * 輸入輸出網路資料 * 狀態資訊的轉換 (Transfer status information) * 使用遠端裝置 * e.g. `pipe()`, `socket()` ### Dual Mode 作業系統存在的意義是提供一個抽象層,讓使用者能夠在一個方便的環境下使用或開發應用程式,這個方便的環境就是 user space。 不同於 kernel space 的程式必須面對 CPU 的指令集和對各式各樣的硬體打交道,user space 的應用程式若需要與作業系統或硬體相關的功能,就必須要仰賴系統呼叫。而在 Linux 中依照其權限分為兩種模式 - **user mode 和 kernel mode**。目的為**保護系統安全**,一般在 user space 的應用程式不允許直接對 kernel space 的資料做存取 ### 呼叫流程 首先我們先要知道 system call 是怎麼進行參數傳遞的,分為三種方法 * 利用 Registers 儲存參數 * Pros : 快速 * Cons : register 數量有限,參數不能太多 * 將參數存在 Memory Block 後,把該 block 的起始位址存在一個 register 中,之後將此 register 傳遞給 OS * Pros : 可存參數較多 * Cons : 速度較慢 * 利用 system stack 儲存參數,藉由 push 來保存參數,pop 來取出參數 * Pros : 可存參數較多 * Cons : 速度較慢 ![](https://hackmd.io/_uploads/BkWsZoZLn.png) ![](https://hackmd.io/_uploads/HyBo-sZI3.png) 上面這兩張圖可以很明顯的知道執行 system call 所經過的流程 1. 在使用者程式中呼叫 system call 2. 藉由一個軟體中斷 trap (svc #0) 進入 kernel mode,此時系統會將 mode bit 由 user mode 改成 kernel mode (1 -> 0) 3. 查詢 system call table 來找尋對應的 trap service routine 4. 當執行完 trap service routine 後發出中斷通知 OS 已經完成 ### vsyscall 和 vDSO 放置一些常用的核心變數,例如:`gettimeofday`、`time`、`getcpu` 等,這些變數放在 vsyscall、vvar,因此程式碼存取這些變數不需要進入 kernel mode,減少模式切換 (mode change) 的時間。 vsyscall (virtual system call) 和 vDSO (virtual Dynamic Shared Object) 的相關說明可參閱︰ * [The vDSO on arm64](https://blog.linuxplumbersconf.org/2016/ocw/system/presentations/3711/original/LPC_vDSO.pdf) * [Anatomy of a system call, part 2](https://lwn.net/Articles/604515/) * [什麼是 Linux vDSO 與 vsyscall?——發展過程](https://alittleresearcher.blogspot.com/2017/04/linux-vdso-and-vsyscall-history.html) ### 搭配閱讀 * Chapter 4 of "Understanding the Linux Kernel, Third Edition" * Chapter 5 of "Linux Kernel Development, Third Edition" * Chapter 2 of "Operating System Concepts, Ninth Edition" * [Anatomy of a system call, part 1](https://lwn.net/Articles/604287/) * [How does the Linux kernel handle a system call](https://0xax.gitbooks.io/linux-insides/SysCall/linux-syscall-2.html) * [Analyzing a Decade of Linux System Calls](http://research.cs.queensu.ca/home/cordy/Papers/BKBHDC_ESE_Linux.pdf) ## 實作 ### 如何新增一個 System Call 1. 建立一個放自定義 `syscall` 的資料夾 * `$ cd linux && mkdir workspace` 2. 寫一個自定義的 `syscall` * `$ vim workspace/hello_world.c` ```C= # include <linux/kernel.h> asmlinkage long sys_hello_world(void) { printk("Hello World!\n"); return 0; } ``` 3. 再建立一個 `Makefile` * `$ vim workspace/Makefile` ```C= obj-y := hello_world.o ``` 4. 改 kernel `Makefile` 來告訴 compiler 說新的 system call 可以在哪邊找到 * `$ vim Makefile` * 找到 `core-y` 並在最後面添加之前所建立的資料夾,這樣 kernel 編譯時才會找到自定義的目錄 ```SHELL=957 ... ifeq ($(KBUILD_EXTMOD),) core-y += kernel/ certs/ mm/ fs/ ipc/ security/ crypto/ block/ workspace/ ... ``` 5. 修改 system call table * `$ vim arch/arm/tools/syscall.tbl` * 在最後一行上添加自定義的 `syscall`,這邊可以先記住 system call number,等一下會用到 ```C=413 ... 397 common statx sys_statx 398 common hello_world sys_hello_world ``` 6. 最後在 system call header file 上添加自定義的 `syscall` 的 prototype * `$ vim include/linux/syscalls.h` ```C=941 ... unsigned mask, struct statx __user *buffer); asmlinkage long sys_hello_world(void); ``` 7. 重新編譯 kernel :::info 在 kernel v4.9 以前,要新增一個 `syscall` 需要修改 `arch/arm/kernel/calls.S`、`asm/unistd.h`、`uapi/asm/unistd.h` 和 `include/linux/syscalls.h` 這些檔案 但是從 kernel v4.10 開始,Russell King 提供了一個自動計算 `__NR_syscalls` 的 shell script,因此只需要更改 `arch/arm/tools/syscall.tbl` 和 `include/linux/syscalls.h` 就可以了 可參閱︰[[PATCH 2/3] ARM: convert to generated system call tables](https://patchwork.kernel.org/patch/9382833/) ::: ### 如何測試一個 System Call #### Method 1 - 透過 Initrd 1. 先寫一個簡單測試 `sys_hello_world` 的程式 * `$ vim hello.c` ```C= #include <linux/kernel.h> #include <sys/syscall.h> #include <unistd.h> #include <stdio.h> int main(void) { long retval = syscall(398); while (1) ; } ``` 2. 使用靜態編譯 * `$ arm-linux-gnueabihf-gcc -static -o hello hello.c` 3. 將 `hello` 製作成 cpio * `$ echo hello | cpio -o --format=newc > rootfs` 4. 使用 QEMU 啟動後即可看見 `sys_hello_world` 所印的結果 * 這邊我們把根目錄直接設成記憶體,啟動用的 `init` 程式直接設成我們之前放進去的 `hello` * ```SHELL $ sudo qemu-system-arm \ -M versatilepb \ -cpu arm1176 \ -m 256 \ -dtb versatile-pb.dtb \ -kernel qemu-kernel-4.14.50 \ -initrd rootfs \ -append "root=/dev/ram rdinit=/hello" \ -nographic ``` > 延伸閱讀︰[什麼是 initrd](https://developer.ibm.com/articles/l-initrd/)、[如何使用 initrd](https://www.kernel.org/doc/html/latest/admin-guide/initrd.html) #### Method 2 - 透過 Raspbian disk image 1. 使用 QEMU 開機後 2. 寫一個簡單測試 `sys_hello_world` 的程式 * `$ vim hello.c` ```C= #include <linux/kernel.h> #include <sys/syscall.h> #include <unistd.h> #include <stdio.h> int main(void) { long retval = syscall(398); return retval; } ``` 3. 編譯並執行 * `$ gcc -o hello hello.c` * `$ ./hello` 4. 查看 printk 印出來的結果,最後一行為 `sys_hello_world` 所印的結果 * `$ dmesg` ``` ... [ 65.952997] Adding 102396k swap on /var/swap. Priority:-2 extents:1 across:102396k [ 78.207853] Hello World! ``` ### 如何追蹤一個 System Call #### 使用 `strace` 1. 使用上面提到的 Method 2 的方法 2. 透過 [strace(1)](http://man7.org/linux/man-pages/man1/strace.1.html) 追蹤系統呼叫 * `$ strace -T ./hello` ```SHELL ... syscall_398(0xbebc1994, 0xbebc199c, 0x104cc, 0, 0, 0) = 0 <0.000875> exit_group(0) =? +++ exited with 0 +++ ``` > 延伸閱讀︰[strace 的其他使用方法](https://blogs.oracle.com/linux/strace-the-sysadmins-microscope-v2) #### 使用 GDB 查閱 [syscall(2)](http://man7.org/linux/man-pages/man2/syscall.2.html) 可以知道 | Arch/ABI | Instruction | System call # | Ret val1 | Ret val2 | | -------- | ----------: | ------------: | -------: | -------: | | arm/OABI | swi NR | - | a1 | - | | arm/EABI | swi 0x0 | r7 | r0 | r1 | | arm64 | svc #0 | x8 | x0 | x1 | | i386 | int $0x80 | eax | eax | edx | 對照 GDB ```SHELL $ gdb ./hello (gdb) start (gdb) step (gdb) disas Dump of assembler code for function syscall: => 0xb6f2d4d0 <+0>: mov r12, sp 0xb6f2d4d4 <+4>: push {r4, r5, r6, r7} 0xb6f2d4d8 <+8>: mov r7, r0 0xb6f2d4dc <+12>: mov r0, r1 0xb6f2d4e0 <+16>: mov r1, r2 0xb6f2d4e4 <+20>: mov r2, r3 0xb6f2d4e8 <+24>: ldm r12, {r3, r4, r5, r6} 0xb6f2d4ec <+28>: svc 0x00000000 0xb6f2d4f0 <+32>: pop {r4, r5, r6, r7} 0xb6f2d4f4 <+36>: cmn r0, #4096 ; 0x1000 0xb6f2d4f8 <+40>: bxcc lr 0xb6f2d4fc <+44>: b 0xb6e767d0 <__syscall_error> End of assembler dump. ``` > 搭配閱讀︰[Computer Science from the Bottom Up: System Calls](https://www.bottomupcs.com/system_calls.xhtml) #### 其他方法 * SystemTap * Kprobes + eBPF * ftrace * ltrace ## 本章節練習與反思 * 在 Linux Kernel 的實作中大量使用了 [asmlinkage](https://elixir.bootlin.com/linux/v4.14.50/source/arch/arm64/kernel/sys.c#L30) 和 [SYSCALL_DEFINEn](https://elixir.bootlin.com/linux/v4.14.50/source/arch/arm64/kernel/sys.c#L40),請問這兩個作用為何?又帶來了什麼好處? * 請說明在 Linux Kernel 中的 syscall 是用什麼方法傳遞參數的 * 請寫出一個計算階乘 $n!$ 的 syscall `factorial()` > 測試程式位於[此](https://github.com/combo-tw/LinuxBookClub/blob/master/chapter/03-syscall/test/factorial_ut.c) > 測試方法為當 QEMU 啟動後,進入 `/home/pi/test` 資料夾,並執行 `make check` 即可 > > Hints: 可以使用 `copy_to_user()` 和 `copy_from_user()` 來對 user space 的 data 進行存取 * 請解釋一般的 function call 與 syscall 的差異與其開銷為何,可分別從名詞解釋、呼叫流程和實驗下手 * 請寫下遇到的困難是怎麼去解決和除錯的 ## 參考資料 * [Wikipedia](https://en.wikipedia.org/wiki/System_call) * [System Call - Sw@y's Notes](http://swaywang.blogspot.com/2011/10/system-call-system-call-process-oslinux.html) * [Syllabus Content For operating system - PadaKuu](http://www.padakuu.com/listing/subject/operating-system) * [What happens once a system call is made by a process in user space ? - Quora](https://www.quora.com/What-happens-once-a-system-call-is-made-by-a-process-in-user-space) * [增加一個 System Call 到 Linux Kernel (v.4.x)](https://wenyuangg.github.io/posts/linux/linux-add-system-call.html) * [製作 initrd,跑個 Hello world 開始](https://www.itread01.com/content/1550477197.html)