Linux系统调用实现简析

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 背景

本篇基于 Linux 4.14 + ARM 32 + glibc-2.31 进行分析。

3. 系统调用的实现

3.1 系统调用的发起

3.1.1 起于用户空间

我们随意挑选一个系统调用,如常见的 write()glibc-2.31 对其实现如下:

ssize_t
__libc_write (int fd, const void *buf, size_t nbytes)
{
  	return SYSCALL_CANCEL (write, fd, buf, nbytes);
}

// 中间省略多个宏定义,对这些细节感兴趣的读者可以自行阅读 glibc 代码。
// ...

# define INTERNAL_SYSCALL_RAW(name, err, nr, args...)		\
  ({								\
       register int _a1 asm ("r0"), _nr asm ("r7");		\
       LOAD_ARGS_##nr (args)					\
       _nr = name;						\
       asm volatile ("swi	0x0	@ syscall " #name	\
		     : "=r" (_a1)				\
		     : "r" (_nr) ASM_ARGS_##nr			\
		     : "memory");				\
       _a1; })

系统调用语句 return SYSCALL_CANCEL (write, fd, buf, nbytes) 最终展开如下(为方便阅读,对最终结果的格式稍作了调整):

return ({
	long int sc_ret;
	
	int sc_cancel_oldtype = LIBC_CANCEL_ASYNC ();
	sc_ret = ({
		unsigned int _sys_result = ({
				register int _a1 asm ("r0")/* 参数 @fd 从寄存器 r0 传入 */, _nr asm ("r7")/* 系统调用编号从寄存器 r7 传入 */;
				int _a3tmp = (int) (nbytes);
				int _a2tmp = (int) (buf);
				int _a1tmp = (int) (fd);
				_a1 = _a1tmp;
				register int _a2 asm ("a2") = _a2tmp; /* 参数 @buf 从寄存器 r1 (即a2) 传入 */
				register int _a3 asm ("a3") = _a3tmp; /* 参数 @nbytes 从寄存器 r2 (即 a3) 传入 */
					
				_nr = __NR_write; /* 赋值系统调用编号 */
				/* arm32 通过 swi 指令,发起系统调用 */
				asm volatile ("swi	0x0	@ syscall __NR_write"
							: "=r" (_a1)
							: "r" (_nr), "r" (_a1), "r" (_a2), "r" (_a3)
							: "memory");
					_a1; /* 系统调用返回值,从寄存器 r0 传回 */
			});
		if (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (_sys_result, ), 0))
		{
			__set_errno (INTERNAL_SYSCALL_ERRNO (_sys_result, )); /* 调用出错,设置错误码 errno */
			_sys_result = (unsigned int) -1; /* 调用出错,返回 -1 */
		}
		(int) _sys_result; /* 系统调用返回值: sc_ret = _sys_result; */
	});
	LIBC_CANCEL_RESET (sc_cancel_oldtype);
	sc_ret; /* write() 调用的返回值 */
});

从上面的代码分析中,我们了解到ARM32平台的系统调用是通过 swi 指令发起,也了解到了系统调用参数传递的细节。接下来,我们继续分析系统调用进入内核空间后的工作细节。
注:寄存器 r0/a1, r1/a2, r2/a3 是等同的,我们可以参看下表:
在这里插入图片描述

3.1.2 进入内核空间

系统调用的过程,是和具体硬件架构相关的,在继续讨论内核空间系统调用的工作细节之前,我们先来了解一点 ARM32 架构和系统调用相关的内容。

3.1.2.1 ARM32 架构系统调用相关知识
3.1.2.1.1 ARM32 CPU 的各种工作模式

在这里插入图片描述
我们重点关注上图中标注的 UserSupervisor 模式。

3.1.2.1.2 ARM32 CPU 各工作模式下寄存器分布

在这里插入图片描述
从上图表格可以看出:

. 有些寄存器是所有 CPU 模式共享的,如 R0~R7, PC, CPSR ;
. 有些寄存器是独立于各 CPU 模式的,模式有自己独立的寄存器 Bank ,如 R14_svc, SPSR_svc 等。

我们有必要对其中的2个寄存器 CPSR, R14 做一下说明:

 CPSR:CPU 当前模式状态寄存器,记录 CPU 当前状态的一些信息。
 R14:链接寄存器(LR: Linker Register),当 CPU 模式从 A 切换到 B 时,
      B 模式的 R14 会自动记录 A 模式下一条待执行指令的地址,也即 A 模式的返回地址。
