前言

ART 虚拟机执行 Java 方法主要有两种模式:quick code 模式和 Interpreter 模式

  • quick code 模式:执行 arm 汇编指令
  • Interpreter 模式:由解释器解释执行 Dalvik 字节码

本篇文章就来讲一下,Interpreter 模式是如何运行的(基于 Android 8.1)

一、 Interpreter 模式


                        点击查看大图
上图是将断点打在 art_quick_invoke_stub 时出现的一段 backtraces,这段 backtraces 很好地描述出了 Interpreter 模式是如何运转的,以及 quick code 模式与 Interpreter 模式之间是如何切换的

1.1 art_quick_to_interpreter_bridge

从 f 19、f 18 可以看到由 quick code 模式进入 Interpreter 模式需要通过 art_quick_to_interpreter_bridge 这个 bridge,

                        点击查看大图
从 f 18 可以看到,artQuickToInterpreterBridge 会通过调用 interpreter::EnterInterpreterFromEntryPoint(self, code_item, shadow_frame); 来进入 Interpreter 模式,查看一下 EnterInterpreterFromEntryPoint 的定义:

JValue EnterInterpreterFromEntryPoint(Thread* self, const DexFile::CodeItem* code_item,ShadowFrame* shadow_frame) {DCHECK_EQ(self, Thread::Current());bool implicit_check = !Runtime::Current()->ExplicitStackOverflowChecks();if (UNLIKELY(__builtin_frame_address(0) < self->GetStackEndForInterpreter(implicit_check))) {ThrowStackOverflowError(self);return JValue();}jit::Jit* jit = Runtime::Current()->GetJit();if (jit != nullptr) {jit->NotifyCompiledCodeToInterpreterTransition(self, shadow_frame->GetMethod());}return Execute(self, code_item, *shadow_frame, JValue());
}

可以看到其会调用 Execute() 函数,结合上面的 backtraces,我们可以将 Execute() 函数看作是 Interpreter 模式的起点

1.2 Execute()

art/runtime/interpreter/interpreter.cc

enum InterpreterImplKind {kSwitchImplKind,        // Switch-based interpreter implementation.kMterpImplKind          // Assembly interpreter
};static constexpr InterpreterImplKind kInterpreterImplKind = kMterpImplKind; // 默认使用 Mterp 类型的实现static inline JValue Execute(Thread* self,const DexFile::CodeItem* code_item,ShadowFrame& shadow_frame,JValue result_register,bool stay_in_interpreter = false) REQUIRES_SHARED(Locks::mutator_lock_) {...if (LIKELY(shadow_frame.GetDexPC() == 0)) {  // Entering the method, but not via deoptimization.if (kIsDebugBuild) {self->AssertNoPendingException();}instrumentation::Instrumentation* instrumentation = Runtime::Current()->GetInstrumentation();ArtMethod *method = shadow_frame.GetMethod();...if (!stay_in_interpreter) {jit::Jit* jit = Runtime::Current()->GetJit();if (jit != nullptr) {jit->MethodEntered(self, shadow_frame.GetMethod());if (jit->CanInvokeCompiledCode(method)) { // 1、jit 不为 nullptr,并且 jit 编译出了对应的 quick code,那么 ArtInterpreterToCompiledCodeBridgeJValue result;// Pop the shadow frame before calling into compiled code.self->PopShadowFrame();// Calculate the offset of the first input reg. The input registers are in the high regs.// It's ok to access the code item here since JIT code will have been touched by the// interpreter and compiler already.uint16_t arg_offset = code_item->registers_size_ - code_item->ins_size_;ArtInterpreterToCompiledCodeBridge(self, nullptr, &shadow_frame, arg_offset, &result);// Push the shadow frame back as the caller will expect it.self->PushShadowFrame(&shadow_frame);return result;}}}}shadow_frame.GetMethod()->GetDeclaringClass()->AssertInitializedOrInitializingInThread(self);// Lock counting is a special version of accessibility checks, and for simplicity and// reduction of template parameters, we gate it behind access-checks mode.ArtMethod* method = shadow_frame.GetMethod();DCHECK(!method->SkipAccessChecks() || !method->MustCountLocks());bool transaction_active = Runtime::Current()->IsActiveTransaction();if (LIKELY(method->SkipAccessChecks())) {// Enter the "without access check" interpreter.if (kInterpreterImplKind == kMterpImplKind) {if (transaction_active) {// No Mterp variant - just use the switch interpreter.return ExecuteSwitchImpl<false, true>(self, code_item, shadow_frame, result_register,false);} else if (UNLIKELY(!Runtime::Current()->IsStarted())) {...} else {while (true) {...bool returned = ExecuteMterpImpl(self, code_item, &shadow_frame, &result_register);if (returned) {return result_register;} else {// Mterp didn't like that instruction.  Single-step it with the reference interpreter.result_register = ExecuteSwitchImpl<false, false>(self, code_item, shadow_frame,result_register, true);if (shadow_frame.GetDexPC() == DexFile::kDexNoIndex) {// Single-stepped a return or an exception not handled locally.  Return to caller.return result_register;}}}}} else {...}} else {// Enter the "with access check" interpreter.if (kInterpreterImplKind == kMterpImplKind) {// No access check variants for Mterp.  Just use the switch version.if (transaction_active) {return ExecuteSwitchImpl<true, true>(self, code_item, shadow_frame, result_register,false);} else {return ExecuteSwitchImpl<true, false>(self, code_item, shadow_frame, result_register,false);}} else {DCHECK_EQ(kInterpreterImplKind, kSwitchImplKind);if (transaction_active) {return ExecuteSwitchImpl<true, true>(self, code_item, shadow_frame, result_register,false);} else {return ExecuteSwitchImpl<true, false>(self, code_item, shadow_frame, result_register,false);}}}
}

