轉自:http://blog.csdn.net/jasonchen_gbd/article/details/44044091
權聲明:本文為博主原創文章,轉載請附上原博鏈接。
異常入口
系統調用是用戶態和內核態通信的一種方式,用戶程序可以直接調用系統調用的接口陷入內核中執行相關任務,完成后返回用戶態繼續運行。
應用程序使用系統調用很簡單,直接調用C庫提供的系統調用接口即可。在C庫中,對用戶傳入的參數進行分析和保存,然后通過syscall指令引發系統調用異常,之后便陷入內核。
內核處理根據系統調用號執行相應的處理函數,並將結果返回到用戶態。
圖1 系統調用大體流程
當發生異常時,協處理器0的Cause寄存器會記錄發生了什么種類的異常。Cause寄存器的每個域如圖2所示。其中bit6-2(ExcCode)位中保存了具體發生了什么異常,系統可以根據異常種類決定調用哪一個異常處理例程。
圖2 Cause寄存器
所有的異常入口都位於mips內存映射中不需要地址轉換的區域——非緩存的kseg1段和緩存的kseg0段。如圖3所示,RAM中的異常入口點的起始地址為BASE+0x000,BASE表示EBase寄存器編程的異常基地址。一些特殊的異常的處理例程有單獨的地址存放其異常處理例程,如緩存異常和TLB重填等,其他異常處理例程都放在BASE+0x180地址處。
圖3 異常處理入口
BASE+0x180共存放了32種異常的入口函數地址,圖4中顯示了部分異常類型對應的ExcCode值,可以看到其中系統調用對應的ExcCode等於8。當發生系統調用時,內核就可以根據Cause寄存器查看異常類型,然后跳轉到BASE+0x180地址處執行,執行的結果就是找到對應的處理函數並跳轉到處理函數的地址去執行。
圖4 異常類型
這些異常處理函數的注冊在trap_init()函數中完成,該函數將上面所說的32個異常的處理函數地址放到一個全局數組exception_handlers中,這個全局變量定義為:
unsigned long exception_handlers[32];
這個全局變量是unsigned long型,每個元素的值就是一種異常向量處理函數的入口地址。
那BASE+0x180地址處的代碼如何找到異常對應的處理函數呢。trap_init()函數中將except_vec3_generic拷貝到了BASE+0x180,這是一個函數,其實現如下:
NESTED(except_vec3_generic, 0, sp) .set push .set noat mfc0 k1, CP0_CAUSE #讀取協處理器0的cause寄存器保存到k1中。 andi k1, k1, 0x7c #取得k1的2-6位,即excCode #取得exception_handlers[excCode]的值 PTR_L k0, exception_handlers(k1) jr k0 #跳轉到excCode對應的處理函數去執行 .set pop END(except_vec3_generic)
由except_vec3_generic的實現可知,它負責讀取Cause寄存器並跳轉到異常處理函數。
產生異常時,MIPS CPU所要做的主要工作為:
- 設置EPC,指向異常返回的位置。
- 置Status寄存器的EXL位,迫使CPU進入內核模式(高特權級)並且禁用中斷。
- 設置Cause寄存器,使得軟件可以看到異常的原因。
- CPU從異常處理入口點取指執行,即執行異常處理程序。
異常處理的流程主要包括以下步驟:
- 保護現場,將各個寄存器的值壓棧,以便處理完之后回到原來的指令流。
- 根據硬件設置的寄存器標志,判斷是什么異常並執行具體的異常處理函數。
- 恢復現場,將棧里保存的寄存器的值再寫回。
- 跳轉到正常指令流斷點,回到CPU正常的指令流。
以系統調用為例,用戶程序執行系統調用后,C庫通過執行syscall指令產生一個軟件異常,進行上面的一系列工作后會定位到系統調用的處理函數handle_sys。
系統調用代碼分析
系統調用表
內核支持的系統調用都放在一張全局的系統調用表sys_call_table中,所有的系統調用按照系統調用號從大到小的順序存放。這個表中每個條目的大小為8字節,定義如下:
.macro sys function, nargs PTR \function LONG (\nargs << 2) - (5 << 2) .endm
可以看出,系統調用表中每個條目由兩部分組成,前4個字節是處理函數function的地址,后4個字節為(\nargs << 2) -(5 << 2),nargs是系統調用的參數個數,這個表達式的結果用來判斷參數個數是否超過4個。
下面通過分析handle_sys函數的實現介紹系統調用在內核態所做的工作。
備份通用寄存器
將異常發生時的當前進程的通用寄存器的值保存起來,並確定異常返回地址epc的值,使其可以正常返回。
在C庫執行syscall指令之前,會先把系統調用號存放到寄存器v0中,並將需要傳遞的參數放到a0-a4中,如果參數個數大於四個,就需要保存在棧里面。
handle_sys的開頭先通過SAVE_SOME宏將當前進程的通用寄存器的值備份到進程棧中:
NESTED(handle_sys, PT_SIZE, sp) .set noat SAVE_SOME # 見下面對該函數的分析 TRACE_IRQS_ON_RELOAD # not implemented STI #進入內核模式,使能全局中斷 .set at lw t1, PT_EPC(sp) # 取出epc的值。這時應該指向syscall指令 /* v0中存放着系統調用號,由於系統調用號是從4000開始的,所以將v0修改為實際的序號: v0 = v0 – 4000 */ subu v0, v0, __NR_O32_Linux /* 判斷系統調用號的合法性 */ sltiu t0,v0, __NR_O32_Linux_syscalls + 1 addiu t1, 4 #skip to next instruction sw t1, PT_EPC(sp) # 跳過syscall指令,這樣返回時可以繼續執行 beqz t0, illegal_syscall # if(t0== 0) illegal syscall.
SAVE_SOME宏的定義如下:
.macro SAVE_SOME .set push .set noat .set reorder mfc0 k0, CP0_STATUS sll k0, 3 /* k0 = k0 << 3,即CU0成了最高位 */ .set noreorder /* 最高位是1就是負數,小於0。CU0=1則得到用戶特權級別 */ bltz k0, 8f /*if k0 < 0, goto 8: */ move k1, sp /* 延遲槽,如果是內核態進來的,直接獲取sp的值放到k1中 */ .set reorder /* 如果是從用戶態進來的,則需要使用kernel中保存的sp */ get_saved_sp /* 讀取全局kernelsp中的sp的值到k1中。 */ 8: move k0, sp /* 把原來的sp的值放到k0中保存。 */ /* sp = k1 - sizeof(struct pt_regs),由於kernelsp存放的是sp + _THREAD_SIZE - 32,所以這里得到的sp就是進程地址空間的棧頂。 */ PTR_SUBU sp, k1, PT_SIZE /* 將k0的值(即剛保存的sp)保存到進程的pt_regs.regs[29] */ LONG_S k0, PT_R29(sp) LONG_S $3, PT_R3(sp) /* 保存v1的值 */ /* * You might think that you don't need to save$0, * but the FPU emulator and gdb remote debugstub * need it to operate correctly */ LONG_S $0, PT_R0(sp) /* 保存$0的值 */ mfc0 v1, CP0_STATUS LONG_S $2, PT_R2(sp) /* 保存v0的值 */ LONG_S v1, PT_STATUS(sp) /* 保存cp0_status的值 */ LONG_S $4, PT_R4(sp) /* 保存a0的值 */ mfc0 v1, CP0_CAUSE LONG_S $5, PT_R5(sp) /* 保存a1的值 */ LONG_S v1, PT_CAUSE(sp) /* 保存cp0_cause的值 */ LONG_S $6, PT_R6(sp) /* 保存a2的值 */ MFC0 v1, CP0_EPC LONG_S $7, PT_R7(sp) /* 保存a3的值 */ LONG_S v1, PT_EPC(sp) /* 保存cp0_epc的值 */ LONG_S $25, PT_R25(sp) /* 保存t9的值 */ LONG_S $28, PT_R28(sp) /* 保存gp的值 */ LONG_S $31, PT_R31(sp) /* 保存ra的值 */ ori $28, sp, _THREAD_MASK /* gp = sp | 0x1FFF */ /* gp = gp ^ 0x1FFF,即sp的末13位清0賦值給gp,內核棧的大小就是8K,所以,這里的結果就是gp指向棧頂。 */ xori $28, _THREAD_MASK .set pop .endm
這里需要說明一下內核線程的內核棧空間,內核棧是從高地址向下延伸的,大小為兩個頁,即8K。為了方便的定位到進程的task_struct結構,進程的thread_info結構被放在棧底(低地址),這樣,在進程地址空間內的任何地址,只需將末13位清零就是thread_info的位置,再通過thread_info結構體的task指針可以很快找到進程的task_struct結構。
在創建進程時,在棧頂(高地址)會預留32字節的空間,這32字節目前沒有被使用,可能是為了防止溢出而導致覆蓋了進程的重要信息。在32字節下面是一個struct pt_regs結構體,它的目的是為了在發生系統調用或其他異常時,保存進程的重要寄存器的值,如通用寄存器和CP0的寄存器。在距離棧頂32Bytes +sizeof(struct pt_regs)的位置才是sp的初始位置。
獲得參數個數並執行處理程序
根據系統調用號在sys_call_table中找到該系統調用需要幾個參數。
# v0左移3位。因為sys_call_table中每個條目占用8字節。 sll t0, v0, 3 la t1, sys_call_table # t1中存放sys_call_table的地址 addu t1, t0 # t1 = t1 + t0。得到要找的系統調用的地址。 lw t2, (t1) # 把處理函數地址放到t2中 lw t3, 4(t1) # t3中存放是否參數個數大於4 beqz t2, illegal_syscall # 如果找不到處理函數,非法 sw a3, PT_R26(sp) # save a3for syscall restarting bgez t3, stackargs # 如果t3>=0,則參數大於4個,需要棧
在上面的代碼中,t2中保存了系統調用處理函數的地址。而t3的值就有兩層意思:
1. 如果t3小於0,說明系統調用的參數少於或等於4個。
2. 如果t3大於等於0,那t3的取值可能是0,4,8,16,分別對應5,6,7,8個參數的情況。這里t3賦值成4的倍數是為了兩個相鄰值之間相差一個指令的長度,在下面獲取參數時利用了這一點。
如果參數個數小於等於4個,就不需要使用棧保存參數,那處理很簡單:如果需要跟蹤系統調用,在執行系統調用之前,需要通知父進程。一般情況下,我們不需要跟蹤系統調用,所以直接跳轉到系統調用的處理函數。
stack_done: lw t0, TI_FLAGS($28) # 得到進程的thread_info.flags li t1, _TIF_SYSCALL_TRACE | _TIF_SYSCALL_AUDIT and t0, t1 # thread_info.flags是否設置了上面兩個標志 bnez t0, syscall_trace_entry # 如果設置了,跳到處理函數 jalr t2 # 進入系統調用處理函數
如果參數個數大於4個,我們需要在棧用獲取多余的參數,然后再跳轉到上面的stack_done調用處理函數。
stackargs: lw t0, PT_R29(sp) # get olduser stack pointer /* * We intentionally keep the kernel stack alittle below the * top of userspace so we don't have to do a slower byteaccurate check here. */ lw t5, TI_ADDR_LIMIT($28) # 獲得thread_info.addr_limit addu t4, t0, 32 # sp + 32就是棧的高地址 and t5, t4 /* * addr_limit有兩種: * 0-0x7FFFFFFF for user-thead * 0-0xFFFFFFFF for kernel-thread */ bltz t5, bad_stack # t5 < 0即位於了內核態,不合法 /* Ok, copy the args fromthe luser stack to the kernel stack. * t3 is the precomputed number of instructionbytes needed to * load or store arguments 6-8. */ la t1, 5f # load up to 3arguments # 通過上面賦值,t3可能取0, 4, 8, 16對應5,6, 7, 8個參數。 subu t1, t3 1: lw t5, 16(t0) # argument #5from usp 取出#5 .set push .set noreorder .set nomacro jr t1 # 根據參數個數跳轉 addiu t1,6f - 5f # 延遲槽,跳轉同時把t1加上6f -5f. 2: lw t8, 28(t0) # argument #8from usp 3: lw t7, 24(t0) # argument #7from usp 4: lw t6, 20(t0) # argument #6from usp 5: jr t1 sw t5,16(sp) # argument #5 to ksp 延遲槽,跳轉同時存入#5 sw t8, 28(sp) # argument #8 toksp sw t7, 24(sp) # argument #7 toksp sw t6, 20(sp) # argument #6 toksp 6: j stack_done # 跳回和小於等於4個參數相同的處理流程 nop .set pop
准備返回到用戶態
系統調用的處理程序執行完成后,就要准備返回用戶空間了。
# # 准備系統調用的返回值。 # li t0, -EMAXERRNO - 1 # error? sltu t0, t0, v0 # if t0< v0, t0 =1, else t0 = 0. sw t0, PT_R7(sp) #把t0的值存到a3里去。 beqz t0, 1f # if t0 == 0,goto 1: negu v0 # error, v0 = -v0 sw v0, PT_R0(sp) # set flagfor syscall # restarting 1: sw v0, PT_R2(sp) # result, v0存到pt_regs[2]中 o32_syscall_exit: local_irq_disable # make sure need_resched and # signalsdont change between # samplingand return # 下面的內容還是和trace syscall相關的,在系統調用完成后,通知父進程。 lw a2, TI_FLAGS($28) #current->work li t0, _TIF_ALLWORK_MASK and t0, a2 bnez t0, o32_syscall_exit_work j restore_partial /* 恢復寄存器,並返回 */
可以看到,系統調用將a3和v0返回給用戶態,經過上面的代碼處理,這兩個寄存器中的值的含義如下:
- a3存放系統調用是否成功,成功就是0,失敗就是1。
- v0存放系統調用的返回值,如果是負數且位於[-EMAXERRNO,-1]之間,v0就是錯誤碼。否則,v0是該系統調用本來想返回的東西。注意,有效錯誤碼的范圍在1~ EMAXERRNO之間。
- 如果v0是錯誤碼,就先轉換成正數,再返回,這樣用戶態可直接識別。
返回到C庫后,會根據a3判斷是成功還是失敗,如果成功就給用戶程序返回v0。如果失敗,就將v0寫到errno中,然后根據該系統調用的規定,給用戶程序返回失敗時的返回值。
代碼的最后跳轉到restore_partial中去,它的定義很簡單:
FEXPORT(restore_partial) #restore partial frame RESTORE_SOME RESTORE_SP_AND_RET
其中RESTORE_SOME對應最開頭的SAVE_SOME。而RESTORE_SP_AND_RET做了兩件事情:
1. 將進程棧中保存的sp的值恢復,賦值給sp寄存器。
2. 將進程棧中保存的epc的值恢復,並跳轉到epc指向的地址。而開頭講到過,這時epc指向syscall指令的下一條指令,即繼續執行C庫中調用syscall指令之后的代碼。
.macro RESTORE_SP_AND_RET .set push .set noreorder LONG_L k0, PT_EPC(sp) LONG_L sp, PT_R29(sp) jr k0 rfe #在異常返回前恢復CPU狀態 .set pop .endm