Try   HackMD

Linux 核心設計: 賦予應用程式生命的系統呼叫

Copyright (慣C) 2018 宅色夫

Linux 系統呼叫作為真正的「九二共識」

直播錄影

儘管 1991 年 Linux 核心的原始程式碼已公開釋出,但到了 1992 年才開始以 GNU GPLv2 釋出原始程式碼,自此透過大量開發者的投入,創造真正的「九二共識」。

Linus Torvalds 在 2001 年紀錄片《Revolution OS》說過:

「作業系統就是你永遠不會看到的東西,因為沒有人直接使用作業系統,人們使用的是程式。在他們的電腦上,作業系統唯一的使命就是,幫助其它程式執行,所以作業系統從未獨立運行,而僅是默默等待程式,來向它要求現有資源、某個存在硬碟上的檔案或要求其它程式將這個程式連接到外面去,然後作業系統再一步步地,試著讓人們寫程式容易一些」

這席話背後的機制,恰好就是系統呼叫。

Linus Torvalds 在 2001 年 10 月重申「九二共識」:

From a technical standpoint, I believe the kernel will be "more of the same" and that all the _really_interesting staff will be going out in user space.

推薦閱讀:論文 Analyzing a Decade of Linux System Calls

從縮減 Hello World 程式談起

C 語言編程透視》是中國友人 falcon 著眼於透視 C 的前世今生,所撰寫的電子書,在 打造史上最小可執行 ELF 檔案 談及系統呼叫使用的機制。

給定 hello.c:

#include <stdio.h>
int main(void) {
    printf("hello, world!\n");
    return 0;
}

以 gcc/clang 編譯 hello.c,隨後用 ltrace 追蹤:

