引言:分析Android源码的过程中,要想从上至下完全明白一行代码,往往涉及app、framework、native一直到kernel,可能迷失到代码世界,明白了系统调用原理,或许能帮你峰回路转,找到进入kernel函数的入口。本文主要讲解ARM架构相关源码:
1 2 3 4 5 6 7 8 9 10 |
/bionic/libc/kernel/uapi/asm-arm/asm/unistd.h /bionic/libc/arch-arm/syscalls/kill.S /kernel/arch/arm/kernel/calls.S /kernel/arch/arm/include/Uapi/asm/unistd.h /kernel/include/uapi/asm-generic/unistd.h /kernel/include/linux/syscalls.h /kernel/kernel/signal.c /kernel/arch/arm/kernel/entry-common.S /kernelarch/arm/kernel/entry-armv.S |
一、Syscall意义
内核提供用户空间程序与内核空间进行交互的一套标准接口,这些接口让用户态程序能受限访问硬件设备,比如申请系统资源,操作设备读写,创建新进程等。用户空间发生请求,内核空间负责执行,这些接口便是用户空间和内核空间共同识别的桥梁,这里提到两个字“受限”,是由于为了保证内核稳定性,而不能让用户空间程序随意更改系统,必须是内核对外开放的且满足权限的程序才能调用相应接口。
在用户空间和内核空间之间,有一个叫做Syscall(系统调用, system call)的中间层,是连接用户态和内核态的桥梁。这样即提高了内核的安全型,也便于移植,只需实现同一套接口即可。Linux系统,用户空间通过向内核空间发出Syscall,产生软中断,从而让程序陷入内核态,执行相应的操作。对于每个系统调用都会有一个对应的系统调用号,比很多操作系统要少很多。
安全性与稳定性:内核驻留在受保护的地址空间,用户空间程序无法直接执行内核代码,也无法访问内核数据,通过系统调用
性能:Linux上下文切换时间很短,以及系统调用处理过程非常精简,内核优化得好,所以性能上往往比很多其他操作系统执行要好。
二、Syscall查找方式
这里以文章理解杀进程的实现原理中的kill()方法为例子,来找一找kill()方法系统调用的过程。
Tips 1: 用户空间的方法xxx
,对应系统调用层方法则是sys_xxx
; Tips 2: unistd.h
文件记录着系统调用中断号的信息。
故用户空间kill
方法则对应系统调用层便是sys_kill
,这个方法去哪里找呢?从/kernel/include/uapi/asm-generic/unistd.h
等还有很多unistd.h
去慢慢查看,查看关键字sys_kill
,便能看到下面几行:
1 2 |
/* kernel/signal.c */ __SYSCALL(__NR_kill, sys_kill) |
根据这个能得到一丝线索,那就是kill对应的方法sys_kill位于/kernel/signal.c
文件。
Tips 3: 宏定义SYSCALL_DEFINEx(xxx,…),展开后对应的方法则是sys_xxx
; Tips 4: 方法参数的个数x,对应于SYSCALL_DEFINEx。
kill(int pid, int sig)
方法共两个参数,则对应方法于SYSCALL_DEFINE2(kill,...)
,进入signalc文件,再次搜索关键字,便能看到方法:
1 2 3 4 5 6 7 8 9 10 |
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig) { struct siginfo info; info.si_signo = sig; info.si_errno = 0; info.si_code = SI_USER; info.si_pid = task_tgid_vnr(current); info.si_uid = from_kuid_munged(current_user_ns(), current_uid()); return kill_something_info(sig, &info, pid); } |
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
基本等价于 asmlinkage long sys_kill(int pid, int sig)
,这里用的是基本等价,往下看会解释原因。
三、Syscall流程
Syscall是通过中断方式实现的,ARM平台上通过swi中断来实现系统调用,实现从用户态切换到内核态,发送软中断swi时,从中断向量表中查看跳转代码,其中异常向量表定义在文件/kernelarch/arm/kernel/entry-armv.S(汇编语言文件)。当执行系统调用时会根据系统调用号从系统调用表中来查看目标函数的入口地址,在calls.S文件中声明了入口地址信息。
总体流程:kill() -> kill.S -> swi陷入内核态 -> 从sys_call_table查看到sys_kill -> ret_fast_syscall -> 回到用户态执行kill()下一行代码。 下面介绍部分核心流程:
3.1: 用户程序通过软中断swi指令切入内核态,执行vector_swi处的指令。vector_swi
在文件/kenel/arch/arm/kernel/entry-common.S
中定义,此处省略。像每一个异常处理程序一样,要做的第一件事当然就是保护现场了。紧接着是获得系统调用的系统调用号
3.2: 仍以kill()函数为例,来详细说说Syscall调用流程,用户空间kill()定义位于文件kill.S
。
1 2 3 4 5 6 7 8 9 10 11 |
#include <private/bionic_asm.h> ENTRY(kill) mov ip, r7 ldr r7, =__NR_kill swi #0 mov r7, ip cmn r0, #(MAX_ERRNO + 1) bxls lr neg r0, r0 b __set_errno_internal END(kill) |
当调用kill时, 系统先保存r7内容, 然后将__NR_kill值放入r7, 再执行swi软中断指令切换进内核态。
3.3: Linux内核中,每个Syscall都有唯一的系统调用号对应,kill的系统调用号为__NR_kill,用户空间的系统调用号定义于/bionic/libc/kernel/uapi/asm-generic/unistd.h
,如下:
1 |
#define __NR_kill (__NR_SYSCALL_BASE + 37) |
其中__NR_SYSCALL_BASE=0,也就是__NR_kill系统调用号=37。
3.4: 在内核中有与系统调用号对应的系统调用表,定义在文件/kernel/arch/arm/kernel/calls.S
,如下:
1 2 3 4 5 |
/* 35 */ CALL(sys_ni_syscall) /* was sys_ftime */ CALL(sys_sync) CALL(sys_kill) //此处为37号 CALL(sys_rename) CALL(sys_mkdir) |