ARM反汇编浅析
一、概述
本节主要以几条ARM指令为例,讲解如何根据文档理解ARM指令,以及ARM汇编知识在分析中的应用。
手机游戏基本上都运行在ARM指令集的CPU上,了解ARM汇编有助于我们对游戏的分析深入native层,为我们修改游戏逻辑提供分析基础。
现在的游戏的逻辑代码多是写成脚本形式,最常见的是Unity3D的C#形式,cocos2dx的lua形式,以及Unity3D的C#和lua混编形式,除此之外基于纯C++的cocos2dx游戏已经相对比较少。
了解汇编就能对游戏引擎进行分析,定位到脚本的加载和执行函数,可以输出函数执行Log,脚本加载Log,dump原始脚本等,对游戏逻辑进行更直接的监视和控制。
建议安装IDA或其他反汇编工具,加载一个ARM指令集的so文件或者ELF可执行文件,在实践中对照学习。
1.1 ARM体系简介
ARM架构,是一个32位精简指令集处理器架构,是目前最为普遍使用的手机处理器架构。
ARM架构表:
(https://zh.wikipedia.org/wiki/ARM_Cortex-A15_MPCore)
关于架构,我们需要了解的是自Cortex A8以后的架构都支持硬件断点,但各个厂商在出厂的时候并未开启这个功能。
ARM CPU有15个通用寄存器,分别编号为R0~R15。其中R13也叫SP,用于保存栈顶地址;R14也叫LR,用于保存函数的返回地址;R15也叫PC,由于ARM架构的指令预读,实际运行时PC寄存器并不指向当前执行的指令,而是指向当前指令地址加8处。
二、指令样例讲解
有关所有ARM指令的文档下载链接如下:
http://bear.ces.cwru.edu/eecs_382/ARM7-TDMI-manual-pt1.pdf
http://bear.ces.cwru.edu/eecs_382/ARM7-TDMI-manual-pt2.pdf
http://bear.ces.cwru.edu/eecs_382/ARM7-TDMI-manual-pt3.pdf
指令比较多,不可能也没必要一条一条讲解,以下会拿几条常用的指令作为例子讲解,其他没有涉及的指令都可以在上面链接下载的文档里找到更详细的解释。
2.1 B/BL
B/BL指令是最常用的几条指令,作用是跳转到指定的位置,相关的文档解释在ARM7-TDMI-manual-pt2.pdf的4-8页。
指令的二进制解释图:
Cond位表示这条指令是会在何种条件下执行,具体到实际中每个条件都会给B指令加上后缀,比如“BNE/BEQ/BGT”等,也就是我们熟悉的条件跳转指令,作为程序的分支使用。
如图是IDA反汇编中一个条件跳转示例:
图中两条指令表示“如果R2等于R0则跳转到0x1C04”。
“101”则是B指令的标识码。
L位表示是否需要保存该指令的下一条指令到LR寄存器,这类B指令就叫BL指令,一般用于跳转到函数内部,其中LR保存返回地址。
Offset是目标地址与该指令的相对偏移(需要考虑到CPU的指令预读,以及左移两位,偏移是按4字节对齐的)。
下面还是按上图的那条指令作例子实际分析各个字段的意思:
对应二进制
按照二进制解释图中的格式排列(从高位到低位)就是:
0000 101 0 0000 0000 0000 0000 0000 1011
Cond op L offset
Cond表示EQ,等于的时候跳转(ARM7-TDMI-manual-pt2.pdf的4-5页)。
101是标识。
L是0,表示不需要保存下一条指令。
Offset是1011,左移两位就是101100,也就是0x2C。当CPU执行到该指令时(0x1BD0处),PC应该指向0x1BD8,所以实际跳转地址就是0x1BD8+0x2C,也就是0x1C04。
2.2 LDR/STR
LDR/STR指令用于向内存读/写数据,相关文档在ARM7-TDMI-manual-pt2.pdf的4-28页。
指令的二进制解释如图:
看起来很复杂,但我们需要关注的地方不多。不再做详细解释,直接看例子:
指令
对应二进制
按照二进制格式排列:
1110 01 011001 1100 0010 0000 0000 0000
Cond op IPUBWL Rn Rd Offset
Cond表示任何条件都可以(ARM7-TDMI-manual-pt2.pdf的4-5页)。
01是指令标识。
I是0,表示offset是一个立即数。
B是0,表示传输的是一个字,ARM指令集中的“字”是8个字节。
L是1,表示是读内存。
Rn是0xC,表示基址寄存器是R12.
Rd是2,表示把内存值读到R2。
Offset是0,表示基址寄存器需要加上0.
所以解释成指令就是:
意思就是,读取R12寄存器指向的内存到R2寄存器中,一共读取4个字节(一个字)。
三、THUMB指令
THUMB指令是ARM指令的精简版,在日常的分析中也经常会碰到,具体的文档在ARM7-TDMI-manual-pt3.pdf。分析方法同上,不再缀述。一段程序中有可能同时出现这两种指令集,主要靠CPSR寄存器中的标识位确定当前执行的是何种指令集:
红框即是当前指令集状态,1表示在THUMB指令集,0表示在ARM指令集。
四、函数传参
从汇编层面来看,不同的编译方式对函数传参的做法不同,一种比较常见的做法是把前四个参数放到R0~3寄存器中,剩下的参数放到栈中,函数的返回值放在R0中。
如图,C语言中一个简单的函数调用:
调用它的汇编代码则是:
可以看到,调用R6中的函数(old_compile)后,把返回值作为参数直接调用hasHooked函数。
五、浮点数基础
在看汇编代码的时候,有时候会碰到浮点数以及浮点指令,这里简单说说浮点数在内存中的二进制形式。具体就是IEEE 754标准。
32位浮点数的二进制格式如图:
S为符号位,Exp为指数位,Fraction为有效数字。 指数部分即使用所谓的偏正值形式表示,偏正值为实际的指数大小与一个固定值(32位的情况是127)的和。采用这种方式表示的目的是简化比较。因为,指数的值可能为正也可能为负,如果采用补码表示的话,全体符号位S和Exp自身的符号位将导致不能简单的进行大小比较。正因为如此,指数部分通常采用一个无符号的正数值存储。单精度的指数部分是−126~+127加上偏移值127,指数值的大小从1~254(0和255是特殊值)。浮点小数计算时,指数值减去偏正值将是实际的指数大小。(摘自维基百科https://zh.wikipedia.org/wiki/IEEE_754)
具体来看个例子,浮点数十六进制:
00 00 80 3F
也就是0x3F800000
二进制
0 01111111 000 0000 0000 0000 0000 0000
S Exp Fraction
S为0,代表这是一个正数。
Exp为0x7F,根据偏正值定义,真实值是0x7F-0x7F=0。
Fraction为0,注意,这里的Fraction是二进制小数。例如,Fraction为1011001则对应二进制小数1.1011001(整数部分恒为1,后接二进制小数),转换成十进制就是2^(-0)+2^(-1)+2^(-3)+2^(-4)+2^(-7).
计算方法就是(S)F*2^E,其中S是符号位,E是指数位Exp,F是有效位Fraction。
所以这个值就是+2^(-0),也就是1.