上面我们知道了IPC的相关东西,并回忆了系统调用的过程。
下面来看看这个IPC具体是怎么实现的。

3 IPC的实现

OP-TEE的IPC机制是通过系统调用陷入到内核中来实现的。调用其他TA的操作有专门的接口,而访问安全驱动和OP-TEE的服务则是通过在内核态中调用服务提供的内核级接口来实现的。

线程–》内核态–》服务/安全驱动给内核的接口

3.1 TA调用其他TA的实现

一个TA调用其他TA时,OP-TEE通过建立两者间的会话,并调用命令来实现。(这不是和TA CA通信一个样儿,当然还是有细微的差别)

GP规范定义了如表17-1中的三个接口,这些接口可在OP-TEE的用户空间被调用。


当一个TA需要调用其他的TA时,首先需要使用TEE_OpenTASession创建两个TA之间的会话,再使用TEE_InvokeTACommand调用到已经建立的会话的TA中的具体操作,待不再需要调用其他TA时,则调用TEE_InvokeTACommand函数关闭会话来断开两个TA间的联系。

下面看看这三个的实现

1. TEE_OpenTASession的实现

TEE_OpenTASession的实现与CA中创建与TA的会话的过程大致相同,但TEE_OpenTASession是通过系统调用的方式来触发OP-TEE分配线程并创建会话,而CA则是通过触发安全监控模式调用(smc)来让OP-TEE分配线程并创建会话。(都是TEE OS一手操办)

TEE_OpenTASession操作的整体流程如图17-1所示。

(感叹老师这个逻辑的清晰,看流程图,关键你就看判断就行。)

函数执行到tee_ta_open_session后,其操作与在CA创建会话的操作完全一致,syscall_open_ta_session函数的说明如下:

        TEE_Result syscall_open_ta_session(const TEE_UUID *dest,unsigned long cancel_req_to,struct utee_params *usr_param, uint32_t *ta_sess,uint32_t *ret_orig){TEE_Result res;uint32_t ret_o = TEE_ORIGIN_TEE;struct tee_ta_session *s = NULL;struct tee_ta_session *sess;struct mobj *mobj_param = NULL;TEE_UUID *uuid = malloc(sizeof(TEE_UUID));struct tee_ta_param *param = malloc(sizeof(struct tee_ta_param));TEE_Identity *clnt_id = malloc(sizeof(TEE_Identity));void *tmp_buf_va[TEE_NUM_PARAMS];struct user_ta_ctx *utc;/* 参数合法性检查 */if (uuid == NULL || param == NULL || clnt_id == NULL) {res = TEE_ERROR_OUT_OF_MEMORY;goto out_free_only;}/* 清空分配的param变量中的数据 */memset(param, 0, sizeof(struct tee_ta_param));/* 获取当前TA的会话信息 */res = tee_ta_get_current_session(&sess);if (res ! = TEE_SUCCESS)goto out_free_only;utc = to_user_ta_ctx(sess->ctx);/* 将用户空间传递的UUID值复制到内核空间中 */res = tee_svc_copy_from_user(uuid, dest, sizeof(TEE_UUID));if (res ! = TEE_SUCCESS)goto function_exit;/* 设定login方式并设定clnt_id->uuid的值,即让当前TA认为是client端*/clnt_id->login = TEE_LOGIN_TRUSTED_APP;memcpy(&clnt_id->uuid, &sess->ctx->uuid, sizeof(TEE_UUID));/* 复制用户空间传递的参数数据到内核空间 */res = tee_svc_copy_param(sess, NULL, usr_param, param, tmp_buf_va,&mobj_param);if (res ! = TEE_SUCCESS)goto function_exit;/* 执行创建会话操作*/res = tee_ta_open_session(&ret_o, &s, &utc->open_sessions, uuid,clnt_id, cancel_req_to, param);if (res ! = TEE_SUCCESS)goto function_exit;/* 更新param的内容 */res = tee_svc_update_out_param(sess, s, param, tmp_buf_va, usr_param);function_exit:if (mobj_param) {mutex_lock(&tee_ta_mutex);mobj_free(mobj_param);mutex_unlock(&tee_ta_mutex);}if (res == TEE_SUCCESS)tee_svc_copy_kaddr_to_uref(ta_sess, s); //将获得的会话的ID值复制到用户空间//复制执行函数的返回值到用户空间tee_svc_copy_to_user(ret_orig, &ret_o, sizeof(ret_o));out_free_only:free(param);free(uuid);free(clnt_id);return res;}

关于tee_ta_open_session函数的执行过程可参阅本书第13章。

2. TEE_InvokeTACommand的实现

调用TEE_InvokeTACommands时带入命令ID的值就能调用TA中具体的命令,其过程与CA的命令调用操作几乎一致,该接口的执行流程如图17-2所示。


Syscall_invoke_ta_command的内容和说明如下:

    TEE_Result syscall_invoke_ta_command(unsigned long ta_sess,unsigned long cancel_req_to, unsigned long cmd_id,struct utee_params *usr_param, uint32_t *ret_orig){TEE_Result res;TEE_Result res2;uint32_t ret_o = TEE_ORIGIN_TEE;struct tee_ta_param param = { 0 };TEE_Identity clnt_id;struct tee_ta_session *sess;struct tee_ta_session *called_sess;struct mobj *mobj_param = NULL;void *tmp_buf_va[TEE_NUM_PARAMS];struct user_ta_ctx *utc;/* 获取当前的TA的session信息 */res = tee_ta_get_current_session(&sess);if (res ! = TEE_SUCCESS)return res;utc = to_user_ta_ctx(sess->ctx);/* 根据session ID从保存已经open的session链表中找到对应的session */called_sess = tee_ta_get_session((vaddr_t)tee_svc_uref_to_kaddr(ta_sess), true,&utc->open_sessions);if (! called_sess)return TEE_ERROR_BAD_PARAMETERS;/* 设定clnt_id的内容,将调用者作为client端处理 */clnt_id.login = TEE_LOGIN_TRUSTED_APP;memcpy(&clnt_id.uuid, &sess->ctx->uuid, sizeof(TEE_UUID));/* 复制从用户空间传入的参数 */res = tee_svc_copy_param(sess, called_sess, usr_param, &param,tmp_buf_va, &mobj_param);if (res ! = TEE_SUCCESS)goto function_exit;/* 开始调用找到的session中的invoke command,根据command ID执行指定的操作 */res = tee_ta_invoke_command(&ret_o, called_sess, &clnt_id,cancel_req_to, cmd_id, &param);/* 更新执行结果到输出参数 */res2 = tee_svc_update_out_param(sess, called_sess, &param, tmp_buf_va,usr_param);if (res2 ! = TEE_SUCCESS) {ret_o = TEE_ORIGIN_TEE;res = res2;}function_exit:tee_ta_put_session(called_sess);if (mobj_param) {mutex_lock(&tee_ta_mutex);mobj_free(mobj_param);mutex_unlock(&tee_ta_mutex);}if (ret_orig)tee_svc_copy_to_user(ret_orig, &ret_o, sizeof(ret_o));return res;}

关于tee_ta_invoke_command函数的执行过程可参阅第13章。

3. TEE_CloseTASession的实现

TEE_CloseTASession接口用于断开TA与其他TA之间的连接,其过程与CA的关闭会话操作几乎一致,该接口的执行流程如图17-3所示。

调用TEE_CloseTASession接口时会产生系统调用,系统会执行关闭会话的操作,syscall_close_ta_session函数的内容和说明如下:

        TEE_Result syscall_close_ta_session(unsigned long ta_sess){TEE_Result res;struct tee_ta_session *sess;TEE_Identity clnt_id;struct tee_ta_session *s = tee_svc_uref_to_kaddr(ta_sess);struct user_ta_ctx *utc;/* 获取当前TA的session信息 */res = tee_ta_get_current_session(&sess);if (res ! = TEE_SUCCESS)return res;utc = to_user_ta_ctx(sess->ctx);/* 设定clnt_id信息 */clnt_id.login = TEE_LOGIN_TRUSTED_APP;memcpy(&clnt_id.uuid, &sess->ctx->uuid, sizeof(TEE_UUID));/* 将需要被关闭的session从保存已经Open的session链表中移除 */return tee_ta_close_session(s, &utc->open_sessions, &clnt_id);}

3.2 TA调用系统服务和安全驱动的实现

动态TA实现具体功能时需要调用到安全驱动或系统底层的资源。例如密码学操作、加载TA镜像文件操作、对SE模块的操作等。

这些资源提供的接口都处于OP-TEE的内核空间,当用户空间的TA需要使用这些资源来实现具体功能时,则需要让TA的调用操作通过系统调用的方式进入到内核空间,然后再调用特定的接口

1. OP-TEE中服务和安全驱动的构成框架

OP-TEE使用系统服务的方式统一管理各功能模块,安全驱动的操作接口会接入到系统服务中,系统服务是在OP-TEE启动过程中执行initcall段中的内容时被启动,service_init的启动等级设置为1,而driver_init的启动等级设置成3。

故在OP-TEE的启动过程中,首先会启动使用service_init宏定义的功能函数,再初始化安全驱动。


OP-TEE中的系统服务提供了类似框架层的功能,安全驱动初始化时会将驱动的操作接口注册到对应的系统服务。

TA可使用的只是各系统服务提供的接口,如果系统服务并不需要给上层TA使用,则不会暴露对应的接口给TA。

当前的OP-TEE中提供了如下三个重要的系统服务:

□ 密码学操作的系统服务;

□ 对SE功能模块进行操作的系统服务;

□ 提供加载TA镜像操作的系统服务;

2. TA对系统服务接口的调用实现

动态TA通过系统调用的方式进入到内核态,然后在内核态调用各系统服务提供的接口。

系统服务为OP-TEE用户态程序提供的接口定义在类似于tee_api_xxx.c的文件中,这些文件根据不同的功能模块定义了用户空间需要使用的密码学操作接口、SE操作接口等。这些接口的调用过程大致相同,如图17-5所示。

用户态的TA通过系统调用陷入OP-TEE内核空间,然后在对应的系统调用中使用系统服务提供的接口或变量来完成对安全驱动或其他资源的操作。

因为在安全,我们知道TEE侧一个重要的功能就是提供加密方面的。

3.3 TA对密码学系统服务的调用实现

TA需要实现计算摘要、产生随机数、加解密、签名验签等操作时就会调用到密码学系统服务提供的接口。

OP-TEE的内核空间中有一个变量——crypto_ops,该变量中保存了各种密码学算法的调用接口,其内容如下:

        const struct crypto_ops crypto_ops = {.name = "LibTomCrypt provider",      //该系统服务的名字//crypto service的初始化函数,在启动过程中将会执行crypto_ops.init指定的函数.init = tee_ltc_init,/* hash类算法的接口,用于计算摘要 */#if defined(_CFG_CRYPTO_WITH_HASH).hash = {.get_ctx_size = hash_get_ctx_size,.init = hash_init,.update = hash_update,.final = hash_final,},#endif/* 对称加解密算法的接口用于对称加解密 */#if defined(_CFG_CRYPTO_WITH_CIPHER).cipher = {.final = cipher_final,.get_block_size = cipher_get_block_size,.get_ctx_size = cipher_get_ctx_size,.init = cipher_init,.update = cipher_update,},#endif/* MAC类算法接口 */#if defined(_CFG_CRYPTO_WITH_MAC).mac = {.get_ctx_size = mac_get_ctx_size,.init = mac_init,.update = mac_update,.final = mac_final,},#endif/* 对称验证加解密算法接口 */#if defined(_CFG_CRYPTO_WITH_AUTHENC).authenc = {.dec_final = authenc_dec_final,.enc_final = authenc_enc_final,.final = authenc_final,.get_ctx_size = authenc_get_ctx_size,.init = authenc_init,.update_aad = authenc_update_aad,.update_payload = authenc_update_payload,},#endif/* 非对称算法(RSA)加解密,签名验签操作接口 */#if defined(_CFG_CRYPTO_WITH_ACIPHER).acipher = {#if defined(CFG_CRYPTO_RSA).alloc_rsa_keypair = alloc_rsa_keypair,.alloc_rsa_public_key = alloc_rsa_public_key,.free_rsa_public_key = free_rsa_public_key,.gen_rsa_key = gen_rsa_key,.rsaes_decrypt = rsaes_decrypt,.rsaes_encrypt = rsaes_encrypt,.rsanopad_decrypt = rsanopad_decrypt,.rsanopad_encrypt = rsanopad_encrypt,.rsassa_sign = rsassa_sign,.rsassa_verify = rsassa_verify,#endif/* 生成key的接口 */#if defined(CFG_CRYPTO_DH).alloc_dh_keypair = alloc_dh_keypair,.gen_dh_key = gen_dh_key,.dh_shared_secret = do_dh_shared_secret,#endif/* DSA算法接口 */#if defined(CFG_CRYPTO_DSA).alloc_dsa_keypair = alloc_dsa_keypair,.alloc_dsa_public_key = alloc_dsa_public_key,.gen_dsa_key = gen_dsa_key,.dsa_sign = dsa_sign,.dsa_verify = dsa_verify,#endif/* ECC算法接口 */#if defined(CFG_CRYPTO_ECC)/* ECDSA and ECDH */.alloc_ecc_keypair = alloc_ecc_keypair,.alloc_ecc_public_key = alloc_ecc_public_key,.gen_ecc_key = gen_ecc_key,.free_ecc_public_key = free_ecc_public_key,/* ECDSA only */.ecc_sign = ecc_sign,.ecc_verify = ecc_verify,/* ECDH only */.ecc_shared_secret = do_ecc_shared_secret,#endif},/* 大整数操作接口 */.bignum = {.allocate = bn_allocate,.num_bytes = num_bytes,.num_bits = num_bits,.compare = compare,.bn2bin = bn2bin,.bin2bn = bin2bn,.copy = copy,.free = bn_free,.clear = bn_clear},#endif /* _CFG_CRYPTO_WITH_ACIPHER *//* 随机系列算法接口 */.prng = {.add_entropy = prng_add_entropy,.read = prng_read,}};

1. crypto service的初始化

OP-TEE在启动时会调用crypto_ops.init指定的函数初始化整个密码学系统服务,即调用tee_ltc_init函数来初始化密码学系统服务,该函数将各种密码学算法的操作接口都注册到特定的变量中,这些变量与对应算法的关系如表17-2所示。(一个个服务组成了大的服务,服务就是对多个相同目的驱动的一个整合吧我觉得,C语言通过结构体去实现面向对象的逻辑)


启动密码学系统服务时,会调用tee_ltc_reg_algs函数将对应算法的操作接口注册到相关变量中,该函数内容如下:

        static void tee_ltc_reg_algs(void){#if defined(CFG_CRYPTO_AES)register_cipher(&aes_desc);    //注册AES算法的操作接口到cipher_descriptor中#endif#if defined(CFG_CRYPTO_DES)register_cipher(&des_desc);    //注册DES算法的操作接口到cipher_descriptor中register_cipher(&des3_desc);   //注册DES3算法的操作接口到cipher_descriptor中#endif#if defined(CFG_CRYPTO_MD5)register_hash(&md5_desc);      //注册MD5算法的操作接口到hash_descriptor中#endif#if defined(CFG_CRYPTO_SHA1)register_hash(&sha1_desc);     //注册SHA1算法的操作接口到hash_descriptor中#endif#if defined(CFG_CRYPTO_SHA224)register_hash(&sha224_desc);   //注册SHA224算法的操作接口到hash_descriptor中#endif#if defined(CFG_CRYPTO_SHA256)register_hash(&sha256_desc);   //注册SHA256算法的操作接口到hash_descriptor中#endif#if defined(CFG_CRYPTO_SHA384)register_hash(&sha384_desc);   //注册SHA384算法的操作接口到hash_descriptor中#endif#if defined(CFG_CRYPTO_SHA512)register_hash(&sha512_desc);   //注册SHA512算法的操作接口到hash_descriptor中#endif//注册prng算法的操作接口到prng_descriptor中#if defined(CFG_WITH_SOFTWARE_PRNG)#if defined(_CFG_CRYPTO_WITH_FORTUNA_PRNG)register_prng(&fortuna_desc);#elseregister_prng(&rc4_desc);#endif#elseregister_prng(&prng_mpa_desc);#endif}

注册过程就是将具体密码学算法的operation变量保存到对应的数组变量元素中。密码学系统服务初始化完成后,内核空间通过调用crypto_ops.xxx.xxx的方式可调用到各种密码学算法的具体实现。

2. TA调用具体算法的实现

调用crypto_ops中的接口时,会根据需要被调用密码学算法的名称从数组变量中找到对应的元素,然后使用元素中保存的算法操作接口来完成密码学操作。

如果芯片集成了硬件加解密引擎,加密算法的实现,则可使用硬件cipher驱动提供的接口来完成(将这个驱动注册进服务,把接口加到对应的结构体里面。)。本节以调用SHA1算法为例介绍其实现过程。


(图17-6 TEE_DigestUpdate操作的实现流程)

下面再来看看SE。

3.4 对SE功能模块进行操作的系统服务

在OP-TEE内核空间调用类似tee_se_reader_xxx的接口会调用到OP-TEE的SE系统服务,用于操作具体的SE模块。

若需要在TA中操作SE模块,可将tee_se_reader_xxx类型的接口重新封装成系统调用,然后在TA中调用封装的接口就能实现TA对SE模块的操作。

在OP-TEE中要使用具体的SE模块需要初始化SE功能模块的系统服务,并挂载具体SE模块的驱动

SE模块的系统服务是通过在OP-TEE启动过程中调用tee_se_manager_init函数来实现的,该函数只会初始化该系统服务的上下文空间,函数内容如下:

        static TEE_Result tee_se_manager_init(void){//定义SE service的上下文变量struct tee_se_manager_ctx *ctx = &se_manager_ctx;context_init(ctx);    //初始化该上下文变量的内容return TEE_SUCCESS;}

SE系统服务的上下文变量 初始化完成后,就需要挂载具体的SE模块驱动,将SE的操作接口注册到上下文中。驱动的挂载和注册过程如图17-7所示。

3.5 加载TA镜像的系统服务

当CA调用libteec库中用于创建与某个动态TA的会话时,会从REE侧的文件系统中加载TA镜像文件到OP-TEE,加载TA镜像的过程就会使用到该系统服务提供的接口函数。(这个系统服务是加载镜像的系统服务)

本书第13章详细介绍了OP-TEE创建会话的实现过程。OP-TEE会使用tee_ta_init_user_ta_session函数来完成加载TA镜像并初始化会话的操作。

加载TA镜像文件时,会使用user_ta_store变量中的接口发送RPC请求,通知tee_supplicant对REE侧文件系统中的TA镜像文件执行打开、读取、获取TA镜像文件大小、关闭TA镜像文件的操作。

user_ta_store变量在该系统服务启动时被赋值,具体函数内容如下:

        static const struct user_ta_store_ops ops = {.open = ta_open,              //发送RPC请求使tee_supplicant打开TA镜像文件.get_size = ta_get_size,     //发送RPC请求,获取TA镜像文件的大小.read = ta_read,              //发送RPC请求读取TA镜像的内容.close = ta_close,            //发送RPC请求关闭打开的TA镜像文件};/* OP-TEE启动时被调用,使用service_init宏将该函数编译到initcall段中 */static TEE_Result register_supplicant_user_ta(void){return tee_ta_register_ta_store(&ops);}/* 将user_ta_store变量的地址赋值成ops */TEE_Result tee_ta_register_ta_store(const struct user_ta_store_ops *ops){user_ta_store = ops;return TEE_SUCCESS;}

小结一下啦

以上就是关于线程的所有部分,我一直以为系统服务是一个大的,原来每个大类都有,但是如果我只有一个对应的驱动,是不是就不用了,或者说我为了以后便于拓展,也可以整一个系统服务。

以上介绍了线程的东西,也讲了线程之间的通信,还对整个TA的调用逻辑梳理了一下。这里我们应该也知道怎么去增加我们自己的安全驱动,比如我要是自己实现了加密的硬件,怎么写驱动,又怎么让用户态的TA调用到我。

其次每个TA具有独立的运行空间,OP-TEE中的一个TA调用另一个TA执行特定操作的过程是OP-TEE中的一种IPC的方式。

OP-TEE中各种系统服务起到类似框架层的作用,安全驱动或其他子模块提供的操作接口接入对应系统服务中。系统服务通过接口变量或其他方式将操作接口暴露给OP-TEE的内核空间用户空间的TA通过系统调用的方式在OP-TEE内核空间调用这些接口,从而实现TA对安全驱动或其他模块的资源操作。

这个关系最后必须再整个图看一下


哈哈 下一站进军中断,进军中断结束后,就来整一个实际的应用开发栗子。

内容全部来自《手机安全和可信应用开发指南》,朋友国庆快乐!!!

参考资料:
《手机安全和可信应用开发指南》

OP-TEE中的线程管理(四)相关推荐

  1. c语言中的线程管理,深入解析C++编程中线程池的使用

    为什么需要线程池目前的大多数网络服务器,包括Web服务器.Email服务器以及数据库服务器等都具有一个共同点,就是单位时间内必须处理数目巨大的连接请求,但处理时间却相对较短. 传 统多线程方案中我们采 ...

  2. java中创建线程的四种方式及线程池详解

    众所周知,我们在创建线程时有四种方法可以用,分别是: 1.继承Thread类创建线程 2.实现Runnable接口创建线程 3.使用Callable和Future创建线程 4.使用线程池创建(使用ja ...

  3. C/C++中退出线程的四种解决方法

    退出线程可以有四种方法: 1.线程函数的return返回(最好这样): 其中用线程函数的return返回, 而终止线程是最安全的, 在线程函数return返回后, 会清理函数内申请的类对象, 即调用这 ...

  4. Python3 threading的多线程管理中的线程管理与锁

    提到Python的多线程,大家都说鸡肋.至于为什么,一定又要说什么"GIL的全称是Global Interpreter Lock(全局解释器锁)"之类的解释了,哥书读的少,听不太懂 ...

  5. C++多线程并发中线程管理

    一.何为并发 刚开始接触计算机编程语言时,我们编写一个程序,在main入口函数中调用其它的函数,计算机按我们设定的调用逻辑来执行指令获得结果.如果我们想在程序中完成多个任务,可以将每个任务实现为一个函 ...

  6. react中数据状态管理的四种方案

    我们为什么需要状态管理? (1) 一个是为了解决相邻组件的通信问题. 虽然可以通过「状态提升」解决,但有两个问题: 每次子组件更新,都会触发负责下发状态的父组件的整体更新(使用 Context 也有这 ...

  7. Spring(四)——AOP、Spring实现AOP、Spring整合Mybatis、Spring中的事务管理

    文章目录 1. 什么是AOP 2. 使用Spring实现AOP 2.1 使用Spring的API 接口实现 2.2 自定义实现 2.3 使用注解实现 3. 整合MyBatis 3.1 MyBatis- ...

  8. 四十七、面试前,必须搞懂Java中的线程池ThreadPoolExecutor(上篇)

    @Author:Runsen @Date:2020/6/9 人生最重要的不是所站的位置,而是内心所朝的方向.只要我在每篇博文中写得自己体会,修炼身心:在每天的不断重复学习中,耐住寂寞,练就真功,不畏艰 ...

  9. 管理学习(2)——职场中最重要的四件事

    职场中最重要的四件事 职场的核心精髓可以概括为职场中最重要的三件事,它们分别是:职场第一要务.职业化.职场的本质. 1.这三件事与身处职场的我们到底有什么关系呢? 就让我用三句话概括一下: 职场第一要 ...

  10. WF4.0 基础篇 (十四) Delay 与WF4中的线程

    本节主要介绍WF的实例是单线程运行的,Delay并不是Thread.Sleep,Parallel是单线程运行的,WorkflowApplication与WorkflowInvoker在调用流程上的区别 ...

最新文章

  1. VS Code 安装插件、自定义模板、自定义配置参数、自定义主题、配置参数说明、常用的扩展插件
  2. mongodb数据库的一些常用命令列表
  3. python for in list
  4. elementui常用知识点总结
  5. leetcode322. 零钱兑换
  6. 2-3:C++快速入门之缺省参数
  7. Linux该如何学习(新手入门必看)
  8. (23)Vue.js组件介绍
  9. 苹果mac3D模型渲染软件:KeyShot
  10. git添加远程库遇到的问题
  11. 计算机二级c语言考试内容有哪些,计算机二级C语言考试内容大纲
  12. 《数据结构复习》扩展线性链表的广义表
  13. 一块硬盘做服务器,服务器4块硬盘做raid几
  14. 测试报告包含哪些内容?(超详细,带图)
  15. React的调和过程
  16. 华为西安工业大学鸿蒙,培养百位将领、19位院士,这所211大学被誉为“华为人的母校”...
  17. java校验特殊字符_java 中文及特殊字符校验
  18. excel文字显示图标集_创建自己的Excel图标集
  19. 实验:使用SSMS创建并管理数据库及其基本表(代码版)
  20. 嵌入式中SD卡接口电路设计

热门文章

  1. 当前有哪些流行的前端开发框架?
  2. 两独立样本非参数检验的Mann-whitneyU检验
  3. bind 完成正确安装
  4. Qt 之 打开pdf文件
  5. 趣味编程入门 Scratch 开发跳一跳小游戏-邵立志-专题视频课程
  6. Linux必会的rpm命令安装软件
  7. 音频系统POP音的原理和解决方法
  8. 硬盘出现异响应急方案
  9. 苹果服务器文件夹共享权限设置,苹果设备如何访问 Windows 文件共享?
  10. R语言使用rgl包的plot3d函数可视化可以交互旋转的3D散点图(Rotating 3D scatter plot produced by the plot3d functio in rgl)