在前面的文章OPTEE学习笔记 - AArch64 RPC(一)中我们分析了fast call的AArch64 RPC。本文基于前文,分析一下std call的实现。

正常执行流程

我们以optee_close_session函数为例,来探究std call的实现。当然我们也可以以其他函数为例,只是这个函数内部实现比较简单,更容易看清调用关系。

optee_close_session调用的关键函数是optee_do_call_with_arg,我们从这个函数说起

u32 optee_do_call_with_arg(struct tee_context *ctx, phys_addr_t parg)
{struct optee *optee = tee_get_drvdata(ctx->teedev);struct optee_call_waiter w;struct optee_rpc_param param = { };struct optee_call_ctx call_ctx = { };u32 ret;param.a0 = OPTEE_SMC_CALL_WITH_ARG; //这个值标志了此次调用是一个std callreg_pair_from_64(&param.a1, &param.a2, parg);/* Initialize waiter */optee_cq_wait_init(&optee->call_queue, &w);while (true) {struct arm_smccc_res res;optee->invoke_fn(param.a0, param.a1, param.a2, param.a3,param.a4, param.a5, param.a6, param.a7,&res);  //转入OPTEE执行具体函数if (res.a0 == OPTEE_SMC_RETURN_ETHREAD_LIMIT) {/** Out of threads in secure world, wait for a thread* become available.*/optee_cq_wait_for_completion(&optee->call_queue, &w); //OPTEE没有空闲thread,会直接返回,在这里等待} else if (OPTEE_SMC_RETURN_IS_RPC(res.a0)) {might_sleep();param.a0 = res.a0;param.a1 = res.a1;param.a2 = res.a2;param.a3 = res.a3;optee_handle_rpc(ctx, &param, &call_ctx); //RPC返回,后面介绍} else {ret = res.a0; //执行成功,把执行得到的结果取到,退出循环,不再执行break;}}optee_rpc_finalize_call(&call_ctx);/** We're done with our thread in secure world, if there's any* thread waiters wake up one.*/optee_cq_wait_final(&optee->call_queue, &w);return ret;
}

invoke_fn函数的执行流程前文已经做了介绍,std call和fast call的差别在于tf-a中opteed_smc_handler的处理方式。fast call中elr_el3被赋值为&optee_vector_table->fast_smc_entry。而std call被赋值为optee_vector_table->yield_smc_entry。其对应的执行函数是OPTEE中的thread_vector_table->vector_std_smc_entry

LOCAL_FUNC vector_std_smc_entry , : , .identity_mapreadjust_pcbl thread_handle_std_smc/** Normally thread_handle_std_smc() should return via* thread_exit(), thread_rpc(), but if thread_handle_std_smc()* hasn't switched stack (error detected) it will do a normal "C"* return.*/mov   w1, w0 //正常执行不会走到这里,但是例如OPTEE没有空闲线程的时候,正常的函数返回是会走到这里ldr   x0, =TEESMC_OPTEED_RETURN_CALL_DONEsmc #0b .   /* SMC should not return */
END_FUNC vector_std_smc_entry

thread_handle_std_smc函数会调用thread_alloc_and_run继续执行。

