天天看點

Linux 系統調用的執行過程

什麼是系統調用

系統調用 (在 Linux 中常稱為 syscalls ) 是應用程式通路硬體裝置之間的橋梁。

系統調用層為使用者空間提供一種硬體的抽象接口,使得使用者不用關注裝置的具體資訊,同時系統調用保證了系統的穩定和安全。

Linux 系統調用的執行過程

在 Linux 中,除了異常和陷入外,系統調用是使用者空間通路核心的唯一手段。

實際上,其他的像裝置檔案和 /proc 之類的方式,最終也還是要通過系統調用的方式進行通路。

系統調用号

在 Linux 中,每個系統調用被賦予一個系統調用号。通過這個獨一無二的調用号就可以關聯具體的系統調用。

在使用者空間執行一個系統調用時候,這個系統調用号就被用來指明到底是要執行哪個系統調用,程序不會提及系統調用的名稱。

系統調用号一旦配置設定就不能再有任何改變,否則編譯好的應用程式就會崩潰。

在核心中通過系統調用表來記錄所有已注冊過的系統調用的清單,存儲在 sys_call_table 中。它與體系結構有關,一般在 entry.s 中定義。這個表中為每一個有效的系統調用制定了一個唯一的系統調用号。

rch\x86\kernel\syscall_table_32.S

ENTRY(sys_call_table)
.long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
.long sys_exit
.long sys_fork
.long sys_read
.long sys_write
.long sys_open /* 5 */
.long sys_close
.long sys_waitpid
.long sys_creat
.long sys_link
.long sys_unlink /* 10 */
.long sys_execve
.long sys_chdir
.long sys_time
.long sys_mknod
.long sys_chmod /* 15 */

...
           

應用程式依靠軟中斷的方式通知核心要進行系統調用。

該通知核心的機制通過引發一個異常來促使系統切換到核心态去執行異常處理程式。此時的異常處理程式實際上就是系統調用處理程式。

在 x86 系統上軟中斷由 int $0X80 指令産生(int 指令是程式用來顯式聲明軟中斷的,故而所謂的“基于int指令的系統調用”便是來源于此)。這條指令會觸發一個異常導緻系統切換到核心态并執行第 128 号異常處理程式,而該程式正是系統調用的處理程式,名為 system_call()。它與硬體體系結構緊密相關。

在 x86 上,系統調用号是通過 eax 寄存器傳遞給核心的。在陷入核心之前,使用者空間就把相應的系統調用号放到 eax 中了。這樣系統調用程式一旦運作,就可以從 eax 擷取系統調用号。

system_call() 通過将給定的系統調用号與 NR_syscalls 做比較來檢查其有效性。若它大于或等于 NR_syscalls,該函數就傳回 -ENOSYS。否則,就執行相應的系統調用。

call *sys_call_table(, %eax, 4)

由于系統調用表中的表項是以 32 位(4位元組)類型存放的,是以核心需要将給定的系統調用号乘以 4,然後用所得的結果在該表中查詢其位置。

如以 read()調用過程如下:

Linux 系統調用的執行過程

參數傳遞

由于使用者空間和核心空間使用不同的棧空間,是以系統調用的參數需要使用寄存器進行傳遞。在 x86 系統上,ebx、ecx、edx、esi 和 edi 按照順序存放前5個參數。若參數大于或等于6個,需要用一個單獨的寄存器存放指向所有這些參數在使用者空間位址的指針。

給使用者空間發的傳回值也通過寄存器傳遞。在 x86 系統上,它存放在 eax 寄存器中。若系統調用産生大量的資料不能通過傳回機制傳遞給使用者程序,那必須通過指定的記憶體區交換該資料。當然,該記憶體區必須在使用者空間中,使得使用者應用層序能夠通路。

在核心通路自身的記憶體區時,虛拟位址和實體記憶體頁之間的映射總是存在的。但使用者空間中的情況有所不同,頁可能被換出,甚至可能尚未配置設定實體記憶體頁。

因而核心不能簡單的反引用使用者空間的指針,而必須采用特定的函數,確定目标記憶體區已經在實體記憶體中,為確定這種約定,使用者空間指針通過_user屬性标記,以支援 C check tools 對源代碼的自動化檢查。

