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          | | 低地址
	 *  |-------------| |
	 *  | 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.
 */
/* 
 * 处理系统调用的脚本,编译时动态生成的文件: 
 * 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)
 *
 * 下面的 (syscall_table_start + syscall + syscall_table_end) 一起,
 * 定义了系统调用函数接口指针表 sys_call_table[__NR_syscalls] 。 
 */
	syscall_table_start sys_call_table
#define COMPAT(nr, native, compat) syscall nr, native
#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. 自动重启:配置信号,让其自动重启。自动重启又细分为【用户空间自动重启】和【内核空间自动重启】。

4. 参考资料

《ARM Architecture Reference Manual.pdf》
《IHI0042J_2020Q2_aapcs32.pdf》