历经一年多时间的系统整理合补充,《手机安全和可信应用开发指南:TrustZone与OP-TEE技术详解 》一书得以出版,书中详细介绍了TEE以及系统安全中的所有内容,全书按照从硬件到软件,从用户空间到内核空间的顺序对TEE技术详细阐述,读者可从用户空间到TEE内核一步一步了解系统安全的所有内容,同时书中也提供了相关的示例代码,读者可根据自身实际需求开发TA。目前该书已在天猫、京东、当当同步上线,链接如下(麻烦书友购书时能给予评论,多谢多谢)

京东购买地址

当当购买地址

天猫购买地址

非常感谢在此期间大家的支持以及各位友人的支持和帮助!!!。

为方便和及时的回复读者对书中或者TEE相关的问题的疑惑,也为了大家能有一个统一的交流平台。我搭建了一个简单的论坛,网址如下:

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

关于您的疑问可在“相关技术讨论“”中发帖,我会逐一回复。也欢迎大家发帖,一起讨论TEE相关的一些有意思的feature。共同交流。同时该论坛中也会添加关于移动端虚拟化的相关技术的板块,欢迎各位共同交流学习

  

ARM支持的trustzone技术后提供了ARM cortex的虚拟化基础。众所周知ARM具有其中运行模式,但是支持trustzone技术的cortex就已经不再仅仅有其中模式了,而是具有八种模式和两个状态:secure world态和non-secure world态。本文将介绍secure world态和non-secure world态之间是如何实现切换的,以OP-TEE中在没有使用ATF的情况下sm操作代码为例。

1. 基础知识

  本章节将介绍理解secure world与non-secure world之间进行切换所需要明白的一些寄存器和相关模式的知识。

1.1 ARM的运行模式

  ARM在未支持trustzone技术之前,ARM具有七种运行模式,分别为:

1. usr模式(用户模式):正常程序的运行时的模式

2. fiq模式(快速中断模式):当配置了快速中断时,如果产生fiq事件,cortex将会切换到该模式

3. irq模式(用户模式):中断模式,一般用于通用中断处理,被ROS使用

4. svc模式(管理模式):操作系统使用的保护模式,

5. sys模式(系统模式):运行具有特权的操作系统任务

6. abt模式(数据访问终止模式):当数据或者指令预取值时终止则会进入该模式

7. und模式(未定义指令模式):当未定义指令执行的时候则会进入该模式

  而支持trustzone技术之后,ARM增加了另外一种mon模式(monitor 模式),而mon模式就起到进行secure world与non-secure world之间进行切换的桥梁作用。所以总结下来在ARM的cortex中具有八种模式两种状态,每种状态下具有自己独立的八种模式。

1.2 NS位

  在持trustzone技术的时候,ARM在AXI系统总线上增加了一个NS位(详细情况请查阅ARM给出的trustzone白皮书),而NS位就是用来标记当前的数据,指令时属于secure world态还是non-secure world态,NS位会被保存到scr寄存器的第0位。当NS=1时,处理器处于non-secure world态,当NS=0时,处理器处于secure world态。

1.3 重要寄存器

VBAR(Vector Base Address Register)寄存器:

  异常向量基地址,该寄存器将保存异常向量表的基地址,在secure world态和non-secure world态都具有各自独有的VBAR寄存器用来存放两种状态各自独有的异常向量表所在的基地址。

MVBAR(Monitor Vector Base Address Register)寄存器:

  monitor模式下的异常向量表基地址寄存器,该寄存器用来保存在monitor模式下异常向量表的基地址,该寄存器在secure world和non-secure world之间进行切换的时候起到关键作用。

SCR(Secure Configuration Register)寄存器

 安全配置寄存器,处理器在运行的时候该寄存器中会保存相关的标志,其中用于标记处理器处于secure world态还是non-secure world态的NS位就被保存在该寄存器中。

SP(Stack Pointer)栈寄存器:

  该寄存器用来存放处理器使用的栈的偏移地址

CPSR(Current Program Status Register)程序状态寄存器:

  该寄存器将保存处理器运行时的各种标志位信息,包括标志域,状态域,扩展域和控制域

SPSR(Saved Program Status Register)程序状态保存寄存器:

  当特定的异常中断发生时,spsr寄存器将保存当前长度cpsr寄存器中的内容,等异常中断退出自后,处理器会使用spsr寄存器中的数据来恢复cpsr寄存器中的数据。

LR(Link Register)子程序链接寄存器:

  该寄存器一般用来保存子程序的返回地址。