大多數情況下,使用者在使用者空間和核心空間之間複制資料的函數使用copy_to_user() 和 copy_from_user(),但還有更多的變體。

注意 copy_to_user() 和 copy_from_user() 都有可能引起阻塞。當包含使用者資料的頁被換出到硬碟上而不是在實體記憶體上的時候,這種情況就會發生。此時,程序就會休眠,直到缺頁處理程式将該頁從硬碟重新換回實體記憶體。

初始化

Linux 核心在啟動過程中會對向量中斷進行初始化,該初始化 trap_init 中會對系統調用設定中斷号,SYSCALL_VECTOR 就是 0x80 中斷号。

void __init trap_init(void)
{
int i;

...

 set_system_gate(SYSCALL_VECTOR,&system_call);

...
cpu_init();

trap_init_hook();
}
           

而 system_call 具體實作在 arch\x86\kernel\entry_32.S 中。

系統調用過程

當使用者程序調用一個系統調用時,使用者程序會觸發一個中斷向量号為 0x80 的軟中斷,最終會執行 system_call 函數。

Linux 系統調用的執行過程

在實際執行中斷向量表中的第 0x80 号所對應的 system_call 函數前,CPU 首先還要進行棧的切換。在 Linux 中,使用者态和核心态使用的是不同的棧,兩者各自負責各自的函數調用,互不幹擾。

在int指令中,CPU 除了切入核心态之外,還要找到目前程序的核心棧,在核心棧中依次壓入目前程序使用者态的寄存器 SS(Stack Segment,堆棧段寄存器)、ESP、EFLAGS、CS(Code Segment,代碼段寄存器 )、EIP。這些中斷指令自動地由硬體完成。

當然,當核心從系統調用中傳回的時候,需要調用 iret 指令來回到使用者态,iret 指令則從核心棧中彈出 SS、ESP、EFLAGS、CS、EIP 的值,使得棧恢複到使用者态的狀态。

Linux 系統調用的執行過程

當 CPU 在 int 指令中切換了棧後,程式通過 0x80 從中斷向量表中擷取中斷處理程式,也即是 system_call(),該函數在 arch\x86\kernel\entry_32.S 中。

# system call handler stub
ENTRY(system_call) #執行int 0x80的下一條指令
RING0_INT_FRAME # can't unwind into user space anyway
pushl %eax # save orig_eax
CFI_ADJUST_CFA_OFFSET 4
SAVE_ALL #将所有寄存器的值在核心态棧上儲存,也就是所謂的儲存現場

# 通過宏擷取目前程序的thread_info 結構位址 #define GET_THREAD_INFO(reg) movl $-THREAD_SIZE, reg; andl %esp, reg
GET_THREAD_INFO(%ebp) # ebp用于存放目前程序thread_info結構的位址
# system call tracing in operation / emulation