3.1.2.1.3 ARM32 架构异常和CPU模式的对应关系

在这里插入图片描述
用户空间发起系统调用时运行的 swi 指令,会导致 ARM32 CPU 产生一个异常,根据上图表格,该异常会导致 CPU 将进入 Supervisor 模式,ARM32 CPU 进入异常时的具体细节如下:

R14_svc = 紧挨 swi 下一条指令的地址(即用户空间的返回地址)
SPSR_svc = CPSR (User 模式的 CPSR)
CPSR[4:0] = 0b10011(切换到 Supervisor 模式)
CPSR[7] = 1(禁用当前 CPU 的一般中断)
...
PC = 异常向量地址(即跳转到异常向量地址处执行)

这些动作,是架构硬件自动完成的,无需软件干预。

3.1.2.2 内核空间调用流程

从上面的 3.1.2.1 小节我们知道,swi 指令将导致 ARM32 CPU 发生异常,进入到 Supervisor 模式,CPU 将跳转到对应的异常向量指向的地址执行。
我们来看 ARM32 架构下,内核的中断异常向量的相关代码。

/* @arch/arm/kernel/vmlinux.lds.S */

/* 中断向量表 */
__vectors_start = .;
.vectors 0xffff0000 : AT(__vectors_start) {
	*(.vectors)
}
. = __vectors_start + SIZEOF(.vectors);
__vectors_end = .;

/* 所有的 .stubs 段位于中断向量表后偏移 0x1000 处 */
.stubs ADDR(.vectors) + 0x1000 : AT(__stubs_start) {
	*(.stubs)
}
. = __stubs_start + SIZEOF(.stubs);
__stubs_end = .;
/* @arch/arm/kernel/entry-armv.S */

	/*
	 * 根据上面的链接脚本,我们知道 .stubs 位于中断向量表后偏移 0x1000 处;
	 * 同时由于中断表起始于标号 .L__vectors_start ,所以 .stubs 的起始位置
	 * 也可以表示成 .L__vectors_start + 0x1000 。
	 */
	.section .stubs, "ax", %progbits
	.word	vector_swi /* swi 指令异常处理函数 */

	...

	.globl	vector_fiq

	/* 中断向量表 */
	.section .vectors, "ax", %progbits
.L__vectors_start:
	W(b)	vector_rst
	W(b)	vector_und
	W(ldr)	pc, .L__vectors_start + 0x1000 /* 软中断向量(swi) */
	W(b)	vector_pabt
	W(b)	vector_dabt
	W(b)	vector_addrexcptn
	W(b)	vector_irq
	W(b)	vector_fiq

从上面可知,执行流程转入了 swi 指令异常处理接口 vector_swi 。我们接着分析 vector_swi 的执行:

/* arch/arm/include/uapi/asm/ptrace.h */
#define ARM_cpsr	uregs[16]
#define ARM_pc		uregs[15]
#define ARM_lr		uregs[14]
#define ARM_sp		uregs[13]
#define ARM_ip		uregs[12]
#define ARM_fp		uregs[11]
#define ARM_r10		uregs[10]
#define ARM_r9		uregs[9]
#define ARM_r8		uregs[8]
#define ARM_r7		uregs[7]
#define ARM_r6		uregs[6]
#define ARM_r5		uregs[5]
#define ARM_r4		uregs[4]
#define ARM_r3		uregs[3]
#define ARM_r2		uregs[2]
#define ARM_r1		uregs[1]
#define ARM_r0		uregs[0]
#define ARM_ORIG_r0	uregs[17]

...

struct pt_regs { /* ARM32 某 CPU 模式的 18 个寄存器 */
	unsigned long uregs[18];
};

...

/* @arch/arm/kernel/asm-offsets.c */
...
DEFINE(TI_FLAGS,		offsetof(struct thread_info, flags));
...
DEFINE(TI_ADDR_LIMIT,		offsetof(struct thread_info, addr_limit));
...
DEFINE(S_R0,			offsetof(struct pt_regs, ARM_r0));
...
DEFINE(S_PC,			offsetof(struct pt_regs, ARM_pc));
DEFINE(S_PSR,			offsetof(struct pt_regs, ARM_cpsr));
DEFINE(S_OLD_R0,		offsetof(struct pt_regs, ARM_ORIG_r0));
DEFINE(PT_REGS_SIZE,		sizeof(struct pt_regs)); /* 18 * sizeof(unsigned long) = 72 */
...
/* @arch/arm/kernel/entry-common.S */

