系統呼叫在Wikipedia中的解釋為:
In computing, a system call is the programmatic way in which a computer program requests a service from the kernel of the operating system it is executed on. This may include hardware-related services (for example, accessing a hard disk drive), creation and execution of new processes, and communication with integral kernel services such as process scheduling. System calls provide an essential interface between a process and the operating system.
In most systems, system calls can only be made from userspace processes, while in some systems, OS/360 and successors for example, privileged system code also issues system calls.
主要意思是:
(1) 系統呼叫是程式以程式化的方式向其執行的作業系統請求服務。
(2) 請求的服務可能包括硬體相關服務(訪問磁碟驅動器)、新行程建立和執行等。
(3) 系統呼叫在程式和作業系統之間提供一個基本介面。
大多數系統中,系統呼叫只由處於使用者態的行程發出。
陳莉君老師的《Linux作業系統原理與應用(第二版)》對Linux系統呼叫解釋為:
系統呼叫的實質就是函式呼叫,只是呼叫的函式是系統函式,處於核心態而已。使用者在呼叫系統呼叫時會向內核傳遞一個系統呼叫號,然後系統呼叫處理程式透過此號從系統呼叫表中找到相應地核心函式執行(系統呼叫服務例程),最後傳回。
總結
作業系統核心提供了許多服務,服務在物理表現上為核心空間的函式,系統呼叫即為在使用者空間對這些核心提供服務的請求,即在使用者空間程式“呼叫”核心空間的函式完成相應地服務。
圖2-1 int80系統呼叫示意圖
下麵基於linux-2.6.39核心進行分析:
1.1 初始化系統呼叫
核心在初始化期間呼叫trap_init()函式建立中斷描述符表(IDT)中128個向量對應的表項。
在arch/x86/kernel/traps.c的trap_init()函式中可以看到:
#ifdef CONFIG_X86_32
set_system_trap_gate(SYSCALL_VECTOR, &system;_call);
set_bit(SYSCALL_VECTOR, used_vectors);
#endif
SYSCALL_VECTOR在arch/x86/include/asm/irq_vectors.h可以看到值為0x80,即系統呼叫對應到0x80號中斷。
set_system_trap_gate即用來在IDT上設定系統呼叫門,在arch/x86/include/asm/desc.h可以看到:
static inline void set_system_trap_gate(unsigned int n, void *addr)
{
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_TRAP, addr, 0x3, 0, __KERNEL_CS);
}
可以看到實際上執行的是__set_gate()函式,這個函式把相關值裝入門描述符的相應域。
n:即為0x80這一中斷號。
GATE_TRAP: 在arch/x86/include/asm/desc_defs.h中定義為0x0F,表示這一中斷(異常)是陷阱。
addr:即為&system;_call,系統呼叫處理程式入口。
0x3: 描述符特權級(DPL),表示允許使用者態行程呼叫這一異常處理程式。
__KERNEL_CS: 由於系統呼叫處理程式處於核心當中,所以應選擇__KERNEL_CS填充段暫存器。
1.2 系統呼叫處理(system_call())
執行int 80指令後,根據向量號在IDT中找到對應的表項,執行system_call()函式,在arch/x86/kernel/entry_32.S中可以看到system_call()函式:
ENTRY(system_call)
RING0_INT_FRAME
pushl_cfi %eax
SAVE_ALL
GET_THREAD_INFO(%ebp)
testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
jnz syscall_trace_entry
cmpl $(nr_syscalls), %eax
jae syscall_badsys
syscall_call:
call *sys_call_table(,%eax,4)
movl %eax,PT_EAX(%esp)
syscall_exit:
LOCKDEP_SYS_EXIT
DISABLE_INTERRUPTS(CLBR_ANY)
TRACE_IRQS_OFF
movl TI_flags(%ebp), %ecx
testl $_TIF_ALLWORK_MASK, %ecx # current->work
jne syscall_exit_work
restore_all:
TRACE_IRQS_IRET
restore_all_notrace:
movl PT_EFLAGS(%esp), %eax # mix EFLAGS, SS and CS
movb PT_OLDSS(%esp), %ah
movb PT_CS(%esp), %al
andl $(X86_EFLAGS_VM | (SEGMENT_TI_MASK << 8) | SEGMENT_RPL_MASK), %eax
cmpl $((SEGMENT_LDT << 8) | USER_RPL), %eax
CFI_REMEMBER_STATE
je ldt_ss
restore_nocheck:
RESTORE_REGS 4
irq_return:
INTERRUPT_RETURN
主要工作有:
(1)儲存現場:
pushl_cfi %eax:先把系統呼叫號儲存棧中。
SAVE_ALL:把異常處理程式可以用到的所有CPU暫存器儲存到棧中。
GET_THREAD_INFO(%ebp):將當前行程PCB地址存放到ebp中,GET_THREAD_INFO()定義在arch/x86/include/asm/thread_info.h。
(2)跳轉到相應服務程式:
cmpl $(nr_syscalls), %eax:先檢查使用者態行程傳來的系統呼叫號是否有效,如果大於等於NR_syscalls,則跳轉到syscall_badsys,終止系統呼叫程式,傳回用戶空間。
syscall_badsys:將-ENOSYS存放到eax暫存器所在棧中位置,再跳轉到resume_userspace傳回用戶空間,傳回後EAX中產生負的ENOSYS。
call *sys_call_table(,%eax,4):根據EAX中的系統呼叫號呼叫對應的服務程式。
(3)退出系統呼叫:
movl %eax, PT_EAX(%esp):儲存傳回值。
syscall_exit_work -> work_pending -> work_notifysig來處理訊號。
可能執行call schedule來進行行程排程;或者跳轉到resume_userspace,呼叫restall_all恢復現場,傳回用戶態。
1.3 系統呼叫表
在system_call()函式中的call *sys_call_table(,%eax,4) 陳述句中,根據eax暫存器中所存的系統呼叫號到sys_call_table系統呼叫表中找到對應的系統呼叫服務程式
由於是32位即每個sys_call_table是4個位元組,如果是64位則程式陳述句為call *sys_call_table(, %eax, 8)
在linux-2.6.39核心原始碼中:
32位下系統呼叫表在arch/x86/kernel/syscall_table_32.S中定義,每個表項包含一個系統呼叫服務例程的地址:
ENTRY(sys_call_table)
.long sys_restart_syscall /* 0 – old “setup()” system call, used for r estarting */
.long sys_exit
.long ptregs_fork
.long sys_read
.long sys_write
.long sys_open /* 5 */
…
64位系統的若要使用syscall指令來進行系統呼叫而不使用int 80,則用到的系統呼叫表在arch/x86/kernel/syscall_64.c中定義:
#define __SYSCALL(nr, sym) [nr] = sym,
const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
[0 … __NR_syscall_max] = &sys;_ni_syscall,
#include
};
可以看到系統呼叫表是include進去的,arch/x86/include/asm/unistd_64.h中放著:
#define __NR_read 0
__SYSCALL(__NR_read, sys_read)
#define __NR_write 1
__SYSCALL(__NR_write, sys_write)
#define __NR_open 2
__SYSCALL(__NR_open, sys_open)
…
所以在宏__SYSCALL的作用下,系統呼叫表為如下定義:
const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
[0 … __NR_syscall_max] = &sys;_ni_syscall,
[0] = sys_read,
[1] = sys_write,
[2] = sys_open,
…
};
在Linux中呼叫系統呼叫的操作代價很大,因為處理器必須中斷當前正在執行的任務並從使用者態切換到核心態,執行完系統呼叫程式後又從核心態切換回用戶態。
為了加快系統呼叫的速度,隨後先後引入了兩種機制——vsycalls和vDSO。
2.1 vsyscalls
vsyscalls的工作原理即為:Linux核心將第一個頁面對映到使用者空間,該頁麵包含一些變數和一些系統呼叫的實現,被對映到使用者空間的系統呼叫即可以在使用者空間執行,不需要進行背景關係切換。
執行命令如下命令可以看到有關vsyscalls記憶體空間的資訊:
$ sudo cat /proc/1/maps | grep vsyscall
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
vsyscall頁面對映從核心啟動開始start_kernel() -> setup_arch() -> map_vsyscall(),map_vsyscall()函式原始碼在arch/x86/entry/vsyscall/vsyscall_64.c中:
void __init map_vsyscall(void)
{
extern char __vsyscall_page;
unsigned long physaddr_vsyscall = __pa_symbol(&__vsyscall_page);
if (vsyscall_mode != NONE) {
__set_fixmap(VSYSCALL_PAGE, physaddr_vsyscall,
PAGE_KERNEL_VVAR);
set_vsyscall_pgtable_user_bits(swapper_pg_dir);
}
BUILD_BUG_ON((unsigned long)__fix_to_virt(VSYSCALL_PAGE) !=
(unsigned long)VSYSCALL_ADDR);
}
可以看到頁面對映函式中首先使用__pa_symbol宏獲取頁面的物理地址。
__vsyscall_page在arch/x86/entry/vsysall/vsyscall_emu_64.S中定義,可以看出來__vsyscall_page包含三個系統呼叫:gettimeofday, time, getcpu:
__vsyscall_page:
mov $__NR_gettimeofday, %rax
syscall
ret
.balign 1024, 0xcc
mov $__NR_time, %rax
syscall
ret
.balign 1024, 0xcc
mov $__NR_getcpu, %rax
syscall
ret
獲取頁面的物理地址之後檢查vsyscall_mode變數的值並使用__set_fixmap宏來設定頁面的修複對映地址(Fix-Mapped Address),__set_fixmap在arch/x86/include/asm/fixmap.h中定義:
(1) 第一個引數是列舉型別fixed_addresses,這裡傳入引數實際值為(0xfffff000 – (-10UL << 20)) >> 12:
#ifdef CONFIG_X86_VSYSCALL_EMULATION
VSYSCALL_PAGE = (FIXADDR_TOP – VSYSCALL_ADDR) >> PAGE_SHIFT,
#endif
(2) 第二個引數是必須對映的頁面的物理地址,這裡傳入透過__pa_symbol宏定義獲取到的物理地址
(3) 第三個引數是頁面的flags,傳入的是PAGE_KERNEL_VVAR,在arch/x86/include/asm/pgtable_types.h中定義,_PAGE_USER意味著可以透過使用者樣式的行程訪問該頁面:
#define default_pgprot(x) __pgprot((x) & __default_kernel_pte_mask)
#define __PAGE_KERNEL_VVAR (__PAGE_KERNEL_RO | _PAGE_USER)
#define PAGE_KERNEL_VVAR default_pgprot(__PAGE_KERNEL_VVAR | _PAGE_ENC)
設定完頁面的修複地址後呼叫set_vsyscall_pgtable_user_bits()函式對改寫VSYSCALL_ADDR的表設定_PAGE_USER;最後使用BUILD_BUG_ON宏來檢查vsyscall頁面的虛擬地址是否等於VSTSCALL_ADDR的值。
2.2 vDSO
雖然引入了vsyscall機制,但是vsyscall存在著問題:
(1)vsyscall的使用者空間對映的地址是固定不變的,容易被駭客利用。
(2)vsyscall能支援的系統呼叫數有限,不易擴充套件。
vDSO是vsyscall的主要替代方案,是一個虛擬動態連結庫,將記憶體頁面以共享物件形式對映到每個行程,使用者程式在啟動的時候透過動態連結操作,把vDSO連結到自己的記憶體空間中。動態連結保證了vDSO每次所在的地址都不一樣,並且可以支援數量較多的系統。
執行下列命令:
$ ldd /bin/uname
linux-vdso.so.1 => (0x00007ffcb75de000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3c36e1d000)
/lib64/ld-linux-x86-64.so.2 (0x00007f3c371e7000)
可以看到uname util與三個庫連結:
– linux-vdso.so.1:提供vDSO功能。
– lib.so.6:C標準庫。
– ld-linux-x86-64.so.2:程式直譯器(聯結器)。
初始化vDSO發生在arch/x86/entry/vdso/vma.c的init_vdso()函式中:
static int __init init_vdso(void)
{
init_vdso_image(&vdso;_image_64);
#ifdef CONFIG_X86_X32_ABI
init_vdso_image(&vdso;_image_x32);
#endif
return 0;
}
使用init_dso_image()函式來初始化vdso_image結構體,vdso_image_64和vdso_image_x32在arch/x86/entry/vdso/vdso-image-64.c和arch/x86/entry/vdso/vdso-image-x32.c中進行定義,例如vdso_image_64
對vDOS系統呼叫的記憶體頁面相關的結構體初始化後,使用從arch/x86/entry/vdso/vma.c中呼叫函式arch_setup_additional_pages()來檢查並呼叫map_vdso_randomized() -> map_vdso()函式來進行記憶體頁面對映:
int arch_setup_additional_pages(struct linux_binprm *bprm, int uses_interp)
{
if (!vdso64_enabled)
return 0;
return map_vdso_randomized(&vdso;_image_64);
}
上面說到的vsyscalls和vDSO都是從機制上對系統呼叫速度進行的最佳化,但是使用軟中斷來進行系統呼叫需要進行特權級的切換這一根本問題沒有解決。
為瞭解決這一問題,Intel x86 CPU從Pentium II (Family6, Model 3, Stepping 3)之後,開始支援快速系統呼叫指令sysenter/sysexit,下篇將進行具體介紹。