1. 程式人生 > >Linux系統調用

Linux系統調用

例如 調用接口 信息保存 roc entry 殺傷力 file stack access

一、前言

當用戶空間的程序調用swi指令發起內核服務請求的時候,實際上程序其實是完成了一次“穿越”,該進程從用戶態穿越到了內核態。這個過程有點象周末你在家裏看片,突然有些內急,隨手按下了pause按鍵,電影裏面的世界嘎然而止了。程序世界亦然,一個swi後,用戶空間的代碼執行暫停了、stack(用戶棧)上的數據,正文段、靜態數據區、heap去的數據……一切都停下來了,程序的執行突然就轉入另外一個世界,使用的棧變成了內核棧、正在執行的正文段程序變成vector_swi開始的binary code、與之匹配數據區也變化了……

一切是怎麽發生的呢?CPU只有一套而已,這裏硬件做了哪些動作?軟件又搞了什麽鬼?穿越到另外的世界當然有趣,但是如何找到回來的路?這一切疑問希望能在這樣的一篇文檔中講述清楚。

本文的代碼來自4.4.6內核,用ARM處理器為例子描述。

二、構建內核棧上的用戶現場

代碼如下(忽略Cortex-M處理器的代碼,忽略THUMB指令集的代碼):

ENTRY(vector_swi)
sub sp, sp, #S_FRAME_SIZE
stmia sp, {r0 - r12} @ Calling r0 - r12
ARM( add r8, sp, #S_PC )
ARM( stmdb r8, {sp, lr}^ ) @ Calling sp, lr
mrs r8, spsr @ called from non-FIQ mode, so ok.
str lr, [sp, #S_PC] @ Save calling PC
str r8, [sp, #S_PSR] @ Save CPSR
str r0, [sp, #S_OLD_R0] @ Save OLD_R0

當執行vector_swi的時候,硬件已經做了不少的事情,包括:

(1)將CPSR寄存器保存到SPSR_svc寄存器中,將返回地址(用戶空間執行swi指令的下一條指令)保存在lr_svc中

(2)設定CPSR寄存器的值。具體包括:CPSR.M = ‘10011‘(svc mode),CPSR.I = ‘1‘(disable IRQ),CPSR.IT = ‘00000000‘(TODO),CPSR.J = ‘0‘(),CPSR.T = SCTLR.TE(J和Tbit和Instruction set state有關,和本文無關),CPSR.E = SCTLR.EE(字節序定義,和本文無關)。

(3)PC設定為swi異常向量的地址

隨後的行為都是軟件行為了,因為代碼中涉及壓棧動作,所以首先要確定的就是當前在哪裏這個問題。sp_svc早在進程切換的時候就已經設定好了,就是該進程的內核棧。

技術分享圖片

當Task A切換到Task B的時候,有一個很重要的步驟就是HW context的切換,由於Task A和Task B都在同一個CPU上運行,因此需要把當前CPU的各種寄存器以及狀態信息保存在一塊memory data block中(也就是硬件上下文了),並且用Task B的硬件上下文的數值來加載CPU,這裏面就包括sp_svc。在內核態,完成進程切換後,最終會返回task B的用戶空間執行,但是這時候Task B對應的內核棧(sp_svc0)是確定的了。

當通過系統調用進入內核的時候,內核棧已經是準備好了,不過這時候內核棧上是空的,執行完上述代碼之後,在內核棧上形成如下的用戶空間現場:

技術分享圖片

代碼我們就不走讀了,很簡單,大家可自行閱讀即可。順便一提的是:你看到這個保存的現場是不是覺得很熟悉?可以看看ARM中斷處理這篇文檔,中斷保存的現場和系統調用是一樣的。另外,保存在內核棧上的用戶空間現場並不是全部的HW Context,HW context是一段內存中的數據,保存了某個時刻CPU的全部狀態,不僅僅是core register,還有很多CPU內部其他的HW block狀態,例如FPU的寄存器和狀態。這時候,問題來了,在通過系統調用進入內核態的時候,僅僅保存core register夠不夠?夠不夠是和系統調用接口的約定相關,實際上,對於linux,我們約定如下:內核態的代碼是不允許執行浮點操作指令的(這裏用FPU例子,其他類似),如果一定要這樣的話,那麽需要在內核使用FPU的代碼前後增加FPU上下文的保存和恢復的代碼,確保在返回用戶空間的時候,FPU的上下文是保持不變的。

最後一個有趣的問題是:為何r0被兩次壓棧?一個是r0,另外一個是old r0。其實在系統調用過程中,r0有兩個角色,一個是傳遞參數,另外一個是返回值。剛進入系統調用現場的時候,old r0和r0其實都是保存了本次系統調用的參數,而在完成系統調用之後,r0保存了返回用戶空間的return value。不過你可能覺得用一個r0就OK了,具體為何如此我們後面還會進行描述。

三、幾個簡單的初始化操作

代碼如下:

zero_fp
alignment_trap r10, ip, __cr_alignment
enable_irq
ct_user_exit
get_thread_info tsk

zero_fp用來清除frame pointer,在debugger做棧的回溯的時候,當fp等於0的時候也就意味著到了最外層函數。對於kernel而言,來到這裏,函數的調用跟蹤就結束了,我們不可能一直回溯到用戶空間的函數調用。上一節,我們說過了,硬件會關閉irq,在這裏,我們通過enable_irq開啟本cpu的中斷處理。ct_user_exit和Context tracking subsystem相關的內容,這裏就不深入了,關於對齊,可以多聊幾句。ARM64的硬件是支持非對齊操作的,當然僅僅限於對normal memory的訪問(對memory order沒有要求的那些訪問,例如exclusive load/store和load-acquire 或者 store-release 指令就不支持了)。由於取指而產生的內存訪問或者是訪問device type的memory都是必須要對齊的。當指令是非對齊的訪問的時候,可以有兩個選擇(SCTLR_ELx.A控制):一個是產生fault,另外一個是執行非對齊訪問(由硬件完成)。對內存的非對齊的訪問在總線上被分解成兩個transaction。所有的ARMv8的處理器的硬件都支持非對齊訪問,因此,在ARM64應該不需要軟件來實現非對齊的訪問。

支持ARMv8的處理器當然不需要考慮對齊問題,不過對於ARM processor,有些硬件是不支持非對齊的訪問的,這時候,內核配置(CONFIG_ALIGNMENT_TRAP)可以用軟件的方法來實現非對齊訪問(這是在硬件不支持的情況下的無奈之舉),但是對性能的殺傷力極大,不到萬不得已不能打開。具體代碼很簡單,這裏就不說明了。

三、如何獲取系統調用號?

系統調用有兩種規範,一種是老的OABI(系統調用號來自swi指令中),另外一種是ARM ABI,也就是EABI(系統調用號來自r7)。如果想要兼容舊的OABI,那麽我們需要定義OABI_COMPAT,這會帶來一點系統調用的開銷,同時讓內核變大一點,對應的好處是使用舊的OABI規格的用戶程序也可以運行在內核之上。當然,如果我們確定用戶空間只是服從EABI規範,那麽可以考慮不定義CONFIG_OABI_COMPAT。

相關的代碼如下:

#if defined(CONFIG_OABI_COMPAT)

USER( ldr r10, [lr, #-4] ) @ get SWI instruction
ARM_BE8(rev r10, r10) @ little endian instruction

#elif defined(CONFIG_AEABI)

#else
/* Legacy ABI only. */
USER( ldr scno, [lr, #-4] ) @ get SWI instruction
#endif

如果是符合EABI規範,那麽直接從r7中獲取系統調用號即可,不需要特別的代碼,因此CONFIG_AEABI的情況下,代碼是空的。如果是老的規範,那麽我們需要從SWI指令那裏獲取系統調用號,這時候,我們需要lr(實際上就是lr_svc,該寄存器保存了swi指令的下一條指令)來找到swi那一條指令碼。

uaccess_disable tbl

adr tbl, sys_call_table @ load syscall table pointer

#if defined(CONFIG_OABI_COMPAT)
bics r10, r10, #0xff000000
eorne scno, r10, #__NR_OABI_SYSCALL_BASE
ldrne tbl, =sys_oabi_call_table
#elif !defined(CONFIG_AEABI)
bic scno, scno, #0xff000000 @ mask off SWI op-code
eor scno, scno, #__NR_SYSCALL_BASE @ check OS number
#endif

取出swi指令中的低24bit就可以得出系統調用號,當然,對於EABI標準,我們使用r7傳遞系統調用號,因此陷入內核的時候永遠使用“swi 0”這樣的方式,因此,如果swi指令中的低24bit是0,則說明是服從EABI規範。

執行完上面的代碼後,r7(scno)中保存了系統調用號,r8(tbl)中是syscall table pointer,通過r7和r8的值,我們已經知道後續的路該如何走了。

四、參數傳遞

使用swi指令的代碼位於glibc中,我們可以大概把代碼認為是如下的格式:

……

return value = swi( 參數1,參數2,……);

……

從這個角度看,系統調用和一個普通的c程序調用是類似的,都有參數和返回值的概念。當然,由於模式也發生了切換,因此這裏的參數傳遞不能使用stack壓棧的方式(swi產生了stack的切換),只能使用寄存器的方式。

對於ARM處理器,標準過程調用約定使用r0~r3來傳遞參數,其余的參數壓入棧中。經過前面兩個小節的描述,我們已經找到系統調用號和系統調用表,下面準備調用內核的系統調用函數,對於內核態的系統調用函數,其格式如下:

……

return value = sys_xxx( 參數1,參數2,……);

……

因此,我們還需要點代碼來過渡到sys_xxx,如下:

local_restart:
ldr r10, [tsk, #TI_FLAGS] @ check for syscall tracing
stmdb sp!, {r4, r5} @ push fifth and sixth args

tst r10, #_TIF_SYSCALL_WORK @ are we tracing syscalls?
bne __sys_trace

cmp scno, #NR_syscalls @ check upper syscall limit
badr lr, ret_fast_syscall @ return address
ldrcc pc, [tbl, scno, lsl #2] @ call sys_* routine

我們這裏需要模擬一個c函數調用,因此需要在棧上壓入系統調用可能存在的第五和第六個參數(有些系統調用超過4個參數,他們使用r0~r5在swi接口上傳遞參數)。如果參數OK的話,那麽ldrcc pc, [tbl, scno, lsl #2]代碼將直接把控制權交給對應的sys_xxx函數。需要註意的是返回地址的設定,我們無法使用bl這樣的匯編指令,因此只能是手動設定lr寄存器了(badr lr, ret_fast_syscall )。

五、返回用戶空間

在返回用戶空間之前會處理很多的事情,例如信號處理、進程調度等,這是通過檢查struct thread_info中的flag標記來完成的,代碼如下:

disable_irq_notrace @ disable interrupts
ldr r1, [tsk, #TI_FLAGS] @ re-check for syscall tracing
tst r1, #_TIF_SYSCALL_WORK | _TIF_WORK_MASK
bne fast_work_pending

restore_user_regs fast = 1, offset = S_OFF

fast_work_pending:
str r0, [sp, #S_R0+S_OFF]! @ returned r0

……

這裏面最著名的flag就是_TIF_NEED_RESCHED,有了這個flag,說明有調度需求。由此可知在系統調用返回用戶空間的時候上有一個調度點。其他的flag和我們這裏的場景無關,這裏就不描述了,總而言之,如果需要有其他額外的事情要處理,我們需要跳轉到fast_work_pending ,否則調用restore_user_regs返回用戶空間現場。這裏有一個小小的細節:如果需要有額外的事項處理(例如有pending signal),那麽r0寄存器實際上會被破壞掉,也就破壞了sys_xxx函數的返回值,這時候,我們把r0保存到了用戶現場(pt_regs)中的S_R0的位置,這也是為何pt_regs有S_R0和S_OLD_R0兩個和r0相關的域。

恢復用戶空間的代碼(restore_user_regs )如下:

mov r2, sp
ldr r1, [r2, #\offset + S_PSR] @ get calling cpsr
ldr lr, [r2, #\offset + S_PC]! @ get pc
msr spsr_cxsf, r1 @ save in spsr_svc
.if \fast
ldmdb r2, {r1 - lr}^ @ get calling r1 - lr
.else
ldmdb r2, {r0 - lr}^ @ get calling r0 - lr
.endif
mov r0, r0 @ ARMv5T and earlier require a nop
@ after ldm {}^
add sp, sp, #\offset + S_FRAME_SIZE
movs pc, lr @ return & move spsr_svc into cpsr

整個代碼比較簡單,就是用進入系統調用時候壓入內核棧的值來進行用戶現場的恢復,其中一個細節是內核棧的操作,在調用movs pc, lr 返回用戶空間現場之前,add sp, sp, #\offset + S_FRAME_SIZE指令確保用戶棧上是空的。此外,我們需要考慮返回用戶空間時候的r0設置問題,畢竟它承載了本次系統調用的返回值,這時候的r0有兩種情況:

(1)在沒有pending work的情況下(fast等於1),r0保存了sys_xxx函數的返回值

(2)在有pending work的情況下(fast等於0),struct pt_regs(返回用戶空間的現場)中的r0保存了sys_xxx函數的返回值

restore_user_regs還有一個參數叫做offset,我們知道,在進入系統調用的時候,我們把參數5和參數6壓入棧上,因此產生了到pt_regs 8個字節的偏移,這裏需要補償回來。

Linux系統調用