从上面可以看到,Execute() 的作用就是:

  • 如果有 jit,并且 jit 编译出了对应 method 的 quick code,那么选择通过 ArtInterpreterToCompiledCodeBridge 这个去执行对应的 quick code
  • 如果上面的条件不满足,那么根据情况选择 Mterp 或者 Switch 类型的解释器实现来解释执行对应的 Dalvik 字节码

因为默认使用 Mterp 类型的 Interpreter 实现,所以大多数情况下会调用 ExecuteMterpImpl() 函数来解释执行 Dalvik 字节码,下面来重点看一下 ExecuteMterpImpl() 的实现

1.3 ExecuteMterpImpl

art/runtime/interpreter/mterp/out/mterp_arm64.S

/* During bringup, we'll use the shadow frame model instead of xFP */
/* single-purpose registers, given names for clarity */
#define xPC      x20
#define xFP      x21
#define xSELF    x22
#define xINST    x23
#define wINST    w23
#define xIBASE   x24
#define xREFS    x25
#define wPROFILE w26
#define xPROFILE x26
#define ip       x16
#define ip2      x17.macro EXPORT_PCstr  xPC, [xFP, #OFF_FP_DEX_PC_PTR]
.endm.macro FETCH_INSTldrh    wINST, [xPC]
.endm.macro GOTO_OPCODE regadd     \reg, xIBASE, \reg, lsl #7br      \reg
.endm.macro GET_INST_OPCODE regand     \reg, xINST, #255
.endm/** Interpreter entry point.* On entry:*  x0  Thread* self/*  x1  code_item*  x2  ShadowFrame*  x3  JValue* result_register**/.global ExecuteMterpImpl.type   ExecuteMterpImpl, %function.balign 16ExecuteMterpImpl:.cfi_startprocSAVE_TWO_REGS_INCREASE_FRAME xPROFILE, x27, 80SAVE_TWO_REGS                xIBASE, xREFS, 16SAVE_TWO_REGS                xSELF, xINST, 32SAVE_TWO_REGS                xPC, xFP, 48SAVE_TWO_REGS                fp, lr, 64add     fp, sp, #64/* Remember the return register */str     x3, [x2, #SHADOWFRAME_RESULT_REGISTER_OFFSET]/* Remember the code_item */str     x1, [x2, #SHADOWFRAME_CODE_ITEM_OFFSET]/* set up "named" registers */mov     xSELF, x0ldr     w0, [x2, #SHADOWFRAME_NUMBER_OF_VREGS_OFFSET]add     xFP, x2, #SHADOWFRAME_VREGS_OFFSET     // point to vregs.add     xREFS, xFP, w0, lsl #2                 // point to reference array in shadow frameldr     w0, [x2, #SHADOWFRAME_DEX_PC_OFFSET]   // Get starting dex_pc.add     xPC, x1, #CODEITEM_INSNS_OFFSET        // Point to base of insns[]add     xPC, xPC, w0, lsl #1                   // Create direct pointer to 1st dex opcodeEXPORT_PC/* Starting ibase */ldr     xIBASE, [xSELF, #THREAD_CURRENT_IBASE_OFFSET]/* Set up for backwards branches & osr profiling */ldr     x0, [xFP, #OFF_FP_METHOD]add     x1, xFP, #OFF_FP_SHADOWFRAMEbl      MterpSetUpHotnessCountdownmov     wPROFILE, w0                // Starting hotness countdown to xPROFILE/* start executing the instruction at rPC */FETCH_INST                          // load wINST from rPCGET_INST_OPCODE ip                  // extract opcode from wINSTGOTO_OPCODE ip                      // jump to next instruction/* NOTE: no fallthrough */

