linux系统调用
关于系统调用
系统调用是操作系统中的一种重要机制,它允许应用程序访问内核提供的各项功能,比如操作文件、分配内存、创建进程等等。像我们平时使用的c库,其中很多库函数其实就是通过系统调用来实现的。系统调用通常通过软件中断指令来触发,应用程序触发系统调用时,会向内核传递系统调用号,内核就根据系统调用号来执行不同的操作,系统调用的基本过程如下:
- 将参数和系统调用号放入寄存器
- 触发软件中断,进入内核空间
- 内核中断处理程序保存中断现场
- 内核中断处理程序使用系统调用号查找系统调用处理函数并调用它
- 恢复中断现场,返回用户空间
笔者工作中最常接触到的c库是musl,所以接下来会以musl库中的write
函数为例来看看它是如何进行系统调用的。
musl库的open函数
musl库中各个库函数的实现位于musl-1.2.3/src
目录下,不同功能的c库接口分布在不同的子目录中,子目录的名字和我们平时include的那些头文件是一一对应的,比如"string.h"对应string目录
在各个子目录中,每个文件基本上都对应着一个c库接口,文件名与c库接口名是一样的:
write
函数的实现如下:
#include <unistd.h>
#include "syscall.h" // 该头文件中定义了所有的系统调用号ssize_t write(int fd, const void *buf, size_t count)
{return syscall_cp(SYS_write, fd, buf, count); // SYS_write是系统调用号
}
write
函数调用的syscall_cp
是一个宏,该宏的定义如下:
// musl-1.2.3/src/internal/syscall.h
#define __syscall1(n,a) __syscall1(n,__scc(a))
#define __syscall2(n,a,b) __syscall2(n,__scc(a),__scc(b))
#define __syscall3(n,a,b,c) __syscall3(n,__scc(a),__scc(b),__scc(c))
#define __syscall4(n,a,b,c,d) __syscall4(n,__scc(a),__scc(b),__scc(c),__scc(d))
#define __syscall5(n,a,b,c,d,e) __syscall5(n,__scc(a),__scc(b),__scc(c),__scc(d),__scc(e))
#define __syscall6(n,a,b,c,d,e,f) __syscall6(n,__scc(a),__scc(b),__scc(c),__scc(d),__scc(e),__scc(f))
#define __syscall7(n,a,b,c,d,e,f,g) __syscall7(n,__scc(a),__scc(b),__scc(c),__scc(d),__scc(e),__scc(f),__scc(g))#define __SYSCALL_NARGS_X(a,b,c,d,e,f,g,h,n,...) n
#define __SYSCALL_NARGS(...) __SYSCALL_NARGS_X(__VA_ARGS__,7,6,5,4,3,2,1,0,)
#define __SYSCALL_CONCAT_X(a,b) a##b
#define __SYSCALL_CONCAT(a,b) __SYSCALL_CONCAT_X(a,b)
#define __SYSCALL_DISP(b,...) __SYSCALL_CONCAT(b,__SYSCALL_NARGS(__VA_ARGS__))(__VA_ARGS__)#define __syscall(...) __SYSCALL_DISP(__syscall,__VA_ARGS__)
#define syscall(...) __syscall_ret(__syscall(__VA_ARGS__))#define socketcall(nm,a,b,c,d,e,f) __syscall_ret(__socketcall(nm,a,b,c,d,e,f))
#define socketcall_cp(nm,a,b,c,d,e,f) __syscall_ret(__socketcall_cp(nm,a,b,c,d,e,f))#define __syscall_cp0(n) (__syscall_cp)(n,0,0,0,0,0,0)
#define __syscall_cp1(n,a) (__syscall_cp)(n,__scc(a),0,0,0,0,0)
#define __syscall_cp2(n,a,b) (__syscall_cp)(n,__scc(a),__scc(b),0,0,0,0)
#define __syscall_cp3(n,a,b,c) (__syscall_cp)(n,__scc(a),__scc(b),__scc(c),0,0,0)
#define __syscall_cp4(n,a,b,c,d) (__syscall_cp)(n,__scc(a),__scc(b),__scc(c),__scc(d),0,0)
#define __syscall_cp5(n,a,b,c,d,e) (__syscall_cp)(n,__scc(a),__scc(b),__scc(c),__scc(d),__scc(e),0)
#define __syscall_cp6(n,a,b,c,d,e,f) (__syscall_cp)(n,__scc(a),__scc(b),__scc(c),__scc(d),__scc(e),__scc(f))#define __syscall_cp(...) __SYSCALL_DISP(__syscall_cp,__VA_ARGS__)
#define syscall_cp(...) __syscall_ret(__syscall_cp(__VA_ARGS__))
根据入参的个数(除系统调用号外的入参个数),syscall_cp
宏会展开成__syscall1
、__syscall2
…__syscall7
,比如syscall_cp(SYS_write, fd, buf, count)
会展开成这样:
__syscall3(SYS_write,__scc(fd),__scc(buf),__scc(count))
不同芯片架构系统调用时使用的寄存器以及触发系统调用的指令都是不同的,因此不同的架构会定义各自的__syscall1
、__syscall2
…__syscall7
函数。在musl-1.2.3/arch/
目录下有各个架构相关的代码,每个架构的目录下都有一个syscall_arch.h
文件,这个文件中会定义__syscall1
、__syscall2
…__syscall7
这些函数。
musl-1.2.3/arch$ find ./ -name "syscall_arch.h"
./i386/syscall_arch.h
./m68k/syscall_arch.h
./microblaze/syscall_arch.h
./s390x/syscall_arch.h
......
以arm为例,arm定义的__syscall3
函数大致如下:
// musl-1.2.3/arch/arm/syscall_arch.h
static inline long __syscall3(long n, long a, long b, long c)
{register long r7 __ASM____R7__ = n; // 系统调用号放入r7register long r0 __asm__("r0") = a; // 第一个入参放入r0register long r1 __asm__("r1") = b; // 第二个入参放入r1register long r2 __asm__("r2") = c; // 第三个入参放入r2__asm_syscall(R7_OPERAND, "0"(r0), "r"(r1), "r"(r2));
}
arm使用r7来传递系统调用号,使用r0-r5来传递其余参数,对于write(int fd, const void *buf, size_t count)
函数而言,r0会被用来存放入参fd
,r1会被用来存放入参buf
,r2会被用来存放入参count
,r3-r5没用到。
__asm_syscall
是一个宏,它也定义于musl-1.2.3/arch/arm/syscall_arch.h
文件中,如下:
// 我们假定编译内核和musl时没有打开CONFIG_ARM_THUMB编译选项,__asm_syscall的定义就如下所示,如果开启了CONFIG_ARM_THUMB编译选项,__asm_syscall的定义会稍微有点不一样
#define __ASM____R7__ __asm__("r7")
#define __asm_syscall(...) do { \__asm__ __volatile__ ( "svc 0" \: "=r"(r0) : __VA_ARGS__ : "memory"); \return r0; \ } while (0)
__asm_syscall
通过__asm__
关键字调用汇编指令svc 0
来触发软件中断,触发后cpu会跳转到异常向量表,还会切换到svc模式。当系统调用返回后,其返回值会被放入r0,所以上面的__asm_syscall
会执行return r0
。
除了触发系统调用的指令和寄存器外,不同架构的系统调用号也是不同的,同样是在musl-1.2.3/arch/
目录下,每个架构的目录下都有一个syscall.h.in
文件,它定义了各架构下的系统调用号。
musl-1.2.3/arch$ find ./ -name "syscall.h.in"
./i386/bits/syscall.h.in
./m68k/bits/syscall.h.in
./microblaze/bits/syscall.h.in
......
syscall.h.in
文件中的内容大致如下,编译musl库时,musl库会根据芯片的架构使用特定的syscall.h.in
来生成一个头文件syscall.h
,实现库函数的c文件中通常都会include这个syscall.h
,比如我们前面看过的write.c
,write
函数使用的调用号是SYS_write
,它的值其实就来自于syscall.h.in
文件中的__NR_write
。
#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
......
内核中的系统调用表
内核会维护一张系统调用表,表里面记录的是系统调用处理函数的地址,每一个系统调用号都有一个对应的处理函数,这个表是在编译阶段生成的。
还是以arm为例,在linux_5.10.161/arch/arm/kernel/entry-common.S
文件中有这样一段代码:
syscall_table_start sys_call_table
#define COMPAT(nr, native, compat) syscall nr, native
#ifdef CONFIG_AEABI
#include <calls-eabi.S>
#else
#include <calls-oabi.S>
#endif
#undef COMPATsyscall_table_end sys_call_table
syscall_table_start
和syscall_table_end
都是宏,它们分别标识了系统调用表的开始和结束。syscall_table_start sys_call_table
展开后会定义一个名为sys_call_table
的符号,sys_call_table
符号所在地址其实就是系统调用表,而系统调用表的条目就位于calls-eabi.S
这个文件里 (假定内核使用的是eabi)。calls-eabi.S
是在内核编译阶段依据linux_5.10.161/arch/arm/tools/syscall.tbl
文件生成的,calls-eabi.S
文件内容如下:
NATIVE(0, sys_restart_syscall)
NATIVE(1, sys_exit)
NATIVE(2, sys_fork)
NATIVE(3, sys_read)
NATIVE(4, sys_write)
NATIVE(5, sys_open)
NATIVE(6, sys_close)
NATIVE(8, sys_creat)
NATIVE(9, sys_link)
......
NATIVE()
是一个宏,它第一个参数是系统调用号,第二个参数是系统调用处理函数,NATIVE()
宏展开后会定义一个long型的变量,变量的值就是处理函数的地址。把所有的宏都展开后,系统调用表就像这样子:
sys_call_table:.long sys_restart_syscall.long sys_exit.long sys_fork.long sys_read.long sys_write.......long sys_ni_syscall // 对于内核不支持的系统调用,内核会将其处理函数被设置为 sys_ni_syscall.long sys_ni_syscall......
不难想到,内核执行系统调用时,从sys_call_table + 4*系统调用号
这个地址处就能获取到系统调用的处理函数。
需要注意的是,不同架构定义的系统调用表可能会不同,相同架构不同内核版本定义的系统调用表也可能会不同,笔者此时阅读的是5.10.161版本的内核并且编译时选则的架构是arm,有可能其它版本内核根本就不会生成calls-eabi.S
和calls-oabi.S
这样的文件。
系统调用处理函数的定义
系统调用表里的函数定义于内核的各个文件中,如果我们通过系统调用表里的函数名去内核的源码中搜索,通常会搜不到该函数的定义,这是因为内核在定义系统调用时,使用了SYSCALL_DEFINE1
、SYSCALL_DEFINE2
…SYSCALL_DEFINE6
这样的宏来定义系统调用处理函数,比如sys_write
这个函数就是由下面的代码来定义的:
// linux_5.10.161/fs/read_write.c
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,size_t, count)
{return ksys_write(fd, buf, count);
}
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)
展开后,其实会生成这样一堆函数:
asmlinkage long sys_write(unsigned int fd, const char _user *buf, size_t count) __attribute__((alias(__stringify(__se_sys_write)))); // sys_write是__se_sys_write的别名__se_sys_write(unsigned int fd, const char _user *buf, size_t count){ long ret = __do_sys_write(fd, buf, count); // __se_sys_write直接调用 __do_sys_write函数return ret;
}
__do_sys_write(unsigned int fd, const char _user *buf, size_t count){return ksys_write(fd, buf, count);
}
我们在系统调用表里看到的sys_write
其实是__se_sys_write
函数的别名,访问sys_write
就等于访问__se_sys_write
。
如果想快速的知道某个系统调用处理函数定义于哪个文件,可以尝试查找linux_5.10.161/include/linux/syscalls.h
文件,这个文件中有些注释信息会告诉我们系统调用处理函数定义于哪个文件:
内核处理系统调用的基本过程
通过前面的分析可以知道,arm的系统调用是通过svc 0
指令产生软件中断来触发的。触发软件中断后,内核会跳转到异常向量表,arm的异常向量表长这样:
// linux_5.10.161/arch/arm/kernel/entry-armv.S.section .vectors, "ax", %progbits
.L__vectors_start:W(b) vector_rstW(b) vector_undW(ldr) pc, .L__vectors_start + 0x1000W(b) vector_pabtW(b) vector_dabtW(b) vector_addrexcptnW(b) vector_irqW(b) vector_fiq
执行svc 0
指令后,cpu会跳转到L__vectors_start + 0x8
的地址处,也就是说W(ldr) pc, .L__vectors_start + 0x1000
指令将会被执行。
在之前介绍中断现场保存和恢复的笔记中(https://blog.csdn.net/maoyichao123/article/details/147349236?spm=1011.2124.3001.6209)曾提到过,内核编译时会把arm的异常向量表放入一个名为.vectors
的段,把vector_rst
、vector_und
…vector_fiq
这些符号处的代码放入一个名为.stubs
的段,内核初始化阶段把会把.vectors
段的内容映射到0xffff0000地址上,同时还会把.stubs
段内容映射到0xffff1000地址上。W(ldr) pc, .L__vectors_start + 0x1000
指令其实就是把.stubs
段开始的那四个字节加载到到pc,那四个字节里面是vector_swi
符号的地址。
// linux_5.10.161/arch/arm/kernel/entry-armv.S.section .stubs, "ax", %progbits@ This must be the first word.word vector_swi // stubs第一个word里放的vector_swi符号的地址
接下来CPU就会跳转到vector_swi
处执行,vector_swi
处的代码如下:
// linux_5.10.161/arch/arm/kernel/entry_common.S
ENTRY(vector_swi)
#ifdef CONFIG_CPU_V7Mv7m_exception_entry
#elsesub sp, sp, #PT_REGS_SIZE // 1 将sp_svc下移PT_REGS_SIZE(PT_REGS_SIZE = sizeof(struct pt_regs),详情见linux_5.10.161/arch/arm/include/uapi/asm/ptrace.h),在svc栈上预留空间,这部分空间会用来暂存软件中断指令前的中断现场(r0-r12,sp,lr,cpsr)stmia sp, {r0 - r12} @ Calling r0 - r12 // 2 将中断前的r0-r12入栈
3:ARM( add r8, sp, #S_PC ) // 3 接下来的两行将中断前的sp和lr入栈ARM( stmdb r8, {sp, lr}^ ) @ Calling sp, lr THUMB( mov r8, sp ) // 可以不用管THUMB()扩起来的指令,看ARM()括起来的和没被扩起来的指令就行THUMB( store_user_sp_lr r8, r10, S_SP ) @ calling sp, lrmrs saved_psr, spsr @ called from non-FIQ mode, so ok. // 4 接下来看到的saved_psr其实是r8的别名, 而saved_pc是r9的别名,这里这一行将spsr_svc(中断前的cpsr)保存到saved_psrTRACE( mov saved_pc, lr ) // 把lr_svc(中断返回地址)保存到saved_pcstr saved_pc, [sp, #S_PC] @ Save calling PC // 5 将中断返回地址入栈str saved_psr, [sp, #S_PSR] @ Save CPSR // 6 将中断前的cpsr入栈str r0, [sp, #S_OLD_R0] @ Save OLD_R0
#endifzero_fpalignment_trap r10, ip, __cr_alignmentasm_trace_hardirqs_on save=0enable_irq_notrace // 7 开启中断。软件中断触发后,硬件会自动关闭中断,系统调用函数执行期间会开启中断ct_user_exit save=0/** Get the system call number.*/// 8 接下来的部分代码会获取系统调用号,并将系统放到scno中,scno其实是r7的别名。需要注意的是,如果我们没有开启CONFIG_ARM_THUMB,musl库本就会把系统调用号放入r7,这种情况下其实啥都不用动。
#if defined(CONFIG_OABI_COMPAT)
...... // 这里省略了OABI的代码#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. */tst saved_psr, #PSR_T_BIT @ this is SPSR from save_user_regsaddne scno, r7, #__NR_SYSCALL_BASE @ put OS number inUSER( ldreq scno, [saved_pc, #-4] )#else/* Legacy ABI only. */USER( ldr scno, [saved_pc, #-4] ) @ get SWI instruction
#endif/* saved_psr and saved_pc are now dead */uaccess_disable tbladr tbl, sys_call_table @ load syscall table pointer // 9 获取系统调用表的地址,放到tbl中,tbl是r8的别名#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, #0xff000000eorne scno, r10, #__NR_OABI_SYSCALL_BASEldrne tbl, =sys_oabi_call_table
#elif !defined(CONFIG_AEABI)bic scno, scno, #0xff000000 @ mask off SWI op-codeeor scno, scno, #__NR_SYSCALL_BASE @ check OS number
#endifget_thread_info tsk/** Reload the registers that may have been corrupted on entry to* the syscall assembly (by tracing or context tracking.)*/TRACE( ldmia sp, {r0 - r3} ) // 10.1 接下来我们就快要进入系统调用的处理函数了,那些函数是c语言编写的函数,我们从汇编进入c函数需要遵循函数调用惯例(前4个参数通过寄存器传递,其余参数通过栈传递)。因为musl库触发软件中断前把入参放入了r0-r5,前面步骤2处,我们把r0-r12都入栈了,这里重新把系统调用的第1-4个入参数读到r0-r3local_restart:ldr r10, [tsk, #TI_FLAGS] @ check for syscall tracingstmdb sp!, {r4, r5} @ push fifth and sixth args // 10.2 把系统调用的第5、第6个入参放入栈中,通过栈传递给系统调用处理函数tst r10, #_TIF_SYSCALL_WORK @ are we tracing syscalls?bne __sys_traceinvoke_syscall tbl, scno, r10, __ret_fast_syscall // 11 invoke_syscall是一个宏,invoke_syscall会把lr设置为__ret_fast_syscall,然后使用系统调用号找到处理函数,然后调用它,处理函数退出时就会跳转到__ret_fast_syscall处add r1, sp, #S_OFF
2: cmp scno, #(__ARM_NR_BASE - __NR_SYSCALL_BASE)eor r0, scno, #__NR_SYSCALL_BASE @ put OS number backbcs arm_syscallmov why, #0 @ no longer a real syscallb 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, saved_pc, #4str lr, [sp, #S_PC]get_thread_info tskb ret_fast_syscall
#endif
ENDPROC(vector_swi)
invoke_syscall
宏如下:
// linux_5.10.161/arch/arm/kernel/entry_header.S.macro invoke_syscall, table, nr, tmp, ret, reload=0
#ifdef CONFIG_CPU_SPECTRE......
#elsecmp \nr, #NR_syscalls @ check upper syscall limit // 检查系统调用号是否超过了最大系统调用号badr lr, \ret @ return address // 把 __ret_fast_syscall放到lr寄存器.if \reloadadd r1, sp, #S_R0 + S_OFF @ pointer to regsldmiacc r1, {r0 - r6} @ reload r0-r6stmiacc sp, {r4, r5} @ update stack arguments.endifldrcc pc, [\table, \nr, lsl #2] @ call sys_* routine // 把sys_call_table + scno << 2处的系统调用处理函数加载到pc
#endif.endm
简单来看vector_swi
做了如下两件事:
- 保存中断现场
- 把系统调用的前4个参数放入r0-r3,把第5、第6个参数放入栈中,然后调用系统调用处理函数
系统调用处理函数执行完后,CPU就会跳转到__ret_fast_syscall
执行,此时sp_svc会重新指向栈上保存中断现场的位置,r0寄存器则存储着系统调用处理函数的返回值。
__ret_fast_syscall
处代码如下所示:
// linux_5.10.161/arch/arm/kernel/entry_common.S.section .entry.text,"ax",%progbits.align 5
#if !(IS_ENABLED(CONFIG_TRACE_IRQFLAGS) || IS_ENABLED(CONFIG_CONTEXT_TRACKING) || \IS_ENABLED(CONFIG_DEBUG_RSEQ))
/** This is the fast syscall return path. We do as little as possible here,* such as avoiding writing r0 to the stack. We only use this path if we* have tracing, context tracking and rseq debug disabled - the overheads* from those features make this path too inefficient.*/
ret_fast_syscall:
__ret_fast_syscall:UNWIND(.fnstart )UNWIND(.cantunwind )disable_irq_notrace @ disable interrupts // 1 关闭irqldr r2, [tsk, #TI_ADDR_LIMIT]cmp r2, #TASK_SIZEblne addr_limit_check_failedldr r1, [tsk, #TI_FLAGS] @ re-check for syscall tracing // 2 获取进程的标志位,判断是否还有其它事要做,比如是否需要调度,是否需要处理信号之类的tst r1, #_TIF_SYSCALL_WORK | _TIF_WORK_MASKbne fast_work_pending // 3 如果还有其它事要做,跳转到fast_work_pending处/* perform architecture specific actions before user return */arch_ret_to_user r1, lrrestore_user_regs fast = 1, offset = S_OFF // 4 如果没有其它事要做,restore_user_regs会负责恢复中断现场,返回用户空间UNWIND(.fnend )
ENDPROC(ret_fast_syscall)/* Ok, we need to do extra processing, enter the slow path. */
fast_work_pending:str r0, [sp, #S_R0+S_OFF]! @ returned r0 // 5 系统调用处理函数退出时,按照函数调用惯例,它会把返回值放到r0。接下来处理调度或者信号,可能要调用其它函数,可能会改变r0的值,所以这里先将r0放入栈中保存起来。/* fall through to work_pending */
#else
...... // 这个else里的内容省略了
#endiftst r1, #_TIF_SYSCALL_WORKbne __sys_trace_return_nosave
slow_work_pending:mov r0, sp @ 'regs'mov r2, why @ 'syscall'bl do_work_pending // 6 跳转到do_work_pending处处理调度或者信号cmp r0, #0beq no_work_pending // 7 如果do_work_pending返回值为0,那就跳转到no_work_pending处。no_work_pending处的代码也是通过restore_user_regs宏来恢复中断现场并返回用户空间movlt scno, #(__NR_restart_syscall - __NR_SYSCALL_BASE)ldmia sp, {r0 - r6} @ have to reload r0 - r6b local_restart @ ... and off we go // 8 某些系统调用被信号打断后会返回ERESTARTSYS之类的值,对于这种情形,内核会跳转到local_restart处重新启动系统调用
ENDPROC(ret_fast_syscall)
ret_fast_syscall
通过restore_user_regs
来恢复中断现场,restore_user_regs
宏定义如下:
// linux_5.10.161/arch/arm/kernel/entry_header.S.macro restore_user_regs, fast = 0, offset = 0uaccess_enable r1, isb=0
#ifndef CONFIG_THUMB2_KERNEL@ ARM mode restoremov r2, sp // 1 用r2暂替spldr r1, [r2, #\offset + S_PSR] @ get calling cpsr // 2 将中断前的cpsr暂存到r1ldr lr, [r2, #\offset + S_PC]! @ get pc // 3 将中断返回地址拷贝到lrtst r1, #PSR_I_BIT | 0x0f // 4 检查中断前的中断位是否被设置,如果被设置了,那就是个bugbne 1fmsr spsr_cxsf, r1 @ save in spsr_svc // 5 将中断前的cpsr拷贝到spsr_svc,稍后会把spsr_svc拷贝到cpsr进而切换回usr模式
#if defined(CONFIG_CPU_V6) || defined(CONFIG_CPU_32v6K)@ We must avoid clrex due to Cortex-A15 erratum #830321strex r1, r2, [r2] @ clear the exclusive monitor
#endif.if \fastldmdb r2, {r1 - lr}^ @ get calling r1 - lr // 6.1 fast为1,表示r0当前存储的就是系统调用函数的返回值,故只恢复r1-lr寄存器即可.elseldmdb r2, {r0 - lr}^ @ get calling r0 - lr // 6.2 fast为0,表示ret_fast_syscall在进入restore_user_regs前处理了调度或者信号,这个时候,系统调用的返回值会被存储在栈里面(见__ret_fast_syscall的注释5处的代码),故需要恢复r0-lr.endifmov r0, r0 @ ARMv5T and earlier require a nop@ after ldm {}^add sp, sp, #\offset + PT_REGS_SIZE // 7 恢复svc模式的栈指针movs pc, lr @ return & move spsr_svc into cpsr // 8 跳转到触发系统调用指令的下一条指令去执行,此外movs会将spsr_svc拷贝到cpsr进而让芯片切换回usr模式
1: bug "Returning to usermode but unexpected PSR bits set?", \@
#elif defined(CONFIG_CPU_V7M)...... // 省略v7m的代码
#else@ Thumb mode restore....... // 省略thumb的代码
#endif /* !CONFIG_THUMB2_KERNEL */.endm
简单来看,__ret_fast_syscall
做了如下三件事:
- 返回用户空间前处理调度和信号
- 恢复中断现场
- 将系统调用处理函数的返回值放到r0寄存器,返回用户空间
系统调用的过程差不多就是这样了,感谢阅读!