---
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 : 速度較慢


上面這兩張圖可以很明顯的知道執行 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)