我們都知道系統功能調用是Unix/Linux操作系統向用戶程序提供支持的接口,通過這些接口應用程序向操作系統請求服務,控制轉向操作系統,而操作系統在完成服務後,將控制和結果返回給用戶程序。
系統調用的主要目的是使得用戶可以使用操作系統提供的有關設備管理、輸入/輸出系統、文件系統和進程控制、通信以及存儲管理等方面的功能,而不必了解系統程序的內部結構和有關硬件細節,從而起到減輕用戶負擔和保護系統以及提高資源利用率的作用。
在進行系統功能調用時會由用戶態(也稱目態)轉到核態(也稱管態)。昨天我的一信安的朋友還問過我管態和目態的區別,我想來想去也就是這裡有大的區別了,在管態下能使用系統的所有資源,調用系統的特權函數,而目態是不行的。在執行系統功能調用時就必須是在管態下執行。
而如果我需要給我的linux系統增加一個系統功能調用的話,就必須弄清楚系統是如何調用那些功能函數的,又是如何由目態變為管態的。在linux下可以通過中斷來進入管態,這類中斷稱為訪管中斷。
內核中系統調用的過程
在Linux系統中,系統調用是作為一種異常類型實現的 ,它將執行相應的機器代碼指令來產生異常信號。產生中斷或異常的重要效果是系統自動將用戶態切換為核心態來對它進行處理。
用戶態的程序只有通過門(gate)陷入(trap)到系統內核中去(執行int指令),才能執行一些具有特權的內核函數。系統調用完成後,系統執行另一組特征指令(iret指令)將系統返回到用戶態,控制權返回給進程。
Linux用來實現系統調用異常的實際指令是:
int $0x80
這一指令使用中斷/異常向量號128(即16進制的80)將控制權轉移給內核(進行模式切換)。
為達到在使用系統調用時不必用機器指令編程,在標准的C語言庫中為每一系統調用提供了一段短的子程序,完成機器代碼的編程工作。
事實上,機器代碼段非常簡短。它所要做的工作只是將送給系統調用的參數加載到CPU寄存器中,接著執行int $0x80指令。然後運行系統調用。
系統調用的返回值將送入CPU的一個寄存器中,標准的庫子程序取得這一返回值,並將它送回用戶程序。
下面以getuid()系統調用為例來看調用過程:
我們可以看到其中有一些宏定義,我們可以看看這些宏的定義
(arch/i386/kernel/ entry.S).
………
#define SAVE_ALL \
cld; \
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;
我們可以看到SAVE_ALL主要是保存寄存器信息,即現場保留。其中, movl $(__USER_DS), %edx;從這一句開始是重新填充DS,ES段。
#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; \
.section .fixup,"ax"; \
3: movl $0,(%esp); \
jmp 1b; \
4: movl $0,(%esp); \
jmp 2b; \
.previous; \
.section __ex_table,"a";\
.align 4; \
.long 1b,3b; \
.long 2b,4b; \
.previous
ENTRY(ret_from_fork)
pushl %eax
call schedule_tail
GET_THREAD_INFO(%ebp)
popl %eax
jmp syscall_exit
這裡主要完成現場恢復並返回。
ENTRY(system_call)
pushl %eax # save orig_eax
SAVE_ALL
GET_THREAD_INFO(%ebp)
# system call tracing in operation
/* Note, _TIF_SECCOMP is bit number 8, and so it needs testw and not testb */
testw $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT|_TIF_SECCOMP),TI_flags(%ebp)
jnz syscall_trace_entry
cmpl $(nr_syscalls), %eax
jae syscall_badsys
syscall_call:
call *sys_call_table(,%eax,4)
movl %eax,EAX(%esp) # store the return value
syscall_exit:
cli # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
movl TI_flags(%ebp), %ecx
testw $_TIF_ALLWORK_MASK, %cx # current->work
jne syscall_exit_work
restore_all:
movl EFLAGS(%esp), %eax # mix EFLAGS, SS and CS
# Warning: OLDSS(%esp) contains the wrong/random values if we
# are returning to the kernel.
# See comments in process.c:copy_thread() for details.
movb OLDSS(%esp), %ah
movb CS(%esp), %al
andl $(VM_MASK | (4 << 8) | 3), %eax
cmpl $((4 << 8) | 3), %eax
je ldt_ss # returning to user-space with LDT SS
restore_nocheck:
RESTORE_REGS
addl $4, %esp
1: iret
這一段中,主要是完成調用。eax放置的是系統調用號 ,因為eax有可能被使用,所以先保存其值。call *sys_call_table(,%eax,4) 這一句是計算調用的入口。
其中,sys_call_table是LINUX的系統調用表,它存在目錄arch/i386/kernel/ sys_call_table.S 下。
.data
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_mq_timedreceive /* 280 */
.long sys_mq_notify
.long sys_mq_getsetattr
.long sys_ni_syscall /* reserved for kexec */
.long sys_waitid
.long sys_ni_syscall /* 285 */ /* available */