linux系统调用过程解析(基于ARM处理器)

来源:互联网 发布:表示喜欢的网络用语 编辑:程序博客网 时间:2024/06/10 23:04

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指令切换到内核态,内核态保存用户态上下文,执行系统调用,判断是否有挂起任务待处理,执行挂起任务或者直接恢复用户态寄存器。
原创粉丝点击