...

saved_psr	.req	r8
saved_pc	.req	lr

...

/*=============================================================================
 * SWI handler
 *-----------------------------------------------------------------------------
 */
 
	.align	5
ENTRY(vector_swi)
	/* 
	 * 在堆栈上预留 18 个寄存器的空间(struct pt_regs),用来保存用户空间(User 模式)
	 * 的寄存器, 以便后续系统调用从内核空间(Supervisor模式)返回(User模式)时恢复它们。
	 */
	sub	sp, sp, #PT_REGS_SIZE
	/*
	 * Usesr 和 Supervisor 模式的 R0~R12 是相同的,先将 User 模式的 
	 * R0~R12 保存到上一条指令预留的堆栈空间上。
	 * 内核 Supervisor 模式下可能会使用这些寄存器,如果不事先保留这些寄存器,后
	 * 面回到用户空间 User 模式将无法恢复它们,用户空间程序也将无法继续正确执行。
	 */
	stmia	sp, {r0 - r12} // pt_regs::uregs[0..12] = r0..r12, sp 值不变
	/* 
	 * 保存 User 模式寄存器 SP,LR (即 R13,R14) 到预留堆栈空间。
	 * 注: STM 指令寄存器组后面加 ^ 指示存储 User 模式寄存器。
	 */
	add	r8, sp, #S_PC // r8 -> pt_regs::uregs[15]
	stmdb	r8, {sp, lr}^ // pt_regs::uregs[14] = LR, pt_regs::uregs[13] = SP
	/* 
	 * 保存 Supervisor 模式的 SPSR 寄存器 (SPSR_svc)。
	 * 从前面 ARM32 架构知识我们了解到,此时的 SPSR_svc 记录的是 User 模式的 CPSR,
	 * 我们要保存它,以便系统调用返回用户空间时恢复 User 模式的 CPSR 。
	 */
	mrs	saved_psr, spsr // r8 = SPSR_svc
	/* 
	 * 保存 User 模式的返回地址(即 LR_svc)到堆栈预留空间。 
	 * 从前面的 ARM32 架构知识我们知道,从 User -> Supervisor 
	 * 模式切换过程中,硬件自动保存 User 模式的返回地址到 
	 * Supervisor 模式的 LR 寄存器 (R14_svc)。
	 */
	str	saved_pc, [sp, #S_PC] // pt_regs::uregs[15] = R14_svc (LR_svc)
	/*
	 * 保存 Supervisor 模式 SPSR_svc 到堆栈预留空间,以便返回用户空间时恢复 
	 * User 模式的 CPSR 。 
	 */
	str	saved_psr, [sp, #S_PSR] // pt_regs::uregs[16] = SPSR_svc
	/*
	 * 系统调用在某些情形下会自动重启,而在这些情形下,因为设置系统调用的返回值,
	 * 内核 pt_regs::uregs[0] 处保存的系统调用的第1个参数会被破坏,在这里重复
	 * 保存 User 模式的 R0 (系统调用的第1个参数) 到预留堆栈空间上,以便在前述
	 * 情形下恢复系统调用的第1个参数。
	 */
	str	r0, [sp, #S_OLD_R0] // pt_regs::uregs[17] = r0
	
	/* 栈指针寄存器FP(Frame Pointer)清0 */
	zero_fp // R11/v8/FP = 0
	...
	/* 使能 IRQ 中断 */
	enable_irq_notrace // CPSR.I = 0
	...

	uaccess_disable tbl
	adr	tbl, sys_call_table // r8 = 系统调用表 sys_call_table[] 的地址
	
	get_thread_info tsk // r9 = 进程的 struct thread_info
	
local_restart:
	ldr	r10, [tsk, #TI_FLAGS] // r10 = thread_info::flags
	/*
	 * 1. 将系统调用的 第4个参数(r4) 和 第5个参数(r5) 压入进程内核栈
	 * 2. sp -= 8
	 *
	 * 压入 r4,r5 之前,sp_svc 指向用来保存用户空间 (User 模式) 参数的 
	 * pt_regs 的开始地址,所以压入 r4,r5 后,进程内核堆栈布局如下:
	 *
	 *  | r4          | | 低地址 <-- sp_svc
	 *  |-------------| |
	 *  | r5          | |
	 *  |-------------| |
	 *  | pt_regs     | | 
	 *  |-------------| |
	 *  | thread_info | |
	 *  |-------------| v 高地址
	 */
	stmdb	sp!, {r4, r5}

	/* 
	 * 调用系统调用接口。 
	 * 我们将汇编宏 invoke_syscall 展开,方便分析。
	 */
	//invoke_syscall tbl, scno, r10, __ret_fast_syscall
	mov	r10, r7 // r10 = r7 (从用户空间的代码分析, r7 是系统调用编号)
	/* 比较 系统调用编号 和 系统支持的最大系统调用编号 NR_syscalls */
	cmp	r10, #NR_syscalls
	/* 如果 系统调用编号 >= NR_syscalls 则 r10 = 0 ,否则不执行 */
	movcs 	r10, #0 // if (r10 >= NR_syscalls) r10 = 0
	csdb
	/* 系统调用的返回地址: 系统调用函数执行完后,返回到 __ret_fast_syscall 继续执行 */
	adr	lr, __ret_fast_syscall
	/*
	 * 如果系统调用号合法,调用系统调用接口。
	 * 系统调用返回时,跳转到 __ret_fast_syscall 执行。 
	 */
	ldrcc	pc, [r8, r10, lsl #2] // if (r10 < NR_syscalls) sys_XXX()
	
	/*
	 * 所有的系统调用,可以分为:
	 * (1) 架构无关的系统调用: 系统调用号 < NR_syscalls
	 * (2) 架构相关的系统调用:系统调用号 >= NR_syscalls
	 * 两大块。
	 * 上面的代码处理了 【架构无关的系统调用】,如果没找到匹配的系统调用
	 * (即系统调用号 >= NR_syscalls),则继续匹配【架构相关的系统调用】,
	 * 如果还是没找到,则调用缺省的系统调用 sys_ni_syscall() ,该函数返
	 * 回 ENOSYS 错误码。
	 */
	add	r1, sp, #S_OFF // r1 = sp + 8 (S_OFF = 8), r1 -> pt_regs::uregs[0]
	eor	r0, scno, #__NR_SYSCALL_BASE // r0 = r7 ^ __NR_SYSCALL_BASE
	/* 
	 * 处理 ARM32 架构相关的系统调用,返回时跳转到 __ret_fast_syscall 处,
	 * 因为前面将 LR_svc 设置为 __ret_fast_syscall 的地址。
	 */
	bcs	arm_syscall
	
	/*
	 * 注意,如果进入了 arm_syscall() ,从该函数 return 返回
	 * 的不是此处,而是 __ret_fast_syscall 处。
	 * 
	 * 走到这里是没有找到任何匹配的系统调用号,调用缺省的系统
	 * 调用接口 sys_ni_syscall() , 该函数返回 ENOSYS 错误码。 
	 */
	mov	why, #0 // r8 = 0
	b	sys_ni_syscall
ENDPROC(vector_swi)

/*
 * ARM 32 平台系统调用表定义。
 */
/* 构造系统调用表头 */ 
.macro	syscall_table_start, sym
	.equ	__sys_nr, 0
	.type	\sym, #object
ENTRY(\sym)
	.endm

	/* 构造系统调用表项(一个系统调用入口) */
	.macro	syscall, nr, func
	.ifgt	__sys_nr - \nr
	.error	"Duplicated/unorded system call entry"
	.endif
	.rept	\nr - __sys_nr // 有系统调用编号没有用到,填充默认接口 sys_ni_syscall()
	.long	sys_ni_syscall
	.endr
	.long	\func // 填入系统调用接口,如 sys_write()
	.equ	__sys_nr, \nr + 1 // 下一个系统调用编号: .equ __sys_nr, 4 + 1
	.endm

	/* 构造系统调用表尾 */ 
	.macro	syscall_table_end, sym
	.ifgt	__sys_nr - __NR_syscalls
	.error	"System call table too big"
	.endif
	.rept	__NR_syscalls - __sys_nr
	.long	sys_ni_syscall
	.endr
	.size	\sym, . - \sym // 系统调用表大小
	.endm

#define NATIVE(nr, func) syscall nr, func

/*
 * This is the syscall table declaration for native ABI syscalls.
 * With EABI a couple syscalls are obsolete and defined as sys_ni_syscall.
 */
/*
 * #include <calls-eabi.S> 导入的文件,是处理系统调用的脚本,
 * 编译时动态生成的文件: 
 * arch/arm/include/generated/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(397, sys_statx)
 *
 * 下面的汇编语句构造了系统调用指针表:sys_call_table[NR_syscalls]
 */
	syscall_table_start sys_call_table
#define COMPAT(nr, native, compat) syscall nr, native
// 根据内核配置的 ABI 不同,这里可能包含另一生成文件 calls-oabi.S, 
// 逻辑上类似的,这里不另做分析。
#include <calls-eabi.S> 
#undef COMPAT
	syscall_table_end sys_call_table

到此,系统调用的发起流程,已经全部分析结束,我们简单的总结一下流程:

1. 用户空间程序,调用 glibc 函数(另一种形式是直接以 syscall(系统调用号) 发起);
2. glibc 通过寄存器设置好系统调动参数、以及系统调用号(R7 寄存器),然后通过 swi 指令进入
   Supervisor 模式的内核空间;
3. Supervisor 模式内核空间通过系统调用号(R7 寄存器),查找系统调用表 sys_call_table ,
   找到系统调用接口然后调用;
4. 系统调用接口执行完成后,返回到 __ret_fast_syscall 继续执行,然后从这里返回用户空间。

3.2 系统调用的返回

系统调用完成后,return 时返回到 __ret_fast_syscall 处代码继续执行:

ret_fast_syscall:
__ret_fast_syscall:
	disable_irq_notrace // 禁用中断
	
	/* 系统调用可能错误的配置了进程地址空间,在返回用户空间之前,要做一下检测 */
	ldr	r2, [tsk, #TI_ADDR_LIMIT] // r2 = thread_info::addr_limit
	cmp	r2, #TASK_SIZE
	blne	addr_limit_check_failed // 地址空间配置错误
	
	/*
	 * 系统调用返回前,检查是否有挂起工作要做: 
	 * . 被动发起的进程调度: (thread_info::flags & _TIF_NEED_RESCHED) != 0
	 * . 挂起的信号:(thread_info::flags & _TIF_SIGPENDING) != 0
	 * . 其它情形。
	 */
	ldr	r1, [tsk, #TI_FLAGS] // r1 = thread_info::flags
	tst	r1, #_TIF_SYSCALL_WORK | _TIF_WORK_MASK // 检查是否有挂起的工作要做
	/*
	 * 检查到有挂起的工作要做,先跳转到 fast_work_pending  做完挂起的工作,
	 * 然后再返回用户空间。
	 */
	bne	fast_work_pending 
	
	/*
	 * 系统调用返回点 1:
	 * 没有挂起的工作,则恢复用户空间上下文,然后直接返回用户空间。
	 * 
	 * 我们将返回用户空间的汇编宏 restore_user_regs 展开,方便分析。
	 * S_OFF = 8
	 */
	// restore_user_regs fast = 1, offset = S_OFF
	uaccess_enable r1, isb=0
	
	mov	r2, sp // r2 = sp (r2 + S_OFF -> pt_regs::uregs[0])
	/* 读取保存的 User 模式 CPSR */
	ldr	r1, [r2, #8 + S_PSR] // r1 =  pt_regs::uregs[16] = SPSR_svc
	ldr	lr, [r2, #8 + S_PC]! // LR_svc =  pt_regs::uregs[15] (用户空间的返回地址), r2 -> pt_regs::uregs[15]
	msr	spsr_cxsf, r1 // SPSR_lvc[19:16] = r1
	strex	r1, r2, [r2] // 
	/*
	 * 恢复用户空间寄存器 R1..R14 ,我们不需要恢复 R0 ,因为 R0 当前保存的,
	 * 就是系统调用的返回值。 这和后面处理挂起工作后返回用户空间不同,该情形
	 * 下需要提前保存 R0 (系统调用的返回值),而在后面的恢复也要包括 R0 ,因为
	 * 后续的挂起工作处理,会覆盖 R0 的值。
	 */
	ldmdb	r2, {r1 - lr}^ // R1..R14 = pt_regs::uregs[1]..pt_regs::uregs[14]
	mov	r0, r0 // nop 指令
	/* 平衡内核堆栈指针 SP_svc 到刚进入内核空间的值,正如前面描述的,代码要遵循调用规范 */
	add	sp, sp, #8 + PT_REGS_SIZE // sp += (8 + sizeof(pt_regs))
	/*
	 * 返回用户空间:
	 * 1. 用 SPSR_svc 恢复用户空间 (User 模式) 的 CPSR: CPSR = SPSR_svc
	 * 2. 从用户空间 swi 下一条指令继续执行。
	 * 这个切换是比较隐晦的, mov 指令后带 s && 目的寄存器是 pc 的情形下,
	 * 会发生 CPSR = SPSR_svc 拷贝动作。
	 */
	movs	pc, lr // CPSR = SPSR_svc, pc = LR_svc
ENDPROC(ret_fast_syscall)

fast_work_pending:
	/*
	 * 如果函数调用时符合调用规范的,从函数调用返回时,SP 指针总是回复到函数调用之前
	 * 的值(堆栈平衡)。
	 * 我们的代码是符合调用规范的,同时此处正是系统调用接口返回的位置,所以我们
	 * 可以认为此时 SP 的值是进入系统调用接口之前的值。回顾一下我们前面的代码分
	 * 析,具体是 local_restart 标号处的代码,我们可以知道,进入系统调用接口之前,
	 * 我们的进程内核栈的状况如下:
	 * SP -> |-------------| | 低地址
	 *       | r4          | |
	 *       |-------------| |
	 *       | r5          | |
	 *       |-------------| |
	 *       | pt_regs     | | 
	 *       |-------------| |
	 *       | thread_info | |
	 *       |-------------| v 高地址
	 * sp + 8 指向的位置,是保存用户空间 (User 模式) r0 寄存器栈空间 
	 * (即 pt_regs::uregs[0])的地址。而此时 r0 的值,是系统调用的返
	 * 回值,所以此处做的工作是:将系统调用的返回值保存到栈空间。
	 * 
	 * 其中:S_R0 = 0, S_OFF = 8
	 */
	/* 
	 * 提前设置系统调用返回值。 
	 * 那么这里我们为什么要保存系统调用的返回值呢? 
 	 * 当前场景,R0 存放的是系统调用的返回值,后面处理在处理
 	 * 挂起工作的过程中,会覆盖 R0 的值,所以我们要提前保存。
 	 * 与之呼应的是,我们看到后面的 restore_user_regs 恢复
 	 * 上下文的过程中,我们恢复了寄存器 R0~R14 ,包括了 R0 。
	 */
	str	r0, [sp, #S_R0+S_OFF]! // pt_regs::uregs[0] = r0, sp +=8, sp -> pt_regs::uregs[0]
	...
slow_work_pending:
	mov	r0, sp // r0 -> pt_regs::uregs[0]
	mov	r2, why // r2 = r8 (sys_call_table 的地址)
	/* 
	 * 做挂起的信号处理,进程调度等工作。
	 * 如果是去处理进程调度工作,那么进程可能从这里切换出去(暂时不被CPU执行)。
	 * 但没关系,下次再切换回来的时候,我们的系统调用返回流程还是从这里继续,
	 * 这和进程不被切换出去,后续返回用户空间的流程是一致的,所以们做区分。
	 */
	bl	do_work_pending
	cmp	r0, #0
	beq	no_work_pending

	/*
	 * do_work_pending() 返回非 0 值,表示:
	 * . 系统调用被信号中断了
	 * . 中断系统调用的信号设置了 SA_RESTART 标记
	 * 这种情形下,我们需要重启系统调用。
	 */
	movlt	scno, #(__NR_restart_syscall - __NR_SYSCALL_BASE)
	ldmia	sp, {r0 - r6} // R0..R6 = pt_regs::uregs[0..6]
	b	local_restart // 重启系统调用
	...
ENDPROC(ret_fast_syscall)

ENTRY(ret_to_user)
ret_slow_syscall:
	...
ENTRY(ret_to_user_from_irq)
no_work_pending:
	...
	/*
	 * 系统调用返回点 2: 
	 * 处理了挂起工作后,然后返回用户空间。 
	 */
	/* 我们将返回用户空间的汇编宏 restore_user_regs 展开,方便分析 */
	//restore_user_regs fast = 0, offset = 0
	uaccess_enable r1, isb=0
	
	mov	r2, sp // r2 = sp (r2 -> pt_regs::uregs[0])
	/* 读取保存的 User 模式 CPSR */
	ldr	r1, [r2, #0 + S_PSR] // r1 =  pt_regs::uregs[16] = SPSR_svc
	ldr	lr, [r2, #0 + S_PC]! // LR_svc =  pt_regs::uregs[15] (用户空间的返回地址), r2 -> pt_regs::uregs[15]
	msr	spsr_cxsf, r1
	/* 恢复用户空间寄存器 R0..R14 */
	ldmdb	r2, {r0 - lr}^  // R0..R14 = pt_regs::uregs[0]..pt_regs::uregs[14]
	mov	r0, r0 // nop
	/* 平衡内核堆栈指针 SP_svc 到刚进入内核空间的值,正如前面描述的,代码要遵循调用规范 */
	add	sp, sp, #0 + PT_REGS_SIZE
	/*
	 * 返回用户空间:
	 * 1. 用 SPSR_svc 恢复用户空间 (User 模式) 的 CPSR: CPSR = SPSR_svc
	 * 2. 从用户空间 swi 下一条指令继续执行。
	 * 这个切换是比较隐晦的, mov 指令后带 s && 目的寄存器是 pc 的情形下,
	 * 会发生 CPSR = SPSR_svc 拷贝动作。
	 */
	movs	pc, lr // CPSR = SPSR_svc, pc = LR_svc
ENDPROC(ret_to_user_from_irq)
ENDPROC(ret_to_user)

3.3 系统调用的重启

3.3.1 手工重启

我们经常应用程序中看到类似如下的代码片段:

	int ret;

retry:
	ret = read(...);
	if (ret < 0 && errno == EINTR)
		goto retry;

这就是系统调用被信号打断后,手工重启的演示代码。

3.3.2 自动重启

我们也可以通过适当的配置,让被信号打断的系统调用自动重新发起。
示例代码片段如下:

/* 对 SIGALRM 信号进行配置,使得因 SIGALRM 而被打断的系统调用,能够自动重新发起 */
struct sigaction action;

action.sa_handler = handler_func;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
action.sa_flags |= SA_RESTART;
sigaction(SIGALRM, &action, NULL);

这样配置后,如果系统调用被 SIGALRM 信号打断,那么系统会自动重启系统调用。我们来分析下内核系统调用自动发起的流程。

3.3.2.1 信号初始化
sys_sigaction()
	struct k_sigaction new_ka;
	do_sigaction(sig, &new_ka, NULL)
		k = &p->sighand->action[sig-1]; /* 当前的信号配置 */
		sigdelsetmask(&act->sa.sa_mask, sigmask(SIGKILL) | sigmask(SIGSTOP));
		*k = *act; /* 更新信号配置: *k ==> *act */
		...
3.3.2.2 系统调用自动重启
/* 从内核返回用户空间时,会做信号处理 */
do_work_pending()
	do {
		if (likely(thread_flags & _TIF_NEED_RESCHED)) {
			schedule(); /* 执行调度 */
		} else {
			local_irq_enable();
			if (thread_flags & _TIF_SIGPENDING) { /* 挂起信号可能导致系统调用的中断 */
				/* 在这里我们不关注进程调度的工作,只关心信号处理和重启系统调用相关的部分 */
				int restart = do_signal(regs, syscall)
					/*
					 * 如果是从系统调用返回路径来到此处(可能从其它代码路径来到此处),
					 * 检查是否需要重启系统调用 
					 */
					if (syscall) {
						/* 紧跟发起系统调用的 swi 指令的下一条指令的地址 */
						continue_addr = regs->ARM_pc; 
						/* 如果是返回用户间后,再重新发起系统调用,要将 PC 重新指向 swi 指令 */
						restart_addr = continue_addr - (thumb_mode(regs) ? 2 : 4);
						retval = regs->ARM_r0; /* 系统调用返回值 */
						/*
						 * Prepare for system call restart.  We do this here so that a
						 * debugger will see the already changed PSW.
						 */
						switch (retval) { /* 系统调用如果返回下面的几个错误码,都指示要重新发起系统调用 */
						case -ERESTART_RESTARTBLOCK:
							restart -= 2;
						case -ERESTARTNOHAND:
						case -ERESTARTSYS:
						case -ERESTARTNOINTR:
							restart++;
							/*
							 * 由于 R0 已经覆写为系统调动的返回值,我们用在进入系统调用进入内核空间时,
							 * 重复保存的 R0 (系统调用的第1个参数) 来恢复系统调用的第1个参数。
							 */
							regs->ARM_r0 = regs->ARM_ORIG_r0;
							/* 返回用户空间后,重新发起系统调用: 将 User 模式的 PC 重新指向 swi 指令 */
							regs->ARM_pc = restart_addr;
							break;
						}
					}
					
					if (get_signal(&ksig)) {
						if (unlikely(restart) && regs->ARM_pc == restart_addr) {
							/*
							 * 还有信号待处理的情形:
							 * 1. 如果系统调用如有返回 -ERESTART_RESTARTBLOCK, -ERESTARTNOHAND 错误码,
							 * 不重启系统调用,仅返回 -EINTR 的错误码。
							 * 2. 如果系统调用返回错误码 -ERESTARTSYS, 但当前被处理的
							 * 信号没有设置 SA_RESTART 标记,不重启系统调用,仅返回 -EINTR 的错误码。
							 * 3. 除此之外的情形,如果系统调用返回 
							 * -ERESTART_RESTARTBLOCK, -ERESTARTNOHAND, -ERESTARTSYS, -ERESTARTNOINTR 
							 * 错误码,从用户空间自动重启系统调用。
							 */
							if (retval == -ERESTARTNOHAND ||
							    retval == -ERESTART_RESTARTBLOCK || 
							    (retval == -ERESTARTSYS && !(ksig.ka.sa.sa_flags & SA_RESTART))) {
								regs->ARM_r0 = -EINTR;
								regs->ARM_pc = continue_addr;
							}
						}
						handle_signal(&ksig, regs);
					} else {
						restore_saved_sigmask();
						/*
						 * 没有信号待处理的情形:
						 * 如果系统调用返回 
						 * -ERESTART_RESTARTBLOCK, -ERESTARTNOHAND, -ERESTARTSYS, -ERESTARTNOINTR 
						 * 错误码,我们不用返回用户空间,直接在内核空间重启系统调用。
						 */
						if (unlikely(restart) && regs->ARM_pc == restart_addr) {
							regs->ARM_pc = continue_addr;
							return restart;
						}
					}
					
				if (unlikely(restart)) { /* 指示重启系统调用,参看上面的汇编代码 */
					/*
					 * Restart without handlers.
					 * Deal with it without leaving
					 * the kernel space.
					 */
					return restart;
				}
			}  else if (thread_flags & _TIF_UPROBE) {
				...
			}  else {
				...
			}
		}
	} ;
	return 0;

我们简单总结一下系统调动的重启方式:

系统调动重启分为两种方式:
1. 手工重启:检测到 EINTR 错误码时,手工重启。
2. 自动重启:配置信号,让其自动重启。自动重启又细分为【用户空间自动重启】和【内核空间自动重启】。

3.4 系统过程总结

系统调用的过程其实比较简单,只不过涉及到较多的细节。我们将 ARM32 架构下的系统调用过程总结如下:

1. 应用程序调用 glibc 函数 (如 write())2. glibc 通过寄存器设置好调用参数和系统调用号,然后用 swi 指令向内核发起系统调用请求;
3. 内核态调用系统调用接口;
4. 内核态设置系统调用返回值,然后恢复用户态上下文返回用户空间。

4. 后记

本篇对 ARM32 架构下的系统调用做了分析,希望能够让读者通过阅读本篇,能够对系统调用有较为清晰的认知。但很遗憾,限于时间和篇幅,我们没有涵盖相关知识的方方面面。在下两个小节里,我列举了一些读者应该补充学习的资料和信息。

4.1 还需了解的

1. 调用规范
   为什么函数用 R0 寄存器存储返回值?函数调用过程中,哪些寄存器是安全的(不用保存)?
   调用规范能回答这些问题。
   篇末附有 ARM32 架构调用规范的文档名,读者可到 ARM 官方网站下载。
2. ABI规范
   我们代码分析使用 EABI 规范上下文,它能解释为什么使用寄存器 R7 传递系统调用号等等。

4.2 如何新添一个系统调用

作为本篇的补充,我的另外一篇博文添加一个Linux内核系统调用,简短地介绍了如何在 ARM32 架构内核下添加一个系统调用。

5. 参考资料

《ARM Architecture Reference Manual.pdf》
《IHI0042J_2020Q2_aapcs32.pdf》
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值