Linux 系統調用過程詳細分析


內核版本:Linux-4.19

操作系統通過系統調用為運行於其上的進程提供服務。

那么,在應用程序內,調用一個系統調用的流程是怎樣的呢?

我們以一個假設的系統調用 xyz() 為例,介紹一次系統調用的所有環節。

如上圖所示,系統調用執行的流程如下:

1. 應用程序代碼調用 xyz(),該函數是一個包裝系統調用的庫函數;
2. 庫函數 xyz() 負責准備向內核傳遞的參數,並觸發軟中斷以切換到內核;
3. CPU 被軟中斷打斷后,執行中斷處理函數,即系統調用處理函數(system_call);
4. 系統調用處理函數調用系統調用服務例程(sys_xyz ),真正開始處理該系統調用。

系統調用的實現來自於Glibc,幾乎所有 C 程序都要調用 Glibc 的動態鏈接庫 libc.so 中的庫函數。這些庫函數的源碼是不可見的,可通過 objdump 或 gdb 等工具對代碼進行匯編反編譯,摸清大體的過程。

我們可不必太過糾結,知道原理就好。

下面繼續分析在內核中的實現過程。

Pure EABI user space always put syscall number into scno (r7).

當從用戶態轉為內核態時,系統會將 syscall number 存儲在寄存器 R7 中,利用 R7 來傳參。

在 entry-header.S 文件中,有如下代碼:

scno	.req	r7		@ syscall number
tbl	.req	r8		@ syscall table pointer
why	.req	r8		@ Linux syscall (!= 0)
tsk	.req	r9		@ current thread_info

類似於給寄存器起了個“別名”。

最后通過

invoke_syscall tbl, scno, r10, __ret_fast_syscall

代碼成功調用 syscall table 中的服務程序。

invoke_syscall 定義如下:

	.macro	invoke_syscall, table, nr, tmp, ret, reload=0
#ifdef CONFIG_CPU_SPECTRE
	mov	\tmp, \nr
	cmp	\tmp, #NR_syscalls		@ check upper syscall limit
	movcs	\tmp, #0
	csdb
	badr	lr, \ret			@ return address
	.if	\reload
	add	r1, sp, #S_R0 + S_OFF		@ pointer to regs
	ldmccia	r1, {r0 - r6}			@ reload r0-r6
	stmccia	sp, {r4, r5}			@ update stack arguments
	.endif
	ldrcc	pc, [\table, \tmp, lsl #2]	@ call sys_* routine
#else
	cmp	\nr, #NR_syscalls		@ check upper syscall limit
	badr	lr, \ret			@ return address
	.if	\reload
	add	r1, sp, #S_R0 + S_OFF		@ pointer to regs
	ldmccia	r1, {r0 - r6}			@ reload r0-r6
	stmccia	sp, {r4, r5}			@ update stack arguments
	.endif
	ldrcc	pc, [\table, \nr, lsl #2]	@ call sys_* routine
#endif
	.endm

回看

invoke_syscall tbl, scno, r10, __ret_fast_syscall

這段代碼。tbl 是指向的何處呢?

接下來,就簡單的介紹一下 syscall table 這個表是怎樣形成的。

查看代碼我們發現,tbl 表示 sys_call_table 的地址:

adr tbl, sys_call_table @ load syscall table pointer

entry-common.S 中有這樣一段代碼:

	syscall_table_start sys_call_table
	
#define COMPAT(nr, native, compat) syscall nr, native
#ifdef CONFIG_AEABI
#include <calls-eabi.S>
#else
#include <calls-oabi.S>
#endif
#undef COMPAT

	syscall_table_end sys_call_table

calls-eabi.S 文件內容如下:

NATIVE(0, sys_restart_syscall)
NATIVE(1, sys_exit)
NATIVE(2, sys_fork)
NATIVE(3, sys_read)
NATIVE(4, sys_write)
NATIVE(5, sys_open)
NATIVE(6, sys_close)
NATIVE(8, sys_creat)
NATIVE(9, sys_link)
NATIVE(10, sys_unlink)
NATIVE(11, sys_execve)
NATIVE(12, sys_chdir)
NATIVE(14, sys_mknod)
NATIVE(15, sys_chmod)
NATIVE(16, sys_lchown16)
NATIVE(19, sys_lseek)
NATIVE(20, sys_getpid)
    ...

以上代碼中宏的定義如下:

    /* 定義 sys_call_table,並將 __sys_nr 清 0 */
	.macro	syscall_table_start, sym
	.equ	__sys_nr, 0
	.type	\sym, #object
ENTRY(\sym)
	.endm

    /* 檢查序號錯誤,並利用 sys_ni_syscall 填充缺少的序號 */
	.macro	syscall, nr, func
	.ifgt	__sys_nr - \nr
	.error	"Duplicated/unorded system call entry"
	.endif
	.rept	\nr - __sys_nr
	.long	sys_ni_syscall
	.endr
	.long	\func
	.equ	__sys_nr, \nr + 1
	.endm

    /* 檢查序號是否超過了 __NR_syscalls,如果不足的話,用 sys_ni_syscall 來填充 */
	.macro	syscall_table_end, sym
	.ifgt	__sys_nr - __NR_syscalls
	.error	"System call table too big"
	.endif
	.rept	__NR_syscalls - __sys_nr
	.long	sys_ni_syscall
	.endr
	.size	\sym, . - \sym
	.endm

    /* NATIVE 宏定義 */
#define NATIVE(nr, func) syscall nr, func

最后會通過SWI中斷號調用到相關系統函數,如 sys_open,而 sys_open 的聲明形式則如下所示, 1、2、3 由函數的形參所定,如在 source insight 中搜索到 sys_open 的函數定義,可搜索關鍵詞 “SYSCALL_DEFINE3(open”。

#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)

到這里應該分析完了系統調用的大概過程,感謝大家花費寶貴的時間瀏覽,如果有什么問題歡迎探討,后期會進行修改和補充!

部分參考於:www.cnblogs.com/fasionchan/p/9431784.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM