24. OP-TEE驅動篇----驅動編譯,加載和初始化(一)

    歷經一年多時間的系統整理合補充,《手機安全和可信應用開發指南:TrustZone與OP-TEE技術詳解 》一書得以出版,書中詳細介紹了TEE以及系統安全中的所有內容,全書按照從硬件到軟件,從用戶空間到內核空間的順序對TEE技術詳細闡述,讀者可從用戶空間到TEE內核一步一步瞭解系統安全的所有內容,同時書中也提供了相關的示例代碼,讀者可根據自身實際需求開發TA。目前該書已在天貓、京東、噹噹同步上線,鏈接如下(麻煩書友購書時能給予評論,多謝多謝)

京東購買地址

噹噹購買地址

天貓購買地址

非常感謝在此期間大家的支持以及各位友人的支持和幫助!!!。

爲方便和及時的回覆讀者對書中或者TEE相關的問題的疑惑,也爲了大家能有一個統一的交流平臺。我搭建了一個簡單的論壇,網址如下:

https://www.huangtengxq.com/discuz/forum.php

關於您的疑問可在“相關技術討論“”中發帖,我會逐一回復。也歡迎大家發帖,一起討論TEE相關的一些有意思的feature。共同交流。同時該論壇中也會添加關於移動端虛擬化的相關技術的板塊,歡迎各位共同交流學習

 

 OP-TEE驅動主要作用是REE與TEE端進行數據交互的橋樑作用。tee_supplicant和libteec調用接口之後幾乎都會首先通過系統調用陷入到kernel space,然後kernel根據傳遞的參數找到OP-TEE驅動,並命中驅動的operation結構體中的具體處理函數來完成實際的操作,對於OP-TEE驅動,一般都會觸發SMC調用,並帶參數進入到ARM cortex的monitor模式,在monitor模式中對執行normal world和secure world的切換,待狀態切換完成之後,會將驅動端帶入的參數傳遞給OP-TEE中的thread進行進一步的處理。OP-TEE驅動的源代碼存放在linux/drivers/tee目錄中,其內容如下;

1. OP-TEE驅動的加載

  OP-TEE驅動的加載過程分爲兩部分,第一部分是創建class和分配設備號,第二部分就是probe過程。在正式介紹之前首先需要明白兩個linux kernel中加載驅動的函數:subsys_initcall和module_init函數。OP-TEE驅動的第一部分是調用subsys_initcall函數來實現,而第二部分則是調用module_init來實現。整個OP-TEE驅動的初始化流程圖如下圖所示:

1.1 OP-TEE驅動模塊的編譯後的存放位置和加載過程

  OP-TEE驅動通過subsys_initcall和module_init宏來告知系統在初始化的什麼時候去加載OP-TEE驅動,subsys_initcall定義在linux/include/init.h文件中,內容如下:

 

#define __define_initcall(fn, id) \
	static initcall_t __initcall_##fn##id __used \
	__attribute__((__section__(".initcall" #id ".init"))) = fn;

#define core_initcall(fn)		__define_initcall(fn, 1)
#define core_initcall_sync(fn)		__define_initcall(fn, 1s)
#define postcore_initcall(fn)		__define_initcall(fn, 2)
#define postcore_initcall_sync(fn)	__define_initcall(fn, 2s)
#define arch_initcall(fn)		__define_initcall(fn, 3)
#define arch_initcall_sync(fn)		__define_initcall(fn, 3s)
#define subsys_initcall(fn)		__define_initcall(fn, 4)
#define subsys_initcall_sync(fn)	__define_initcall(fn, 4s)
#define fs_initcall(fn)			__define_initcall(fn, 5)
#define fs_initcall_sync(fn)		__define_initcall(fn, 5s)
#define rootfs_initcall(fn)		__define_initcall(fn, rootfs)
#define device_initcall(fn)		__define_initcall(fn, 6)
#define device_initcall_sync(fn)	__define_initcall(fn, 6s)
#define late_initcall(fn)		__define_initcall(fn, 7)
#define late_initcall_sync(fn)		__define_initcall(fn, 7s)

  使用subsys_initcall宏定義的函數最終會被編譯到.initcall4.init段中,linux系統在啓動的時候會執行initcallx.init段中的所有內容,而使用subsys_initcall宏定義段的執行優先級爲4.

 

module_init的定義和相關擴展在linux/include/linux/module.h文件和linux/include/linux/init.h中,內容如下:

 

#define device_initcall(fn)		__define_initcall(fn, 6)
#define __initcall(fn) device_initcall(fn)
#define module_init(x)  __initcall(x);

  由此可見,使用module_init宏構造的函數將會在編譯的時候被編譯到initcall6.init段中,該段在linux系統啓動的過程中的優先等級爲6.

 

  結合上述兩點看,在系統加載OP-TEE驅動的時候,首先會執行OP-TEE驅動中使用subsys_init定義的函數,然後再執行使用module_init定義的函數。在OP-TEE驅動源代碼中使用subsys_init定義的函數爲tee_init,使用module_init定義的函數爲optee_driver_init。

1.2 tee_init函數初始化設備號和class

  該函數定義在linux/drivers/tee/tee_core.c文件中,主要完成class的創建和設備號的分配,其內容如下:

static int __init tee_init(void)
{
	int rc;

/* 分配OP-TEE驅動的class */
	tee_class = class_create(THIS_MODULE, "tee");
	if (IS_ERR(tee_class)) {
		pr_err("couldn't create class\n");
		return PTR_ERR(tee_class);
	}

/* 分配OP-TEE的設備號 */
	rc = alloc_chrdev_region(&tee_devt, 0, TEE_NUM_DEVICES, "tee");
	if (rc) {
		pr_err("failed to allocate char dev region\n");
		class_destroy(tee_class);
		tee_class = NULL;
	}

	return rc;
}

設備號和class將會在驅動執行probe的時候被使用到

 

1.3 optee_driver_init函數執行

  linux啓動過程中會執行moudule_init宏定義的函數,在OP-TEE驅動的掛載過程中將會執行optee_driver_init函數,該函數定義在linux/drivers/tee/optee/core.c文件中,其內容如下:

static int __init optee_driver_init(void)
{
	struct device_node *fw_np;
	struct device_node *np;
	struct optee *optee;

	/* Node is supposed to be below /firmware */
/* 從device tree中查找到firware的節點 */
	fw_np = of_find_node_by_name(NULL, "firmware");
	if (!fw_np)
		return -ENODEV;

/* 匹配device tree中firmware節點下節點爲linaro,optee-tz內容的節點 */
	np = of_find_matching_node(fw_np, optee_match);
	of_node_put(fw_np);
	if (!np)
		return -ENODEV;

/* 使用查找到的節點執行OP-TEE驅動的probe操作 */
	optee = optee_probe(np);
	of_node_put(np);

	if (IS_ERR(optee))
		return PTR_ERR(optee);
/* 保存初始化完成之後OP-TEE的設備信息到optee_svc中,以備在卸載的是使用 */
	optee_svc = optee;

	return 0;
}

 

2. OP-TEE驅動初始化時的probe操作

  OP-TEE驅動在optee_driver_init函數來完成probe操作。該函數首先會通過device tree找到OP-TEE驅動設備信息,然後將獲取到的信息傳遞給optee_probe函數執行probe操作。optee_probe函數內容如下:

 

 

static struct optee *optee_probe(struct device_node *np)
{
	optee_invoke_fn *invoke_fn;
	struct tee_shm_pool *pool;
	struct optee *optee = NULL;
	void *memremaped_shm = NULL;
	struct tee_device *teedev;
	u32 sec_caps;
	int rc;

/* 獲取OP-TEE驅動在device tree中節點描述內容中定義的執行切換到monitor模式的接口 */
	invoke_fn = get_invoke_func(np);
	if (IS_ERR(invoke_fn))
		return (void *)invoke_fn;

/* 調用到secure world中獲取API的版本信息是否匹配 */
	if (!optee_msg_api_uid_is_optee_api(invoke_fn)) {
		pr_warn("api uid mismatch\n");
		return ERR_PTR(-EINVAL);
	}

/* 調用到secure world中獲取版本信息檢查是否匹配 */
	if (!optee_msg_api_revision_is_compatible(invoke_fn)) {
		pr_warn("api revision mismatch\n");
		return ERR_PTR(-EINVAL);
	}

/* 調用到secure world中獲取secure world是否reserved share memory */
	if (!optee_msg_exchange_capabilities(invoke_fn, &sec_caps)) {
		pr_warn("capabilities mismatch\n");
		return ERR_PTR(-EINVAL);
	}

	/*
	 * We have no other option for shared memory, if secure world
	 * doesn't have any reserved memory we can use we can't continue.
	 */
/* 判定sercure world中是否reserve了share memory,如果沒有則報錯 */
	if (!(sec_caps & OPTEE_SMC_SEC_CAP_HAVE_RESERVED_SHM))
		return ERR_PTR(-EINVAL);

/* 配置secure world與驅動之間的share memory,並進行地址映射建立共享內存池 */
	pool = optee_config_shm_memremap(invoke_fn, &memremaped_shm);
	if (IS_ERR(pool))
		return (void *)pool;

/* 在kernel space內存空間中分配一塊內存用於存放OP-TEE驅動的結構體變量 */
	optee = kzalloc(sizeof(*optee), GFP_KERNEL);
	if (!optee) {
		rc = -ENOMEM;
		goto err;
	}

/* 將驅動用於實現進入monitor模式的接口賦值到optee結構體中的invoke_fn成員中 */
	optee->invoke_fn = invoke_fn;

/* 分配設備信息,填充被libteec使用的驅動文件信息和operation結構體並創建/dev/tee0文
件,libteec將會使用該文件來使用op-tee驅動 */
	teedev = tee_device_alloc(&optee_desc, NULL, pool, optee);
	if (IS_ERR(teedev)) {
		rc = PTR_ERR(teedev);
		goto err;
	}
	optee->teedev = teedev;	//libteec使用的驅動文件信息填充到optee中的teedev成員中

/* 分配設備信息,填充被tee_supplicant使用的驅動文件信息和operation結構體並創
   建/dev/teepriv0文件,tee_supplicant將會使用該文件來使用op-tee驅動 */
	teedev = tee_device_alloc(&optee_supp_desc, NULL, pool, optee);
	if (IS_ERR(teedev)) {
		rc = PTR_ERR(teedev);
		goto err;
	}
//將tee_supplicant使用的驅動文件信息填充到optee中的supp_teedev成員中
	optee->supp_teedev = teedev;	

/* 將被libteec使用的設備信息註冊到系統設備中 */
	rc = tee_device_register(optee->teedev);
	if (rc)
		goto err;

/* 將被tee_supplicant使用的設備信息註冊到系統設備中 */
	rc = tee_device_register(optee->supp_teedev);
	if (rc)
		goto err;

	mutex_init(&optee->call_queue.mutex);
	INIT_LIST_HEAD(&optee->call_queue.waiters);

 /* 初始化RPC操作隊列 */
	optee_wait_queue_init(&optee->wait_queue);

 /* 初始化被tee_supplicant用到的用於存放來自TA的請求的隊列 */
	optee_supp_init(&optee->supp);

/* 填充optee中的共享內存地址信息和共享內存池信息成員 */
	optee->memremaped_shm = memremaped_shm;	
	optee->pool = pool;

/* 使能共享內存的cache */
	optee_enable_shm_cache(optee);

	pr_info("initialized driver\n");
	return optee;
err:
	if (optee) {
		/*
		 * tee_device_unregister() is safe to call even if the
		 * devices hasn't been registered with
		 * tee_device_register() yet.
		 */
		tee_device_unregister(optee->supp_teedev);
		tee_device_unregister(optee->teedev);
		kfree(optee);
	}
	if (pool)
		tee_shm_pool_free(pool);
	if (memremaped_shm)
		memunmap(memremaped_shm);
	return ERR_PTR(rc);
}

 

2.1 獲取切換到monitor模式的接口

 

  normal world穿透到secure world是通過在monitor模式下設定SCR寄存器中的NS位來實現的,OP-TEE驅動被上層調用時,最終會通過出發smc切換到monitor,見數據發送給secure world來進行處理。而用戶出發smc請求的接口函數將在驅動初始化的時候被填充到OP-TEE驅動的device info中,在OP-TEE驅動中通過調用get_invoke_func函數來獲取,該函數的內如如下:

 

static optee_invoke_fn *get_invoke_func(struct device_node *np)
{
	const char *method;

	pr_info("probing for conduit method from DT.\n");

/* 獲取op-tee驅動在device tree中的節點中的method屬性的值 */
	if (of_property_read_string(np, "method", &method)) {
		pr_warn("missing \"method\" property\n");
		return ERR_PTR(-ENXIO);
	}

/* 判定op-tee驅動是使用smc的方式還是使用hvc的方式來實現進入monitor模式的操作,
    根據method的值與hvc還是smc匹配來決定那種切換方法,並將用於切換到monitor的
    接口 */
	if (!strcmp("hvc", method))
		return optee_smccc_hvc;
	else if (!strcmp("smc", method))
		return optee_smccc_smc;

	pr_warn("invalid \"method\" property: %s\n", method);
	return ERR_PTR(-EINVAL);
}

以optee_smccc_smc爲例,該函數的內容如下:

 

 

static void optee_smccc_smc(unsigned long a0, unsigned long a1,
			    unsigned long a2, unsigned long a3,
			    unsigned long a4, unsigned long a5,
			    unsigned long a6, unsigned long a7,
			    struct arm_smccc_res *res)
{
	arm_smccc_smc(a0, a1, a2, a3, a4, a5, a6, a7, res);
}

  也即是函數get_invoke_func執行完成之後會返回arm_smccc_smc函數的地址。arm_smccc_smc函數就是驅動用來將cortex切換到monitor模式的函數,該函數是以彙編的方式編寫,定義在linux/arch/arm/kernel/smccc-call.S文件中。如果是64位系統,則該函數定義在linux//arch/arm64/kernel/smccc-call.S目錄中,本文以32位系統爲例,該函數內容如下:

/*
	 * Wrap c macros in asm macros to delay expansion until after the
	 * SMCCC asm macro is expanded.
	 */
/*SMCCC_SMC宏,觸發smc*/
	.macro SMCCC_SMC
	__SMC(0)
	.endm

/*SMCCC_HVC宏,觸發hvc*/
	.macro SMCCC_HVC
	__HVC(0)
	.endm

/* 定義SMCCC宏,其參數爲instr */
	.macro SMCCC instr
/* 將normal world中的寄存器入棧,保存現場 */
UNWIND(	.fnstart)
	mov	r12, sp
	push	{r4-r7}
UNWIND(	.save	{r4-r7})
	ldm	r12, {r4-r7}
	\instr    /* 執行instr參數的內容,即執行smc切換 */
	pop	{r4-r7}   /* 出棧操作,恢復現場 */
	ldr	r12, [sp, #(4 * 4)]
	stm	r12, {r0-r3}
	bx	lr
UNWIND(	.fnend)
	.endm

/*
 * void smccc_smc(unsigned long a0, unsigned long a1, unsigned long a2,
 *		  unsigned long a3, unsigned long a4, unsigned long a5,
 *		  unsigned long a6, unsigned long a7, struct arm_smccc_res *res)
 */
ENTRY(arm_smccc_smc)
	SMCCC SMCCC_SMC
ENDPROC(arm_smccc_smc)

 

2.2 校驗API的UID和OP-TEE的版本信息

 

  驅動加載過程中獲取到REE與TEE之間進行交互的接口函數(調用get_invoke_func函數返回的函數地址)之後,op-tee驅動會對API的UID和版本信息進行校驗。上述操作是通過調用optee_msg_api_uid_is_optee_api函數和optee_msg_api_revision_is_compatible函數來實現的。兩個函數的內容如下:

static bool optee_msg_api_uid_is_optee_api(optee_invoke_fn *invoke_fn)
{
	struct arm_smccc_res res;

/* 調用執行smc操作的接口函數,帶入的commond ID爲OPTEE_SMC_CALLS_UID */
	invoke_fn(OPTEE_SMC_CALLS_UID, 0, 0, 0, 0, 0, 0, 0, &res);

/* 比較返回的UID的值與在驅動中定義的UID的值是否匹配 */
	if (res.a0 == OPTEE_MSG_UID_0 && res.a1 == OPTEE_MSG_UID_1 &&
	    res.a2 == OPTEE_MSG_UID_2 && res.a3 == OPTEE_MSG_UID_3)
		return true;
	return false;
}

static bool optee_msg_api_revision_is_compatible(optee_invoke_fn *invoke_fn)
{
	union {
		struct arm_smccc_res smccc;
		struct optee_smc_calls_revision_result result;
	} res;

/* 調用執行smc操作的接口函數,帶入的commond ID爲OPTEE_SMC_CALLS_REVISION*/
	invoke_fn(OPTEE_SMC_CALLS_REVISION, 0, 0, 0, 0, 0, 0, 0, &res.smccc);

/* 比較返回的版本信息的值與驅動中定義的版本值是否匹配 */
	if (res.result.major == OPTEE_MSG_REVISION_MAJOR &&
	    (int)res.result.minor >= OPTEE_MSG_REVISION_MINOR)
		return true;
	return false;
}

 

 

 

2.3 判定secure world是否預留了驅動與secure world之間的共享內存空間

 

  驅動與secure world之間需要進行數據的交互,而進行數據交互則需要一定的共享內存來保存sercure world和驅動之間共有的數據。所以在驅動初始化的時候需要檢查該共享內存空間是否被預留出來。通過獲取secure world中的相關變量的值並判定該flag是否相等來判定secure world是否預留了共享內存空間,在OP-TEE OS啓動的時候,執行MMU初始化的時候會初始化該變量。在驅動端通過調用optee_msg_exchange_capabilities函數來獲取該變量的值,其內容如下:

static bool optee_msg_exchange_capabilities(optee_invoke_fn *invoke_fn,
					    u32 *sec_caps)
{
	union {
		struct arm_smccc_res smccc;
		struct optee_smc_exchange_capabilities_result result;
	} res;
	u32 a1 = 0;

	/*
	 * TODO This isn't enough to tell if it's UP system (from kernel
	 * point of view) or not, is_smp() returns the the information
	 * needed, but can't be called directly from here.
	 */
	if (!IS_ENABLED(CONFIG_SMP) || nr_cpu_ids == 1)
		a1 |= OPTEE_SMC_NSEC_CAP_UNIPROCESSOR;

/* 調用smc操作接口,獲取secure world中的變量 */
	invoke_fn(OPTEE_SMC_EXCHANGE_CAPABILITIES, a1, 0, 0, 0, 0, 0, 0,
		  &res.smccc);

	if (res.result.status != OPTEE_SMC_RETURN_OK)
		return false;

	*sec_caps = res.result.capabilities;	//將返回值中的變量賦值爲sec_caps
	return true;
}

  當驅動獲取到sec_caps的值之後會查看該值是否爲宏OPTEE_SMC_SEC_CAP_HAVE_RESERVED_SHM定義的值BIT(0),如果該值不爲BIT(0),則會報錯,因爲在secure world端都沒有預留share memory空間,那驅動與secure world之間也就沒法傳輸數據,所以有沒有驅動也就沒有必要了。

 

下一章節將介紹驅動與secure world之間share memory的配置,共享池的設置以及OP-TEE驅動加載的剩下部分

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章