1.4 smc汇编指令

  当需要让处理器进入到monitor模式的时候,ARM要求执行smc指令来实现。如果该汇编指令执行成功,则处理器就切换到了monitor模式下并且更新monitor模式下的重要寄存器,包括CPSR, SPSR, LR。该操作与ARM进入到IRQ, ABT等模式的操作一样,采取的是产生异常来进行模式的切换。当系处理器进入到monitor之后,处理器就回去找寻该模式下的异常处理向量表的位置,而monitor模式下独有的异常向量表的基地址被保存在MVBAR寄存器中。

2. monitor模式下的处理过程

  在secure world态或者是non-secure world态调用smc指令之后,处理器将会触发异常操作进入到monitor模式中并更新monitor模式下的CPSR, SPSR, LR,并从MVBAR寄存器中获取到monitor模式的异常中断向量表基地址,进而找到smc操作的异常处理函数。在《16. OP-TEE中的中断处理(二)------系统FIQ事件的处理》一文中介绍了monitor模式的异常中断向量表基地址是如何保存到MVBAR寄存器中的,在此就不再做冗余介绍。monitor模式下整个处理逻辑如下图所示。

2.1 进入到monitor模式的中断向量后的处理过程

  在OP-TEE中monitor模式的异常中断向量表定义在optee_os/core/arch/arm/sm/sm_a32.S文件总,其内容如下:

LOCAL_FUNC sm_vect_table , :
UNWIND( .fnstart)
UNWIND( .cantunwind)b   .       /* Reset            */b .       /* Undefined instruction    */b sm_smc_entry    /* Secure monitor call      */b .       /* Prefetch abort       */b .       /* Data abort           */b .       /* Reserved         */b .       /* IRQ              */b sm_fiq_entry    /* FIQ              */
UNWIND( .fnend)
END_FUNC sm_vect_table

  所以当调用smc之后,处理器切换到monitor模式,查找到异常中断向量表,并执行b sm_smc_entry来对smc异常进行处理。该函数定义在optee_os/core/arch/arm/sm/sm_a32.S文件中。其完整内容如下:

LOCAL_FUNC sm_smc_entry , :
UNWIND( .fnstart)
UNWIND( .cantunwind)srsdb   sp!, #CPSR_MODE_MON//将当前模式的lr和spsr寄存器中的值分别存储在monitor模式的sp中push  {r0-r7} //将r0到r7中的值压入栈(sp)clrex     /* Clear the exclusive monitor *///独占清除,可以将关系紧密的独占访问监控器返回为开放模式/* Find out if we're doing an secure or non-secure entry */read_scr r1 //获取当前scr寄存器中的值,并将值保存在r1寄存器中tst r1, #SCR_NS //判定scr寄存器中的值的NS位是否为1,如果是1则将会改变CPSR中的条件标志位为0bne  .smc_from_nsec //如果请求来自于non-secur world,则跳转到smc_from_nsec进行执行/** As we're coming from secure world (NS bit cleared) the stack* pointer points to sm_ctx.sec.r0 at this stage. After the* instruction below the stack pointer points to sm_ctx.*///将当前处于secure world态中,secure world的运行栈存放在r0中//所以将当前sp的值减去offset就可以得到secure world的运行栈地址//并将sp的值指向得到的secure world的运行栈地址sub sp, sp, #(SM_CTX_SEC + SM_SEC_CTX_R0)  /* Save secure context */add    r0, sp, #SM_CTX_SEC//将sp的值加上secure world context的长度保存在r0寄存器中//保存secure world中8中模式的主要寄存器的值,并将值存放到r0寄存器,//而r0寄存器已经指向了CPU栈的位置中以便实现secure context的保存bl    sm_save_modes_regs  /** On FIQ exit we're restoring the non-secure context unchanged, on* all other exits we're shifting r1-r4 from secure context into* r0-r3 in non-secure context.*/add    r8, sp, #(SM_CTX_SEC + SM_SEC_CTX_R0)   //将sp的值将上secure world context中r0存放的位置ldm   r8, {r0-r4} //将r8寄存器中的值指向的地址中的值依次赋值给r0到r4mov_imm    r9, TEESMC_OPTEED_RETURN_FIQ_DONE   //  将FIQ指向完的值保存到r9寄存器中cmp   r0, r9  //对比r0寄存器和r9寄存器中的值addne r8, sp, #(SM_CTX_NSEC + SM_NSEC_CTX_R0) //如果r0与r9不相等则将sp加上non-secure context中的r0的值保存到r8寄存器中stmne   r8, {r1-r4} //如果r0与r9不相等,则将r1到r4寄存器中的值依次加载到r8指定的位置/* Restore non-secure context */add    r0, sp, #SM_CTX_NSEC    //将sp的值加上non-secure world context的长度保存到r0寄存去中bl sm_restore_modes_regs   //获取non-secure context的内容//执行返回到non-seure world的操作
.sm_ret_to_nsec:/** Return to non-secure world*///将sp的值加上non-secure world context中从起始位置到r8寄存器的偏移值//然后将结果保存到r0寄存器中add     r0, sp, #(SM_CTX_NSEC + SM_NSEC_CTX_R8) ldm   r0, {r8-r12}    //r0寄存器中的值指向的地址中的值一次次赋值给r8和r12寄存器/* Update SCR */read_scr r0 //获取当前scr寄存器的值,并保存到r0寄存器中//将scr中的NS位和FIQ位置1orr  r0, r0, #(SCR_NS | SCR_FIQ) /* Set NS and FIQ bit in SCR */write_scr r0    //将修改后的r0的值写入到scr寄存器中//将sp的值加上non-secure world context中从起始位置到r0寄存器的偏移值//然后将结果保存到sp中add   sp, sp, #(SM_CTX_NSEC + SM_NSEC_CTX_R0)b   .sm_exit    //跳转到sm_exit函数继续执行//指向切换到secure world的操作
.smc_from_nsec:/** As we're coming from non-secure world (NS bit set) the stack* pointer points to sm_ctx.nsec.r0 at this stage. After the* instruction below the stack pointer points to sm_ctx.*///当前处于non-secure world态,栈指针就是sp//所以将当前sp的值减去offset就可以得到non-secure world的运行栈地址//并将sp的值指向得到的non-secure world的运行栈地址sub  sp, sp, #(SM_CTX_NSEC + SM_NSEC_CTX_R0)//清除r1寄存器中的NS位和FIQ位bic  r1, r1, #(SCR_NS | SCR_FIQ) /* Clear NS and FIQ bit in SCR */write_scr r1    //将r1寄存器中的值写入到scr寄存器中//将sp的值将上non-secure world context中r8存放的位置//然后将结果保存到r0寄存器中add  r0, sp, #(SM_CTX_NSEC + SM_NSEC_CTX_R8)stm r0, {r8-r12}    //将r8到r12寄存器中的值保存到r0指向的地址位置mov  r0, sp  //将sp的值赋值给r0寄存器bl   sm_from_nsec    //跳转到secure world中进行处理来之non-secure world的smc请求cmp   r0, #0  //对比返回值是否为零,即帕丁sm_form_nsec函数是否执行成功beq   .sm_ret_to_nsec //如果执行成功则执行返回到non-secure world的操作/** Continue into secure world*///如果sm_from_nsec函数并未执行成功,//则将sp的值将上secure world context中r8存放的位置//然后将结果保存到sp中add sp, sp, #(SM_CTX_SEC + SM_SEC_CTX_R0)  //执行退出sm操作
.sm_exit:pop    {r0-r7} //将栈中的r0到r7寄存去中的值进行出栈操作rfefd    sp! //使用sp寄存器中的数据执行返回操作
UNWIND( .fnend)
END_FUNC sm_smc_entry

2.2 non-secure world态触发的smc异常的处理过程

  当smc异常是在non-secure world态中触发时,则SCR寄存器中的NS位必定为1,则处理器会执行smc_from_nsec的分支正式进入对来之non-secure world的smc请求的具体处理。整个执行过程的流程图如下:

  在整个处理过程中,当SCR寄存器中的NS位被设定之后即表示处理器的状态已经处于secure world态或者是non-secure world态。当判定该smc异常来自于non-secure world后将会执行到sm_smc_entry函数中的smc_from_nsec代码块。该段代码块如下:

.smc_from_nsec:/** As we're coming from non-secure world (NS bit set) the stack* pointer points to sm_ctx.nsec.r0 at this stage. After the* instruction below the stack pointer points to sm_ctx.*///当前处于non-secure world态,栈指针就是sp//所以将当前sp的值减去offset就可以得到non-secure world的运行栈地址//并将sp的值指向得到的non-secure world的运行栈地址sub   sp, sp, #(SM_CTX_NSEC + SM_NSEC_CTX_R0)//清除r1寄存器中的NS位和FIQ位bic  r1, r1, #(SCR_NS | SCR_FIQ) /* Clear NS and FIQ bit in SCR */write_scr r1    //将r1寄存器中的值写入到scr寄存器中//将sp的值将上non-secure world context中r8存放的位置//然后将结果保存到r0寄存器中add  r0, sp, #(SM_CTX_NSEC + SM_NSEC_CTX_R8)stm r0, {r8-r12}    //将r8到r12寄存器中的值保存到r0指向的地址位置mov  r0, sp  //将sp的值赋值给r0寄存器bl   sm_from_nsec    //跳转到secure world中进行处理来之non-secure world的smc请求cmp   r0, #0  //对比返回值是否为零,即帕丁sm_form_nsec函数是否执行成功beq   .sm_ret_to_nsec //如果执行成功则执行返回到non-secure world的操作/** Continue into secure world*///如果sm_from_nsec函数并未执行成功,//则将sp的值将上secure world context中r8存放的位置//然后将结果保存到sp中add sp, sp, #(SM_CTX_SEC + SM_SEC_CTX_R0)  

  在该代码段中有重要的两条语句,将从sm_smc_entry开始获取到的scr值保存到r1寄存器中,然后清空r1寄存器中的NS为和FIQ位完成设定处理器状态和使能FIQ,然后再将r1寄存器重新载入到scr寄存器中来完成non-secure world态带secure world态的切换。

  当non-secure world态中的smc请求被TEE处理完成之后,处理器将调用sm_ret_to_nsec函数重新回到non-secure world态。其中从secure world态切换到non-secure world态是通过以下代码来实现的,读取当前的scr寄存器的值到r0寄存器,然后将r0寄存器中的值的NS位和FIQ位设置成1来完成将处理器切换回non-secure world态和屏蔽FIQ的功能。再通过write_scr函数将修改后的r0寄存器的值重新载入到scr寄存器中。

2.3 secure world态中触发的smc异常的处理过程

  当smc异常是在secure world态中触发时,则SCR寄存器中的NS位必定为0,则处理器会执行smc_ret_to_nsec的分支正式进入对来之secure world的smc请求的具体处理。整个执行过程的流程图如下:

  在上述过程中,从secure world切换到non-secure world的方法也是通过修改SCR寄存器中的NS位来实现的。在执行切换之前需要保存secure world态的上下文信息并将当前处理器的上下文信息恢复到non-secure world态的上下文信息。等non-secure world上下文信息恢复之后再修改SCR寄存器的NS位来实现world的切换。保存secure world的上下文信息和恢复non-secure world的上下文信息的操作分别通过sm_save_modes_regs和sm_restore_modes_regs函数来实现,上述两个函数都定义在optee_os/core/arch/arm/sm/sm_a32.S文件中,内容如下,主要是保存或者恢复ARM其中模式的sp, lr信息:

FUNC sm_save_modes_regs , :
UNWIND( .fnstart)
UNWIND( .cantunwind)/* User mode registers has to be saved from system mode */cps   #CPSR_MODE_SYS  //设置CPSR寄存器中的相关位并进入到的SYS模式stm   r0!, {sp, lr}   //将sp和lr寄存器中的值保存到r0指向的地址位置cps   #CPSR_MODE_IRQ  //设置CPSR寄存器中的相关位并进入到IRQ模式中mrs   r2, spsr    //复制spsr(备份程序状态寄存)到r2中stm   r0!, {r2, sp, lr}   //将r2, sp, lr中的值保存到r0指向的地址中cps  #CPSR_MODE_FIQ  //设置CPSR寄存器中的相关位并进入到FIQ模式中mrs   r2, spsr    //复制spsr(备份程序状态寄存)到r2中stm   r0!, {r2, sp, lr}   //将r2, sp, lr中的值保存到r0指向的地址中cps  #CPSR_MODE_SVC  //设置CPSR寄存器中的相关位并进入到SVC模式中mrs   r2, spsr        //复制spsr(备份程序状态寄存)到r2中stm   r0!, {r2, sp, lr}   //将r2, sp, lr中的值保存到r0指向的地址中cps  #CPSR_MODE_ABT  //设置CPSR寄存器中的相关位并进入到ABT模式中mrs   r2, spsr        //复制spsr(备份程序状态寄存)到r2中stm   r0!, {r2, sp, lr}   //将r2, sp, lr中的值保存到r0指向的地址中cps  #CPSR_MODE_UND  //设置CPSR寄存器中的相关位并进入到UND模式中mrs   r2, spsr    //复制spsr(备份程序状态寄存)到r2中stm   r0!, {r2, sp, lr}   //将r2, sp, lr中的值保存到r0指向的地址中cps  #CPSR_MODE_MON  //设置CPSR寄存器中的相关位并进入到monitor模式中bx    lr  //返回调用函数
UNWIND( .fnend)
END_FUNC sm_save_modes_regs/* Restores the mode specific registers */
FUNC sm_restore_modes_regs , :
UNWIND( .fnstart)
UNWIND( .cantunwind)/* User mode registers has to be saved from system mode */cps   #CPSR_MODE_SYS  //设置CPSR寄存器中的相关位并进入到的SYS模式ldm   r0!, {sp, lr}   //将r0寄存器中指向的位置中的值加载到sp和lr寄存器中cps    #CPSR_MODE_IRQ  //设置CPSR寄存器中的相关位并进入到IRQ模式中ldm   r0!, {r2, sp, lr}   //将r0寄存器中指向的位置中的值加载到r2,sp和lr寄存器中msr spsr_fsxc, r2   //将r2寄存器指向的地址的数据赋值到spsr寄存器的标志域,状态域,扩展域,控制域cps  #CPSR_MODE_FIQ  //设置CPSR寄存器中的相关位并进入到FIQ模式中ldm   r0!, {r2, sp, lr}   //将r0寄存器中指向的位置中的值加载到r2,sp和lr寄存器中msr spsr_fsxc, r2   //将r2寄存器指向的地址的数据赋值到spsr寄存器的标志域,状态域,扩展域,控制域cps  #CPSR_MODE_SVC  //设置CPSR寄存器中的相关位并进入到SVC模式中ldm   r0!, {r2, sp, lr}   //将r0寄存器中指向的位置中的值加载到r2,sp和lr寄存器中msr spsr_fsxc, r2   //将r2寄存器指向的地址的数据赋值到spsr寄存器的标志域,状态域,扩展域,控制域cps  #CPSR_MODE_ABT  //设置CPSR寄存器中的相关位并进入到ABT模式中ldm   r0!, {r2, sp, lr}   //将r0寄存器中指向的位置中的值加载到r2,sp和lr寄存器中msr spsr_fsxc, r2   //将r2寄存器指向的地址的数据赋值到spsr寄存器的标志域,状态域,扩展域,控制域cps  #CPSR_MODE_UND  //设置CPSR寄存器中的相关位并进入到UND模式中ldm   r0!, {r2, sp, lr}   //将r0寄存器中指向的位置中的值加载到r2,sp和lr寄存器中msr spsr_fsxc, r2   //将r2寄存器指向的地址的数据赋值到spsr寄存器的标志域,状态域,扩展域,控制域cps  #CPSR_MODE_MON  //设置CPSR寄存器中的相关位并进入到MON模式中bx    lr  //返回调用函数
UNWIND( .fnend)
END_FUNC sm_restore_modes_regs

3. 总结

  non-secure world与secure world之间的切换都是通过修改当前的SCR寄存器中的NS位来实现的,而且在secure world态时需要使能FIQ的支持,而在non-secure world态时则需要禁止FIQ的支持。其在两种态之间的切换的时候需要做到对应的上下文的保存和恢复操作。

18. OP-TEE中secur world和non-secure world的切换过程相关推荐

  1. java1.8+pydev_Ubuntu 18.04.4 中使用 Eclipse+PyDev 配置 Python 开发环境

    先学习一下深度学习必备的编程语言 Python.工欲善其事,必先利其器.一个好用的 IDE 将会使你的学习事半功倍,下面就是我本人在 Ubuntu 18.04.4 中使用 Eclipse+PyDev ...

  2. qtabwidget切换tab事件_某超超临界机组初压/限压切换过程中扰动原因分析

    严寒夕  浙江浙能台州第二发电有限责任公司 [摘要]某火电厂汽轮机在初压/限压切换过程中出现负荷瞬时上升问题.从初压/限压切换的逻辑及切换过程中主要参数的变化分析,确定原因为压力控制器指令上升瞬间和转 ...

  3. linux ubuntu安装 mono,在Ubuntu 18.04系统中安装Mono及基本使用Mono的方法

    本文介绍在Ubuntu 18.04操作系统中安装Mono及基本使用Mono的方法.Mono是一个基于ECMA/ISO标准开发和运行跨平台应用程序的平台,它是Microsoft .NET框架的免费开源实 ...

  4. 在Ubuntu 18.04系统中使用Netplan工具配置网络

    Netplan 是一款使用在终端的配置网络工具,本文介绍在 Ubuntu 18.04 系统中使用 Netplan 来配置网络,新的配置文件.网络设备名称.配置静态 IP 地址.测试配置并应用.配置 D ...

  5. ubuntu18的网关ip在哪里配_技术|如何在 Ubuntu 18.04 LTS 中配置 IP 地址

    在 Ubuntu 18.04 LTS 中配置 IP 地址的方法和以往使用的配置方法有很大的不同.和旧版本的不同之处在于,Ubuntu 18.04 使用 Netplan 来配置 IP 地址,Netpla ...

  6. 记录在Ubuntu 18.04系统中安装Apache, MySQL和PHP环境

    虽然我们在Linux VPS.服务器安装WEB环境比较方便,可以选择面板或者一键包,但是有些我们需要深入学习的网友不会选择一键安装,而是会尝试编译安装.这样可以学到一些内在的技术.一般我们较为习惯选择 ...

  7. linux系统下载18.04,在Ubuntu 18.04系统中下载安装Persepolis Download Manager

    本文介绍在Ubuntu 18.04系统中下载和安装Persepolis Download Manager的方法,也适用在Linux Mint版本中,它是一个用Python编写的免费开源下载管理器,支持 ...

  8. 如何在 Ubuntu 18.04 LTS 中配置 IP 地址?

    在 Ubuntu 18.04 LTS 上配置 IP 地址的方法与旧方法有很大不同,与以前的版本不同,Ubuntu 18.04 使用Netplan实用程序,它是一个新的命令行网络配置实用程序,用于配置 ...

  9. 《树莓派实战秘籍》——1.18 技巧18在移动中给Pi供电

    本节书摘来异步社区<树莓派实战秘籍>一书中的第1章,第1.18节,作者:[美]Ruth Suehle ,Tom Callaway,更多章节内容可以访问云栖社区"异步社区" ...

  10. erp服务器安装Linux,在Ubuntu 18.04系统中安装Dolibarr ERP/CRM的步骤

    本文将介绍在服务器Ubuntu 18.04系统中安装Dolibarr ERP/CRM的步骤,该软件支持Windows.BSD.Mac.Solaris.Linux平台.Dolibarr是一款非常有能力的 ...

最新文章

  1. 使用命名空间:别名/导入
  2. spring in action 读书笔记
  3. 浅谈云计算三个层次SaaS/PaaS/IaaS
  4. [读书笔记] - 《深度探索C++对象模型》第5章 构造、解构、拷贝语意学
  5. python变量标识符_Python 1基础语法三(变量和标识符的区别)
  6. nginx 启动报错 “/var/run/nginx/nginx.pid failed” 解决方法
  7. iOS开发最新之CocoaPods环境配置教程
  8. 孙鑫VC学习笔记:第十讲画图 创建设置画笔的对话框 并且可以预览效果
  9. java内存结构不包含堆,JVM之详细分析java内存结构模型
  10. css鼠标滑过变大,css 鼠标移上去会变大(示例代码)
  11. java动态时钟_java实现动态时钟并设置闹钟功能
  12. DEDECMS专题制作
  13. 特殊的Excel填充序号技巧,总有一种你会遇到【特别实用,赶紧收藏】
  14. 微信订阅推送通知实现
  15. 小程序常用单位解释大全
  16. 联想小新520新品实测,对比当贝投影D3X竟无还手之力
  17. python 白噪声检验-利用python实现平稳时间序列的建模方式
  18. 矩阵知识:正交矩阵、行列式、子式与代数余子式
  19. Mysql-如何建表更符合业务
  20. 【VJudge】【Legilimens Contest 1】

热门文章

  1. coverity中碰到的错误
  2. 分享一款好用的英语词频统计软件
  3. android极光推送问题,极光推送- 常见问题 - 极光文档
  4. 在线作诗,做诗机,一键生成藏头诗,藏头诗在线制作,藏头诗生成器, 姓名藏头诗,姓名作诗
  5. 360手机java手机管家软件_3d藏机诗佳人泪两行在以前那期有
  6. mongoDB下载安装
  7. MongoDB下载、安装和配置教程
  8. mmsi是代表船舶什么_海上移动业务识别码(MMSI)是一种九位识别码,主要分配给()在DSC和NBDP通信中相互识别身份是使用。...
  9. Spark编程基础-(一)大数据技术概述
  10. 移动安全-安卓Smail代码入门讲解