arm系统调用过程

文章详细介绍了ARM处理器的寄存器结构,包括通用寄存器、状态寄存器及其在不同处理器模式下的使用。重点讲解了用户模式和特权模式之间的切换,特别是在执行svc或swi指令进行系统调用时的寄存器保存和异常处理流程。系统调用参数通过寄存器传递,而非栈,因为模式切换后栈指针会改变。文章还探讨了内核栈的设置和进程上下文切换时的堆栈管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

寄存器介绍来自于
https://blog.csdn.net/a999818/article/details/123837580
ARM 处理器一般共有 37 个寄存器,其中包括:
1) 31 个通用寄存器,包括 PC(程序计数器)在内,都是 32 位的寄存器。
2) 6 个状态寄存器,都是 32 位的寄存器。
ARM 处理器共有 7 种不同的处理器模式:
(1)USR(10000):正常用户模式,程序正常执行模式。
(2)FIQ(10001):快速中断模式,以处理快速情况,支持高速数据传输或通道处理。
(3)IRQ(10010):外部中断模式,普通中断处理
(4)SVC(10011):操作系统保护模式(管理模式),即操作系统使用的特权模式(内核),处理软件中断swi reset
(5)abt(10111):数据访问中止模式,用于 虚拟存储器 和 存储器 保护
(6)und(11011):未定义指令终止模式,用于支持通过软件仿真硬件的协处理器
(7)sys(11111):系统模式,用于运行特权级的操作系统任务( armv4 以上版本才具有)
1、 在所有的寄存器中,有些是各模式共用同一个物理寄存器,有些寄存器是各个模式自己拥有独立的物理寄存器
2、CPSR和SPSR都是程序状态寄存器,其中SPSR是用来保存中断前的CPSR中的值,以便在中断返回之后恢复处理器程序状态。

从下图可以看到system和user模式的所有寄存器是共享的。而svc和user模式的r13和r14是不共享。这意味着当从用户态态转到svc模式时,同样是在访问寄存器r13。这个r13在不同模式下的值是不同的.

例如针对同样的汇编指令

mov r0,r13(user模式)-->(假设这条指令直接到了svc模式下)mov r0,r13//svc下的r13就不是用户态的r13。他们只是同名而已,其实是两个不同的寄存器了。

这是为什么说arm有31个通用寄存器 16(user&system)+ 7 (FIQ)+2(svc)+2(abort)+2(irq)+2(undef) = 31

状态寄存器6个=1+1+1+1+1+1

除用户模式之外的其他6种处理器模式被称为特权模式。在这些模式下,程序可以访问所有的系统资源,也可以进行处理器模式切换

系统调用产生过程

  1. 如:open ---> __libc_open() ---> svc/swi ---> vector_swi --->system_call ---> sys_func

svc和swi的区别
svc和swi都是supervisor call指令,都是系统调用.
再armv7之前,用的都是swi, 触发异步异常,进入vector_swi异常向量表;
在armv8-arch64架构下,抛弃了swi,改用了 svc,触发的是同步异常,进入同步异常向量表el1_sync

系统调用和普通的函数调用是不同的。对于普通的函数调用,参数被保存到了r0-r3中,如果形参超过了4个,那么多余的参数可以用栈进行传递。但是对于系统调用(swi/svc),会引起处理器模式切换user切换到svc,而svc模式下的sp和user模式的sp是不同的。因此系统调用无法使用栈传入参数,需要将所有的参数通过寄存器传递。arm最多支持6个(r0-r5)。sp究竟是怎么切换的呢?

随便在glibc里面找到了clone.S。看看clone的实现