/* Note, _TIF_SECCOMP is bit number 8, and so it needs testw and not testb */
#檢測目前程序是否被跟蹤,也即是_TIF_SYSCALL_TRACE、_TIF_SYSCALL_AUDIT 被置1,若發生被跟蹤情況則轉向相應的處理指令處
testw $(_TIF_SYSCALL_EMU|_TIF_SYSCALL_TRACE|_TIF_SECCOMP|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
jnz syscall_trace_entry
# 對使用者程序傳遞過來的系統調用号進行合法性檢查,若不合法,則跳到syscall_badsys處
cmpl $(nr_syscalls), %eax
jae syscall_badsys # 不合法,跳入到異常處理

#若系統調用号合法,則跳入到相應系統調用号所對應的服務曆程當中,也即是從 sys_call_table表中找到相應的入口函數。
syscall_call:
call *sys_call_table(,%eax,4) #由于表中的表項占4個位元組,是以擷取服務曆程的方法為:sys_call_table表基位址+%eax系統調用号*4
# %eax儲存的是目前系統調用傳回值,把該傳回值儲存在曾儲存使用者态eax寄存器值的那個棧單元位置上。使用者态就可以從eax寄存器中擷取系統調用的傳回碼了。
movl %eax,PT_EAX(%esp) # store the return value
syscall_exit:
LOCKDEP_SYS_EXIT
DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
TRACE_IRQS_OFF
testl $TF_MASK,PT_EFLAGS(%esp) # If tracing set singlestep flag on exit
jz no_singlestep
orl $_TIF_SINGLESTEP,TI_flags(%ebp)
no_singlestep:
movl TI_flags(%ebp), %ecx
testw $_TIF_ALLWORK_MASK, %cx # 檢查目前程序是否還有工作沒有完成,若有,跳到 syscall_exit_work
jne syscall_exit_work #程序排程時機

restore_all:
movl PT_EFLAGS(%esp), %eax # mix EFLAGS, SS and CS
# Warning: PT_OLDSS(%esp) contains the wrong/random values if we
# are returning to the kernel.
# See comments in process.c:copy_thread() for details.
movb PT_OLDSS(%esp), %ah
movb PT_CS(%esp), %al
andl $(VM_MASK | (SEGMENT_TI_MASK << 8) | SEGMENT_RPL_MASK), %eax
cmpl $((SEGMENT_LDT << 8) | USER_RPL), %eax
CFI_REMEMBER_STATE
je ldt_ss # returning to user-space with LDT SS
restore_nocheck:
TRACE_IRQS_IRET
restore_nocheck_notrace:
RESTORE_REGS # 恢複了save_all儲存的所有寄存器的值
addl $4, %esp # skip orig_eax/error_code
CFI_ADJUST_CFA_OFFSET -4
1: INTERRUPT_RETURN #中斷傳回 相當于iret,程式将回到使用者态繼續執行
.section .fixup,"ax"
iret_exc:
pushl $0 # no error code
pushl $do_iret_error
jmp error_code
.previous
.section __ex_table,"a"
.align 4
.long 1b,iret_exc
.previous

CFI_RESTORE_STATE
ldt_ss:
larl PT_OLDSS(%esp), %eax
jnz restore_nocheck
testl $0x00400000, %eax # returning to 32bit stack?
jnz restore_nocheck # allright, normal return

#ifdef CONFIG_PARAVIRT
/*
* The kernel can't run on a non-flat stack if paravirt mode
* is active. Rather than try to fixup the high bits of
* ESP, bypass this code entirely. This may break DOSemu
* and/or Wine support in a paravirt VM, although the option
* is still available to implement the setting of the high
* 16-bits in the INTERRUPT_RETURN paravirt-op.
*/
cmpl $0, pv_info+PARAVIRT_enabled
jne restore_nocheck
#endif

/* If returning to userspace with 16bit stack,
* try to fix the higher word of ESP, as the CPU
* won't restore it.
* This is an "official" bug of all the x86-compatible
* CPUs, which we can try to work around to make
* dosemu and wine happy. */
movl PT_OLDESP(%esp), %eax
movl %esp, %edx
call patch_espfix_desc
pushl $__ESPFIX_SS
CFI_ADJUST_CFA_OFFSET 4
pushl %eax
CFI_ADJUST_CFA_OFFSET 4
DISABLE_INTERRUPTS(CLBR_EAX)
TRACE_IRQS_OFF
lss (%esp), %esp
CFI_ADJUST_CFA_OFFSET -8
jmp restore_nocheck
CFI_ENDPROC
ENDPROC(system_call)

# perform work that needs to be done immediately before resumption
ALIGN
RING0_PTREGS_FRAME # can't unwind into user space anyway
work_pending:
testb $_TIF_NEED_RESCHED, %cl #判斷是否需要程序排程
jz work_notifysig
work_resched:
call schedule #執行程序排程
LOCKDEP_SYS_EXIT
DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
TRACE_IRQS_OFF # 關閉中斷跟蹤
movl TI_flags(%ebp), %ecx # 檢測是否還有其他任務 
andl $_TIF_WORK_MASK, %ecx # is there any work to be done other
# than syscall tracing?
jz restore_all # 傳回restore_all
testb $_TIF_NEED_RESCHED, %cl
jnz work_resched

work_notifysig: # 處理未決信号集 # deal with pending signals and
# notify-resume requests
#ifdef CONFIG_VM86
testl $VM_MASK, PT_EFLAGS(%esp)
movl %esp, %eax
jne work_notifysig_v86 # returning to kernel-space or
# vm86-space
xorl %edx, %edx
call do_notify_resume
jmp resume_userspace_sig

ALIGN
work_notifysig_v86:
pushl %ecx # save ti_flags for do_notify_resume
CFI_ADJUST_CFA_OFFSET 4
call save_v86_state # %eax contains pt_regs pointer
popl %ecx
CFI_ADJUST_CFA_OFFSET -4
movl %eax, %esp
#else
movl %esp, %eax
#endif
xorl %edx, %edx
call do_notify_resume # 将信号傳遞到程序
jmp resume_userspace_sig
END(work_pending)

# perform syscall exit tracing
ALIGN
syscall_trace_entry:
movl $-ENOSYS,PT_EAX(%esp)
movl %esp, %eax
xorl %edx,%edx
call do_syscall_trace
cmpl $0, %eax
jne resume_userspace # ret != 0 -> running under PTRACE_SYSEMU,
# so must skip actual syscall
movl PT_ORIG_EAX(%esp), %eax
cmpl $(nr_syscalls), %eax
jnae syscall_call
jmp syscall_exit
END(syscall_trace_entry)

# perform syscall exit tracing
ALIGN
syscall_exit_work:
testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT|_TIF_SINGLESTEP), %cl
jz work_pending
TRACE_IRQS_ON
ENABLE_INTERRUPTS(CLBR_ANY) # could let do_syscall_trace() call
# schedule() instead
movl %esp, %eax
movl $1, %edx
call do_syscall_trace
jmp resume_userspace # 恢複使用者空間
END(syscall_exit_work)
CFI_ENDPROC

