FEATURED · 精选文章

ADVMP源码分析与VMP壳简单上手

发布时间 / 2026/8/18 16:59:11
来源 / 创域科博编辑部
栏目 / 资讯中心
ADVMP源码分析与VMP壳简单上手 我们今天的课程的主要内容是从开发的角度去带着大家去看一下我们要想去开发一个VMP壳需要解决哪些问题。然后我们的参考来源又有哪些我们要开发一个东西不可能是从零开始的肯定是站在前人已有的经验上的能够避免踩一些坑我们的参考主要是有两个一个是安卓自己的这个解释器的内容。那在Dalvik时代呢还有这个ART时代它都是有自己的这个解释器的。虽然说ART时代为了提高APP的这个运行效率然后引入了这个DFOAT的一个流程啊当然依然是有这个解释器的,而且随着这个google对安卓版本的这个不断更新,那每一个版本的这个安卓上的这个解释器的实现呢往往又会有区别,我一会儿还会带大家呢再去看一下这个源代码看一下google在对这个安卓的这个解释器发展的过程中又有哪些这个变化。那还有就是这个ADVMP,ADVMP就是说现在网上能够找到的一个初具VMP的这个雏形的一个简单的一个实现啊虽然说它的这个年代比较久远它是这个Dalvik时代的。还有就是它并没有对所有的这个Dalvik这个opcode操作码进行一个实现但是却已经具有了这个VMP的一个雏形啊。进入到今天的这个课程当中,那我们首先我们讲的这节课讲的内容。还有这个APP加壳的技术发展历史当中非常关键的一个环节那就离不开这个Dalvik这个字节码解释器我们可以把这个解释器理解为一个抽象虚拟的CPU而对于这个Dalvik的这个字节码解释器来说呢他要解释的就是smali的这个指令集。也就是Dalvik他自己定义的这个指令集。那可能大家都理解啊这个字节码的一个概念啊那它不是这个Dalvik所特有的任何解释型的语言它都是有自己的字节码的比如说java的这个字节码JVM字节码python的字节码等等。很多都有自己的解释器有自己的字节码了。因此对于其他平台的这个其他语言或者说运行时的这个环境它也有自己的这个定义的VMP壳保护比如说python我通过修改这个python的解释器然后来达到对编写的源代码的一个保护那这种也就类似于安卓当中这个VMP了。我们接下来呢就看一下这个从Dalvik发展而来的这个解释器还有它的这个指令集相关的这个内容。那我们先来看一下这个DEX当中最为关键的包含了这个函数的smali一个指令流的部分。这里的字节最前面的16个字节每一个函数都是有的不包含后面的指令集DexcodeDEX code的结构DEX code主要指的是方法中的字节码指令部分结构大致包括DEX code结构字段及其字节大小字段名类型字节数说明registers_sizeuint162方法使用的寄存器数量ins_sizeuint162输入参数占用的寄存器数量outs_sizeuint162调用其他方法时需要的寄存器数量tries_sizeuint162异常处理相关的结构数量debug_info_offuint324调试信息的文件偏移insns_sizeuint324字节码指令的长度单位为16位insnsuint16[]2 * insns_size字节码指令数组包含opcode和操作数那就是什么呢DEXcode那这个呢在是在这个Dalvik时代呢叫这个DEXcode然后在这个ART当中叫Codeltem我们都可以去在源代码当中去找到它的相关的这个定义啊我们可以通过在线的这个源码网站我们找一下。那Dalvik时代就是4.4以前的当然4.4里面两个都有Dexcode4.4以后的系统搜索Codeltem// Raw code_item. // 原始的 code_item 结构体表示方法的代码信息 struct CodeItem { uint16_t registers_size_; // 方法使用的寄存器数量16位无符号整数 uint16_t ins_size_; // 输入参数占用的寄存器数量16位无符号整数 uint16_t outs_size_; // 调用其他方法时需要的寄存器数量16位无符号整数 uint16_t tries_size_; // 异常处理块的数量16位无符号整数 uint32_t debug_info_off_; // 调试信息在文件中的偏移32位无符号整数 uint32_t insns_size_in_code_units_; // 字节码指令数组大小单位为2字节16位代码单元32位无符号整数 uint16_t insns_[1]; // 字节码指令数组至少一个16位代码单元 private: DISALLOW_COPY_AND_ASSIGN(CodeItem); // 禁止拷贝构造和赋值操作防止误用 };字节最前面的16个字节每一个函数都是有的不包含后面的指令集16个字节之后跟着的部分才是指令部分。那前面十六个字节部分就包含了当前函数所需要的这个虚拟寄存器的一个过程。我们这里需要注意的就是它是虚拟寄存器而不是真实的这个CPU上的这个寄存器可能就是使用的一个数组来表示的uint16_t registers_size_; // 方法使用的寄存器数量16位无符号整数上面是最前面的2个字节表示这个函数需要多少个虚拟的寄存器寄存器的表示方法有v命名法和p命名法上面属于V命名法没有出现P命名法dexcode的字节一共16个字节上面一排就是16个字节07 00表示2个字节十六进制07 00转十进制就是7这里表示7个虚拟寄存器02 00表示输入的参数是2个因为这个函数是实例函数第一个参数是当前函数的实例第2个参数是view,如果是静态函数那就是只一个参数这里的v5是当前函数的实例v6是参数view那这里为什么v5是参数的起始位置了这个是由于Dalvik或者说ART虚拟机在这个参数函数调用过程当中呢发生的这个参数传递来定义的是由它这个虚拟机决定的从当前的寄存器减去参数的个数这个下标开始。那参数个数这里面是两个然后寄存器总7减2从第五开始的那就是v5和v6。那么接下来如果我们尝试去读这个smali代码的时候我们就知道这个v5就是当前的这个实例了。那v0就相当于大家可以理解为前面的五个参数v0到v5呢就是它用来这个做变量使用的那这第一行这个smali指令它的意思呢就是从当前的实例当中呢取出其中的这个属性v5 索引2552的这个属性放在这个v0当中然后再往下呢involke-virtual那就是去调用这个edittText那这样呢就是我们来阅读这个smile代码的部分。那如果大家都看过这个相关的从入门的时候看的话呢往往都会接触到这些这个内容。那其中还有一个就是什么指令的大小这里是七十四。我们来看一下0000是异常处理 cf 26 13 00是调试信息4a 00 00 00是条数4a转换成十进制就是74表示一共74条smile指令一个指令用2个字节来表示这个函数里面的字节等于74*2148个字节那由于它没有这个异常处理相关的这个内容呢也就是后面没有这个try-catch的部分了。对于这个onCreate函数而言它的这个codeitem的大小是多少呢就是148加上前面的这个16等于164用个字节的也有6个字节的opCode操作码Smali指令的官方权威资料https://source.android.com/docs/core/runtime/dalvik-bytecode?hlzh-cnhttps://source.android.google.cn/docs/core/runtime/instruction-formats?hlzh-cnSmanli指令操作码实例资料http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html解释器实现smali指令集的取指、译码、执行的过程即为解释器可以将这个解释器理解为一颗虚拟的CPU即可自然就包含有寄存器、pc等Dalvik时代:由C语言实现ART的解释器继承至Dalvik的C时代原理一致ART时代:由C实现取指、译码、执行是什么阶段ART解释器中的含义关键点取指从DEX字节码数组中读取当前指令指令指针PC字节码数组译码解析指令类型和操作数跳转表、函数指针、指令格式解析执行执行指令对应的操作寄存器操作、方法调用、控制流处理Smali指令集的解释器实现:只需要支持对一个字节的opcode即总数最多为256个指令的每一条指令的解释执行即可。上面的解释器对每一个opcode都是有独立处理的部分字节码指令由以下几个部分组成组成部分说明Opcode操作码1字节表示指令的类型和操作类别操作数Operands0个或多个字节表示指令的参数如寄存器索引、常量池索引、偏移量等指令长度指令长度不固定取决于指令格式通常为1~6字节上面这个解释器使用的是case来解释操作码一共有256个操作码上面会有256个case解释器的类型里面enum InterpreterImplKind { kSwitchImpl, // 基于switch语句的解释器实现方式。 kComputedGotoImplKind // 基于计算跳转computed goto的解释器实现方式。 };kSwitchImpl解释器通过switch语句来分发执行不同的字节码指令结构简单但性能相对较低。kComputedGotoImplKind解释器采用“计算跳转”技术利用跳转表直接跳转到对应指令处理代码减少分支开销提高执行效率。5.0:Switch和GotoTable6.0:Switch和GotoTable7.0:Switch、GotoTable和Mterp8.0:Switch和Mterp9.0:Switch和MterpSwitch类型可读性强效率低30分钟这个是switch的解释器但是要写一堆的case最终有256个case找一下他switch解释器的这个实现这里有一个问题对于每一条opcode字节码它都要经历这个256个case,从0开始每个比较所以效率很低。所以就有了下面的goto_tabl表类型的解释器goto_tabl效率高它将原来的这每一种case它都标记了这个地址goto_table解释器的核心思想 利用**计算机语言的标签地址label address和间接跳转computed goto**技术构建一个跳转表jump table。 跳转表中每个元素是对应指令处理代码的标签地址。 通过goto *jump_table[opcode]直接跳转到对应指令处理代码避免了switch的多分支判断。 // 定义跳转标签 static void* jump_table[] { op_nop, op_add, op_sub, // 其他指令标签... }; pc code_start; while (running) { opcode *pc; goto *jump_table[opcode]; op_nop: // 执行NOP指令 pc 1; goto fetch; op_add: // 执行ADD指令 pc 1; goto fetch; op_sub: // 执行SUB指令 pc 1; goto fetch; fetch: opcode *pc; goto *jump_table[opcode]; }就是初始化了256个指令从当前的指令拿到这个指令表去比较查询出来是哪个地址直接跳过去Mterp汇编解释器那汇编来实现的话效率更高。那因此现在默认的实现默认使用的解释器都是由这个都是汇编。当然了我们可以在前面讲这个脱壳和ART定制的课程当中呢我们可以手工的把它改成这个switch类型的这个解释器啊方便我们对这个解释器的执行过程进行一个分析。那我们自己也可以快速的在解释器当中去插入我们需要的一些逻辑。那这个就是ART当中的这个解释器发展的一个历史对于VMP开发厂商来说呢虽然说这个汇编实现的效率最高但是对于厂开发人员来说肯定也不会轻易的去选择这个汇编来实现这个解释器那最多依然是使用了这个switch和这个goto这两种那它的实现了这个更容易一些而且对开发人员也更直观一些 也很容易的去调试它。vmp厂商选择的解释器那因此现在看到的这个自定义的在VMP当中的自定义的解释器,基本都是这个switch和goto跳转表这两种形式呢来实现。解释器的实现invoke类型指令“invoke”类型指令是Dalvik字节码和ART虚拟机中非常重要的一类指令主要用于方法调用。它们负责实现Java方法的调用机制包括普通方法调用、接口方法调用、虚方法调用、静态方法调用等。 下面我帮你详细介绍“invoke”类型指令的分类、工作原理和执行流程。 一、invoke指令的分类 Dalvik字节码中invoke指令主要有以下几种 | 指令名 | 说明 | |----------------------|----------------------------------| | invoke-virtual | 调用虚方法实例方法支持动态绑定 | | invoke-super | 调用父类方法 | | invoke-direct | 调用私有方法、构造函数等直接调用 | | invoke-static | 调用静态方法 | | invoke-interface | 调用接口方法 | | invoke-virtual/range | 虚方法调用参数使用寄存器范围传递 | | invoke-static/range | 静态方法调用参数使用寄存器范围传递 | | invoke-direct/range | 直接调用参数使用寄存器范围传递 | | invoke-interface/range | 接口调用参数使用寄存器范围传递 | 二、invoke指令的工作原理 1. **解析方法引用** - 指令中包含一个方法索引指向常量池中的方法符号引用。 - 解释器或运行时通过该索引找到对应的类和方法信息。 2. **参数准备** - 根据指令格式从虚拟寄存器中取出调用参数。 - 参数可能是基本类型、对象引用或宽类型long/double。 3. **方法查找与分派** - 根据调用类型虚拟、静态、接口等确定具体调用目标。 - 虚方法调用需要动态查找实际对象的类支持多态。 - 静态调用直接定位方法不涉及多态。 4. **调用执行** - 创建新的栈帧ShadowFrame传递参数。 - 跳转到目标方法的字节码执行入口。 - 返回时将结果写回调用者寄存器。 三、invoke指令的执行流程示意 text 调用者方法 1. 读取invoke指令解析方法索引 2. 从寄存器取参数 3. 查找目标方法动态绑定或静态绑定 4. 创建新栈帧传递参数 5. 跳转执行目标方法 目标方法 6. 执行方法字节码 7. 返回结果 调用者方法 8. 处理返回值继续执行 四、invoke指令在ART解释器中的实现要点 - **参数传递**通过ShadowFrame管理寄存器调用时将参数复制到新栈帧。 - **方法查找**调用虚方法时ART会根据对象实际类型查找方法实现动态分派。 - **异常处理**调用过程中可能抛出异常ART解释器负责捕获和处理。 - **性能优化**ART中JIT和AOT编译器会对invoke调用做内联缓存、内联展开等优化减少调用开销。 五、总结 - invoke指令是实现Java方法调用的基础指令支持多种调用类型。 - 其核心是方法解析、参数传递、动态分派和执行跳转。 - ART解释器通过管理寄存器帧和跳转机制实现invoke指令的高效执行。 - 理解invoke指令对深入掌握Android虚拟机执行机制非常重要。看一下这个davik或者说ART当中的解释器对involve类型的这个指令,它这个解释执行的一个流程。ART解释器中invoke指令的解释执行流程 以invoke-virtual为例其他invoke类型指令流程类似区别主要在方法查找和绑定方式。 1. 读取指令和解析方法索引 解释器从当前方法的字节码流中读取invoke-virtual指令。 指令中包含一个方法索引method_idx指向Dex文件的方法符号表。 通过method_idxART解析出调用目标的类和方法符号信息。 2. 准备参数 invoke指令会指定调用参数所在的虚拟寄存器Dalvik寄存器。 解释器从当前ShadowFrameART中管理虚拟寄存器的结构中读取这些参数。 参数包括调用对象引用this指针和方法参数。 这些参数会被复制到新创建的目标方法的ShadowFrame中。 3. 方法查找与动态分派 对于invoke-virtualART需要根据调用对象的实际类型进行动态方法查找 取出调用对象的类obj-clazz。 在该类及其父类链中查找对应的方法实现虚方法表查找。 对于invoke-static等直接定位目标方法无需动态查找。 4. 创建新的栈帧ShadowFrame ART解释器为目标方法创建一个新的ShadowFrame用于保存该方法的寄存器状态。 将参数复制到新栈帧的对应寄存器。 设置新的程序计数器PC指向目标方法的字节码入口。 5. 跳转执行目标方法 解释器切换执行上下文开始执行目标方法的字节码。 目标方法的执行过程和调用者类似逐条解释执行。 6. 方法返回与结果处理 目标方法执行完毕后返回结果如果有会被写回调用者的寄存器。 解释器恢复调用者的ShadowFrame和程序计数器继续执行后续字节码。 7. 异常处理可选 在调用过程中如果目标方法抛出异常解释器会捕获异常并进行相应处理如查找异常处理表、执行catch块等。调用者解释器 ├─ 读取invoke指令 ├─ 解析方法索引 ├─ 读取参数寄存器 ├─ 动态查找目标方法虚方法 ├─ 创建目标方法ShadowFrame ├─ 复制参数 ├─ 跳转执行目标方法字节码 目标方法解释器 ├─ 逐条解释执行字节码 ├─ 执行完毕返回结果 调用者解释器 ├─ 写回返回值 ├─ 继续执行后续指令如果官方的这个代码来说它是集成到这个ART当中的,然后他对这个involve类型的指令的这个操作,就非常的这个底层啊涉及到这个参数的传递然后目标函数类型的一个判断等等,那对于这个VMP开发厂商来说呢。它为了这个兼容性和实现的这个容易程度来说呢就会使用这个JNI当中的call开头的这个函数完成对involve类型的指令的一个译码和解释执行的一个过程那在这个过程当中就会涉及到什么就会涉及到怎么去通过它的这个method-id找到它的这个函数名然后找到它的函数签名以及通过当前的这个对象找到这个函数呢所在的这个类那自然而然就会涉及到FindClass查找Java类GetMethodID/GetStaticMethodID获取实例方法或静态方法的IDCallTypeMethod/CallStaticTypeMethod调用Java实例或静态方法的这个操作了,那这个就是在VMP壳实现的过程当中呢对invoke类型的这个解释执行为什么要使用这个JNI的接口来实现的一个原因。那为也是为了这个兼容性.对VMP或者dex2C保护的函数的内部逻辑,可以根据JNI接口的这个调用,来探测出来内部逻辑的一个这个原因啊就是在这个地方。解释器的实现解释器的实现为了解决VMP实现的兼容性和复杂性问题,VMP使用标准JNI调用java:函数的流程来实现对invoke类型指令的解释执行1-解释invoke指令的参数部分得到Methodldx2-解析dex结构,获得当前要调用的Methodldx的类名和函数属性3-调用JNI的Findclass函数,获得对应类的JClass4-调用JNl的GetMethodlD或GetStaticMethodlD得到函数的Methodl5-调用JNl的CallXXXMethod完成对函数的调用就是第一步取出这个OPcode然后取出OPcode以后呢它可能对于VMP厂商来说可能是进行了这个映射映射成了他自己的OPcode而不是原来的。你比如说这里的这个involve那原始的6e,那它可能被厂商改成了6e 7f 00等就会出现这个映射的关系。这就是VMP的实现的一个基础它进行了这个映射成了它自定义的一个OPcode。41分那除了invoke类型的指令是索引类型的指令以外还有哪些指令iget也是一个索引类型的指令。因为后面都是一个索引值包括什么const_string啊等这些它都是这个索引类型的指令。自然而然就需要去解析这个DEX,通过这个DEX的结构解析得到的去查询他要调的这个目标函数a20 到底是哪一个函数那自然而然呢这里呢已经给我们解析出来了就是对应于后面的这个函数。那我们来看一下我们使用这个010给它打开,我们手工的去通过他的Methodldx,找到这个函数的所在的类class啊还有它的这个函数名和函数签名的过程就是VMP对invoke类型指令解释执行的过程当中呢要做的这个事情。比如这个指令。那首先它是一个调用的是一个对象方法。那为v0这个对象是什么呢是valsinputEditrext类型然后后面的这个函数是0a20。a20转换成十进制2592应于在它的这个method表里面呢就它就会去解析。上面是010的内容是这个methodID这个表里面就是它的索引类型就可以找到这个2592他就会找到这个函数的函数名完整的一个函数名解析出来以后呢就是getText()然后还有他的这个签名信息。那接下来他解析出来这些信息以后呢就可以先调用这个findclass然后找到这个class然后再通过这个GetMethodID或者说getstartedMethodID,拿到它的这个MethodID。那接下来去调用它就ok了。那这个就是对VMP对这个involve类型的指令具体的一个这个实现了。MethodldxDex文件中的method_idx**它在Android运行时ART/Dalvik中的作用和使用流程。 --- # 一、什么是 method_idx - method_idx是Dex文件格式中**方法标识符表method_ids**的索引。 - 它是一个无符号16位整数指向Dex文件中method_ids表的某一项。 - 每个method_ids表项描述一个方法的元信息包括所属类、方法名和方法签名。 --- # 二、Dex文件中method_ids表结构 method_ids表是Dex文件的核心结构之一表项格式大致如下 | 字段 | 类型 | 说明 | |---------------|------------|------------------------------| | class_idx | uint16 | 指向type_ids表表示方法所属类 | | proto_idx | uint16 | 指向proto_ids表表示方法签名 | | name_idx | uint32 | 指向string_ids表表示方法名 | - 通过class_idx可以定位方法所属的类名。 - 通过proto_idx可以定位方法参数和返回值类型。 - 通过name_idx可以定位方法名称字符串。 --- # 三、method_idx的作用 - 在Dalvik/ART字节码中调用方法的指令如invoke-virtual、invoke-static等都包含一个method_idx字段。 - 解释器或JIT通过method_idx快速定位调用的方法元信息。 - 这是实现方法调用、动态分派、多态等机制的基础。 --- # 四、method_idx的使用流程简要 1. **字节码读取** - 解释器读取invoke指令中的method_idx。 2. **方法解析** - 通过method_idx索引查找Dex文件的method_ids表项。 - 解析出方法所属类、方法名和签名。 3. **类加载和验证** - 确保方法所属类已加载到内存。 - 验证方法签名和访问权限。 4. **方法查找** - 对于虚方法ART根据调用对象的实际类型在类层次结构中查找具体实现。 - 对于静态方法直接定位。 5. **调用执行** - 创建新的调用栈帧传递参数执行方法。 --- # 五、示例说明 假设Dex字节码中有一条指令 invoke-virtual {v0, v1}, method_idx 0x1234 - 解释器读取0x1234作为method_idx。 - 查找method_ids[0x1234]得到方法所属类、名称和签名。 - 根据调用对象v0的类型找到具体方法实现。 - 调用该方法传入参数v1。 --- # 六、总结 - method_idx是Dex文件中方法的唯一索引标识。 - 它连接了字节码指令和方法元信息是方法调用的关键桥梁。 - ART解释器和JIT编译器都依赖method_idx实现高效的Java方法调用。总结因此我们就可以得到一画出来一张图可以大概的去带大家去理解一下这个加固厂商去实现这个VMP的时候的一个具体的一个逻辑啊那从初始化开始那这个虚拟机对于这个VMP加固厂商它里面有一个自定义的一个虚拟机。它将初始化好相关的这个变量以后就会取出第一条指令你要第一条opcode然后就会判断这个指令的类型。那判断如果是一个合法的一个指令以后呢他就会去按照不同类型的指令去进入到它具体的这个分支执行对应的这个handle句柄。什么是句柄Handle句柄本质上是一种抽象的引用或标识符用来间接访问某个资源或对象。它通常是一个整数、指针或者某种标识符不直接暴露资源的内部结构。句柄的目的是隐藏资源的具体实现细节提供一种安全、统一的访问方式。那执行完以后呢它的PC值呢就会递加然后再次进入这个取值解释执行的这个过程当中。那这里呢只是简单的带着大家去理解这个VMP的一个实现。那事实上到最后真正的去实现一个可用的比较健壮的VMP还是很难的它涉及到这个异常处理啊涉及到这个异常处理部分。还有在这个呃switch case啊等等啊非常多要注意的的地方以及这个锁呀等等。这些很多要注意的这个地方而且不同的这个版本不同的安卓版本呢我们还要需要注意到它内部实现的一个差异。那因此VMP实现的这个兼容性还是需要去经过大量的这个测试的。如果大家发现自己使用了某个加固厂商的这个等VMP加固以后所以VMP在某些机型上有这个崩溃的现像这些都是很正常的事情。安卓源码解释器Switch我们来看一下安卓源码当中的这个解释器。对于需要在这个解释模式下运行的Java函数来说最后会进入到Execute。art/runtime/interpreter/interpreter.cc那由他来决定使用哪一个解释器来解释执行具体的指令流。那对应于这里面就是code_item,这里面就有它需要的寄存器然后数量还有参数的个数以及对应的指令流的这个部分内容。然后我们就来看一下这个ART当中关于这个解释器的一个具体实现。我们找一些指令来看一下具体的实现到底是怎么操作的。这个switch类型的这个解释器当函数进入的时候呢这个dex_pc就为0,code_item它是个结构体。那对应于这里呢一开始呢跟大家说在ART当中呢是这个code_item那在4.4以下呢就是Dexcode。那其中的这个指针指向的位置呢就是第一条这个指令。就是这个constinsnsconstInstruction*inst Instruction::At(insns dex_pc);然后这个dex_pc就可以把它理解成CPU执行每一条汇编指令的PC那大家可能知道由于在这个传统的CPU上面为了加快它的这个执行速度又引入了这个流水线就会导致这个PC当前执行的地址处实际上PC的值是在指向后面的这个位置下面的这个位置是为了提高它的效率。那这里肯定是没有它一次就是取出来一条指令对应于Instruction*然后设置好当前的这个PC就相当于我们就可以把它理解为它的一个虚拟的一个寄存器。那实际上就是一个变量然后就取出它的这个opcode的之后就根据这个case不同的OPcode进行这个不同的分发的一个处理。我们看一下NOPcase Instruction::NOP: PREAMBLE(); inst inst-Next_1xx(); break;那对于NOP指令来说呢只是简单的将这个当前的游标移到下一条指令当中。那这个就是两个字节往前移动两个字节的内容。case Instruction::MOVE: PREAMBLE(); shadow_frame.SetVReg(inst-VRegA_12x(inst_data), shadow_frame.GetVReg(inst-VRegB_12x(inst_data))); inst inst-Next_1xx(); break;那如果是MOVE指令的话我们想一下你比如说这里的这个MOVE找个具体的一个实例。它是一个这个虚拟寄存器实际上就是一个数组啊一片连续的内存空间可以这样理解。你比如说这里是MOVE指令后面的这个v0和v1呢可以理解为内存当中的游标它的第几个那当这一条这个MOVE的时候就是把原寄存器的这个数据呢放到这个MOVE的寄存器当中然后它的这个寄存器呢都是32的。那直接就调用了这个SetVRegshadow_frame.SetVReg(inst-VRegA_12x(inst_data),SetVReg实际上就是虚拟寄存器的意思SetVReg首先获取到这个源寄存器的一个inst索引然后得到这个寄存器当中的一个值取出这个索引当中的值然后设置好这个目的寄存器其中的这个值就ok了。shadow_frame.SetVReg(inst-VRegA_12x(inst_data), shadow_frame.GetVReg(inst-VRegB_12x(inst_data)));这里调用inst-VRegA_12x(inst_data)和inst-VRegB_12x(inst_data)分别获取指令中的寄存器编号。shadow_frame.GetVReg(...)读取源寄存器的值。shadow_frame.SetVReg(...)将该值写入目标寄存器。这实现了MOVE指令的核心功能寄存器值复制。那其他的指令也都是有不同的这个处理。那在经过编译器编译完以后就每一条这个指令都对应于不同的这个分支。那我就是已经能把它给编译完了用ida定位到这个解释器的实现由于这个ExecuteSwitchImpl它是一个这个模板函数。那在这个模板函数的实例化的过程当中就会出现了有多个这个不同的实现。我们直接看一下它的流程图它的控制流的这个图我们看一下这个由于它这个分支是非常的这个多的那就二百五十六条指令的这个不同的这个分发。那其实这个图还是挺复杂的非常多。那每一条指令处理完了以后都会进入到这个下一条取值指令当中。对应于哪然后取出下一条这个inst。对于这个switch行实现的这个解释器我们就可以跟着它的这个控制流找到它的这个分发处理我们就可以找到这个跳转表。那这个IDA已经很智能的帮我们识别出来了它的这个跳转表了,FE那FE是多少254当这个大于254的时候就会跳到默认的这个case当中。那对于正常的指令它都是比它这个要小的就会进入到。那我们就看到了它有一条这个汇编指令TBH.W。那这个是什么这个是这个用于实现这个switch case的这个指令那这条汇编指令这个是经过这个编译器对这个当含有这个大量的case然后又是连续的时候 就会对这个switch case的这个结构进行一个编译优化然后优化成了这个跳转表的这种形式那我们把它的这个地址,还有他这个因为汇编代码的长度都给显示出来。那显示出来以后我们就可以去计算一下它。也就OPcode为0的时候它跳转的这个目标地址。我们来看一下那对于这条指令它跳转的这个范围是一个半字的一个范围。后面是他要去查的这个表。那我们看当case是零的时候要对应R3为零就是OPcode。那已经这IDA已经识别的非常的这个准确了它有这个相关的这个注释。那当case为零的时候我们的这个跳转的地址是多少最后会变成多少由于这个流水线的这个原因当前的PC实际上是PC加上4。那我们这个PC这个地址没有显示出来是吧然后我们加上得到case零的时候是R3乘以2R3的值取出来以后乘以2我们看到当0的时候是117取出来的值那自然就是加上这个117乘以2的这个117是个半字的两个字节。那ID也是给我们正确的这个识别出来。那接样我们加上这个117然后再加一个117那就是当case零跳转的这个地址。那对不对我们可以看CBA90你跳过去。然后这个时候我们发现这个IDA也注释里面已经给我们帮助我们这个注释出来了。那这个就是他的这个查这个跳转表然后去得到对应的这handa的一个地址的过程。那下面这些就是有多少个OPcode的一个判断的处理自然而然就有多少项那每一项里面都是要计算要和当前的这个PC进行一个偏移的一个计算然后得到这个对应的handa的地址.那这个是这个switch体结构实现的这个解释器经过这个编译器编译优化了以后变成了跳转表形式的效率提高了很多goto1.04分goto前面的部分和swich一样的区别在后面的部分Ida识别出了去查询这个表下面的内容ADvmp是一个非常老的VMP也是vmp的雏形
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