ENTRY(__clone)
    @ sanity check args
    cmp    r0, #0
    ite    ne
    cmpne    r1, #0
    moveq    r0, #-EINVAL
    beq    PLTJMP(syscall_error)

    @ insert the args onto the new stack
    str    r3, [r1, #-4]!
    str    r0, [r1, #-4]!

    @ do the system call
    @ get flags
    mov    r0, r2
    mov    ip, r2
    @ new sp is already in r1
    push    {r4, r7}
    cfi_adjust_cfa_offset (8)
    cfi_rel_offset (r4, 0)
    cfi_rel_offset (r7, 4)
    ldr    r2, [sp, #8]
    ldr    r3, [sp, #12]
    ldr    r4, [sp, #16]
    ldr    r7, =SYS_ify(clone)
    swi    0x0//产生软中断
    cfi_endproc
    cmp    r0, #0
    beq    1f
    pop    {r4, r7}
    blt    PLTJMP(C_SYMBOL_NAME(__syscall_error))
......................
    ldr    r7, =SYS_ify(clone)
    swi    0x0

可以看到系统调用号被保存到了r7里面.并且是使用的swi指令触发异常进入内核态。

在异常产生的时候,会发生什么?硬件做了哪些事,软件需要做哪些事?这是我们必须要知道的,下面就是异常发生时 arm 处理器自动执行的操作:

1、将异常发生前所属模式的 CPSR(user下为APSR) 拷贝到异常发生后将要进入模式的 SPSR_mode(我们这个例子是SPSR_svc) 中,除了 System 模式,其它所有 PL1 特权级模式都有 SPSR bank 寄存器,这个操作并不难理解,就是保存现场,方便在异常处理完成之后还原之前的 CPSR.
2、将返回地址保存到 LR 寄存器中,返回地址自然是当前指令的下一条指令的地址,但是因为指令流水线的存在,PC寄存器中保存的是当前指令地址 +8 处的指令,所以需要针对 PC 做一个偏移,这个偏移并不是固定的,而是根据不同模式有不同的值.
3、修改 CPSR 中的某些 bit。修改 CPSR 的 mode 部分,修改为异常处理模式下的模式;设置 CP15 的 TE bit;J(指令集模式) bit被清除,同时 E(大小端) bit 设置为 EE(exception 大小端) bit. 主要是根据预先的设置配置异常处理的指令集模式和大小端; 设置 PC 指针到对应的异常模式向量处,执行软件定义的异常处理程序.

根据硬件自动所做的动作以及上面保存现场的代码可以看到,r13和r14里面保存的是用户态的sp和lr。r15(pc)里面保存的则是用户态返回的地址( str lr, [sp, #S_PC]),即用户态调用swi指令的下一条地址。r16里面则是用户态的cpsr寄存器。所以说内核栈里面的pt_regs里面保存的都是用户态的现场信息。然后还会将PC设置为异常模式向量表的起始地址,执行对应预先定义的异常处理程序。

下面这个是向量表。因此swi指令触发异常,进入内核态是从这里开始执行的。至于怎么走到vector_swi没有看明白。具体可以参考https://dandelioncloud.cn/article/details/1570975835916300289

__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
ENTRY(vector_swi)
#ifdef CONFIG_CPU_V7M
    v7m_exception_entry
#else
    /************** 保存断点部分 ***************/
    /* S_FRAME_SIZE=72=sizeof(struct pt_regs) */
    sub    sp, sp, #S_FRAME_SIZE
    /*
    将r0-r12寄存器保存到栈中,目前还没有保存r13(sp),r14(lr),r15(pc),cpsr,orig_r0
    sp不是sp!,因此这里只是将13个寄存器保存到了sp里面地址开始的空间中,sp的值并未变化
    这里首先保存r0-r12,估计是因为user模式和svc模式,这几个寄存器是共享的。直接使用会
    破坏用户态的现场
    */
    stmia    sp, {r0 - r12}            @ Calling r0 - r12
    /* 
    60 offsetof(struct pt_regs, ARM_pc)
    r8 = sp + 60
    因此此时r8是指向了最开始开辟的72字节栈中的第60字节(pc).
    但是前面只是保存了13*4=48字节的内存。此时sp+60里面没有有效数据
    */
 ARM(    add    r8, sp, #S_PC        )
     /*
     将用户空间的 sp 和 lr 保存到 r8 地址处,那这里的sp还是用户栈咯?
     前面不是开辟了72字节的栈空间嘛?这部分也是用户空间的栈??这里其实已经到了内核栈
     stmdb压栈指令,从右到左入栈 r8=r8-4, [r8]=lr
    因此相当于将sp_user和lr_user保存到uregs[14]和uregs[13]中,
    至此pt_regs中r0-r14保存完毕
    */
 ARM(    stmdb    r8, {sp, lr}^        )    @ Calling sp, lr
 THUMB(    mov    r8, sp            )
 THUMB(    store_user_sp_lr r8, r10, S_SP    )    @ calling sp, lr
     /*  spsr 中保存了 user 下的 APSR,将spsr给r8 */
    mrs    r8, spsr            @ called from non-FIQ mode, so ok.
    /*
     lr里面保存了返回地址(即用户空间执行swi指令的下一条指令的地址。将返回地址保存到lr是硬件自己做的),因此将其存入pc
     这样等系统调用返回时,就从这个返回地址开始执行 
    */
    str    lr, [sp, #S_PC]            @ Save calling PC
    str    r8, [sp, #S_PSR]        @ Save CPSR
    str    r0, [sp, #S_OLD_R0]        @ Save OLD_R0
#endif
 ARM(    stmdb    r8, {sp, lr}^        )    @ Calling sp, lr

stm* 指令后添加 ^,表示操作的不是当前模式下的 sp,lr,而是 USER 模式下的寄存器

当通过系统调用进入内核的时候,内核栈已经是准备好了,不过这时候内核栈上是空的,执行完上述代码之后,在内核栈上形成如下的用户空间现场:
上面这段(vector_swi)会被是保存user模式下的寄存器断点保存。所以网上说struct pt_regs保存了 用户态的信息。但是我感觉很多汇编代码在 重复保存寄存器呢???前面不是已经保存了嘛??
1、ARM( stmdb r8, {sp, lr}^ ) @ Calling sp, lr //这个不就是将用户模式的sp,lr保存到内核栈嘛?
2、
THUMB(mov r8, sp)
THUMB(store_user_sp_lr r8, r10, S_SP)@ calling sp, lr
等价于
mrs r10, cpsr
eor r10, r10, #(SVC_MODE ^ SYSTEM_MODE)
msr cpsr_c,r10 @ switch to the SYS mode 系统模式和用户模式公用sp和lr
str sp, [r8, #S_SP] @ save sp_usr 系统模式和svc模式共用r8,因此r8指向了
@ 内核栈指针sp。这不就是向内核栈里面保存用户态的sp
@ 和lr嘛
str lr, [r8, #S_SP + 4] @ save lr_usr
eor r10, r10, #(SVC_MODE ^ SYSTEM_MODE)
ms rcpsr_c, r10 @ switch back to the SVC mode 最后保存完了,返回svc
感觉这上面的代码功能是一样的呢?不过arm的汇编不熟,后面再来看吧
好像arm有两种指令状态arm和THUMB,是不是这两个里面只会执行一个哦

还有就是用户空间进入到内核空间,内核栈是在何时设定的呢?如何就切换为当前进程的内核栈呢?

答案来自于
X86切换方式,好像操作系统真像还原那本书里面也提了这个
https://blog.csdn.net/orchidofocean/article/details/56972305
https://www.cnblogs.com/justcxtoworld/p/3155741.html
arm
https://tech.hqew.com/fangan_1727106

每个进程都一个用户栈和内核栈。当进程从用户态进行到内核态时,可以看到上面vector_swi里面直接就默认sp是内核栈。但是既然每个进程都有一个单独的内核栈,那通过svc/swi进入到内核态,它是怎么确定改用哪个进程的内核栈呢?代码里面也没有看到传了什么进程号之类的东西,也没有看到去找对应的内核栈。

上面的文章里面是这样解释的(这个解释对于用户态被中断打断进入内核态,感觉也是适用的。应该是从用户态到内核态):

关于SP寄存器,这里有一个问题值得澄清一下,前面提到“在进入SVC模式时,SP/R13寄存器所指向的位置就正好是当前进程的内核栈”,原因是什么呢?
  1. svc模式和user模式下的sp和lr都是独立的。

  1. 当前进程被中断打断或者通过系统调用主动陷入内核态,有个前提就是当前进程是被cpu选中,正在被调度。因此一定有这样一个过程当前进程被调度(schedule)(从内核态进入了用户态),然后在运行的过程中进入内核态。在进行schedule的时候会进行上下文切换的时候就会设置内核栈,即sp_svc。设置完了之后返回用户态。由于sp_svc和sp_user都是独立的。因此当进程在用户态进入内核态时,此时的sp_svc还是在当前进程被schedule的时候设置的sp_svc。因此进入svc模式时的sp刚好就是当前进程的内核栈。

接下来继续看vector_swi的执行。

    ...........................
     /*  spsr 中保存了 user 下的 APSR,将spsr给r8 */
    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
#endif
    /**************** 系统设置 ****************/
    /* mov    fp, #0,具体见entry_header.S中zero_fp */
    zero_fp
    alignment_trap ip, __cr_alignment
    enable_irq
    ct_user_exit
    /* 将r9设置为thread_info的基地址 */
    get_thread_info tsk

    /*
     * Get the system call number.
     */
    /*
    EABI和OABI是arm平台架构中两种不同的系统调用规则
    OABI 和 EABI 最大的区别在于,
    OABI 的系统调用指令需要传递参数来指定系统调用号,
    而 EABI 中将系统调用号保存在 r7 中.
    */
#if defined(CONFIG_OABI_COMPAT)

    /*
     * If we have CONFIG_OABI_COMPAT then we need to look at the swi
     * value to determine if it is an EABI or an old ABI call.
     */
#ifdef CONFIG_ARM_THUMB
    tst    r8, #PSR_T_BIT//判断是arm状态还是Thumb状态
    movne    r10, #0                @ no thumb OABI emulation
 USER(    ldreq    r10, [lr, #-4]        )    @ get SWI instruction
#else
 USER(    ldr    r10, [lr, #-4]        )    @ get SWI instruction
#endif
 ARM_BE8(rev    r10, r10)            @ little endian instruction

#elif defined(CONFIG_AEABI)

    /*
     * Pure EABI user space always put syscall number into scno (r7).
     */
#elif defined(CONFIG_ARM_THUMB)
    /* Legacy ABI only, possibly thumb mode. */
    /* T[5] : 状态位,标记是arm状态或者Thumb状态 */
    tst    r8, #PSR_T_BIT            @ this is SPSR from save_user_regs
    /* r7保存系统调用号,scno是r7的别称 */
    addne    scno, r7, #__NR_SYSCALL_BASE    @ put OS number in
 USER(    ldreq    scno, [lr, #-4]        )

#else
    /* Legacy ABI only. */
 USER(    ldr    scno, [lr, #-4]        )    @ get SWI instruction
#endif
    /* tbl是r8的别称 将sys_call_table的地址给r8*/
    adr    tbl, sys_call_table        @ load syscall table pointer

#if defined(CONFIG_OABI_COMPAT)
    /*
     * If the swi argument is zero, this is an EABI call and we do nothing.
     *
     * If this is an old ABI call, get the syscall number into scno and
     * get the old ABI syscall table address.
     */
    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

local_restart:
    ldr    r10, [tsk, #TI_FLAGS]        @ check for syscall tracing
    // 将 r4 和 r5 的值保存在栈上,因为arm只有r0-r34个寄存器保存参数,
    //超过了需要保存到栈上??
    // 这两个寄存器中保存着系统调用第五个和第六个参数(如果存在)
    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
    /* 将返回地址设置为ret_fast_syscall */
    adr    lr, BSYM(ret_fast_syscall)    @ return address
    /*
    pc = tbl + scno * (2<<2)(一个异常处理程序地址占用4字节) 
    应该就是从这里就跳转到真正的系统调用函数那里去了
    等到函数执行完了,就会通过lr转到到ret_fast_syscall
    */
    ldrcc    pc, [tbl, scno, lsl #2]        @ call sys_* routine

    /* 这下面的应该是系统调用号超过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
    mov    why, #0                @ no longer a real syscall
    b    sys_ni_syscall            @ not private func

#if defined(CONFIG_OABI_COMPAT) || !defined(CONFIG_AEABI)
    /*
     * We failed to handle a fault trying to access the page
     * containing the swi instruction, but we're not really in a
     * position to return -EFAULT. Instead, return back to the
     * instruction and re-enter the user fault handling path trying
     * to page it in. This will likely result in sending SEGV to the
     * current task.
     */
9001:
    sub    lr, lr, #4
    str    lr, [sp, #S_PC]
    b    ret_fast_syscall
#endif
ENDPROC(vector_swi)

系统调用执行完毕之后,由于lr寄存器被设置为了ret_fast_syscall。因此在系统调用返回的时候,是执行ret_fast_syscall

ret_fast_syscall:
 UNWIND(.fnstart    )
 UNWIND(.cantunwind    )
    disable_irq                @ disable interrupts
    ldr    r1, [tsk, #TI_FLAGS]
    tst    r1, #_TIF_WORK_MASK
    /*  如果存在未处理的信号或者进程可被抢占时,执行 fast_work_pending */
    bne    fast_work_pending
    asm_trace_hardirqs_on

    /* perform architecture specific actions before user return */
    arch_ret_to_user r1, lr
    ct_user_enter
    /* 返回代码 */
    restore_user_regs fast = 1, offset = S_OFF
 UNWIND(.fnend        )

ret_fast_syscall-->fast_work_pending:如果有信号需要处理走这个流程:没有看明白

fast_work_pending:
    str    r0, [sp, #S_R0+S_OFF]!        @ returned r0
work_pending:
    mov    r0, sp                @ 'regs'
    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为什么又要重新跳转回去??

反之直接返回用户态:

1、restore_user_regs fast = 1, offset = S_OFF。fast = 1 offset = 8。感觉这个offer=8就是因为我们将额外的r4和r5入栈了。因此会多8字节的偏移。并且代码里面的注释也能对应上。

2、stmdb sp!, {r4, r5} 。这个指令会将sp的值改变。因此offset为8也能对上;

3、为什么执行到这里sp并没有改变。我感觉类似于函数调用。f1调用f2,f2调用f3;f3执行完出栈,回到f2,f2执行完出栈回到f1。在回到f1的时候,sp和调用f2时的sp是一样的

@
@ Most of the stack format comes from struct pt_regs, but with
@ the addition of 8 bytes for storing syscall args 5 and 6.
@ This _must_ remain a multiple of 8 for EABI.
@
#define S_OFF        8

下面的代码我们就假设sp刚好执行pt_regs,offset为0

.macro    restore_user_regs, fast = 0, offset = 0
    /* r1 = spsr_svc, spsr_svc其实在进入svc模式时,保存了用户态的cpsr*/
    ldr    r1, [sp, #\offset + S_PSR]    @ get calling cpsr
    /*
     这里面保存的是用户态的返回地址
    vector_swi里面有一句
    lr里面保存的是用户态的返回地址,这个指令将其保存到pt_regs里面的pc里面去了
    str    lr, [sp, #S_PC]            @ Save calling PC
     */
    ldr    lr, [sp, #\offset + S_PC]!    @ get pc
    /* 将r1保存到spsr的控制域(c),状态域(s),x,f.感觉就是等同spsr */
    msr    spsr_cxsf, r1            @ save in spsr_svc
#if defined(CONFIG_CPU_V6)
    strex    r1, r2, [sp]            @ clear the exclusive monitor
#elif defined(CONFIG_CPU_32v6K)
    clrex                    @ clear the exclusive monitor
#endif
    .if    \fast
    /* 将之前保存的用户态的寄存器值会恢复到用户态的寄存器中
    (注意^,只不过部分寄存器user和svc共用)r1-lr中
    r0 ~ r12 是共用的寄存器,r0 中保存了系统调用返回值,
      不需要返回到断点 r0 
     */
    ldmdb    sp, {r1 - lr}^            @ get calling r1 - lr
    .else
    /* 如果执行的是 fast_work_pending 分支,r0 是保存在栈上的,也需要一并重新加载 */
    ldmdb    sp, {r0 - lr}^            @ get calling r0 - lr
    .endif
    mov    r0, r0                @ ARMv5T and earlier require a nop
                        @ after ldm {}^
    /*
     恢复内核栈指针为初始值 
     ldmdb指令会修改sp的值
    因此此时sp回到原来位置,只需要增加S_FRAME_SIZE - S_PC大小
    */
    add    sp, sp, #S_FRAME_SIZE - S_PC
    /* 将用户态返回地址设置为pc,去执行用户态代码 */
    movs    pc, lr    
/* 将用户态返回地址设置为pc,去执行用户态代码,并且movs,隐含将spsr复制到cpsr中,这个也实现了arm处理器模式切换为user模式 */
    movs    pc, lr    

最终通过指令通过movs pc, lr返回用户空间,将arm处理器模式切换为user模式

 ldmdb    sp, {r1 - lr}^            @ get calling r1 - lr

没有看明白这条指令。ldmdb不是先sp = sp-4,然后lr_user=[sp - 4];sp = sp-4,sp_user=[sp - 4].

那这不就意味着sp在最上面吗???

ldr    lr, [sp, #\offset + S_PC]!    @ get pc

但是看这句,有感觉sp在下面 lr = sp + s_pc。这个不是矛盾的吗??没有看明白。。。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值