Zephyr用户模式-系统调用

本文说明zephyr系统调用的原理

本文主要说明Zephyr用户模式-简介中简介的系统调用访问系统调用堆栈,和ARM相关的技术基础请参考Zephyr用户模式-技术基础

用户模式下,代码被限制使用特权指令和访问内存。由于系统调用工作在特权模式下,并且系统调用在zephyr下是可信代码,因此用户模式线程可以通过系统调用安全使用特权指令和访问受限内存。下图摘自Day1-PM-2-1 Zephyr Memory Protection - Wentong Wu》2019 IoT-Open-Source-Forum说明了系统调用 syscall

系统调用定义 链接到标题

原则 链接到标题

为确保安全性定义系统调用API有两个原则:

  1. 内核私有数据必须通过系统调用提供,不能直接提供给用户模式线程
  2. 系统调用不能将用户模式的回调函数注册进入内核执行

方法 链接到标题

对于不同的系统调用有很多工作都是一样的,为了节省代码工作量,zephyr的系统调用是显示声明系统调用API和定义系统调用实现API,隐式转换为实际系统调用。这里以device_get_binding为例,说明如何定义系统调用:

Step1-声明系统调用API 链接到标题

在header文件中用**__syscall**声明API为系统调用API,例如 device.h

__syscall struct device *device_get_binding(const char *name);

Step2-定义系统调用实现API 链接到标题

在对应的C文件添加系统调用的实现API,这部分代码会在内核下被执行,实现API名一定是固定模式名z_impl_<syscall API name>,例如device_get_binding系统调用的API对应的实现API: device.c

struct device *z_impl_device_get_binding(const char *name)
{
    ...
}

Step3-定义系统调用验证API 链接到标题

在对应的C文件中添加系统调用的验证API,验证API一定是固定模式名z_vrfy_<syscall API name>, 验证API包装实现API,与实现API具有相同的返回类型和参数类型。验证API主要是附加完成对系统调用API的参数检查和在用户模式与内核模式之间转换参数。在验证包装函数的下面include一个生成文件syscalls/<syscall API name>_mrsh.c例如: device.c

static inline struct device *z_vrfy_device_get_binding(const char *name)
{
	char name_copy[Z_DEVICE_MAX_NAME_LEN];

	if (z_user_string_copy(name_copy, (char *)name, sizeof(name_copy))
	    != 0) {
		return 0;
	}

	return z_impl_device_get_binding(name_copy);
}

#include <syscalls/device_get_binding_mrsh.c>

系统调用生成 链接到标题

生成文件 链接到标题

上一小节说明了代码中如何定义一个系统调用,这一节来看这些是如何串接生成的系统调用,系统调用主要是由script/gen_syscall.py扫描头文件和c文件,将去发现__syscall, z_impl_,z_vrfy_,然后按规则在编译文件夹的 build/zephyr/include/generated/下生成下面的文件:

公共文件 链接到标题

syscall_list.h
syscall_dispatch.c
driver-validation.h

syscall_list.h 中生成的是各系统调用号,例如

...
#define K_SYSCALL_DEVICE_GET_BINDING 23             //device_get_binding的系统调用号
....
#define K_SYSCALL_SENSOR_ATTR_SET 163
#define K_SYSCALL_SENSOR_CHANNEL_GET 164
#define K_SYSCALL_SENSOR_SAMPLE_FETCH 165
#define K_SYSCALL_SENSOR_SAMPLE_FETCH_CHAN 166
...
#define K_SYSCALL_BAD 213
#define K_SYSCALL_LIMIT 214

syscall_dispatch.c中生成的是系统调用表和每个系统调用的弱符号,当用户代码进行了某个系统调用时如果配置未启动对应的内核代码,则调用弱符号程序handler_no_syscall。 表中对应了系统调用内核调用函数。

__weak ALIAS_OF(handler_no_syscall)
u32_t z_mrsh_device_get_binding(u32_t arg1, u32_t arg2, u32_t arg3,
         u32_t arg4, u32_t arg5, u32_t arg6, void *ssf);

__weak ALIAS_OF(handler_no_syscall)
u32_t z_mrsh_sensor_attr_set(u32_t arg1, u32_t arg2, u32_t arg3,
         u32_t arg4, u32_t arg5, u32_t arg6, void *ssf);

__weak ALIAS_OF(handler_no_syscall)
u32_t z_mrsh_sensor_sample_fetch(u32_t arg1, u32_t arg2, u32_t arg3,
         u32_t arg4, u32_t arg5, u32_t arg6, void *ssf);

__weak ALIAS_OF(handler_no_syscall)
u32_t z_mrsh_sensor_sample_fetch_chan(u32_t arg1, u32_t arg2, u32_t arg3,
         u32_t arg4, u32_t arg5, u32_t arg6, void *ssf);

__weak ALIAS_OF(handler_no_syscall)
u32_t z_mrsh_sensor_channel_get(u32_t arg1, u32_t arg2, u32_t arg3,
         u32_t arg4, u32_t arg5, u32_t arg6, void *ssf);

const _k_syscall_handler_t _k_syscall_table[K_SYSCALL_LIMIT] = {
	[K_SYSCALL_DEVICE_GET_BINDING] = z_mrsh_device_get_binding,
    ...
    [K_SYSCALL_SENSOR_ATTR_SET] = z_mrsh_sensor_attr_set,
	[K_SYSCALL_SENSOR_SAMPLE_FETCH] = z_mrsh_sensor_sample_fetch,
	[K_SYSCALL_SENSOR_SAMPLE_FETCH_CHAN] = z_mrsh_sensor_sample_fetch_chan,
	[K_SYSCALL_SENSOR_CHANNEL_GET] = z_mrsh_sensor_channel_get,
    ...
    [K_SYSCALL_BAD] = handler_bad_syscall
};

driver-validation.h 内生成的是各个驱动系统调用的检查宏,本文不做详细介绍。

各系统调用的文件 链接到标题

syscalls/<file>.h                                               
syscalls/<syscall API name>_mrsh.c      //系统调用代码

<file>.h 这个文件内列出并实现了所有系统调用的用户模式代码。<file>.h中有多少个系统调用,就对应多少个<syscall API name>_mrsh.c 文件,这些文件内实现了系统调用内核模式的代码 以device为例生成的就是

syscalls/device.h
syscalls/device_get_binding_mrsh.c

以sensor为例生成的就是

syscalls/sensor.h
syscalls/sensor_sample_fetch_mrsh.c
syscalls/sensor_sample_fetch_chan_mrsh.c
syscalls/sensor_channel_get_mrsh.c
syscalls/sensor_attr_set_mrsh.c

各系统调用文件说明 链接到标题

这里还是以device为例展开说明 syscalls/device.h是device_get_binding用户模式下的实现:

extern struct device * z_impl_device_get_binding(const char * name);
static inline struct device * device_get_binding(const char * name)
{
#ifdef CONFIG_USERSPACE     //如果不支援用户模式,就直接执行系统调用实现z_impl_device_get_binding
	if (z_syscall_trap()) {//当前是用户模式才执行系统调用, kerner mode还是直接执行z_impl_device_get_binding
		return (struct device *) z_arch_syscall_invoke1(*(u32_t *)&name, K_SYSCALL_DEVICE_GET_BINDING);
	}
#endif
	compiler_barrier();
	return z_impl_device_get_binding(name);
}

生成的syscalls/device.h将被include/device.h包含(见前文代码),因此呼叫device_get_binding的时候就是呼叫这个API。

syscalls/device_get_binding_mrsh.c是device_get_binding在内核中系统调用的实现,可以看到最后调用到的是device.c中的z_vrfy_device_get_binding

extern struct device * z_vrfy_device_get_binding(const char * name);
u32_t z_mrsh_device_get_binding(u32_t arg0, u32_t arg1, u32_t arg2,
		u32_t arg3, u32_t arg4, u32_t arg5, void *ssf)
{
	_current_cpu->syscall_frame = ssf;
	(void) arg1;	/* unused */
	(void) arg2;	/* unused */
	(void) arg3;	/* unused */
	(void) arg4;	/* unused */
	(void) arg5;	/* unused */
	struct device * ret = z_vrfy_device_get_binding(*(const char **)&arg0)
;
	return (u32_t) ret;
}

系统调用过程 链接到标题

下图摘自《Day1-PM-2-1 Zephyr Memory Protection - Wentong Wu》2019 IoT-Open-Source-Forum,说明了系统调用的代码过程: syscallflow

这一节以device_get_binding为例,将前面的零散的分析对应上图串起来,看一下用户模式代码如何通过系统调用进入内核模式执行内核代码的,

用户空间 链接到标题

static inline struct device * device_get_binding(const char * name)
{
	if (z_syscall_trap()) {
		return (struct device *) z_arch_syscall_invoke1(*(u32_t *)&name, K_SYSCALL_DEVICE_GET_BINDING);
	}
	compiler_barrier();
	return z_impl_device_get_binding(name);
}

先展开看z_syscall_trap zephyr/include/syscall.h

static ALWAYS_INLINE bool z_syscall_trap(void)
{
	bool ret = false;
	ret = z_arch_is_user_context();
	return ret;
}

通过z_arch_is_user_context判断是否在用户空间 zephyr/include/arch/arm/syscall.h

static inline bool z_arch_is_user_context(void)
{
	u32_t value;

	//通过寄存器IPSR判断目前是否在exception处理中,如果是exception处理,就已经在内核模式,无需再进行系统调用
	__asm__ volatile("mrs %0, IPSR\n\t" : "=r"(value));
	if (value) {
		return false;
	}

	//通过寄存器CONTROL的bit0判断目前是否在特权模式下,如果是特权模式说明是内核模式线程,无需再进行系统调用
	__asm__ volatile("mrs %0, CONTROL\n\t" : "=r"(value));
	return (value & 0x1) ? true : false;
}

确认是在用户模式后,开始系统调用zephyr/include/arch/arm/syscall.h 的系统调用函数发起系统调用

z_arch_syscall_invoke1(*(u32_t *)&name, K_SYSCALL_DEVICE_GET_BINDING);

static inline u32_t z_arch_syscall_invoke1(u32_t arg1, u32_t call_id)
{
	register u32_t ret __asm__("r0") = arg1;        //r0 保存系统调用参数&name
	register u32_t r6 __asm__("r6") = call_id;      //r6 保存系统调用号K_SYSCALL_DEVICE_GET_BINDING

	__asm__ volatile("svc %[svid]\n"                    //通过svc _SVC_CALL_SYSTEM_CALL指令进入svc exception
			 : "=r"(ret)
			 : [svid] "i" (_SVC_CALL_SYSTEM_CALL),
			   "r" (ret), "r" (r6)
			 : "r8", "memory", "r1", "r2", "r3");
	return ret;
}

svc号定义在zephyr/include/arch/arm/syscall.h系统调用的为3

#define _SVC_CALL_SYSTEM_CALL		3

svc过程 链接到标题

zephyr/arch/arm/core/swap_helper.S中包含了SVC处理exception,系统调用摘要代码如下:

SECTION_FUNC(TEXT, __svc)
    tst lr, #0x4    /* did we come from thread mode ? */
    ite eq  /* if zero (equal), came from handler mode */
        mrseq r0, MSP   /* handler mode, stack frame is on MSP */
        mrsne r0, PSP   /* thread mode, stack frame is on PSP */

    //从堆栈中取出svc number放入r1
    ldr r1, [r0, #24]   
    ldrh r1, [r1, #-2]

    ands r1, #0xff
#if defined(CONFIG_USERSPACE)
    mrs r2, CONTROL

    //如果是3系统调用,开始执行系统调用
    cmp r1, #3
    beq _do_syscall

    //不是正常的svc,跳到_oops处理
    tst r2, #0x1
    bne _oops


_do_syscall:
    //将系统调用z_arm_do_syscall函数放入堆栈中PC的位置
    ldr r8, [r0, #24]   /* grab address of PC from stack frame */
    ldr r1, =z_arm_do_syscall
    str r1, [r0, #24]   /* overwrite the PC to point to z_arm_do_syscall */

    //r6中保存了系统调用号,这里检查r6中系统调用号是否有效
    ldr ip, =K_SYSCALL_LIMIT
    cmp r6, ip
    //系统调用有效,跳到valid_syscall_id处理
    blt valid_syscall_id

    //系统调用号无效,重写K_SYSCALL_BAD到r6
    str r6, [r0, #0]
    ldr r6, =K_SYSCALL_BAD

    /* Bad syscalls treated as valid syscalls with ID K_SYSCALL_BAD. */

valid_syscall_id:
    push {r0, r1}
    ldr r0, =_kernel
    ldr r0, [r0, #_kernel_offset_to_current]
    ldr r1, [r0, #_thread_offset_to_mode]
    bic r1, #1
    /* Store (privileged) mode in thread's mode state variable */
    str r1, [r0, #_thread_offset_to_mode]
    dsb
    //从用户模式切换到内核模式(特权级)
    bic r2, #1
    msr CONTROL, r2

    /* ISB is not strictly necessary here (stack pointer is not being
     * touched), but it's recommended to avoid executing pre-fetched
     * instructions with the previous privilege.
     */
    isb
    pop {r0, r1}

    //函数返回,由于之前修改了堆栈中PC位置的值为z_arm_do_syscall, 因此这里返回后将执行z_arm_do_syscall
     bx lr
#endif

z_arm_do_syscall在zephyr/arch/arm/core/userspace.S中

SECTION_FUNC(TEXT, z_arm_do_syscall)
    //获取用户模式线程特权模式的专用堆栈
    ldr ip, =_kernel
    ldr ip, [ip, #_kernel_offset_to_current]
    ldr ip, [ip, #_thread_offset_to_priv_stack_start]    /* priv stack ptr */
    add ip, #CONFIG_PRIVILEGED_STACK_SIZE

    //将堆栈指针切换到用户线程特权模式的专用堆栈上
    subs ip, #8
    str sp, [ip, #0]
    str lr, [ip, #4]

    msr PSP, ip

    //检查是否是错误的系统调用
    ldr ip, =K_SYSCALL_BAD
    cmp r6, ip
    //调到系统调用函数执行
    bne valid_syscall

    /* BAD SYSCALL path */
    /* fixup stack frame on the privileged stack, adding ssf */
    mov ip, sp
    push {r4,r5,ip,lr}
    //错误的系统调用处理
    b dispatch_syscall

valid_syscall:
    /* push args to complete stack frame */
    push {r4,r5}

dispatch_syscall:
    //在系统调用表_k_syscall_table查找r6中保存的系统调用号对应的系统调用函数,并放到ip中。
    //我们分析的例子里这时r6=K_SYSCALL_DEVICE_GET_BINDING
    //因此ip是syscall_dispatch.c的_k_syscall_table[K_SYSCALL_DEVICE_GET_BINDING] = z_mrsh_device_get_binding
    ldr ip, =_k_syscall_table
    lsl r6, #2
    add ip, r6
    ldr ip, [ip]	/* load table address */
    /* execute function from dispatch table */
    //跳到找到的系统调用函数执行,这里是z_mrsh_device_get_binding
    blx ip

    /* restore LR */
    ldr lr, [sp,#12]

    //切回用户线程堆栈
    ldr ip, [sp,#8]
    msr PSP, ip

    push {r0, r1}
    ldr r0, =_kernel
    ldr r0, [r0, #_kernel_offset_to_current]
    ldr r1, [r0, #_thread_offset_to_mode]
    orrs r1, r1, #1
    /* Store (unprivileged) mode in thread's mode state variable */
    str r1, [r0, #_thread_offset_to_mode]
    dsb
    
    //切回用户模式
    mrs ip, CONTROL
    orrs ip, ip, #1
    msr CONTROL, ip

    /* ISB is not strictly necessary here (stack pointer is not being
     * touched), but it's recommended to avoid executing pre-fetched
     * instructions with the previous privilege.
     */
    isb
    pop {r0, r1}

    /* Zero out volatile (caller-saved) registers so as to not leak state from
     * kernel mode. The C calling convention for the syscall handler will
     * restore the others to original values.
     */
    mov r1, #0
    mov r2, #0
    mov r3, #0

   //从svc返回
    mov ip, r8
    orrs ip, ip, #1
    bx ip

内核空间 链接到标题

内核空间的系统调用实际处理函数关系如下: 在系统调用svc下找到 zephyr/include/generated/syscall_dispatch.c _k_syscall_table[K_SYSCALL_DEVICE_GET_BINDING] = z_mrsh_device_get_binding 之后的调用关系 syscalls/device_get_binding_mrsh.c z_mrsh_device_get_binding zephyr/kernel/device.c z_impl_device_get_binding 其它的系统调用也是一样的套路

系统调用的其它说明 链接到标题

参数传递 链接到标题

参考zephyr/include/arch/arm/syscall.h,以下几点

  • 输入参数小于等于6个时分别用r0~r5传递
  • 输入参数大于6个时r0~r4传递前5个,r5作为指针传递其它参数
  • r6传递系统调用号

数据传递 链接到标题

用户模式和内核模式传递数据需要专用的API,数据传递都是在系统调用的内核模式内执行的 int z_user_to_copy(void *dst, const void *src, size_t size) 从内核拷贝数据到用户 – 检查dst是否有写权限 int z_user_from_copy(void *dst, const void *src, size_t size) 从用户拷贝数据到内核 – 检查src是否有读权限

系统调用的堆栈 链接到标题

在编译后期gen_priv_stacks.py脚本将扫描elf文件,分析找出其中的线程堆栈对象,生成priv_stacks_hash.c文件,在该文件中并为每个堆栈对象建立一个大小为CONFIG_PRIVILEGED_STACK_SIZE的privileged堆栈:

static u8_t __used __aligned(Z_PRIVILEGE_STACK_ALIGN) priv_stack_20007800[CONFIG_PRIVILEGED_STACK_SIZE];
static u8_t __used __aligned(Z_PRIVILEGE_STACK_ALIGN) priv_stack_20008000[CONFIG_PRIVILEGED_STACK_SIZE];
...

当某个线程进入用户模式时(或是创建成用户模式线程),会将priv_stack查找出来并放到priv_stack_start中,将其作为系统调用时特权模式下的堆栈 zephyr/arch/arm/core/thread.c

FUNC_NORETURN void z_arch_user_mode_enter(k_thread_entry_t user_entry,
	void *p1, void *p2, void *p3)
{

	/* Set up privileged stack before entering user mode */
	_current->arch.priv_stack_start =
		(u32_t)z_priv_stack_find(_current->stack_obj);
}

查找函数z_priv_stack_find也是在priv_stacks_hash.c, 这里是gen_priv_stacks.py扫描elf文件,生成priv_stacks_hash.gperf文件,然后由gperf工具生成priv_stacks_hash.c,目标是建立一个hash查询函数,gperf细节可自行查找。 前面系统调用的分析代码有对应汇编代码分析如何切换,这里不做详细分析,流程如下: 在系统调用期间,用户模式线程对系统调用的访问权限以及传入的参数进行验证。然后将用户模式线程提升为特权模式,将堆栈切换为特权堆栈priv_stack_,然后调用内核API。从内核API返回时,线程将被设置回用户模式,堆栈将还原到用户堆栈。

总结 链接到标题

系统调用和生成的整个流程可总结如下图: syscallcall

参考 链接到标题

https://docs.zephyrproject.org/latest/reference/usermode/syscalls.html Day1-PM-2-1 Zephyr Memory Protection - Wentong Wu — IoT-Open-Source-Forum-06-11-2019