在 gdb 中查看上面这一段即为:

Dump of assembler code for function ExecuteMterpImpl:0x000000790e52e280 <+0>: stp x26, x27, [sp,#-80]!0x000000790e52e284 <+4>: stp x24, x25, [sp,#16]0x000000790e52e288 <+8>:   stp x22, x23, [sp,#32]0x000000790e52e28c <+12>:  stp x20, x21, [sp,#48]0x000000790e52e290 <+16>:  stp x29, x30, [sp,#64]0x000000790e52e294 <+20>:  add x29, sp, #0x40/* Remember the return register */0x000000790e52e298 <+24>:    str x3, [x2,#16]/* Remember the code_item */0x000000790e52e29c <+28>:    str x1, [x2,#32]/* set up "named" registers */0x000000790e52e2a0 <+32>:    mov x22, x00x000000790e52e2a4 <+36>: ldr w0, [x2,#48]0x000000790e52e2a8 <+40>:    add x21, x2, #0x3c0x000000790e52e2ac <+44>:  add x25, x21, x0, uxtx #20x000000790e52e2b0 <+48>:   ldr w0, [x2,#52]          // w0 指向 SHADOWFRAME 的 dex_pc_0x000000790e52e2b4 <+52>:    add x20, x1, #0x10        // xPC 指向 CodeItem 中 bytecode array 的起点, 即 base of insns[]0x000000790e52e2b8 <+56>:    add x20, x20, x0, uxtx #1 // xPC 指向第一条 dex opcode0x000000790e52e2bc <+60>:   stur    x20, [x21,#-36]/* Starting ibase */0x000000790e52e2c0 <+64>: ldr x24, [x22,#1736]0x000000790e52e2c4 <+68>:    ldur    x0, [x21,#-52]0x000000790e52e2c8 <+72>:  sub x1, x21, #0x3c0x000000790e52e2cc <+76>:  bl  0x790e52e02c <MterpSetUpHotnessCountdown(art::ArtMethod*, art::ShadowFrame*)>0x000000790e52e2d0 <+80>: mov w26, w0/* start executing the instruction at rPC */0x000000790e52e2d4 <+84>: ldrh    w23, [x20]  // Fetch the next instruction from xPC into w230x000000790e52e2d8 <+88>: and x16, x23, #0xff // 将 instruction's opcode field 放到特殊寄存器 x16 当中0x000000790e52e2dc <+92>: add x16, x24, x16, lsl #7 0x000000790e52e2e0 <+96>:  br  x16             // Begin executing the opcode in x160x000000790e52e2e4 <+100>:   nop

1.4

执行 opcode,每个 opcode 以 128 字节对齐,并且绝大多数 opcode 都会包含如下指令:

FETCH_ADVANCE_INST 2 // 此处的数字可以是其他的,譬如 1、3
GET_INST_OPCODE ip
GOTO_OPCODE ip

看一下 FETCH_ADVANCE_INST 的定义:

.macro FETCH_ADVANCE_INST countldrh    wINST, [xPC, #((\count)*2)]!
.endm

这几条指令的作用是,移动 xPC 到下一条 instruction,并将移动后的 xPC 的值 load 到 wINST 中,然后跳转去执行 opcode

例如:

/* ------------------------------ */.balign 128
.L_op_nop: /* 0x00 */
/* File: arm64/op_nop.S */FETCH_ADVANCE_INST 1                // advance to next instr, load rINSTGET_INST_OPCODE ip                  // ip<- opcode from rINSTGOTO_OPCODE ip                      // execute it/* ------------------------------ */

这条 opcode 相当于什么都没做,然后移动 xPC,再去执行下一条 instruction;
通过上面这些分析,我们可以看出对 Dalvik 字节码解释执行的运行模式:

  • 在 Mterp 解释器当中维护了一种对应关系:opcode 与实现这个 opcode 的汇编指令的对应关系
  • 我们在解释执行的时候,实际上是取出一条指令,通过 opcode 找到对应的汇编实现,然后运行
  • 大部分 opcode 中都会包含取出下一条 instruction、然后跳转执行的操作,形成一个循环
  • 一些带有 invoke 操作的 opcode 将会开启下一个 Java 调用

例如,图1中的 f 16:

/* ------------------------------ */.balign 128
.L_op_invoke_virtual_quick: /* 0xe9 */
/* File: arm64/op_invoke_virtual_quick.S */
/* File: arm64/invoke.S *//** Generic invoke handler wrapper.*//* op vB, {vD, vE, vF, vG, vA}, class@CCCC *//* op {vCCCC..v(CCCC+AA-1)}, meth@BBBB */.extern MterpInvokeVirtualQuickEXPORT_PCmov     x0, xSELFadd     x1, xFP, #OFF_FP_SHADOWFRAMEmov     x2, xPCmov     x3, xINSTbl      MterpInvokeVirtualQuickcbz     w0, MterpExceptionFETCH_ADVANCE_INST 3bl      MterpShouldSwitchInterpreterscbnz    w0, MterpFallbackGET_INST_OPCODE ipGOTO_OPCODE ip/* ------------------------------ */

从图1中可以看到,其后会经过:

MterpInvokeVirtualQuick|_DoInvokeVirtualQuick|_DoCall|_DoCallCommon|_PerformCall|_ArtInterpreterToInterpreterBridge|_Execute

的调用栈开启下一个 method 的解释执行

1.4.2 PerformCall

art/runtime/common_dex_operations.h

inline void PerformCall(Thread* self,const DexFile::CodeItem* code_item,ArtMethod* caller_method,const size_t first_dest_reg,ShadowFrame* callee_frame,JValue* result,bool use_interpreter_entrypoint)REQUIRES_SHARED(Locks::mutator_lock_) {if (LIKELY(Runtime::Current()->IsStarted())) {if (use_interpreter_entrypoint) {interpreter::ArtInterpreterToInterpreterBridge(self, code_item, callee_frame, result);} else {interpreter::ArtInterpreterToCompiledCodeBridge(self, caller_method, callee_frame, first_dest_reg, result);}} else {interpreter::UnstartedRuntime::Invoke(self, code_item, callee_frame, result, first_dest_reg);}
}

可以看到在上面的调用栈中,在执行 PerformCall 方法时会判断 use_interpreter_entrypoint 是否为 true,从而选择是跳转到 ArtInterpreterToInterpreterBridge 进行解释执行还是通过 ArtInterpreterToCompiledCodeBridge 跳转到被调用 method 的 entry_point_from_quick_compiled_code_ 入口

1.4.3 DoCallCommon

art/runtime/interpreter/interpreter_common.cc

template <bool is_range,bool do_assignability_check>
static inline bool DoCallCommon(ArtMethod* called_method,Thread* self,ShadowFrame& shadow_frame,JValue* result,uint16_t number_of_inputs,uint32_t (&arg)[Instruction::kMaxVarArgRegs],uint32_t vregC) {bool string_init = false;...// Compute method information.const DexFile::CodeItem* code_item = called_method->GetCodeItem();// Number of registers for the callee's call frame.uint16_t num_regs;// 1、是否使用 interpreter 模式const bool use_interpreter_entrypoint = !Runtime::Current()->IsStarted() ||ClassLinker::ShouldUseInterpreterEntrypoint(called_method,called_method->GetEntryPointFromQuickCompiledCode());if (LIKELY(code_item != nullptr)) {...} else {DCHECK(called_method->IsNative() || called_method->IsProxyMethod());num_regs = number_of_inputs;}// 2、Hack for String init:...// Parameter registers go at the end of the shadow frame.DCHECK_GE(num_regs, number_of_inputs);size_t first_dest_reg = num_regs - number_of_inputs;DCHECK_NE(first_dest_reg, (size_t)-1);// 3、Allocate shadow frame on the stack.const char* old_cause = self->StartAssertNoThreadSuspension("DoCallCommon");ShadowFrameAllocaUniquePtr shadow_frame_unique_ptr =CREATE_SHADOW_FRAME(num_regs, &shadow_frame, called_method, /* dex pc */ 0);ShadowFrame* new_shadow_frame = shadow_frame_unique_ptr.get();// 4、Initialize new shadow frame by copying the registers from the callee shadow frame....// 5、PerformCallPerformCall(self,code_item,shadow_frame.GetMethod(),first_dest_reg,new_shadow_frame,result,use_interpreter_entrypoint);if (string_init && !self->IsExceptionPending()) {SetStringInitValueToAllAliases(&shadow_frame, string_init_vreg_this, *result);}return !self->IsExceptionPending();
}

可以看到在 DoCallCommon 中主要做了5件事,详细的细节暂时先不关注,主要看一下 use_interpreter_entrypoint,其是通过 ClassLinker::ShouldUseInterpreterEntrypoint 方法取得的值

1.4.4 ClassLinker::ShouldUseInterpreterEntrypoint

art/runtime/class_linker.cc

bool ClassLinker::ShouldUseInterpreterEntrypoint(ArtMethod* method, const void* quick_code) {if (UNLIKELY(method->IsNative() || method->IsProxyMethod())) {return false;}if (quick_code == nullptr) {return true;}Runtime* runtime = Runtime::Current();instrumentation::Instrumentation* instr = runtime->GetInstrumentation();if (instr->InterpretOnly()) {return true;}if (runtime->GetClassLinker()->IsQuickToInterpreterBridge(quick_code)) {// Doing this check avoids doing compiled/interpreter transitions.return true;}if (Dbg::IsForcedInterpreterNeededForCalling(Thread::Current(), method)) {// Force the use of interpreter when it is required by the debugger.return true;}if (runtime->IsJavaDebuggable()) {// For simplicity, we ignore precompiled code and go to the interpreter// assuming we don't already have jitted code.// We could look at the oat file where `quick_code` is being defined,// and check whether it's been compiled debuggable, but we decided to// only rely on the JIT for debuggable apps.jit::Jit* jit = Runtime::Current()->GetJit();return (jit == nullptr) || !jit->GetCodeCache()->ContainsPc(quick_code);}if (runtime->IsNativeDebuggable()) {DCHECK(runtime->UseJitCompilation() && runtime->GetJit()->JitAtFirstUse());// If we are doing native debugging, ignore application's AOT code,// since we want to JIT it (at first use) with extra stackmaps for native// debugging. We keep however all AOT code from the boot image,// since the JIT-at-first-use is blocking and would result in non-negligible// startup performance impact.return !runtime->GetHeap()->IsInBootImageOatFile(quick_code);}return false;
}

可以看到上面每个判断条件都会作为是否使用 Interpreter 模式的一个依据,我们主要关注一下下面几个条件:

  • quick_code == nullptr
  • instr->InterpretOnly()
  • IsQuickToInterpreterBridge(quick_code)
  • ……

当上面这几个条件有一个满足时,ShouldUseInterpreterEntrypoint 就会返回 true,使用 Interpreter 模式

二、总结

Interpreter 模式的运行流程如下所示:

ART 虚拟机 — Interpreter 模式相关推荐

  1. Java方法在art虚拟机中的执行

    前言 ART 虚拟机执行 Java 方法主要有两种模式:quick code 模式和 Interpreter 模式 quick code 模式:执行 arm 汇编指令 Interpreter 模式:由 ...

  2. Android ART虚拟机执行引擎-Interpreter(八)

    ART虚拟机是一个Interpreter+JIT+AOT的共存环境. ART虚拟机中解释器的实现源码在art/runtime/interpreter中,其中与平台相关的汇编代码保存在目录art/run ...

  3. AOSP 8.0 系统启动之四ART虚拟机启动(一)

    目录 前言 一.创建虚拟机 1.1 JniInvocation.Init 1.2 startVm 1.2.1 JNI_CreateJavaVM 1.2.2 Runtime::Create 1.2.3  ...

  4. android art 远程控制,IT之家学院:认识Android中的Dalvik与ART虚拟机

    又是一年高考时啊,在文章开始之前,IT之家先祝各位高考考生金榜题名~ 每到这个时候,小编就会想起自己的高三时光和高考经历,那段时光真是让人难忘.提起高三生活,可能很多同学都会想到桌子上堆得厚厚的书,黑 ...

  5. android dalvik虚拟机的作用,IT之家学院:认识Android中的Dalvik与ART虚拟机

    又是一年高考时啊,在文章开始之前,IT之家先祝各位高考考生金榜题名~ 每到这个时候,小编就会想起自己的高三时光和高考经历,那段时光真是让人难忘.提起高三生活,可能很多同学都会想到桌子上堆得厚厚的书,黑 ...

  6. JVM(JAVA虚拟机)、DVM(Dalvik虚拟机)和ART虚拟机

    一.什么是DVM,和JVM有什么不同? JVM是Java Virtual Machine,而DVM就是Dalvik Virtual Machine,是安卓中使用的虚拟机,所有安卓程序都运行在安卓系统进 ...

  7. 【Android 逆向】ART 脱壳 ( DexClassLoader 脱壳 | ART 虚拟机下 DexClassLoader 类加载器脱壳点总结 )

    文章目录 一.ART 虚拟机下 DexClassLoader 类加载器脱壳点总结 1.file_magic.cc#OpenAndReadMagic 函数 2.dex_file.cc#DexFile:: ...

  8. 【Java 虚拟机原理】动态字节码技术 | Dalvik ART 虚拟机 | Android 字节码打包过程

    文章目录 一.动态字节码技术 二.Dalvik & ART 虚拟机 三.Android 字节码打包过程 总结 一.动态字节码技术 动态字节码技术 就是在 运行时 , 动态修改 Class 字节 ...

  9. 设计模式--解析器(Interpreter)模式

    模式定义 给定一个语言,定义它的文法的一种表示,并定义一种解释器,这个解释器使用该表示来解释语言中的句子 类图 要点总结 Interpreter模式的应用场合是Interpreter模式应用中的难点, ...

  10. [转]虚拟机网络模式简介

    虚拟机网络模式 全文转自 http://blog.csdn.net/youxin2012/article/details/17231149 无论是vmware,virtual box,virtual ...

最新文章

  1. 2022-2028年中国液体燃料行业市场研究及前瞻分析报告
  2. Linux的简单介绍.
  3. 对10个元素进行快速排序,在最好情况下,元素间的比较次数为( )次。
  4. OpenSSL--Window生成证书实战
  5. Gradle Introduction
  6. Javaweb MVC设计模式、Modle发展史、项目分层和三层架构
  7. 3- 基于代理 Dao 实现 CRUD 操作
  8. Tmk吃汤饭(模拟)
  9. 董明珠:格力绝不裁员;腾讯缺席首批游戏版号;iPhone XS Max 口袋自燃 | 极客头条...
  10. 程序员为什么热衷造轮子?
  11. ShellCode欺骗的艺术!
  12. 手机模拟门禁卡 — 加密门禁卡模拟教程
  13. QML 编译release 报错: qmlcache_loader.cpp:-1: error: undefined reference to `__imp__ZN11QQmlPrivate13qml
  14. 全球光纤接头闭合器(FOSC)收入预计2028年达到42.159亿美元
  15. 03-12306验证码文字 识别
  16. 求职面试时,如何从面试官话语中揣测是否被录用?
  17. 家庭局域网网站服务器,1000元打造家庭局域网
  18. 基本面分析中必须了解的88条避雷常识
  19. 2020年度总结 | 葡萄城软件开发技术回顾
  20. 如何在Linux中轻松隐藏文件和文件夹

热门文章

  1. 自监督学习(四)Joint Unsupervised Learning of Deep Representations and Image Clusters
  2. matlab 从字符串里面提取出数字
  3. 数据分析入门系列教程-SVM实战
  4. 2019电商数据分析师实战项目教程 电商数据分析报告 电商运营数据分析 电商数据分析流程
  5. php能做定时关机吗,windows怎么定时关机?
  6. unity 打包APK 应用未安装
  7. Windows10专业版系统“本地组策略编辑器”丢失解决方案
  8. 跨境支付产品:现钞与现汇
  9. 【Windows】网线直连实现两台电脑共享文件夹
  10. 【真北读书】弗兰克意义三途径,让你人生的意义不漂移