第四十一期-ARM Linux内核的系统调用(1)

作者:罗宇哲,中国科学院软件研究所智能软件研究中心

上一期中我们介绍了工作队列相关的关键函数,这一期我们将介绍ARM Linux内核中的系统调用。

一、ARM Linux内核中的系统调用

在ARM Linux内核中,系统调用是一种特殊的异常,通常被归于同步异常的范畴,这是因为它是通过SVC指令触发的。我们在第二十七期中提到过,同步异常是由正在运行的指令或指令运行的结果造成的异常。

SVC指令在ARMv8体系中被归于异常处理类指令,该指令能允许用户程序调用内核,其格式如下[1]:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WlqHkA2P-1592910198500)(media/b6887918b5edc1b4f6580e21e126843e.png)]
异常处理程序可以从异常症状寄存器(Exception Syndrome Register,ESR)中得到SVC指令使用的立即数。从异常返回则可以使用ERET指令,该指令从SPSR寄存器中恢复处理器状态PSTATE,而且将返回到ELR寄存器保存的返回地址上。以上过程的具体情况我们在第三十期中提到过。

系统调用是操作系统内核为用户程序提供系统服务的接口,操作系统将一些需要在内核态运行的公共服务通过系统调用封装并提供给应用程序。用户程序在使用系统调用时将陷入内核态,并调用系统用的处理函数。不同的系统调用有不同的编号,它们被称为系统调用号。ARM64处理器中使用SVC指令触发系统调用的约定如下[2]:

  • 64位用户程序使用寄存器x8传递系统调用号,32位用户程序使用寄存器x7传递系统调用号;
  • 使用寄存器x0-x6传递系统调用所需参数,最多可传递7个参数;
  • 系统调用执行完后,用寄存器x0存放返回值。

二、系统调用的定义

ARM Linux内核中使用SYSCALL_DEFINEn(…)宏来定义一个系统调用,其中n为非负整数,表示后面括号中参数的数目。以内核中用于向进程发送信号的kill系统调用为例,其定义代码在openeuler/kernel/blob/kernel-4.19/kernel/signal.c文件中可以找到:
第四十一期-ARM Linux内核的系统调用(1)_第1张图片
SYSCALL_DEFINE的相关宏定义可以在openeuler/kernel/blob/kernel-4.19/include/linux/syscalls.h中找到(以下相关汇编代码在同一文件中):
第四十一期-ARM Linux内核的系统调用(1)_第2张图片
在C语言宏定义中##被解释成分隔与连接一个符号,符号可以是一个变量,也就是说“##name”被编译器理解为两段:“”和“name”,而name和前面的输入参数可以匹配,所以“##name”实际上是将输入参数name前面加了“”,例如输入“kill”就会变成“_kill”。__VA_ARGS__关键字等价于可变参数列表…中省略的内容。

SYSCALL_DEFINEx宏则会调用SYSCALL_METADATA宏对系统调用名称进行进一步变化:
第四十一期-ARM Linux内核的系统调用(1)_第3张图片
SYSCALL_METADATA中设置了与系统调用有关的参数,例如系统调用的参数类型列表、系统调用的参数值列表和系统调用的基本信息结构体等:
第四十一期-ARM Linux内核的系统调用(1)_第4张图片
例如系统调用的名字.name变成了“sys”#sname。宏定义中#表示字符串化,也就是说当sname为_kill时.name的值就是“sys”“_kill”。__MAP宏可以将多个参数分解为参数对,其参数对数目由输入参数nb决定:
第四十一期-ARM Linux内核的系统调用(1)_第5张图片
__SYSCALL_DEFINEx宏则定义了函数的调用接口:
第四十一期-ARM Linux内核的系统调用(1)_第6张图片
这段代码首先声明了一个函数sys##name,然后使该函数成为__se_sys##name函数的别名(也就是说调用这两个函数是等价的)。但是sys##name和__se_sys##name两个函数之间存在一个类型转换,在sys##name中的原输入类型在__se_sys##name的参数列表中都变为了long类型,这个类型转换是通过__SC_DECL宏和__SC_LONG宏实现的。接着函数__se_sys##name调用了函数__do_sys##name,实际上__do_sys##name函数的内容就是我们对系统调用的定义。在这段代码的最后部分,__do_sys##name的函数声明后面没有加分号,这样它就能和内核中SYSCALL_DEFINE宏后面的函数体构成函数定义。根据前文说述的调用关系,使用sys##name就能调用__do_sys##name定义的函数。我们可以看到__do_sys##name函数又将long类型的输入转换为了原输入的参数类型。之所以要做这种转换是为了对可能会放在64位寄存器中的输入参数(例如32位输入参数)进行符号扩展,从而防止用户程序通过传入特定的参数取得权限提升或导致系统崩溃[3]。这种情况曾经在CVE-2009-0029漏洞中出现,如果传入系统调用的参数的高32位为非法值,就有可能访问到非法的地址[4]。

__SC_DECL宏、__SC_CAST和__SC_LONG宏的定义如下:
第四十一期-ARM Linux内核的系统调用(1)_第7张图片
它们的作用分别是使参数保持输入参数列表中的类型、对参数列表中参数的类型进行强制类型装换和将参数列表中的参数定义为long类型。

以kill系统调用为例,其定义语句为SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)

说明该系统调用有两个参数pid和sig,其类型分别为pid_t和int。pid是目标进程的进程号,sig是要发送的信号。kill系统调用的定义过程如下图所示:
第四十一期-ARM Linux内核的系统调用(1)_第8张图片
__do_sys##name函数对应的函数声明为__do_sys_kill(pid_t pid,int sig),其函数体为SYSCALL_DEFINE2宏下面定义的函数体。__do_sys_kill()函数是在__se_sys_kill()函数中被调用的,__se_sys_kill()函数的声明为__se_sys_kill(long pid,long sig)。__se_sys_kill()函数为sys_kill()函数的别名,sys_kill()函数的声明为sys_kill(pid_t pid,int sig),通过调用sys_kill()函数可其使用kill系统调用。

系统调用号与系统调用处理函数的映射关系被保存在系统调用表中,系统调用表的相关代码在openeuler/kernel/blob/kernel-4.19/arch/arm64/kernel/sys.c文件中可以找到:
第四十一期-ARM Linux内核的系统调用(1)_第9张图片
__SYSCALL宏用于注册系统调用表。syscall_fn_t是系统调用处理函数的函数指针类型,其定义在syscalls.h文件中:
在这里插入图片描述
pt_regs结构体包含异常发生时栈上寄存器中保存的信息,其代码在openeuler/kernel/blob/kernel-4.19/arch/arm64/include/asm/ptrace.h文件中:

/*

* This struct defines the way the registers are stored on the stack during an

* exception. Note that sizeof(struct pt_regs) has to be a multiple of 16 (for

* stack alignment). struct user_pt_regs must form a prefix of struct pt_regs.

*/

struct pt_regs {

union {

struct user_pt_regs user_regs;

struct {

u64 regs[31];

u64 sp;

u64 pc;

u64 pstate;

};

};

u64 orig_x0;

#ifdef __AARCH64EB__

u32 unused2;

s32 syscallno;

#else

s32 syscallno;

u32 unused2;

#endif

u64 orig_addr_limit;

/* Only valid when ARM64_HAS_IRQ_PRIO_MASKING is enabled. */

u64 pmr_save;

u64 stackframe[2];

};

从该结构体的定义代码中我们可以看到它包含了31个通用寄存器、栈指针寄存器SP、程序计数器PC和处理器状态PSTATE等信息。

系统调用表中#include了asm/unistd.h文件,该文件在openeuler/kernel/blob/kernel-4.19/arch/arm64/include/asm/unistd.h,它包含了系统调用号等信息。如果定义了__COMPAT_SYSCALL_NR宏,使用Compat系统调用号:
第四十一期-ARM Linux内核的系统调用(1)_第10张图片
否则最终使用openeuler/kernel/blob/kernel-4.19/include/uapi/asm-generic/unistd.h文件中定义的系统调用映射关系:

#define __NR_io_setup 0//系统调用号0

__SC_COMP(__NR_io_setup, sys_io_setup,
compat_sys_io_setup)//系统调用号0对应的系统调用为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)

……

#undef __NR_syscalls

#define __NR_syscalls 294

该文件中共定义了294个系统调用。__SC_COMP宏的定义如下:
第四十一期-ARM Linux内核的系统调用(1)_第11张图片
当定义了__SYSCALL_COMPAT宏时,系统调用号对应的系统调用为宏定义的第三个输入参数,否则为第二个输入参数。

三、结语

本期我们介绍了ARM Linux内核中的系统调用和定义系统调用的流程,下一期中我们将介绍执行系统调用的过程。

参考文献

[1] ARM® Cortex® -A Series Version: 1.0 Programmer’s Guide for ARMv8-A

[2]《Linux内核深度解析》,余华兵著,2019

[3] https://blog.csdn.net/hxmhyp/article/details/22699669

[4] https://blog.csdn.net/hxmhyp/article/details/22619729

你可能感兴趣的:(内核,linux,java,python,c++)