RING0_INT_FRAME # can't unwind into user space anyway
syscall_fault:
pushl %eax # save orig_eax
CFI_ADJUST_CFA_OFFSET 4
SAVE_ALL
GET_THREAD_INFO(%ebp)
movl $-EFAULT,PT_EAX(%esp)
jmp resume_userspace
END(syscall_fault)
           

在執行系統調用前會把寄存器中儲存的使用者态資訊儲存到核心棧中,然後通過系統調用号找到從 sys_call_table 中找到具體的系統調用入口,執行系統調用。系統調用執行完後,把傳回值儲存到 eax% 中,使用者态程式可以從 eax% 中擷取系統調用結果。

對于宏 SAVE_ALL 來說,會把将寄存器的值壓入堆棧當中,壓入順序對應struct pt_regs ,出棧時調用 RESTORE_REGS 恢複 SAVE_ALL 壓入的寄存器的值。

#define SAVE_ALL \
cld; \
pushl %fs; \
pushl %es; \
pushl %ds; \
pushl %eax; \
pushl %ebp; \
pushl %edi; \
pushl %esi; \
pushl %edx; \
pushl %ecx; \
pushl %ebx; \
movl $(__USER_DS), %edx; \
movl %edx, %ds; \
movl %edx, %es; \
movl $(__KERNEL_PERCPU), %edx; \
movl %edx, %fs

struct pt_regs {
long ebx;
long ecx;
long edx;
long esi;
long edi;
long ebp;
long eax;
int xds;
int xes;
int xfs;
/* int xgs; */
long orig_eax;
long eip;
int xcs;
long eflags;
long esp;
int xss;
};
           

恢複現場的宏 RESTORE_REGS ,中斷傳回時,恢複相關寄存器的内容是通過RESTORE_REGS 宏完成的,同時也可以看出,SAVE_ALL 和 RESTORE_REGS 遙相呼應,當執行 iret指令時,核心棧又恢複了進入中斷前的狀态,并使 CPU 從中斷中傳回。

#define RESTORE_INT_REGS \
popl %ebx; \
popl %ecx; \
popl %edx; \
popl %esi; \
popl %edi; \
popl %ebp; \
popl %eax; \

#define RESTORE_REGS \
RESTORE_INT_REGS; \
1: popl %ds; \

2: popl %es; \

3: popl %fs; \

...
           

具體系統調用流程如下:

Linux 系統調用的執行過程

附:

SS(Stack Segment)為堆棧段寄存器,存放棧頂的段位址

SP(Stack Pointer) 為堆棧指針寄存器, 存放棧頂的偏移位址

任意時刻,SS:SP指向棧頂元素。

關系如下圖

Linux 系統調用的執行過程

繼續閱讀