void thread_alloc_and_run(uint32_t a0, uint32_t a1, uint32_t a2, uint32_t a3)
{size_t n;struct thread_core_local *l = thread_get_core_local();bool found_thread = false;assert(l->curr_thread == -1);thread_lock_global();for (n = 0; n < CFG_NUM_THREADS; n++) {if (threads[n].state == THREAD_STATE_FREE) { //寻找空闲线程threads[n].state = THREAD_STATE_ACTIVE;found_thread = true;break;}}thread_unlock_global();if (!found_thread)return; //如果没有空闲线程,这里会直接返回l->curr_thread = n;threads[n].flags = 0;init_regs(threads + n, a0, a1, a2, a3); //初始化空闲线程的上下文thread_lazy_save_ns_vfp();l->flags &= ~THREAD_CLF_TMP;thread_resume(&threads[n].regs);/*NOTREACHED*/panic();
}

init_regs里会配置空闲线程对应的一些寄存器值,其实就是这个线程对应的上下文。

static void init_regs(struct thread_ctx *thread, uint32_t a0, uint32_t a1,uint32_t a2, uint32_t a3)
{thread->regs.pc = (uint64_t)thread_std_smc_entry;/** Stdcalls starts in SVC mode with masked foreign interrupts, masked* Asynchronous abort and unmasked native interrupts.*/thread->regs.cpsr = SPSR_64(SPSR_64_MODE_EL1, SPSR_64_MODE_SP_EL0,THREAD_EXCP_FOREIGN_INTR | DAIFBIT_ABT);/* Reinitialize stack pointer */thread->regs.sp = thread->stack_va_end;/** Copy arguments into context. This will make the* arguments appear in x0-x7 when thread is started.*/thread->regs.x[0] = a0;thread->regs.x[1] = a1;thread->regs.x[2] = a2;thread->regs.x[3] = a3;thread->regs.x[4] = 0;thread->regs.x[5] = 0;thread->regs.x[6] = 0;thread->regs.x[7] = 0;/* Set up frame pointer as per the Aarch64 AAPCS */thread->regs.x[29] = 0;
}

stack_va_end是此条线程对应的栈空间,栈空间是通过DECLARE_STACK(stack_thread, CFG_NUM_THREADS, STACK_THREAD_SIZE + CFG_STACK_THREAD_EXTRA, static);来声明的一个数组,stack_va_end指向数组尾。

thread->resg.pc指向的是thread_std_smc_entry函数。

在thread_alloc_and_run函数的最后调用了thread_resume函数,输入参数是thread的regs。

struct thread_ctx_regs {uint64_t sp;uint64_t pc;uint64_t cpsr;uint64_t x[31];uint64_t tpidr_el0;
};FUNC thread_resume , : //此时x0的地址为struct thread_ctx_regs的首地址load_xregs x0, THREAD_CTX_REGS_SP, 1, 3 //把x0为起始地址,THREAD_CTX_REGS_SP偏移的数据存到x1~x3寄存器,即赋值sp, elr_el1, spsr_el1到x1~x3load_xregs x0, THREAD_CTX_REGS_X4, 4, 30 //把x0为起始地址,THREAD_CTX_REGS_X4偏移的数据存到x4~x30寄存器,即赋值x4~x30mov sp, x1msr   elr_el1, x2msr  spsr_el1, x3ldr x1, [x0, THREAD_CTX_REGS_TPIDR_EL0]msr  tpidr_el0, x1b_if_spsr_is_el0 w3, 1f //init_regs中赋值为el1,所以这里不跳转。同时我们eret以后还是在el1load_xregs x0, THREAD_CTX_REGS_X1, 1, 3 //把x0为起始地址,THREAD_CTX_REGS_X1偏移的数据存到x1~x3寄存器,即赋值x1~x3ldr   x0, [x0, THREAD_CTX_REGS_X0] //x0赋值为struct thread_ctx_regs的x0的值return_from_exception //eret,还是在el1中1:    load_xregs x0, THREAD_CTX_REGS_X1, 1, 3ldr  x0, [x0, THREAD_CTX_REGS_X0]msr spsel, #1store_xregs sp, THREAD_CORE_LOCAL_X0, 0, 1b    eret_to_el0
END_FUNC thread_resume

return_from_exception其实就是调用了eret。由于spse_el1配置的下一跳的el还是el1,所以eret以后还是el1。之所以这么做,我的猜测是为了要通过spsr_el1更新pstate。

跳转后的地址在thread_std_smc_entry。thread_std_smc_entry函数第一条语句是调用了__thread_std_smc_entry

uint32_t __weak __thread_std_smc_entry(uint32_t a0, uint32_t a1, uint32_t a2,uint32_t a3)
{uint32_t rv = 0;#ifdef CFG_VIRTUALIZATIONvirt_on_stdcall();
#endifrv = std_smc_entry(a0, a1, a2, a3); //真正的处理函数if (rv == OPTEE_SMC_RETURN_OK) {struct thread_ctx *thr = threads + thread_get_id();thread_rpc_shm_cache_clear(&thr->shm_cache);if (!thread_prealloc_rpc_cache) {thread_rpc_free_arg(mobj_get_cookie(thr->rpc_mobj));mobj_put(thr->rpc_mobj);thr->rpc_arg = 0;thr->rpc_mobj = NULL;}}return rv;
}

std_smc_entry函数继续调用,最终调用到__tee_entry_std完成具体的功能。

uint32_t __tee_entry_std(struct optee_msg_arg *arg, uint32_t num_params)
{uint32_t rv = OPTEE_SMC_RETURN_OK;/* Enable foreign interrupts for STD calls */thread_set_foreign_intr(true);switch (arg->cmd) {case OPTEE_MSG_CMD_OPEN_SESSION:entry_open_session(arg, num_params);break;case OPTEE_MSG_CMD_CLOSE_SESSION:entry_close_session(arg, num_params);break;case OPTEE_MSG_CMD_INVOKE_COMMAND:entry_invoke_command(arg, num_params);break;case OPTEE_MSG_CMD_CANCEL:entry_cancel(arg, num_params);break;
#ifndef CFG_CORE_FFA
#ifdef CFG_CORE_DYN_SHMcase OPTEE_MSG_CMD_REGISTER_SHM:register_shm(arg, num_params);break;case OPTEE_MSG_CMD_UNREGISTER_SHM:unregister_shm(arg, num_params);break;
#endif
#endifdefault:EMSG("Unknown cmd 0x%x", arg->cmd);rv = OPTEE_SMC_RETURN_EBADCMD;}return rv;
}

值得注意的是thread_set_foreign_intr函数在这里打开了irq和fiq的中断,以后的逻辑中TEE是可以被中断打断,这也是std call和fast call的区别,fast call是不允许被打断的。

最终在thread_std_smc_entry函数中,调用__thread_std_smc_entry结束退出,重新mask掉所有的中断,然后使用smc重新进入el3。

后面的流程和fast call就相同了,最终返回到了REE侧。

TEE侧被REE中断打断

这里需要介绍一种情况,就是当PE执行在TEE侧的时候,REE的中断到来,后面是怎样的执行流程。这里以GICV3为例,当PE执行在TEE侧,从REE侧来的中断是一个FIQ中断。CPU在接收到中断以后会mask掉FIQ和IRQ。此时OPTEE运行在EL1上,设置了SCR_EL3寄存器的FIQ bit为0,即FIQ的中断不会陷入EL3,还是会在EL1处理。所以会跳入OPTEE设置在VABR_EL1的中断向量继续执行。

FUNC thread_excp_vect , : align=2048/* -----------------------------------------------------* EL1 with SP0 : 0x0 - 0x180* -----------------------------------------------------*/.balign    128, INV_INSN
el1_sync_sp0:store_xregs sp, THREAD_CORE_LOCAL_X0, 0, 3b    el1_sync_abortcheck_vector_size el1_sync_sp0.balign 128, INV_INSN
el1_irq_sp0:store_xregs sp, THREAD_CORE_LOCAL_X0, 0, 3b elx_irqcheck_vector_size el1_irq_sp0.balign 128, INV_INSN
el1_fiq_sp0:store_xregs sp, THREAD_CORE_LOCAL_X0, 0, 3b elx_fiqcheck_vector_size el1_fiq_sp0.balign 128, INV_INSN
el1_serror_sp0:b    el1_serror_sp0check_vector_size el1_serror_sp0/* -----------------------------------------------------* Current EL with SP1: 0x200 - 0x380* -----------------------------------------------------*/

跳转到函数elx_fiq,elx_fiq的实现为

LOCAL_FUNC elx_fiq , :
#if defined(CFG_ARM_GICV3)foreign_intr_handler  fiq
#elsenative_intr_handler    fiq
#endif
END_FUNC elx_fiq

由于我们基于的是GICV3,所以我们需要看一下foreign_intr_handler的实现。

.macro foreign_intr_handler mode:req/** Update core local flags*/ldr w1, [sp, #THREAD_CORE_LOCAL_FLAGS]lsl   w1, w1, #THREAD_CLF_SAVED_SHIFTorr  w1, w1, #THREAD_CLF_TMP.ifc \mode\(),fiqorr w1, w1, #THREAD_CLF_FIQ.elseorr w1, w1, #THREAD_CLF_IRQ.endifstr    w1, [sp, #THREAD_CORE_LOCAL_FLAGS]/* get pointer to current thread context in x0 */get_thread_ctx sp, 0, 1, 2/* Keep original SP_EL0 */mrs  x2, sp_el0/* Store original sp_el0 */str    x2, [x0, #THREAD_CTX_REGS_SP]/* Store tpidr_el0 */mrs   x2, tpidr_el0str    x2, [x0, #THREAD_CTX_REGS_TPIDR_EL0]/* Store x4..x30 */store_xregs x0, THREAD_CTX_REGS_X4, 4, 30/* Load original x0..x3 into x10..x13 */load_xregs sp, THREAD_CORE_LOCAL_X0, 10, 13/* Save original x0..x3 */store_xregs x0, THREAD_CTX_REGS_X0, 10, 13/* load tmp_stack_va_end */ldr   x1, [sp, #THREAD_CORE_LOCAL_TMP_STACK_VA_END]/* Switch to SP_EL0 */msr  spsel, #0mov    sp, x1
.../** Mark current thread as suspended*/mov    w0, #THREAD_FLAGS_EXIT_ON_FOREIGN_INTRmrs   x1, spsr_el1mrs x2, elr_el1bl   thread_state_suspend //函数返回当前thread id。存在x0中/* Update core local flags *//* Switch to SP_EL1 */msr  spsel, #1ldr    w1, [sp, #THREAD_CORE_LOCAL_FLAGS]lsr   w1, w1, #THREAD_CLF_SAVED_SHIFTorr  w1, w1, #THREAD_CLF_TMPstr  w1, [sp, #THREAD_CORE_LOCAL_FLAGS]msr   spsel, #0/** Note that we're exiting with SP_EL0 selected since the entry* functions expects to have SP_EL0 selected with the tmp stack* set.*//* Passing thread index in w0 */b   thread_foreign_intr_exit
.endm

需要注意的一点是,中断触发后,sp用的是el1的sp,而不是el0的sp了。el1的sp,指向的还是一个context,是一个struct thread_core_local类型的数组中的一个元素:struct thread_core_local thread_core_local[CFG_TEE_CORE_NB_CORE]。具体是哪一个元素是根据当前是哪个核来决定的。例如,如果当前是主核调用的流程,那么sp就是指向的thread_core_local[0]。设置sp的代码如下

 .macro set_spbl __get_core_poscmp   x0, #CFG_TEE_CORE_NB_CORE/* Unsupported CPU, park it before it breaks something */bge   unhandled_cpuadr    x1, stack_tmp_strideldr w1, [x1]mul x1, x0, x1adrp  x0, stack_tmp_exportadd x0, x0, :lo12:stack_tmp_exportldr   x0, [x0]msr spsel, #0add    sp, x1, x0bl    thread_get_core_localmsr    spsel, #1mov    sp, x0msr   spsel, #0.endm

foreign_intr_handler函数的主要作用是保存当前各寄存器状态,然后把OPTEE的thread置为suspend(其实就是保存OPTEE thread的一些上下文),然后通过thread_foreign_intr_exit进入EL3,准备返回REE。

FUNC thread_foreign_intr_exit , :mov w4, w0 //w0保存的是OPTEE thread idldr   w0, =TEESMC_OPTEED_RETURN_CALL_DONEldr w1, =OPTEE_SMC_RETURN_RPC_FOREIGN_INTRmov  w2, #0mov   w3, #0smc   #0b .   /* SMC should not return */
END_FUNC thread_foreign_intr_exit

进入EL3以后,还是走的TF-A的EL3返回的路径,最后会退回到REE侧调用smc的位置,以上面close session为例,REE返回的位置会是optee_do_call_with_arg的optee->invoke_fn的位置

u32 optee_do_call_with_arg(struct tee_context *ctx, phys_addr_t parg)
{struct optee *optee = tee_get_drvdata(ctx->teedev);struct optee_call_waiter w;struct optee_rpc_param param = { };struct optee_call_ctx call_ctx = { };u32 ret;param.a0 = OPTEE_SMC_CALL_WITH_ARG;reg_pair_from_64(&param.a1, &param.a2, parg);/* Initialize waiter */optee_cq_wait_init(&optee->call_queue, &w);while (true) {struct arm_smccc_res res;optee->invoke_fn(param.a0, param.a1, param.a2, param.a3,param.a4, param.a5, param.a6, param.a7,&res);if (res.a0 == OPTEE_SMC_RETURN_ETHREAD_LIMIT) {/** Out of threads in secure world, wait for a thread* become available.*/optee_cq_wait_for_completion(&optee->call_queue, &w); } else if (OPTEE_SMC_RETURN_IS_RPC(res.a0)) {might_sleep();param.a0 = res.a0;param.a1 = res.a1;param.a2 = res.a2;param.a3 = res.a3;optee_handle_rpc(ctx, &param, &call_ctx); //RPC返回,后面介绍} else {ret = res.a0;break;}}optee_rpc_finalize_call(&call_ctx);/** We're done with our thread in secure world, if there's any* thread waiters wake up one.*/optee_cq_wait_final(&optee->call_queue, &w);return ret;
}

这里需要注意的是,在TEE侧触发中断以后,中断源一直没有被deactive,由于进入中断后CPU中断被mask,所以我们在感知不到中断。但是跳出EL3以后,CPU中断被硬件unmask,此时CPU会立即感受到中断,同时触发REE侧的中断,注意此时的中断变为了IRQ。

所以后续流程会跳到REE侧的中断处理函数进行处理,处理完成后回到现在的位置,即optee_do_call_with_arg的optee->invoke_fn,中断处理这段过程我们暂且不研究。

在TF-A的opteed_smc_handler函数中处理了smc跳入EL3的情况,我们注意一下在thread_foreign_intr_exit函数中几个寄存器的值如下:

w0 TEESMC_OPTEED_RETURN_CALL_DONE
w1 OPTEE_SMC_RETURN_RPC_FOREIGN_INTR
w2 0
w3 0
w4 optee thread id

进入opteed_smc_handler后,先处理了w0,即进入TEESMC_OPTEED_RETURN_CALL_DONE分支。然后在最后用了SMC_RET4(ns_cpu_context, x1, x2, x3, x4);函数return。在这里,x1~x4被赋值给了x0~x3,即return后的寄存器值为

w0 OPTEE_SMC_RETURN_RPC_FOREIGN_INTR
w1 0
w2 0
w3 optee thread id

然后eret回到了optee_do_call_with_arg的optee->invoke_fn的位置。optee->invoke_fn下一条指令先会判断是否是OPTEE_SMC_RETURN_ETHREAD_LIMIT,即线程不够用;然后判断当前的call是不是来自RPC,正好当前的x0的值就是来自RPC(OPTEE_SMC_RETURN_RPC_FOREIGN_INTR),所以顺理成章的走了这条分支。我们可以看到前面的寄存器的值都被赋值给了param:

param.a0 OPTEE_SMC_RETURN_RPC_FOREIGN_INTR
param.a1 0
param.a2 0
param.a3 optee thread id

然后调用了optee_handle_rpc。这个函数对于param.a0==OPTEE_SMC_RETURN_RPC_FOREIGN_INTR没有做什么处理,只是param.a0=OPTEE_SMC_CALL_RETURN_FROM_RPC,换了一个id,然后就退出了。

那么在optee_do_call_with_arg中就会经过一个while循环,重新走optee->invoke_fn,进入EL3。

还是走正常的smc流程,由于OPTEE_SMC_RETURN_RPC_FOREIGN_INTR是std call,所以最后又走到了thread_handle_std_smc。


uint32_t thread_handle_std_smc(uint32_t a0, uint32_t a1, uint32_t a2,uint32_t a3, uint32_t a4, uint32_t a5,uint32_t a6 __unused, uint32_t a7 __maybe_unused)
{uint32_t rv = OPTEE_SMC_RETURN_OK;thread_check_canaries();#ifdef CFG_VIRTUALIZATIONif (!virt_set_guest(a7))return OPTEE_SMC_RETURN_ENOTAVAIL;
#endif/** thread_resume_from_rpc() and thread_alloc_and_run() only return* on error. Successful return is done via thread_exit() or* thread_rpc().*/if (a0 == OPTEE_SMC_CALL_RETURN_FROM_RPC) {thread_resume_from_rpc(a3, a1, a2, a4, a5); //resume上次suspend的threadrv = OPTEE_SMC_RETURN_ERESUME;} else {thread_alloc_and_run(a0, a1, a2, a3);rv = OPTEE_SMC_RETURN_ETHREAD_LIMIT;}#ifdef CFG_VIRTUALIZATIONvirt_unset_guest();
#endifreturn rv;
}

thread_resume_from_rpc就会resume上次suspend的thread,恢复所有相关的寄存器,包括pc指针。然后继续执行。

有关注suspend/resume的同学可以对比查看这两个函数:foreign_intr_handler和thread_resume_from_rpc。

TEE侧执行完成后,就会按照fast call的方式返回到REE侧。

至此,一整套REE/TEE RPC通信就介绍完了

OPTEE学习笔记 - AArch64 RPC(二)相关推荐

  1. OPTEE学习笔记 - AArch64 RPC(一)

    前文OPTEE学习笔记 - REE与TEE通信记录了AArch32的RPC调用流程,这边总结一下OPTEE AArch64的RPC调用流程,基于optee 3.11版本以及TF-A 2.4 REE侧E ...

  2. tensorflow学习笔记(三十二):conv2d_transpose (解卷积)

    tensorflow学习笔记(三十二):conv2d_transpose ("解卷积") deconv解卷积,实际是叫做conv_transpose, conv_transpose ...

  3. BizTalk学习笔记系列之二:实例说明如何使用BizTalk

    BizTalk学习笔记系列之二:实例说明如何使用BizTalk --.BizTalk学习笔记系列之二<?XML:NAMESPACE PREFIX = O /> Aaron.Gao,2006 ...

  4. Windows保护模式学习笔记(十二)—— 控制寄存器

    Windows保护模式学习笔记(十二)-- 控制寄存器 控制寄存器 Cr0寄存器 Cr2寄存器 Cr4寄存器 控制寄存器 描述: 控制寄存器有五个,分别是:Cr0 Cr1 Cr2 Cr3 Cr4 Cr ...

  5. 汇编入门学习笔记 (十二)—— int指令、port

    疯狂的暑假学习之  汇编入门学习笔记 (十二)--  int指令.port 參考: <汇编语言> 王爽 第13.14章 一.int指令 1. int指令引发的中断 int n指令,相当于引 ...

  6. OpenCV学习笔记(十二):边缘检测:Canny(),Sobel(),Laplace(),Scharr滤波器

    OpenCV学习笔记(十二):边缘检测:Canny(),Sobel(),Laplace(),Scharr滤波器 1)滤波:边缘检测的算法主要是基于图像强度的一阶和二阶导数,但导数通常对噪声很敏感,因此 ...

  7. QT学习笔记(十二):透明窗体设置

    QT学习笔记(十二):透明窗体设置 创建 My_Widget 类 基类为QWidget , My_Widget.cpp 源文件中添加代码 #include "widget.h" # ...

  8. Python基础学习笔记之(二)

    Python基础学习笔记之(二) zouxy09@qq.com http://blog.csdn.net/zouxy09 六.包与模块 1.模块module Python中每一个.py脚本定义一个模块 ...

  9. Kinect开发学习笔记之(二)Kinect开发学习资源整理

    Kinect开发学习笔记之(二)Kinect开发学习资源整理 zouxy09@qq.com http://blog.csdn.net/zouxy09 刚刚接触Kinect,在网上狂搜资料,获得了很多有 ...

最新文章

  1. 英特尔AI医疗实战手册曝光:医生诊断提速10倍,推理时间减少85%
  2. 关于Excel导入的问题记录
  3. Spring Cloud GatewayAPI网关服务
  4. html点击圆点箭头分页,css实现小箭头的实现方式
  5. Codevs 2756 树上的路径
  6. 无法从套接字中获取更多数据_数据科学中应引起更多关注的一个组成部分
  7. 国外知名的开源项目托管网站
  8. python去重复元素_Python实现去除列表中重复元素的方法总结【7种方法】
  9. Oracle锁庞大大引见
  10. Tiled Map的使用说明
  11. 2021 泰迪杯 A 题思路
  12. 黑龙江省黑河市谷歌高清卫星地图下载
  13. 手把手教你写数独计算器(1)
  14. [SSL_CHX][2021-08-19]子矩阵求和
  15. 2016清华集训滚粗记
  16. 科学计算法(e/E表示规则)
  17. 本人秋招结束了,愿所有人都拿到满意的offer
  18. 几十秒生产一瓶 浙江警方破获制售假冒“飞天茅台”案
  19. Python x OpenCV+Numpy 函数参考列表
  20. Python小工具——格雷码转换器

热门文章

  1. 统计java类含有多少个方法_35个Java代码优化的小技巧,你知道几个?
  2. Unity3d的安装
  3. jquery上传图片本地预览插件V1.2
  4. Real user ID, effective user ID, set user ID
  5. JS高级前端开发群加群说明
  6. 一文带你吃透 Kafka 这些原理
  7. java ltp4j_博客 | 收藏 | 100多个DL框架、AI库、ML库、NLP库、CV库汇总,建议收藏!...
  8. 从算法上解读自动驾驶是如何实现的?
  9. mysql向表中插中文显示,针对mysql数据库无法在表中插入中文字符的解决方案(彻底解决jav...
  10. Scanner警告问题