Linux系统调用执行流程
Linux 的提供的编程接口可以分成系统调用和应用编程接口(API),而 API 的大部分功能都是依赖于系统提供的标准系统调用。使用系统调用也有两种途径:通过 API 和使用软件中断汇编指令进入内核空间,而实质上前者也是通过后者实现功能的。所以 API 的执行流程可以分为:
- 应用调用 API;
- API 根据应用参数进入内核空间调用系统调用;
- 内核根据传入的系统调用号(即软件中断传入的立即数)在系统调用表中定位到具体的 syscall;
- 执行 syscall 或者处理未定义的系统调用;
- 返回用户空间, API 返回操作结果。
1. 系统调用的 C 库阶段
以 ARM 和 musl c 库为例,它的 socket API 定义如下:(其他系统调用同理)
int socket(int domain, int type, int protocol)
{
int s = socketcall(socket, domain, type, protocol, 0, 0, 0);
...
return s;
}
socketcall
是一个宏,展开后最终调用的是 __syscall
:
__syscall:
mov ip,sp
stmfd sp!,{r4,r5,r6,r7}
mov r7,r0
mov r0,r1
mov r1,r2
mov r2,r3
ldmfd ip,{r3,r4,r5,r6}
svc 0
ldmfd sp!,{r4,r5,r6,r7}
bx lr
其中最重要的命令就是 svc 0
,通过这条指令切换到 svc 模式(svc 替代了以前的 swi 指令,是 arm 提供的系统调用指令),进入到软件中断处理函数( SWI handler )。
2. 系统调用的 kernel 阶段
SWI handler 在 arch/arm/kernel/entry-common.S
定义:
/*=============================================================================
* SWI handler
*-----------------------------------------------------------------------------
*/
ENTRY(vector_swi)
...
/*
* Get the system call number.
*/
/*
- Pure EABI user space always put syscall number into scno (r7).
*/
adr tbl, sys_call_table @ load syscall table pointer
...
bic scno, scno, #0xff000000 @ mask off SWI op-code
eor scno, scno, #__NR_SYSCALL_BASE @ check OS number
...
cmp scno, #NR_syscalls @ check upper syscall limit
adr lr, BSYM(ret_fast_syscall) @ return address
ldrcc pc, [tbl, scno, lsl #2] @ call sys_* routine
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
...
ENDPROC(vector_swi)
主要步骤包括:
- 获取系统调用号(scno);
- 在系统调用表(sys_call_table) 中找到系统调用的入口,并调用系统调(
ldrcc pc, [tbl, scno, lsl #2] @ call sys_* routine
); - 使用预定义的函数处理不支持的系统调用(
bcs arm_syscall
和b sys_ni_syscall
)。
其中 NR_syscalls
为 arm linux 系统调用的总数, sys_call_table
是系统调用表的基地址,系统调用表定义在 calls.S
, 然后被包含在 entry-common.S
:
.type sys_call_table, #object
ENTRY(sys_call_table)
#include "calls.S"
#undef ABI
#undef OBSOLETE
3. ppc 和 x86
PPC 和 x86 的处理流程也一样,不同只是底层汇编不同: x86 使用 syscall
、 PPC 使用 sc
进入软件中断处理函数。
x86
__syscall:
movq %rdi,%rax
movq %rsi,%rdi
movq %rdx,%rsi
movq %rcx,%rdx
movq %r8,%r10
movq %r9,%r8
movq 8(%rsp),%r9
syscall
ret
ppc
__syscall:
mr 0, 3 # Save the system call number
mr 3, 4 # Shift the arguments: arg1
mr 4, 5 # arg2
mr 5, 6 # arg3
mr 6, 7 # arg4
mr 7, 8 # arg5
mr 8, 9 # arg6
sc
bnslr+ # return if not summary overflow
#else error:
# return negated value.
neg 3, 3
blr
进入内核后,ppc 的 swi handler 位于 arch/powerpc/kernel/entry_32.S
:
/*
* Handle a system call.
*/
.stabs "arch/powerpc/kernel/",N_SO,0,0,0f
.stabs "entry_32.S",N_SO,0,0,0f
0:
_GLOBAL(DoSyscall)
...
x86 的 swi handler 位于 arch/x86/kernel/entry_32.S
/*
* syscall stub including irq exit should be protected against kprobes
*/
.pushsection .kprobes.text, "ax"
# system call handler stub
ENTRY(system_call)
...