ltrace hello 
__libc_start_main(0x400526, 1, 0x7fffb57761d8, 0x400540 <unfinished ...>
puts("hello, world!"hello, world!
)                                                                            = 14
+++ exited (status 0) +++

幾個觀察:

對 gcc 輸出的組合語言進行縮減,得到以下 hello.s:

.LC0:
	.string "Hello world"
	.text
.globl main
	.type   main, @function
main:
	leal    4(%esp), %ecx
	andl    $-16, %esp
	pushl   -4(%ecx)
	pushl   %ebp
	movl    %esp, %ebp
	pushl   %ecx
	subl    $4, %esp
	movl    $.LC0, (%esp)
	call    puts
	movl    $0, (%esp)
	call    _exit

組譯和連結:

$ as --32 -o hello.o hello.s
$ ld -melf_i386 -o hello hello.o

你所不知道的 C 語言: 編譯器和最佳化原理篇 提及 gcc 會將 printf("hello, world!\n") 最佳化為 puts("hello, world!\n"),以降低解析 format string 和對應處理的成本。qrintf - sprintf accelerator 展示了字串處理的執行時期開銷。對應到組合語言就是 call puts,以及之前參數傳遞的指令。

可避免動態連結函式庫中的 printfputs,也不用直接呼叫 _exit,而在組合語言裡頭使用系統呼叫,即可可以去掉和動態連結關聯的內容。重寫後得到如下 x86 程式碼:

.LC0:
	.string "Hello world!\xa\x0"
	.text
.global _start
_start:
	xorl   %eax, %eax
	movb   $4, %al          #eax = 4, sys_write(fd, addr, len)
	xorl   %ebx, %ebx
	incl   %ebx             #ebx = 1, standard output
	movl   $.LC0, %ecx      #ecx = $.LC0, the address of string
	xorl   %edx, %edx
	movb   $14, %dl         #edx = 14, the length of .string
	int    $0x80

	xorl   %eax, %eax
	movl   %eax, %ebx       #ebx = 0
	incl   %eax             #eax = 1, sys_exit
	int    $0x80

對應的 x86_64 版本:

.data
msg:
    .ascii "Hello, world!\n"
    len = . - msg
.text
    .global _start

_start:
    movq  $1, %rax
    movq  $1, %rdi
    movq  $msg, %rsi
    movq  $len, %rdx
    syscall

    movq  $60, %rax
    xorq  %rdi, %rdi
    syscall

組譯和連結:

$ gcc -c hello.s
$ ld -o hello hello.o

原來,無論是 x86 的 int $0x80 抑或 x86_64 的 syscall 都是系統呼叫的 call gate,後期 Intel 引入快速系統呼叫 (fast system call)。

此處所指的 call gate 為涉及到特權模式移轉的操作,可以讓使用者在特權等級較低的情況下,跳到較高的特權等級(通常意味著作業系統),而非指 x86 特有的機制 CALL FAR instruction


搭配閱讀: System Calls Make the World Go Round

使用 syscall(2) 改寫為以下:

#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h>
int main() {
    return syscall(__NR_write, 1, "Hello, world!\n", 14);
}

透過 strace(1) 追蹤系統呼叫:

write(1, "hello, world!\n", 14hello, world!
)         = 14

查閱 syscall(2):

arch/ABI instruction syscall # retval
arm/OABI swi NR - a1
arm/EABI swi 0x0 r7 r0
arm64 svc #0 x8 x0
i386 int $0x80 eax eax
x86_64 syscall rax rax
x32 syscall rax rax

x32 ABI allows programs to take advantage of the benefits of x86-64 instruction set (larger number of CPU registers, better floating-point performance, faster position-independent code, shared libraries, function parameters passed via registers, faster syscall instruction) while using 32-bit pointers and thus avoiding the overhead of 64-bit pointers.
_syscall(2) 已棄置

搭配閱讀: Computer Science from the Bottom Up: System Calls

透過 kprobes + eBPF 來追蹤系統呼叫

Kernel Probes (Kprobes) 僅能讀取系統呼叫的參數和返回值,無法變更暫存器內含值。

複習 透過 eBPF 觀察作業系統行為

準備 call.py

from bcc import BPF bpf_text = """ #include <net/inet_sock.h> #include <bcc/proto.h> int kprobe__inet_listen(struct pt_regs *ctx, struct socket *sock, int backlog) { bpf_trace_printk("Hello World!\\n"); return 0; }; """ b = BPF(text=bpf_text) while True: print b.trace_readline()

先在一個終端機執行:

$ sudo python call.py

等待五秒,再開啟另一個終端機並執行以下命令:

$ nc -l 0 4242

依據 nc(1):

arbitrary TCP and UDP connections and listens
-l' Used to specify that nc should listen for an incoming connection rather than initiate a connection to a remote host.

0 是 hostname (也就是本地端),4242 是 port

預期將看到以下訊息:

nc-10348 [027] .... 12355941.551058: 0x00000001: Hello World!

修改上述程式的第 8 行為:

bpf_trace_printk("Listening with %d pending connections!\\n", backlog);

重作實驗,預期得到以下訊息:

 nc-10842 [025] .... 12356439.738348: 0x00000001: Listening with 1 pending connections!

修改上述 kprobe__inet_listen 函式,取代為以下:

    struct inet_sock *inet = inet_sk(sock->sk);
    u32 laddr = 0; u16 lport = 0;
    bpf_probe_read(&laddr, sizeof(laddr), &(inet->inet_rcv_saddr));
    bpf_probe_read(&lport, sizeof(lport), &(inet->inet_sport));
    bpf_trace_printk("Listening on %x %d\\n", ntohl(laddr), ntohs(lport));
    return 0;

重作實驗,預期得到以下訊息:

nc-11250 [020] .... 12356849.241049: 0x00000001: Listening on 0 4242

延伸閱讀:

Linux 核心對系統呼叫的實作機制

檔案 linux/arch/x86/entry/syscall_64.c:

/* this is a lie, but it does not hurt as sys_ni_syscall just returns -EINVAL */
extern asmlinkage long sys_ni_syscall(const struct pt_regs *);
#define __SYSCALL_64(nr, sym, qual) extern asmlinkage long sym(const struct pt_regs *);
#include <asm/syscalls_64.h>
#undef __SYSCALL_64

#define __SYSCALL_64(nr, sym, qual) [nr] = sym,

asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
	/*
	 * Smells like a compiler bug -- it doesn't work
	 * when the & below is removed.
	 */
	[0 ... __NR_syscall_max] = &sys_ni_syscall,
        #include <asm/syscalls_64.h>      
};

搭配閱讀 How does the Linux kernel handle a system callAnatomy of a system call, part 1

vsyscall 和 vDSO

問問 git (有飯桶」和「笨蛋」的意思) 需要什麼?

$ ldd /usr/bin/git
	linux-vdso.so.1 =>  (0x00007ffd83ff1000)
	libpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007f7035583000)
	libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f7035369000)
	libresolv.so.2 => /lib/x86_64-linux-gnu/libresolv.so.2 (0x00007f703514e000)
	libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f7034f31000)
	librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f7034d29000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f703495f000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f70357f3000)

等等,檔案 linux-vdso.so.1 在哪?

參見 什麼是 Linux vDSO 與 vsyscall?——發展過程

benefits to gettimeofday() is implemented with a userspace-only vsyscall/vdso, which avoids the syscall
overhead.
gettimeofday、clockgettime 以及不同時鐘源的影響

在 32-bit 和 64-bit 環境還有更多考量,參見 vDSO on arm64

搭配閱讀: