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 的各种工作模式
我们重点关注上图中标注的 User
和 Supervisor
模式。
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》