linux系統調用過程解析(基於ARM處理器)

1、系統調用相關代碼註釋

1.1、中斷向量

.section .vectors, "ax", %progbits
__vectors_start:
W(b) vector_rst ;// 系統復位中斷向量
W(b) vector_und ;// 未定義指令中斷向量
W(ldr) pc, __vectors_start + 0x1000 ;// swi軟中斷中斷向量(系統調用入口)
W(b) vector_pabt
W(b) vector_dabt
W(b) vector_addrexcptn
W(b) vector_irq ;// irq中斷向量
W(b) vector_fiq

1.2、swi軟中斷(系統調用)

1.2.1、系統調用進入

.align 5
ENTRY(vector_swi)
        ......
sub sp, sp, #S_FRAME_SIZE ;// ARM棧由高地址往低地址遞減,減去72個字節地址(共18個寄存器大小),用於保存中斷上下文
stmia sp, {r0 - r12}@ Calling r0 - r12 ;// 用戶模式與SVC特權模式共用r0-r12寄存器(13*4=52字節),"ia"即increase after,低寄存器存低地址,高寄存器存高地址,從低地址到高地址依次存儲r0-r12,sp指向r0 (sp等價於pt_regs的低地址),此時棧內容爲r0-r12
 ARM( add r8, sp, #S_PC ) ;// sp + pt_regs->pc偏移存儲到r8(ARM pc偏移地址爲15*4)
 ARM( stmdb r8, {sp, lr}^ )@ Calling sp, lr ;// 用戶模式sp、lr寄存器存儲到棧裏面,r8 = offset(pt_regs-pc),r8先減4再入棧,即將用戶模式sp,lr保存到pc位置之前
 THUMB( mov r8, sp )
 THUMB( store_user_sp_lr r8, r10, S_SP)@ calling sp, lr
mrs r8, spsr@ called from non-FIQ mode, so ok. ;// 保存系統調用前程序狀態寄存器spsr(cpsr_usr)到r8寄存器
str lr, [sp, #S_PC]@ Save calling PC ;// 此處的lr是svc模式lr,保存的是用戶模式的pc寄存器值,系統調用前指令的位置,保存到棧對應偏移位置
str r8, [sp, #S_PSR]@ Save CPSR ;// 用戶模式的保存到棧對應偏移位置
str r0, [sp, #S_OLD_R0]@ Save OLD_R0 ;// r0寄存器的值到棧對應偏移位置(r0保存了兩次)
#endif
zero_fp
alignment_trap r10, ip, __cr_alignment
enable_irq
ct_user_exit
get_thread_info tsk (將sp低13位清0即爲線程棧的起始地址,棧底保存了線程的thread_info信息,tsk爲r9寄存器)


/*
* Get the system call number.
*/


#if defined(CONFIG_OABI_COMPAT)

        ......

#elif defined(CONFIG_AEABI)


/*
* Pure EABI user space always put syscall number into scno (r7).
*/
#elif defined(CONFIG_ARM_THUMB)
......

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


uaccess_disable tbl


adr tbl, sys_call_table@ load syscall table pointer ;// 獲取系統調用表的地址到r8寄存器

......

local_restart:
ldr r10, [tsk, #TI_FLAGS]@ check for syscall tracing ;// 讀取thread_info->flags到r10
stmdb sp!, {r4, r5}@ push fifth and sixth args ;// 第5、6個參數入棧(1-4參數保存在r0-r3裏面,這個過程沒有修改,直接傳遞到下一級函數,第5、6個參數入棧應該是編譯器參數傳遞規則,可以找個有5個以上參數的函數反彙編,查看參數怎麼取的即可驗證入參規則)


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


cmp scno, #NR_syscalls@ check upper syscall limit ;// scno即r7寄存器,glibc系統調用的時候將系統調用號保存在r7裏面,檢查系統調用號是否超出範圍(系統調用號有可能編譯到指令裏面,具體看編譯器)
badr lr, ret_fast_syscall@ return address ;// 設置系統調用返回地址(系統調用完還處於內核狀態,需要從內核態返回到用戶態,或者調度等)
ldrcc pc, [tbl, scno, lsl #2]@ call sys_* routine ;// scno左移兩位等價於scno * 4,因爲每個系統調用函數指針佔4個字節,系統調用表就是個函數指針數組,系統調用表基址加上偏移即得到真正的系統調用入口函數地址(scno小於NR_syscalls才執行此命令,即系統調用號有效才執行)


add r1, sp, #S_OFF
2: cmp scno, #(__ARM_NR_BASE - __NR_SYSCALL_BASE)
eor r0, scno, #__NR_SYSCALL_BASE@ put OS number back
bcs arm_syscall ;// 執行arm_syscall,注意此處以及ldrcc系統調用都是直接跳轉的,返回地址都是lr(ret_fast_syscall)而不是下一條命令
mov why, #0@ no longer a real syscall ;// 無效的系統調用,r8設置爲0
b sys_ni_syscall@ not private func ;// 跳轉到sys_ni_syscall,該函數將返回-ENOSYS錯誤碼並保存到r0寄存器
......
ENDPROC(vector_swi)

1.2.2、系統調用返回

前面執行真正的系統調用前已經將lr設置爲ret_fast_syscall,lr即爲c語言函數的返回地址,函數執行完返回的時候會將pc設置爲lr,即可返回到指定地址繼續執行。

ret_fast_syscall:
 UNWIND(.fnstart        )
 UNWIND(.cantunwind     )
        str     r0, [sp, #S_R0 + S_OFF]!        @ save returned r0 ;// 系統調用函數(例如:SyS_write)會把return的值保存在r0裏面,c函數返回值都是保存在r0裏面的,將執行結果保存到系統棧裏面
        disable_irq_notrace                     @ disable interrupts
        ldr     r1, [tsk, #TI_FLAGS]            @ re-check for syscall tracing
        tst     r1, #_TIF_SYSCALL_WORK | _TIF_WORK_MASK ;// _TIF_WORK_MASK=_TIF_NEED_RESCHED|_TIF_SIGPENDING|_TIF_NOTIFY_RESUME|_TIF_UPROBE,系統調用有可能會阻塞,例如讀寫硬盤,需要等待,或者系統調用就是執行任務切換或者sleep操作,那麼新的線程進程就需要重新調度,而不是直接返回到用戶進程,系統調用函數就會設置_TIF_NEED_RESCHED標誌位,對於信號理論上應該是一致的,信號函數優先級高於用戶態函數
        beq     no_work_pending ;// 如果沒有標誌位被設置(沒有掛起的待處理工作要運行),則跳轉到no_work_pending繼續執行
 UNWIND(.fnend          )
ENDPROC(ret_fast_syscall)


        /* Slower path - fall through to work_pending */
#endif


        tst     r1, #_TIF_SYSCALL_WORK
        bne     __sys_trace_return_nosave
slow_work_pending:
        mov     r0, sp                          @ 'regs' ; // sp棧保存了進程上下文,作爲第一個參數傳遞給do_work_pending,因爲do_work_pending裏面有可能需要切換等,有等待信號要處理的情況下,得先執行信號函數,而信號函數是在用戶態執行,因此需要將當前上下文額外保存,對於Nucleus Plus是將信號處理函數相關寄存器(入口以及參數等)構造一個新的恢復上下文,先恢復信號上下文,再由信號處理函數恢復系統調用上下文,linux內核信號處理流程不在此處細看
        mov     r2, why                         @ 'syscall'
        bl      do_work_pending
        cmp     r0, #0
        beq     no_work_pending
        movlt   scno, #(__NR_restart_syscall - __NR_SYSCALL_BASE)
        ldmia   sp, {r0 - r6}                   @ have to reload r0 - r6
        b       local_restart                   @ ... and off we go
ENDPROC(ret_fast_syscall)

......

ENTRY(ret_to_user)            
ret_slow_syscall:             
        disable_irq_notrace                     @ disable interrupts                                                                                                                              
ENTRY(ret_to_user_from_irq)
        ldr     r1, [tsk, #TI_FLAGS]   
        tst     r1, #_TIF_WORK_MASK    
        bne     slow_work_pending
no_work_pending:              
        asm_trace_hardirqs_on save = 0                                                                                                                                                            


        /* perform architecture specific actions before user return */                                                                                                                            
        arch_ret_to_user r1, lr        
        ct_user_enter save = 0


        restore_user_regs fast = 0, offset = 0 ;// 系統調用如果不需要重新調度或者信號等處理的話,mmu是沒有切換的,因此直接恢復程序狀態寄存器、返回值寄存器r0及其他通用寄存器即可(lr->pc, spsr->cpsr),詳細細節參考restore_user_regs定義
ENDPROC(ret_to_user_from_irq) 
ENDPROC(ret_to_user)

1.2.3、恢復用戶進程上下文

        .macro  restore_user_regs, fast = 0, offset = 0
        uaccess_enable r1, isb=0
#ifndef CONFIG_THUMB2_KERNEL
        @ ARM mode restore
        mov     r2, sp
        ldr     r1, [r2, #\offset + S_PSR]      @ get calling cpsr ;// 從內核棧獲取用戶態cpsr
        ldr     lr, [r2, #\offset + S_PC]!      @ get pc ;// 從內核棧獲取用戶態pc,即下一條指令的地址(多級流水線,pc並不是系統調用地址,而是下一條指令的地址,具體pc值可參考相關書籍,查看swi指令是否有對pc減4操作)
        msr     spsr_cxsf, r1                   @ save in spsr_svc ;// 將用戶態的cpsr保存到內核態的spsr,因爲,狀態切換是會將當前模式的spsr恢復到cpsr
#if defined(CONFIG_CPU_V6) || defined(CONFIG_CPU_32v6K)
        @ We must avoid clrex due to Cortex-A15 erratum #830321
        strex   r1, r2, [r2]                    @ clear the exclusive monitor
#endif
        .if     \fast
        ldmdb   r2, {r1 - lr}^                  @ get calling r1 - lr
        .else
        ldmdb   r2, {r0 - lr}^                  @ get calling r0 - lr ;// 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 ;// 恢復pc寄存器(跳轉到中斷前指令地址),同時恢復cpsr
#elif defined(CONFIG_CPU_V7M)
        @ V7M restore.
        @ Note that we don't need to do clrex here as clearing the local
        @ monitor is part of the exception entry and exit sequence.
        .if     \offset
        add     sp, #\offset
        .endif
        v7m_exception_slow_exit ret_r0 = \fast
#else
        @ Thumb mode restore
        mov     r2, sp
        load_user_sp_lr r2, r3, \offset + S_SP  @ calling sp, lr
        ldr     r1, [sp, #\offset + S_PSR]      @ get calling cpsr
        ldr     lr, [sp, #\offset + S_PC]       @ get pc
        add     sp, sp, #\offset + S_SP
        msr     spsr_cxsf, r1                   @ save in spsr_svc


        @ We must avoid clrex due to Cortex-A15 erratum #830321
        strex   r1, r2, [sp]                    @ clear the exclusive monitor


        .if     \fast
        ldmdb   sp, {r1 - r12}                  @ get calling r1 - r12
        .else
        ldmdb   sp, {r0 - r12}                  @ get calling r0 - r12
        .endif
        add     sp, sp, #S_FRAME_SIZE - S_SP
        movs    pc, lr                          @ return & move spsr_svc into cpsr
#endif  /* !CONFIG_THUMB2_KERNEL */
        .endm

2、系統調用執行流程

上面已經介紹了個模塊內部執行細節,在此從總體上介紹下系統調用流程。

2.1、glibc系統調用

2.1.1、參數傳遞

參數傳遞大致如下,就是將參數按順序保存到寄存器裏面
#define LOAD_ARGS_0()
#define ASM_ARGS_0
#define LOAD_ARGS_1(a1) \
  _a1 = (int) (a1); \
  LOAD_ARGS_0 ()
#define ASM_ARGS_1 ASM_ARGS_0, "r" (_a1)
#define LOAD_ARGS_2(a1, a2) \
  register int _a2 asm ("a2") = (int) (a2); \
  LOAD_ARGS_1 (a1)
#define ASM_ARGS_2 ASM_ARGS_1, "r" (_a2)
#define LOAD_ARGS_3(a1, a2, a3) \
  register int _a3 asm ("a3") = (int) (a3); \
  LOAD_ARGS_2 (a1, a2)

2.1.2、系統調用(進入內核態)

ARM最終都是通過swi指令切換的svc模式

#define INTERNAL_SYSCALL_RAW(name, err, nr, args...)\
  ({ unsigned int _sys_result; \
     { \
       register int _a1 asm ("a1"); \
       LOAD_ARGS_##nr (args) \
       asm volatile ("swi %1@ syscall " #name\
    : "=r" (_a1)\
    : "i" (name) ASM_ARGS_##nr\
    : "memory");\
       _sys_result = _a1; \
     } \
     (int) _sys_result; })

2.2、系統調用(中斷向量)

__vectors_start + 0x1000地址正好存儲vector_swi函數地址,具體參考vmlinux.lds.S以及early_trap_init中斷向量拷貝函數

__vectors_start:         
        W(b)    vector_rst        
        W(b)    vector_und        
W(ldr)  pc, __vectors_start + 0x1000
        W(b)    vector_pabt       
        W(b)    vector_dabt       
        W(b)    vector_addrexcptn 
        W(b)    vector_irq        
        W(b)    vector_fiq

3、系統調用總結

系統調用主要就是glibc通過寄存器及swi指令切換到內核態,內核態保存用戶態上下文,執行系統調用,判斷是否有掛起任務待處理,執行掛起任務或者直接恢復用戶態寄存器。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章