歡迎光臨
每天分享高質量文章

張凱捷——系統呼叫分析(1)

系統呼叫的概念

 

//

 

1

Wikipedia系統呼叫的解釋

 

系統呼叫在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) 系統呼叫在程式和操作系統之間提供一個基本接口。

大多數系統中,系統呼叫只由處於用戶態的行程發出。

 
2

Linux操作系統原理與應用》解釋:

陳莉君老師的《Linux操作系統原理與應用(第二版)》對Linux系統呼叫解釋為:

        系統呼叫的實質就是函式呼叫,只是呼叫的函式是系統函式,處於內核態而已。用戶在呼叫系統呼叫時會向內核傳遞一個系統呼叫號,然後系統呼叫處理程式通過此號從系統呼叫表中找到相應地內核函式執行(系統呼叫服務例程),最後傳回。

 

3

總結

操作系統內核提供了許多服務,服務在物理表現上為內核空間的函式,系統呼叫即為在用戶空間對這些內核提供服務的請求,即在用戶空間程式“呼叫”內核空間的函式完成相應地服務。

//

 

系統呼叫實現分析

 

int / iret
0
1
早些時候,通過int 80來進行系統呼叫,呼叫一個系統呼叫示意圖:

 

圖2-1 int80系統呼叫示意圖

下麵基於linux-2.6.39內核進行分析:

1.1 初始化系統呼叫

內核在初始化期間呼叫trap_init()函式建立中斷描述符表(IDT)128個向量對應的表項。

arch/x86/kernel/traps.ctrap_init()函式中可以看到:

#ifdef CONFIG_X86_32

set_system_trap_gate(SYSCALL_VECTOR, &system;_call);

set_bit(SYSCALL_VECTOR, used_vectors);

#endif

 

SYSCALL_VECTORarch/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_table4個位元組,如果是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,

        …

        };

 

vsyscalls vDSO
0
2

Linux中呼叫系統呼叫的操作代價很大,因為處理器必須中斷當前正在執行的任務並從用戶態切換到內核態,執行完系統呼叫程式後又從內核態切換回用戶態。

為了加快系統呼叫的速度,隨後先後引入了兩種機制——vsycallsvDSO

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_pagearch/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_fixmaparch/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能支持的系統呼叫數有限,不易擴展。

vDSOvsyscall的主要替代方案,是一個虛擬動態鏈接庫,將記憶體頁面以共享物件形式映射到每個行程,用戶程式在啟動的時候通過動態鏈接操作,把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.6C標準庫。

    – ld-linux-x86-64.so.2:程式解釋器(聯結器)

初始化vDSO發生在arch/x86/entry/vdso/vma.cinit_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_64vdso_image_x32arch/x86/entry/vdso/vdso-image-64.carch/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);

}

 

上面說到的vsyscallsvDSO都是從機制上對系統呼叫速度進行的優化,但是使用軟中斷來進行系統呼叫需要進行特權級的切換這一根本問題沒有解決。

為瞭解決這一問題,Intel x86 CPUPentium II (Family6, Model 3, Stepping 3)之後,開始支持快速系統呼叫指令sysenter/sysexit,下篇將進行具體介紹。

 

    赞(0)

    分享創造快樂