目录
环境
- linux 4.19
- arm64
一直都知道系统调用的原理,但是从来没有去看过linux中的源码,所以结合一个一直想了解的轮询函数,来分析一下用户态调用轮询函数实现的功能以及实现过程。
一、系统调用
用户态的函数最终是怎么调用到内核中的呢?老生常谈的那些都知道,用户态调用系统函数,引起异常trap到内核,保存用户态上下文,然后内核执行对应的系统调用函数,然后恢复上下文这一套,但是在linux中是如何实现的,以及特定架构(比如armv8)下的实现过程浑然不知,所以打算研究下系统调用的全过程。
所以在知道poll()的系统调用是哪个函数的情况下吗,打算逆向工程一波,看一下linux armv8架构下系统调用是如何进行的
1.1 编译工具链做的工作
代码用什么编译工具链编译的,就去对应的编译工具链找一下。以arm-linux的交叉编译工具链(aarch64的编译工具链没在虚拟机中,所以没去看,应该与32位arm是同理的)为例,全局搜索一下poll,看能不能找到些蛛丝马迹。在poll.h中找到了函数声明:
arm-linux-gnueabihf/libc/usr/include/sys/poll.h:
...
extern int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);
...
但是函数的定义没有找到,于是去全局搜索一下seletc和epoll,发现也是只有函数声明,找不到函数的定义。因为并不是很了解编译工具链做了哪些工作,所以猜测函数被封装到动态库或者静态库中了。在搜索select时发现有这么个信息:
gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/usr/share/info/libc.info-5:
-- Function: int select (int NFDS, fd_set *READ-FDS, fd_set *WRITE-FDS,
fd_set *EXCEPT-FDS, struct timeval *TIMEOUT)
Preliminary: | MT-Safe race:read-fds race:write-fds race:except-fds
| AS-Safe | AC-Safe | *Note POSIX Safety Concepts::.
The 'select' function blocks the calling process until there is
activity on any of the specified sets of file descriptors, or until
the timeout period has expired.
...
...
这是libc的信息,那么这些函数可能就封装在libc中,虽然知道libc是C库,但是还真没注意过他做了什么工作,于是去了解了一下libc里面具体做了什么工作,但是并没有找到很多资料,找到的有一个对于libc源码分析的博客贴在了下面:
该博客提到,libc的接口函数,最终也是通过内联汇编,调用了SVC指令,触发了软中断,引起了处理器模式从user->svc的切换,此时就会去中断向量表中找到对应中断的处理函数。中断处理函数在哪呢?先不着急找中断处理函数,因为我一开始也没找到在哪。咱们先来找一下系统调用表,一般这个表中维护着系统调用的最终处理函数。
1.2 系统调用表
之前看的书是《LINUX内核设计与实现》,这本书是以linux下x86架构的源码为例子进行的讲解,而且是比较老的Linux版本,其维护了一个用汇编实现的sys_call_table
。这与arm64还是有些区别的,在linux源码下有些不同,在arch/arm64下全局搜索,找到的sys_call_table是这样的:
arch/arm64/kernel/sys.c:
#undef __SYSCALL
#define __SYSCALL(nr, sym) asmlinkage long __arm64_##sym(const struct pt_regs *);
#include <asm/unistd.h>
#undef __SYSCALL
#define __SYSCALL(nr, sym) [nr] = __arm64_##sym,
const syscall_fn_t sys_call_table[__NR_syscalls] = {
[0 ... __NR_syscalls - 1] = __arm64_sys_ni_syscall,
#include <asm/unistd.h>
};
可以说是跟《LINUX内核设计与实现》中的x86的系统调用表完全不一样,书中描述的系统调用表是以汇编的形式实现的。现在我们来分析一下这段代码。很明显,花括号的代码是初始化sys_call_table
这个数组,那想必这里就是系统调用表注册的地方了。更诡异的是,这个系统调用表注册的花括号里面,还引用了一个头文件<asm/unistd.h>,并且注意看,在这之前还引用了一次该头文件,所以这个头文件,应该是有点说法的。
我们来看一下这个头文件,直接看到他最终的引用:
arch/arm64/include/asm/unistd.h:
/*
* Copyright (C) 2012 ARM Ltd.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifdef CONFIG_COMPAT
#define __ARCH_WANT_COMPAT_STAT64
#define __ARCH_WANT_SYS_GETHOSTNAME
#define __ARCH_WANT_SYS_PAUSE
#define __ARCH_WANT_SYS_GETPGRP
#define __ARCH_WANT_SYS_LLSEEK
#define __ARCH_WANT_SYS_NICE
#define __ARCH_WANT_SYS_SIGPENDING
#define __ARCH_WANT_SYS_SIGPROCMASK
#define __ARCH_WANT_COMPAT_SYS_SENDFILE
#define __ARCH_WANT_SYS_FORK
#define __ARCH_WANT_SYS_VFORK
/*
* Compat syscall numbers used by the AArch64 kernel.
*/
#define __NR_compat_restart_syscall 0
#define __NR_compat_exit 1
#define __NR_compat_read 3
#define __NR_compat_write 4
#define __NR_compat_sigreturn 119
#define __NR_compat_rt_sigreturn 173
/*
* The following SVCs are ARM private.
*/
#define __ARM_NR_COMPAT_BASE 0x0f0000
#define __ARM_NR_compat_cacheflush (__ARM_NR_COMPAT_BASE + 2)
#define __ARM_NR_compat_set_tls (__ARM_NR_COMPAT_BASE + 5)
#define __ARM_NR_COMPAT_END (__ARM_NR_COMPAT_BASE + 0x800)
#define __NR_compat_syscalls 399
#endif
#define __ARCH_WANT_SYS_CLONE
#ifndef __COMPAT_SYSCALL_NR
#include <uapi/asm/unistd.h>
#endif
#define NR_syscalls (__NR_syscalls)
做了一些#define
的工作,并且引用了<uapi/asm/unistd.h>这个文件:
arch/arm64/include/uapi/asm/unistd.h:
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
/*
* Copyright (C) 2012 ARM Ltd.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#define __ARCH_WANT_RENAMEAT
#include <asm-generic/unistd.h>
又引用了<asm-generic/unistd.h>这个文件,而这个文件里面就包含了真正的系统调用函数:
include/uapi/asm-generic/unistd.h:
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#include <asm/bitsperlong.h>
/*
* This file contains the system call numbers, based on the
* layout of the x86-64 architecture, which embeds the
* pointer to the syscall in the table.
*
* As a basic principle, no duplication of functionality
* should be added, e.g. we don't use lseek when llseek
* is present. New architectures should use this file
* and implement the less feature-full calls in user space.
*/
#ifndef __SYSCALL
#define __SYSCALL(x, y)
#endif
#if __BITS_PER_LONG == 32 || defined(__SYSCALL_COMPAT)
#define __SC_3264(_nr, _32, _64) __SYSCALL(_nr, _32)
#else
#define __SC_3264(_nr, _32, _64) __SYSCALL(_nr, _64)
#endif
#ifdef __SYSCALL_COMPAT
#define __SC_COMP(_nr, _sys, _comp) __SYSCALL(_nr, _comp)
#define __SC_COMP_3264(_nr, _32, _64, _comp) __SYSCALL(_nr, _comp)
#else
#define __SC_COMP(_nr, _sys, _comp) __SYSCALL(_nr, _sys)
#define __SC_COMP_3264(_nr, _32, _64, _comp) __SC_3264(_nr, _32, _64)
#endif
#define __NR_io_setup 0
__SC_COMP(__NR_io_setup, sys_io_setup, compat_sys_io_setup)
#define __NR_io_destroy 1
__SYSCALL(__NR_io_destroy, sys_io_destroy)
#define __NR_io_submit 2
__SC_COMP(__NR_io_submit, sys_io_submit, compat_sys_io_submit)
#define __NR_io_cancel 3
__SYSCALL(__NR_io_cancel, sys_io_cancel)
#define __NR_io_getevents 4
__SC_COMP(__NR_io_getevents, sys_io_getevents, compat_sys_io_getevents)
/* fs/xattr.c */
#define __NR_setxattr 5
__SYSCALL(__NR_setxattr, sys_setxattr)
#define __NR_lsetxattr 6
__SYSCALL(__NR_lsetxattr, sys_lsetxattr)
#define __NR_fsetxattr 7
__SYSCALL(__NR_fsetxattr, sys_fsetxattr)
#define __NR_getxattr 8
__SYSCALL(__NR_getxattr, sys_getxattr)
#define __NR_lgetxattr 9
__SYSCALL(__NR_lgetxattr, sys_lgetxattr)
#define __NR_fgetxattr 10
__SYSCALL(__NR_fgetxattr, sys_fgetxattr)
#define __NR_listxattr 11
__SYSCALL(__NR_listxattr, sys_listxattr)
#define __NR_llistxattr 12
__SYSCALL(__NR_llistxattr, sys_llistxattr)
#define __NR_flistxattr 13
__SYSCALL(__NR_flistxattr, sys_flistxattr)
#define __NR_removexattr 14
__SYSCALL(__NR_removexattr, sys_removexattr)
#define __NR_lremovexattr 15
__SYSCALL(__NR_lremovexattr, sys_lremovexattr)
#define __NR_fremovexattr 16
__SYSCALL(__NR_fremovexattr, sys_fremovexattr)
/* fs/dcache.c */
#define __NR_getcwd 17
__SYSCALL(__NR_getcwd, sys_getcwd)
/* fs/cookies.c */
#define __NR_lookup_dcookie 18
__SC_COMP(__NR_lookup_dcookie, sys_lookup_dcookie, compat_sys_lookup_dcookie)
/* fs/eventfd.c */
#define __NR_eventfd2 19
__SYSCALL(__NR_eventfd2, sys_eventfd2)
/* fs/eventpoll.c */
#define __NR_epoll_create1 20
__SYSCALL(__NR_epoll_create1, sys_epoll_create1)
#define __NR_epoll_ctl 21
__SYSCALL(__NR_epoll_ctl, sys_epoll_ctl)
#define __NR_epoll_pwait 22
__SC_COMP(__NR_epoll_pwait, sys_epoll_pwait, compat_sys_epoll_pwait)
...
...
...
看到这里还是有点懵的,东西比较多。现在先不管其他的#define
,我们只把一条摘出来:
include/uapi/asm-generic/unistd.h:
/* fs/eventpoll.c */
#define __NR_epoll_create1 20
__SYSCALL(__NR_epoll_create1, sys_epoll_create1)
然后回到最开始的sys.c下,那么代码就变成了:
arch/arm64/kernel/sys.c:
#undef __SYSCALL
#define __SYSCALL(nr, sym) asmlinkage long __arm64_##sym(const struct pt_regs *);
/* #include <asm/unistd.h> */
/* -------------替换include -------------- */
#define __NR_epoll_create1 20
__SYSCALL(__NR_epoll_create1, sys_epoll_create1)
/* ----------------------------------------- */
#undef __SYSCALL
#define __SYSCALL(nr, sym) [nr] = __arm64_##sym,
const syscall_fn_t sys_call_table[__NR_syscalls] = {
[0 ... __NR_syscalls - 1] = __arm64_sys_ni_syscall,
/* #include <asm/unistd.h> */
/* -------------替换include -------------- */
#define __NR_epoll_create1 20
__SYSCALL(__NR_epoll_create1, sys_epoll_create1)
/* ----------------------------------------- */
};
就很容易看出是做了什么工作:首先第一个#include <asm/unistd.h>
之前,__SYSCALL
被定义为:
asmlinkage long __arm64_##sym(const struct pt_regs *);
那第一个include做的工作其实就是在声明函数,声明的函数名:
asmlinkage long __arm64_sys_epoll_create1(const struct pt_regs *);
紧接着就重新#define __SYSCALL
,则花括号内的#include
就被替换成了:
#undef __SYSCALL
#define __SYSCALL(nr, sym) [nr] = __arm64_##sym,
...
const syscall_fn_t sys_call_table[__NR_syscalls] = {
[0 ... __NR_syscalls - 1] = __arm64_sys_ni_syscall,
[20] = __arm64_sys_epoll_create1, // 被替换为了这个
};
所以,这里所做的工作就是完成了系统调用表的注册。并且我们可以知道他给系统调用号20注册的系统调用函数为__arm64_sys_epoll_create1
。那我们就全局搜索一下这个函数,看一下他定义在哪个位置,结果就是:搜不到。那么肯定有其他的#define
进行了一些操作。
1.3 SYSCALL_DEFINE宏定义
那我们直接全局搜索__arm64_:
sys.c这个文件就是刚才咱们看的代码所在的文件,没啥东西。sys32.c这个文件是一些给32位用户提供的系统调用,可以认为是arm32的系统调用,其中也维护这一个系统调用表,后面看poll()系统调用的时候会用到。那就要去syscall_wrapper.h里面看一下怎么个事儿了。毕竟这名字很直白,都叫wrapper了。我就直接贴出来找到的代码:
arch/arm64/include/asm/syscall_wrapper.h:
#define __SYSCALL_DEFINEx(x, name, ...) \
asmlinkage long __arm64_sys##name(const struct pt_regs *regs); \
ALLOW_ERROR_INJECTION(__arm64_sys##name, ERRNO); \
static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)); \
asmlinkage long __arm64_sys##name(const struct pt_regs *regs) \
{ \
return __se_sys##name(SC_ARM64_REGS_TO_ARGS(x,__VA_ARGS__)); \
} \
static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \
{ \
long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__)); \
__MAP(x,__SC_TEST,__VA_ARGS__); \
__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__)); \
return ret; \
} \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
这倒是跟x86对上了,__SYSCALL_DEFINE
是用来定义函数的,先不急着去看__SYSCALL_DEFINE
是什么东西,我们先来看一下这一坨代码做了什么工作:
- 第1行:asmlinkage long __arm64_sys##name(const struct pt_regs *regs);:声明了__arm64_sys##name这个函数
- 第2行:不管,我也没去看
- 第3行:声明__se_sys##name这个函数
- 第4行:声明__do_sys##name这个函数
- 第5行:定义__arm64_sys##name这个函数,函数里面唯一做的工作就是调用__se_sys##name这个函数(在第3行声明,第9行定义)
- 第9行:定义__se_sys##name这个函数里面做的最主要的工作,就是调用__do_sys##name这个函数。
- 第16行:写了一行代码叫static inline long __do_sys##name(__MAP(x,__SC_DECL,VA_ARGS))
所以,当使用这个__SYSCALL_DEFINEx
宏的时候,你做的最重要的工作就是:
- 定义了
__arm64_sys##name
函数,他最终的最终会调用__do_sys##name()
函数,并返回返回值 - 写了一行莫名其妙的代码
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
,像是在声明或者定义函数,而且正是在第9行的__se_sys##name
中,调用了__do_sys##name
函数
先不管最后一行代码,我们先看一下哪里用到了__SYSCALL_DEFINE
:
include/linux/syscalls.h:
#ifndef SYSCALL_DEFINE0
#define SYSCALL_DEFINE0(sname) \
SYSCALL_METADATA(_##sname, 0); \
asmlinkage long sys_##sname(void); \
ALLOW_ERROR_INJECTION(sys_##sname, ERRNO); \
asmlinkage long sys_##sname(void)
#endif /* SYSCALL_DEFINE0 */
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE_MAXARGS 6
#define SYSCALL_DEFINEx(x, sname, ...) \
SYSCALL_METADATA(sname, x, __VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
很明显可以看到,当我们使用宏SYSCALL_DEFINE1
的时候,最终会使用宏__SYSCALL_DEFINEx
,那我们全局搜索哪里使用了SYSCALL_DEFINE1
:
很明显,括号后面长得都很像是一些系统调用啊,那我们直接去看一下epoll():
fs/eventpoll.c
SYSCALL_DEFINE1(epoll_create1, int, flags)
{
return do_epoll_create(flags);
}
再结合刚才分析的代码,把SYSCALL_DEFINE1(epoll_create1, int, flags)
拆开:
/* SYSCALL_DEFINE1(epoll_create1, int, flags) */
asmlinkage long __arm64_sys_epoll_create1(const struct pt_regs *regs); \
ALLOW_ERROR_INJECTION(__arm64_sys##name, ERRNO); \
static long __se_sys_epoll_create1(__MAP(x,__SC_LONG,__VA_ARGS__)); \
static inline long __do_sys_epoll_create1(__MAP(x,__SC_DECL,__VA_ARGS__)); \
// 系统调用向量表[20] 对应的就是下面这个__arm64_sys_epoll_create1(),调用step1
asmlinkage long __arm64_sys_epoll_create1(const struct pt_regs *regs) \
{ \
// 该函数返回__se_sys_epoll_create1(),调用step2
return __se_sys_epoll_create1(SC_ARM64_REGS_TO_ARGS(x,__VA_ARGS__)); \
} \
static long __se_sys_epoll_create1(__MAP(x,__SC_LONG,__VA_ARGS__)) \
{ \
// 接下来会调用__do_sys_epoll_create1(),调用step 3
long ret = __do_sys_epoll_create1(__MAP(x,__SC_CAST,__VA_ARGS__)); \
__MAP(x,__SC_TEST,__VA_ARGS__); \
__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__)); \
return ret; \
} \
static inline long __do_sys_epoll_create1(__MAP(x,__SC_DECL,__VA_ARGS__)) // 宏定义的最后一句就是在定义函数__do_sys_epoll_create1()
{
return do_epoll_create(flags); // 该系统调用最终定义的函数,是在这里,调用step final
}
这不就是定义了一个函数吗?所以我们我们系统表注册的函数,最终的最终的主要功能实现就是在SYSCALL_DEFINEx
下(省略中间做的一些其他工作,虽然我也没去看那些injection是做什么工作的)。所以如果我们要是调用系统调用表的第20(index为19,对应epoll)个位置的函数指针,就会调用到SYSCALL_DEFINE1(epoll_create1, int, flags)下面的代码中去。
1.4 如何调用到系统调用表?
我们继续搜索一下,哪里用了系统调用表?
我也不再叙述找的过程了,直接找到位置:
arch/arm64/kernel/syscall.c:
asmlinkage void el0_svc_handler(struct pt_regs *regs)
{
sve_user_discard();
el0_svc_common(regs, regs->regs[8], __NR_syscalls, sys_call_table); // 用到了sys_call_table系统调用表
}
...
...
arch/arm64/kernel/syscall.c:
static void el0_svc_common(struct pt_regs *regs, int scno, int sc_nr,
const syscall_fn_t syscall_table[])
{
unsigned long flags = current_thread_info()->flags;
regs->orig_x0 = regs->regs[0];
regs->syscallno = scno;
cortex_a76_erratum_1463225_svc_handler();
local_daif_restore(DAIF_PROCCTX);
user_exit();
if (has_syscall_work(flags)) {
/* set default errno for user-issued syscall(-1) */
if (scno == NO_SYSCALL)
regs->regs[0] = -ENOSYS;
scno = syscall_trace_enter(regs);
if (scno == NO_SYSCALL)
goto trace_exit;
}
invoke_syscall(regs, scno, sc_nr, syscall_table); // 用到了系统调用表
/*
* The tracing status may have changed under our feet, so we have to
* check again. However, if we were tracing entry, then we always trace
* exit regardless, as the old entry assembly did.
*/
if (!has_syscall_work(flags) && !IS_ENABLED(CONFIG_DEBUG_RSEQ)) {
local_daif_mask();
flags = current_thread_info()->flags;
if (!has_syscall_work(flags)) {
/*
* We're off to userspace, where interrupts are
* always enabled after we restore the flags from
* the SPSR.
*/
trace_hardirqs_on();
return;
}
local_daif_restore(DAIF_PROCCTX);
}
trace_exit:
syscall_trace_exit(regs);
}
...
...
arch/arm64/kernel/syscall.c:
static void invoke_syscall(struct pt_regs *regs, unsigned int scno,
unsigned int sc_nr,
const syscall_fn_t syscall_table[])
{
long ret;
if (scno < sc_nr) {
syscall_fn_t syscall_fn;
syscall_fn = syscall_table[array_index_nospec(scno, sc_nr)]; // 从表中取出了某个函数,实际上取出的是系统调用号对应的函数
ret = __invoke_syscall(regs, syscall_fn);
} else {
ret = do_ni_syscall(regs, scno);
}
regs->regs[0] = ret;
}
...
...
arch/arm64/kernel/syscall.c:
static long __invoke_syscall(struct pt_regs *regs, syscall_fn_t syscall_fn)
{
return syscall_fn(regs);
}
上面这一套流程走下来,最终是调用到了我们的系统调用表中的某个函数__arm64_sys##name(const struct pt_regs *regs)
,然后最终又调用到了SYSCALL_DEFINE1
下的代码。那么是谁调用了el0_svc_handler()
这个函数呢?同样全局搜索,最终我们在entry.S
文件下找到了对el0_svc_handler()
的调用:
arch/arm64/kernel/entry.S:
...
el0_sync:
kernel_entry 0
mrs x25, esr_el1 // read the syndrome register
lsr x24, x25, #ESR_ELx_EC_SHIFT // exception class
cmp x24, #ESR_ELx_EC_SVC64 // SVC in 64-bit state
b.eq el0_svc
cmp x24, #ESR_ELx_EC_DABT_LOW // data abort in EL0
b.eq el0_da
cmp x24, #ESR_ELx_EC_IABT_LOW // instruction abort in EL0
b.eq el0_ia
cmp x24, #ESR_ELx_EC_FP_ASIMD // FP/ASIMD access
b.eq el0_fpsimd_acc
cmp x24, #ESR_ELx_EC_SVE // SVE access
b.eq el0_sve_acc
cmp x24, #ESR_ELx_EC_FP_EXC64 // FP/ASIMD exception
b.eq el0_fpsimd_exc
cmp x24, #ESR_ELx_EC_SYS64 // configurable trap
b.eq el0_sys
cmp x24, #ESR_ELx_EC_SP_ALIGN // stack alignment exception
b.eq el0_sp_pc
cmp x24, #ESR_ELx_EC_PC_ALIGN // pc alignment exception
b.eq el0_sp_pc
cmp x24, #ESR_ELx_EC_UNKNOWN // unknown exception in EL0
b.eq el0_undef
cmp x24, #ESR_ELx_EC_BREAKPT_LOW // debug exception in EL0
b.ge el0_dbg
b el0_inv
...
...
/*
* SVC handler.
*/
.align 6
el0_svc:
mov x0, sp
bl el0_svc_handler
b ret_to_user
ENDPROC(el0_svc)
这时候的代码已经是汇编了,其中的b el0_svc_handler()
就起到了跳转到el0_svc_handler()
的作用。先不去分析这一段汇编做了什么工作,这些后面在分析,现在的首要目的是找到系统调用的源头。而跳转到el0_svc
是el0_sync
做的工作。但是在哪里跳转到el0_sync
我却没有找到。当时在这里想了很久找了很久,把entry.S这个文件看了一遍,最终发现有这么个东西:
/*
* Exception vectors.
*/
.pushsection ".entry.text", "ax"
.align 11
ENTRY(vectors)
kernel_ventry 1, sync_invalid // Synchronous EL1t
kernel_ventry 1, irq_invalid // IRQ EL1t
kernel_ventry 1, fiq_invalid // FIQ EL1t
kernel_ventry 1, error_invalid // Error EL1t
kernel_ventry 1, sync // Synchronous EL1h
kernel_ventry 1, irq // IRQ EL1h
kernel_ventry 1, fiq_invalid // FIQ EL1h
kernel_ventry 1, error // Error EL1h
kernel_ventry 0, sync // Synchronous 64-bit EL0
kernel_ventry 0, irq // IRQ 64-bit EL0
kernel_ventry 0, fiq_invalid // FIQ 64-bit EL0
kernel_ventry 0, error // Error 64-bit EL0
#ifdef CONFIG_COMPAT
kernel_ventry 0, sync_compat, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_compat, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid_compat, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_compat, 32 // Error 32-bit EL0
#else
kernel_ventry 0, sync_invalid, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_invalid, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_invalid, 32 // Error 32-bit EL0
#endif
END(vectors)
这有个关键字vectors
,可以说是非常像中断向量表了,而且里面又出现了sync
,在研究了一下这一段汇编之后,发现这里确实可以跳转到el0_sync
,具体是怎么做到的呢,其中有个关键字kernel_ventry
很重要,其实这是该文件下的一个宏定义,其源码如下:
.macro kernel_ventry, el, label, regsize = 64
.align 7
#ifdef CONFIG_UNMAP_KERNEL_AT_EL0
alternative_if ARM64_UNMAP_KERNEL_AT_EL0
.if \el == 0
.if \regsize == 64
mrs x30, tpidrro_el0
msr tpidrro_el0, xzr
.else
mov x30, xzr
.endif
.endif
alternative_else_nop_endif
#endif
sub sp, sp, #S_FRAME_SIZE
#ifdef CONFIG_VMAP_STACK
/*
* Test whether the SP has overflowed, without corrupting a GPR.
* Task and IRQ stacks are aligned to (1 << THREAD_SHIFT).
*/
add sp, sp, x0 // sp' = sp + x0
sub x0, sp, x0 // x0' = sp' - x0 = (sp + x0) - x0 = sp
tbnz x0, #THREAD_SHIFT, 0f
sub x0, sp, x0 // x0'' = sp' - x0' = (sp + x0) - sp = x0
sub sp, sp, x0 // sp'' = sp' - x0 = (sp + x0) - x0 = sp
b el\()\el\()_\label
0:
/*
* Either we've just detected an overflow, or we've taken an exception
* while on the overflow stack. Either way, we won't return to
* userspace, and can clobber EL0 registers to free up GPRs.
*/
/* Stash the original SP (minus S_FRAME_SIZE) in tpidr_el0. */
msr tpidr_el0, x0
/* Recover the original x0 value and stash it in tpidrro_el0 */
sub x0, sp, x0
msr tpidrro_el0, x0
/* Switch to the overflow stack */
adr_this_cpu sp, overflow_stack + OVERFLOW_STACK_SIZE, x0
/*
* Check whether we were already on the overflow stack. This may happen
* after panic() re-enables interrupts.
*/
mrs x0, tpidr_el0 // sp of interrupted context
sub x0, sp, x0 // delta with top of overflow stack
tst x0, #~(OVERFLOW_STACK_SIZE - 1) // within range?
b.ne __bad_stack // no? -> bad stack pointer
/* We were already on the overflow stack. Restore sp/x0 and carry on. */
sub sp, sp, x0
mrs x0, tpidrro_el0
#endif
b el\()\el\()_\label
.endm
在 ARM 汇编中,“.macro” 是用来定义一个宏的指令。宏可以看作是一个可以在汇编程序中重用的代码段。当调用宏时,汇编器会将宏的内容插入到调用位置。可以认为是C语言中的#define,并且跟#define一样也能起到传递参数的效果。在这一段代码中,el, label, regsize = 64
都可以算是它的参数。而.endm则标志着这一段宏定义的结束。
先不考虑其中的其余汇编代码,我们直接看最后一行:
b el\()\el\()_\label
在.macro定义的宏中,如果带参数,则可以是用"\"
来转义参数,而"\()"
符号我也没找到具体什么意思,唯一找到的一个解释就是这标志着字符串的结束,那么就表示"()"之间的数字表示的是字符串/字符,而不是整型。然后我们结合vectors
中使用的kernel_ventry
来看一下:
ENTRY(vectors)
...
...
// kernel_ventry 0, sync // Synchronous 64-bit EL0
// 将其展开,就是
.macro kernel_ventry, 0, sync, regsize = 64
...
...
// b el\()\el\()_\label
b el0_sync
.endm
...
...
#endif
END(vectors)
"\()"
的作用就是把一个整数类型0转换成了字符串。可以看到,最终通过vectors
,跳转到了el0_sync
。
1.5 Armv8的中断机制与异常等级
说实话,找到了中断向量表vectors
之后就有点无能为力了,再通过搜索关键字的方法已经找不到是如何执行到vectors的了,因为实在是太多了。通过搜索其他资料,学习到了linux下armv8架构调用中断向量表的过程,写的比较好的博客贴到下面:
不得不说,《LINUX内核设计与实现》这本书貌似提到过arm架构比x86架构的系统调用、进程调度等等都更快,就是因为arm架构有更多的寄存器。在armv7上还没太看出来,现在了解了下armv8的异常模型以及armv8的寄存器之后,发现还真是没错,太多了。。。。。。
接下来简单梳理一下上述博客中学习到的相关的内容,作为前提知识:
1.5.1 Armv8异常等级
在Armv8中,有四个异常等级如下图所示
在 ARMv8 中,程序执行发生在四个异常级别中的一个。在AArch64中,异常级别决定了权限级别,其方式与ARMv7中定义的权限级别类似。因此在ELn执行对应于权限PLn。同样,数字越小,异常越低,权限越低。异常级别提供了软件执行权限的逻辑分离,适用于ARMv8架构的所有操作状态。它类似于并支持计算机科学中常见的分层保护域的概念。
以下是在每个异常级别上运行的软件的典型例子:
- EL0 普通用户应用程序。
- EL1 操作系统内核通常被描述为有特权的。
- EL2 虚拟化管理程序。
- EL3 低级别的固件,包括Secure Monitor。
对于EL0来讲,就是我们用户态的程序,对于EL1来讲,就是我们的操作系统(如Linux内核),对于EL2来讲,可以认为是虚拟机,如果我们要切换在EL1运行的操作系统(比如从Linux切换到其他操作系统),我们就要提升异常等级到EL2进行切换。
而对于EL3,我也不知道EL3是干嘛用的,完全没有理解。贴上一段GPT的回答,自行体会:
因为EL3与我们的主题没那么相关,所以先不去深入研究了,有空再看。对于异常等级,还需要知道的几点是:
- 异常等级为EL3时,处理器可以访问任何资源(包括安全状态和非安全状态下)。处理器上电之后运行在最高的异常等级EL3(所以Secure Boot需要在EL3中实现?)
- 异常级别切换只能通过软件触发同步异常,或者同步异常处理返回后,不是通过中断实现的。
- Armv8的异常切换指令有三个:SVC(Supervisor Call),允许用户模式(EL0)下的程序请求os(EL1)服务;HVC(Hypervisor Call),允许操作系统(EL1)请求虚拟化(EL2)服务;SMC(Secure Moniter Call),允许EL1/EL2程序请求安全(EL3)服务;处于高异常等级的软件处理完低异常等级软件的请求后,可通过ERET指令切换到低异常等级。
通过这些,那我们应该也能知道了,系统调用一定要通过SVC指令,切换到内核态,也就是EL1异常等级。
1.5.2 Armv8中断向量表寄存器
Armv8运行环境提供了31个64bit的通用寄存器:X0-X31,同时他们也都有32bit的形式:W0-W31,他们对应映射到64bit寄存器的低32位。读取W寄存器,将会只读X的低32位。写W寄存器,将会将X的高32位写为0。
除此之外,Armv8还有很多系统寄存器。可以使用MSR和MRS指令,通过系统寄存器来进行系统的配置。在Armv7中,是使用协处理器CP15进行系统配置的(可以认为是一组寄存器组,通过MSR和MRS指令的排列组合获取CP15不同功能的寄存器),c1寄存器可以设置中断向量表的基地址,c12寄存器可以设置中断向量表偏移。贴一张正点的教程:
而Armv8的系统寄存器,其实就是把CP15协处理器中的寄存器拆出来,每一个都有自己的名字,可以通过下述汇编代码来直接操作系统寄存器,而不用再像Armv7那样排列组合了:
MRS x0, 某个系统寄存器名// 将某个系统寄存器内容写进x0
MSR 某个系统寄存器名, x0 // 将x0内容写进某个系统寄存器
Armv8有很多系统寄存器,跟我们主题相关的是VBAR_ELn
寄存器,它的描述是这样的:
Holds the vector base address for any exception that is taken to ELn.
也就是说,这个寄存器中存放着异常向量表(中断向量表)的基地址,而且存放的是虚拟地址。这样的寄存器一共有三个,分别是VBAR_EL1,VBAR_EL2,VBAR_EL3
,分别对应Armv8的三种异常等级EL1,EL2,EL3
。当系统调用触发软中断陷入EL1
时,就要去VBAR_EL1
中找到中断向量表的基地址,然后调用对应的中断处理函数。
至于在哪里把中断向量表加载进该寄存器的,可以看下面贴出来的博客:
1.5.3 Armv8中断向量表
刚才已经说到,Armv8的异常向量表的起始虚拟地址存放在寄存器 VBAR_ELn中。每个异常向量表有 16 项,分为 4 组,每组 4项,每项的长度是 128 字节(可以存放32 条指令)中,再重新看一下汇编部分的中断向量表:
/*
* Exception vectors.
*/
.pushsection ".entry.text", "ax"
.align 11
ENTRY(vectors)
kernel_ventry 1, sync_invalid // Synchronous EL1t
kernel_ventry 1, irq_invalid // IRQ EL1t
kernel_ventry 1, fiq_invalid // FIQ EL1t
kernel_ventry 1, error_invalid // Error EL1t
kernel_ventry 1, sync // Synchronous EL1h
kernel_ventry 1, irq // IRQ EL1h
kernel_ventry 1, fiq_invalid // FIQ EL1h
kernel_ventry 1, error // Error EL1h
kernel_ventry 0, sync // Synchronous 64-bit EL0
kernel_ventry 0, irq // IRQ 64-bit EL0
kernel_ventry 0, fiq_invalid // FIQ 64-bit EL0
kernel_ventry 0, error // Error 64-bit EL0
#ifdef CONFIG_COMPAT
kernel_ventry 0, sync_compat, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_compat, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid_compat, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_compat, 32 // Error 32-bit EL0
#else
kernel_ventry 0, sync_invalid, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_invalid, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_invalid, 32 // Error 32-bit EL0
#endif
END(vectors)
可以看到,每一个ELn等级的中断向量表中包含4张向量表,每张向量表有4种异常:同步异常、irq异常、fiq异常、系统错误异常,而4张表分别对应:
- 发生中断时,异常等级不发生变化,并且不管怎么异常模式,sp只用SP_EL0
- 发生中断时,异常等级不发生变化,并且sp用对应异常私有的SP_ELx
- 发生中断时,异常等级发生变化,这种情况一般是用户态向内核态发生迁移,当前表示64位用户态向64位内核态发生迁移
- 发生中断时,异常等级发生变化,这种情况一般是用户态向内核态发生迁移,当前表示32位用户态向64位/32位内核发生迁移
每个异常的向量地址不再是4字节(Armv7的中断向量表),而是0x80字节,也就是kernel_ventry这个宏展开后的指令大小,所以在终端向量表之前要使用.align 11
,表示按照2的11次方,也就是0x80字节对齐,可以存放32条指令。第一张表表示异常发生在EL0,处理异常的特权等级也是EL0,因为使用的SP寄存器为SP_EL0。并且因为EL0(用户态)不会处理异常,所以后面的处理函数跟的名字是xxx_invalid
。可以看一下el0_sync_invalid
这个函数:
/*
* Invalid mode handlers
*/
.macro inv_entry, el, reason, regsize = 64
kernel_entry \el, \regsize
mov x0, sp
mov x1, #\reason
mrs x2, esr_el1
bl bad_mode
ASM_BUG()
.endm
el0_sync_invalid:
inv_entry 0, BAD_SYNC
ENDPROC(el0_sync_invalid)
bad_mode()
这个函数在arch/arm64/kernel/traps.c下:
arch/arm64/kernel/traps.c:
/*
* bad_mode handles the impossible case in the exception vector. This is always
* fatal.
*/
asmlinkage void bad_mode(struct pt_regs *regs, int reason, unsigned int esr)
{
console_verbose();
pr_crit("Bad mode in %s handler detected on CPU%d, code 0x%08x -- %s\n",
handler[reason], smp_processor_id(), esr,
esr_get_class_string(esr));
local_daif_mask();
panic("bad mode");
}
看linux的注释,意思就是这个函数用来处理不可能的异常模式。
第二张表表示异常发生在ELn,处理异常的特权等级也是ELn,此时使用的SP寄存器为SP_ELn。linux内核运行在EL1,使用的向量表也是存放在VBAR_EL1中的向量表,所以这一组在Linux内核中就表示异常发生在EL1,也就是内核,异常处理也要在内核中处理。比如当内核发生缺页异常时,就会去调用el1_sync()
而后面两张表则是异常的发生和处理不在同一个特权级别,需要进行特区那级别的切换。而他们的区别就是,第三组表示异常发生在64位环境下,第四组表示异常发生在32位环境下。不管是哪一种,对于linux内核来讲,都是从EL0切到EL1。所以系统调用,就发生在后两张表中。而每一张表又有四个异常向量,它们对应着:
- 同步异常,包括未定义异常、非法执行状态异常、未对齐异常、系统调用异常、陷阱执行异常、指令数据终止异常、debug异常
- 中断(normal priority interrupt,IRQ),即普通优先级的中断。
- 快速中断(fast interrupt,FIQ),即高优先级的中断。
- 系统错误(System Error,SError),是由硬件错误触发的异常,例如最常见的是把脏数据从缓存行写回内存时触发异步的数据中止异常。
而对于系统调用来讲,最终会执行异常向量表中的第九个向量(其实有很多条指令),并执行el0_sync()
函数:
kernel_ventry 0, sync // Synchronous 64-bit EL0
1.6 其余问题记录
1.6.1 如何判断当前产生异常的原因是系统调用
是通过读取esr_el1
寄存器来判断的,该寄存器保存着异常发生的原因。判断得到发生的同步异常为系统调用,进而执行到el0_svc(
)函数
/*
* EL0 mode handlers.
*/
.align 6
el0_sync:
kernel_entry 0
mrs x25, esr_el1 // read the syndrome register, 寄存器esr_el1是在权限级EL1下可以访问的系统寄存器,该寄存器的相关状态就表明了异常发生的具体原因。
lsr x24, x25, #ESR_ELx_EC_SHIFT // exception class, lsr: 逻辑右移指令,实现将寄存器进行右移操作, 將x25寄存器的值右移ESR_ELx_EC_SHIFT位后赋值给x24寄存器
cmp x24, #ESR_ELx_EC_SVC64 // SVC in 64-bit state
b.eq el0_svc // b.eq:表示条件分支指令,当某个条件满足时,跳转到某个地址
...
...
1.6.2 系统调用的参数传递和返回值
ARMV8的系统调用通过寄存器x0-x5传递函数参数,之后x0存放函数返回值,x8存放系统调用号。ARMv8函数传参可以用函数调前8个通用寄存器 X0~X7(多余的用栈传参),但是SYSCALL_DEFINE最多到SYSCALL_DEFINE6,最多的系统调用也只有6个参数
现在顺序理一遍系统调用的过程:
根据中断的类型,cpu执行到对应的中断向量表中的中断函数。
这一步由硬件完成,但是向量表的中断函数由软件定义。
-->el0_sync()
-->kernel_entry()
-->el0_svc()
-->el0_svc_handler()
-->el0_svc_common(regs, regs->regs[8], __NR_syscalls, sys_call_table) // 通过regs[8],也就是x8传系统调用号
-->invoke_syscall()
-->syscall_fn = syscall_table[array_index_nospec(scno, sc_nr)]; // 找到系统调用号对应的系统调用函数
-->ret = __invoke_syscall()
-->syscall_fn(regs) // __invoke_syscall()执行找到的系统调用函数,参数为regs
-->regs->regs[0] = ret; // 通过x0获取系统调用返回值
-->ret_to_user()
其中kernel_entry()执行中断现场的保存工作,有点复杂,没完全看明白,后续再研究下
1.6.2
在5.15内核中中断向量表变了,不叫el0_sync
了,现在叫el0t_64_sync_handler
:
.macro kernel_ventry, el:req, ht:req, regsize:req, label:req
.align 7
.Lventry_start\@:
.if \el == 0
/*
* This must be the first instruction of the EL0 vector entries. It is
* skipped by the trampoline vectors, to trigger the cleanup.
*/
b .Lskip_tramp_vectors_cleanup\@
.if \regsize == 64
mrs x30, tpidrro_el0
msr tpidrro_el0, xzr
.else
mov x30, xzr
.endif
.Lskip_tramp_vectors_cleanup\@:
.endif
sub sp, sp, #PT_REGS_SIZE
#ifdef CONFIG_VMAP_STACK
/*
* Test whether the SP has overflowed, without corrupting a GPR.
* Task and IRQ stacks are aligned so that SP & (1 << THREAD_SHIFT)
* should always be zero.
*/
add sp, sp, x0 // sp' = sp + x0
sub x0, sp, x0 // x0' = sp' - x0 = (sp + x0) - x0 = sp
tbnz x0, #THREAD_SHIFT, 0f
sub x0, sp, x0 // x0'' = sp' - x0' = (sp + x0) - sp = x0
sub sp, sp, x0 // sp'' = sp' - x0 = (sp + x0) - x0 = sp
b el\el\ht\()_\regsize\()_\label // g, 跳转到处理函数
0:
/*
* Either we've just detected an overflow, or we've taken an exception
* while on the overflow stack. Either way, we won't return to
* userspace, and can clobber EL0 registers to free up GPRs.
*/
/* Stash the original SP (minus PT_REGS_SIZE) in tpidr_el0. */
msr tpidr_el0, x0
/* Recover the original x0 value and stash it in tpidrro_el0 */
sub x0, sp, x0
msr tpidrro_el0, x0
/* Switch to the overflow stack */
adr_this_cpu sp, overflow_stack + OVERFLOW_STACK_SIZE, x0
/*
* Check whether we were already on the overflow stack. This may happen
* after panic() re-enables interrupts.
*/
mrs x0, tpidr_el0 // sp of interrupted context
sub x0, sp, x0 // delta with top of overflow stack
tst x0, #~(OVERFLOW_STACK_SIZE - 1) // within range?
b.ne __bad_stack // no? -> bad stack pointer
/* We were already on the overflow stack. Restore sp/x0 and carry on. */
sub sp, sp, x0
mrs x0, tpidrro_el0
#endif
b el\el\ht\()_\regsize\()_\label
.org .Lventry_start\@ + 128 // Did we overflow the ventry slot?
.endm
通过b el\el\ht\()_\regsize\()_\label
这条指令,跳转:
SYM_CODE_START_LOCAL(el\el\ht\()_\regsize\()_\label)
kernel_entry \el, \regsize
mov x0, sp
bl el\el\ht\()_\regsize\()_\label\()_handler
.if \el == 0
b ret_to_user
.else
b ret_to_kernel
.endif
SYM_CODE_END(el\el\ht\()_\regsize\()_\label)
最终跳转到el\el\ht\()_\regsize\()_\label\()_handler
。那么中断向量表变成什么样了呢?
SYM_CODE_START(vectors)
kernel_ventry 1, t, 64, sync // Synchronous EL1t
kernel_ventry 1, t, 64, irq // IRQ EL1t
kernel_ventry 1, t, 64, fiq // FIQ EL1h
kernel_ventry 1, t, 64, error // Error EL1t
kernel_ventry 1, h, 64, sync // Synchronous EL1h
kernel_ventry 1, h, 64, irq // IRQ EL1h
kernel_ventry 1, h, 64, fiq // FIQ EL1h
kernel_ventry 1, h, 64, error // Error EL1h
kernel_ventry 0, t, 64, sync // Synchronous 64-bit EL0
kernel_ventry 0, t, 64, irq // IRQ 64-bit EL0
kernel_ventry 0, t, 64, fiq // FIQ 64-bit EL0
kernel_ventry 0, t, 64, error // Error 64-bit EL0
kernel_ventry 0, t, 32, sync // Synchronous 32-bit EL0
kernel_ventry 0, t, 32, irq // IRQ 32-bit EL0
kernel_ventry 0, t, 32, fiq // FIQ 32-bit EL0
kernel_ventry 0, t, 32, error // Error 32-bit EL0
SYM_CODE_END(vectors)
拿第三个表的sync异常来说吧,解析出来就是el0t_64_sync
,最终跳转到el0t_64_sync_handler
:
arch/arm64/kernel/entry-common.c:
asmlinkage void noinstr el0t_64_sync_handler(struct pt_regs *regs)
{
unsigned long esr = read_sysreg(esr_el1);
switch (ESR_ELx_EC(esr)) {
case ESR_ELx_EC_SVC64:
el0_svc(regs);
break;
case ESR_ELx_EC_DABT_LOW:
el0_da(regs, esr);
break;
case ESR_ELx_EC_IABT_LOW:
el0_ia(regs, esr);
break;
case ESR_ELx_EC_FP_ASIMD:
el0_fpsimd_acc(regs, esr);
break;
case ESR_ELx_EC_SVE:
el0_sve_acc(regs, esr);
break;
case ESR_ELx_EC_FP_EXC64:
el0_fpsimd_exc(regs, esr);
break;
case ESR_ELx_EC_SYS64:
case ESR_ELx_EC_WFx:
el0_sys(regs, esr);
break;
case ESR_ELx_EC_SP_ALIGN:
el0_sp(regs, esr);
break;
case ESR_ELx_EC_PC_ALIGN:
el0_pc(regs, esr);
break;
case ESR_ELx_EC_UNKNOWN:
el0_undef(regs);
break;
case ESR_ELx_EC_BTI:
el0_bti(regs);
break;
case ESR_ELx_EC_BREAKPT_LOW:
case ESR_ELx_EC_SOFTSTP_LOW:
case ESR_ELx_EC_WATCHPT_LOW:
case ESR_ELx_EC_BRK64:
el0_dbg(regs, esr);
break;
case ESR_ELx_EC_FPAC:
el0_fpac(regs, esr);
break;
default:
el0_inv(regs, esr);
}
}
这一步就没咋变化了,还是读取ESR寄存器判断异常类型。EL1t: t表示选择SP_EL0 EL1h:h表示选择SP_ELx(x>0)