Copyright (慣C) 2018 宅色夫
儘管 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
《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) +++
幾個觀察:
__libc_start_main
是程式真正的進入點,詳見 你所不知道的 C 語言: 執行階段程式庫 (CRT)puts
函式的參數puts
函式返回值為 14
,表示字串長度status 0
為 status code,也就是 main 函式的return
值對 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
,以及之前參數傳遞的指令。
可避免動態連結函式庫中的 printf
或 puts
,也不用直接呼叫 _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
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/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 call 和 Anatomy of a system call, part 1
問問 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
搭配閱讀: