Zephyr用户模式-内核对象

本文简要说明zephyr内核对象原理

为了系统的安全性,Zephyr的用户模式线程只能在被授权后通过系统调用访问内核对象,本文通过分析代码来看zephyr如何实现这些功能。

内核对象简介 链接到标题

zephyr的内核对象可以分为以下三种:

  • 核心内核对象,例如信号量,线程,PIPE等
  • 线程堆栈(由K_THREAD_STACK_DEFINE定义)
  • subsystem中的设备驱动(struct device)实例 详细的可以在gen_kobject_list.py中找到
kobjects = OrderedDict([
    ("k_mem_slab", (None, False)),
    ("k_msgq", (None, False)),
    ("k_mutex", (None, False)),
    ("k_pipe", (None, False)),
    ("k_queue", (None, False)),
    ("k_poll_signal", (None, False)),
    ("k_sem", (None, False)),
    ("k_stack", (None, False)),
    ("k_thread", (None, False)),
    ("k_timer", (None, False)),
    ("_k_thread_stack_element", (None, False)),
    ("device", (None, False)),
    ("sys_mutex", (None, True)),
    ("k_futex", (None, True))
])

subsystems = [
    "adc_driver_api",
    "aio_cmp_driver_api",
    "counter_driver_api",
    "crypto_driver_api",
    "dma_driver_api",
    "flash_driver_api",
    "gpio_driver_api",
    "i2c_driver_api",
    "i2s_driver_api",
    "ipm_driver_api",
    "led_driver_api",
    "pinmux_driver_api",
    "pwm_driver_api",
    "entropy_driver_api",
    "sensor_driver_api",
    "spi_driver_api",
    "uart_driver_api",
    "can_driver_api",
    "ptp_clock_driver_api",
]

内核对象的产生 链接到标题

内核对象分为静态和动态两种,由于动态内核对象在目前的zephyr代码中没有使用,因此不做详细说明。

静态内核对象 链接到标题

过程 链接到标题

静态内核对象struct _k_object是隐式生成,当定义的内核对象变量被编译入elf后,gen_kobject_list.py脚本会扫描elf文件,生成kobject_hash.gperf文件,然后由gperf工具生成kobject_hash.c,目标是建立一个内核对象hash查询函数。对于某个内核对象来说,这个过程是在elf中查找内核对象,并将它们的内存地址放在内核对象元hash表中,达到生成struct _k_object的目的。以dht驱动为例 在代码中的定义:

DEVICE_AND_API_INIT(dht_dev, CONFIG_DHT_NAME, &dht_init, &dht_data,
		    NULL, POST_KERNEL, CONFIG_SENSOR_INIT_PRIORITY, &dht_api);

展开后相当于是定义了一个全局的struct device

struct device __device_dht_dev = {};

在__device_dht_dev编译进elf后,gen_kobject_list.py对elf中的struct device的符号进行扫描,获取到__device_dht_dev和其内存地址0x2000c1b0

gen_kobject_list.py: symbol '__device_dht_dev' at 0x2000c1b0 contains 1 object(s)

然后在kobject_hash.c中产生的hash表中就生成了对应的struct _k_object

static struct _k_object wordlist[] =
{
    ...
     {(char *)0x2000c1b0, {}, K_OBJ_DRIVER_SENSOR, 0 | K_OBJ_FLAG_DRIVER, 0},
     ...
}

限制条件 链接到标题

  • 内核对象必须位于内核保留内存中(不会被用户线程通过内存地址直接访问)
  • 内核对象必须是全局变量(才能出现在elf符号表中,而被脚本扫描到)

动态内核对象 链接到标题

CONFIG_DYNAMIC_OBJECTS配置后可在运行时分配内核对象,用下面几个API进行操作

  • void *k_object_alloc(enum k_objects otype) 从被调用的线程池中分配内核对象
  • void k_object_free(void *obj) 特权线程使用该API强制释放内核对象
  • void k_object_release(void *object) 用户线程使用该API放弃内核对象的权限,当动态内核对象不再被引用时将会自动释放
  • void k_object_access_revoke(void *object, struct k_thread *thread) 特权线程使用该API撤销用户线程对内核对象的权限 和静态对象使用hash表不一样,动态内核对象使用红黑树管理

内核对象的使用 链接到标题

本节以一个实例说明用户线程如何使用内核对象

// use_space_test是在特权线程下面执行的代码
void use_space_test(void)
{
    //创建一个用户线程tPT,该用户线程希望访问dht11驱动
    tPT = k_thread_create(&pt_thread, pt_stack, STACKSIZE,
			(k_thread_entry_t)pt, NULL, NULL, NULL,
			-1, K_USER,
			K_FOREVER);
     //获取dht11内核对象       
	struct device *dev = device_get_binding("DHT11"); 
    //在特权线程中给予用户tPT访问dht11的权限
	k_thread_access_grant(tPT, dev);
}

// pt是在用户空间中执行
void pt(void)
{
    while(1)
    {
        struct sensor_value temp, humidity;
        struct device *dev1 = device_get_binding("DHT11"); 
        //由于在特权线程中,给tPT赋予了dht11的权限,因此这里可以访问dht11
        sensor_sample_fetch(dev1);								  
        sensor_channel_get(dev1, SENSOR_CHAN_AMBIENT_TEMP, &temp);
        sensor_channel_get(dev1, SENSOR_CHAN_HUMIDITY, &humidity);
        printk("temp: %d.%06d; humidity: %d.%06d\n",
		   temp.val1, temp.val2, humidity.val1, humidity.val2);

           k_sleep(5000);
    }
}

如果在特权线程中没有使用**k_thread_access_grant(tPT, dev)**给予tPT权限,运行sensor_sample_fetch的时候将看到下面exception发生

thread 0x2000076c (1) does not have permission on sensor driver 0x2000c1b0 [0000]
syscall z_vrfy_sensor_sample_fetch failed check: access denied
[00:00:02.037,139] <err> os: r0/a1:  0x00000000  r1/a2:  0x00000000  r2/a3:  0x00000000
[00:00:02.037,139] <err> os: r3/a4:  0x00000000 r12/ip:  0x00000000 r14/lr:  0x00000000
[00:00:02.037,139] <err> os:  xpsr:  0x00000000
[00:00:02.037,139] <err> os: Faulting instruction address (r15/pc): 0x00000000
[00:00:02.037,170] <err> os: >>> ZEPHYR FATAL ERROR 3: Kernel oops
[00:00:02.037,170] <err> os: Current thread: 0x2000076c (unknown)
[00:00:02.110,748] <err> os: Halting system

内核对象控制原理 链接到标题

本节来看一下前面说的权限控制是如何实现的。

权限存放位置 链接到标题

从上一节的使用可以看到,内核对象会判断每个用户线程对它的使用权限,这个权限的标志就放在内核对象结构体中的perms

struct _k_object {
	char *name;
	u8_t perms[CONFIG_MAX_THREAD_BYTES];
	u8_t type;
	u8_t flags;
	u32_t data;
} __packed __aligned(4);

在perms中一个用户线程占用一个bit,当某个用户线程拥有操作该内核对象的权限时,对应的bit就被置1。

权限设置原理 链接到标题

可以在特权线程下使用k_thread_access_grant让用户线程获取内核对象的权限 k_thread_access_grant -> z_impl_k_object_access_grant

void z_impl_k_object_access_grant(void *object, struct k_thread *thread)
{
	struct _k_object *ko = z_object_find(object);       //查找object对应的_k_object,也就是前面说的放在kobject_hash.c的内核对象元

	if (ko != NULL) {
		z_thread_perms_set(ko, thread);  //设置thread对该对象的权限
	}
}

void z_thread_perms_set(struct _k_object *ko, struct k_thread *thread)
{
	int index = thread_index_get(thread);       //获取thread的index

	if (index != -1) {
		sys_bitfield_set_bit((mem_addr_t)&ko->perms, index);    //将perms对应的bit置1
	}
}

要想内核对象能支援用户线程的数量由CONFIG_MAX_THREAD_BYTES决定,可以根据你系统使用用户线程的情况来设置CONFIG_MAX_THREAD_BYTES的大小

权限判断原理 链接到标题

用户线程使用内核对象时必须通过系统调用,在系统调用时会首先对执行系统调用的线程进行内核对象权限的检查,对不同的内核对象检查的宏不太一样,例如:

Z_OOPS(Z_SYSCALL_OBJ(q, K_OBJ_MSGQ));
Z_OOPS(Z_SYSCALL_DRIVER_ADC(dev, channel_setup));
Z_OOPS(Z_SYSCALL_DRIVER_SENSOR(dev, sample_fetch));

但最后都包装到宏Z_SYSCALL_OBJ:

Z_OOPS(Z_SYSCALL_OBJ(obj, type));

Z_SYSCALL_OBJ按照如下展开 Z_SYSCALL_OBJ->Z_SYSCALL_IS_OBJ->Z_SYSCALL_VERIFY_MSG(z_obj_validation_check(z_object_find((void *)ptr), (void *)ptr, type, init) == 0, “access denied”), 因此最后是z_obj_validation_check进行检:

static inline int z_obj_validation_check(struct _k_object *ko,
					void *obj,
					enum k_objects otype,
					enum _obj_init_check init)
{
	int ret;
    //检查当前thread对ko是否由访问权限
	ret = z_object_validate(ko, otype, init);

#ifdef CONFIG_PRINTK
	if (ret != 0) {
        //如果检查没有权限(EPERM),将通过这里打印出来原因(dump_permission_error)
		z_dump_object_error(ret, obj, ko, otype);
	}
#else
	ARG_UNUSED(obj);
#endif

	return ret;
}

//z_object_validate检查权限就是调用thread_perms_test完成,这里对thread_perms_test进行分析

static int thread_perms_test(struct _k_object *ko)
{
	int index;

	if ((ko->flags & K_OBJ_FLAG_PUBLIC) != 0U) {
		return 1;
	}
    //获取当前thrad index
	index = thread_index_get(_current);
	if (index != -1) {
        //检查perms对应的bit是否为1
		return sys_bitfield_test_bit((mem_addr_t)&ko->perms, index);
	}
	return 0;
}

Z_SYSCALL_OBJ检查完毕后,系统调用会通过Z_OOPS来判断Z_SYSCALL_OBJ的检查结果,如果结果不过Z_OOPS就会抛出exception

#define Z_OOPS(expr) \
	do { \
		if (expr) { \
			z_arch_syscall_oops(_current_cpu->syscall_frame); \
		} \
	} while (false)

参考 链接到标题

https://docs.zephyrproject.org/latest/reference/usermode/kernelobjects.html