安卓逆向系列教程(一)Dalvik 指令集
作者:飞龙
寄存器
Dalvik 指令集完全基于寄存器,也就是说,没有栈。
所有寄存器都是 32 位,无类型的。也就是说,虽然编译器会为每个局部变量分配一个寄存器,但是理论上一个寄存器中可以存放一个int
,之后存放一个String
(的引用),之后再存放一个别的东西。
如果要处理 64 位的值,需要连续的两个寄存器,但是代码中仍然只写一个寄存器。这种情况下,你在代码中看到的vx
实际上是指vx
和vx + 1
。
寄存器有两种命名方法。v
命名法简单直接。假设一共分配了 10 个寄存器,那么我们可以用v0
到v9
来命名它们。
除此之外,还可以用p
命名法来命名参数所用的寄存器,参数会占用后面的几个寄存器。假如上面那个方法是共有两个参数的静态方法,那么,我们就可以使用p0
和p1
取代v8
和v9
。如果是实例方法,那么可以用p0 ~ p2
取代v7 ~ v9
,其中p0
是this
引用。
但在实际的代码中,一般不会声明所有寄存器的数量,而是直接声明局部变量所用的寄存器(后面会看到)。也就是说局部变量和参数的寄存器是分开声明的。我们无需关心vx
是不是py
,只需知道所有寄存器的数量是局部变量与参数数量的和。
数据类型
Dalvik 拥有独特的数据类型表示方法,并且和 Java 类型一一对应:
Java 类型 | Dalvik 表示 |
---|---|
boolean | Z |
byte | B |
short | S |
char | C |
int | I |
long | J |
float | F |
double | D |
void | V |
对象类型 | L |
数组类型 | [ |
其中对象类型由L<包名>/<类名>;
(完全限定名称)表示,要注意末尾有个分号,比如String
表示为Ljava/lang/String;
。
数组类型是[
加上元素类型,比如int[]
表示为[I
。左方括号的个数也就是数组的维数,比如int[][]
表示为[[I
。
类定义
一个 smali 文件中存放一个类,文件开头保存类的各种信息。类的定义是这样的。
- 1
- 2
- 3
比如这是某个MainActivity
:
- 1
- 2
- 3
我们可以看到该类是public
的,完整名称是net.flygon.myapplication.MainActivity
,继承了android.app.Activity
,在源码中是MainActivity.java
。如果类是abstract
或者final
的,会在public/private/protected
后面表示。
类可以实现接口,如果类实现了接口,那么这三条语句下面会出现.implements <接口的完全限定名称>
。比如通常用于回调的匿名类中会出现.implements Landroid/view/View$OnClickListener;
。
类还可以拥有注解,同样,这三条语句下方出现这样的代码:
- 1
- 2
- 3
- 4
这些语句下面就是类拥有的字段和方法。
字段定义
字段定义如下:
- 1
其中非权限修饰符可以为final
或者abstract
。
比如我在MainActivity
中定义一个按钮:
- 1
方法定义
方法定义如下:
- 1
- 2
- 3
要注意如果有多个参数,参数之间是紧密挨着的,没有逗号也没有空格。如果某个方法的参数是int, int, String
,那么应该表示为IILjava/lang/String;
。
.locals
方法里面可以包含很多很多东西,可以说是反编译的重点。首先,方法开头处可能会含有局部变量个数声明和参数声明。.locals <个数>
可以用于变量个数声明,比如声明了.locals 10
之后,我们就可以直接使用v0
到v9
的寄存器。
.param
另外,参数虽然也占用寄存器,但是声明是不在一起的。.param px,"<名称>"
用于声明参数。不知道是不是必需的。
.prologue
之后.prologue
的下面是方法中的代码。代码是接下来要讲的东西。
.line
代码之间可能会出现.line <行号>
,用来标识 Java 代码中对应的行,不过这个是非强制性的,修改之后对应不上也无所谓。
.local
还可能出现局部变量声明,.local vx, "<名称>":<类型>
。这个也是非强制性的,只是为了让你清楚哪些是具名变量,哪些是临时变量。临时变量没有这种声明,照样正常工作。甚至你把它改成不匹配的类型(int
改成Object
),也可以正常运行。
数据定义
指令 | 含义 |
---|---|
const/4 vx,lit4 | 将 4 位字面值lit4 (扩展为 32 位)存入vx |
const/16 vx,lit16 | 将 16 位字面值lit16 (扩展为 32 位)存入vx |
const vx, lit32 | 将 32 位字面值lit32 存入vx |
const-wide/16 vx, lit16 | 将 16 位字面值lit16 (扩展为 64 位)存入vx 及vx + 1 |
const-wide/32 vx, lit32 | 将 32 位字面值lit32 (扩展为 64 位)存入vx 及vx + 1 |
const-wide vx, lit64 | 将 64 位字面值lit64 存入vx 及vx + 1 |
const/high16 v0, lit16 | 将 16 位字面值lit16 存入vx 的高位 |
const-wide/high16, lit16 | 将 16 位字面值lit16 存入vx 和vx + 1 的高位 |
const-string vx, string | 将指字符串常量(的引用)string 存入vx |
const-class vx, class | 将指向类对象(的引用)class 存入vx |
这些指令会在我们给变量赋字面值的时候用到。下面我们来看看这些指令如何与 Java 代码对应,以下我定义了所有相关类型的变量。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
编译之后的代码可能是这样:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
我们可以看到,boolean
、byte
、short
、int
都是使用const
系列指令来加载的。我们在这里为其赋了比较小的值,所以它用了const/4
。如果我们选择一个更大的值,编译器会采用const/16
或者const
指令。然后我们可以看到const-wide/16
用于为long
赋值,说明const-wide
系列指令用于处理long
。
接下来,float
使用const
指令处理,double
使用const-wide
指令处理。以float
为例,它的const
语句的字面值是0x3dcccccd
,比较费解。实际上它是保持二进制数据不变,将其表示为int
得到的。
我们可以用这段 c 代码来验证。
- 1
- 2
- 3
- 4
- 5
- 6
结果是0.100000
,的确是我们当初赋值的 0.1。
最后,const-string
用于加载字符串,const-class
用于加载类对象。虽然文档中写着“字符串的 ID”,但实际的反编译代码中是字符串字面值,比较方便。对于类对象来说,代码中出现的是完全先定名称。
数据移动
数据移动指令就是大名鼎鼎的move
:
指令 | 含义 |
---|---|
move vx,vy | vx = vy |
move/from16 vx,vy | vx = vy |
move/16 vx,vy | vx = vy |
move-wide vx,vy | vx, vx + 1 = vy, vy + 1 |
move-wide/from16 vx,vy | vx, vx + 1 = vy, vy + 1 |
move-wide/16 vx,vy | vx, vx + 1 = vy, vy + 1 |
move-object vx,vy | vx = vy |
move-object/from16 vx,vy | vx = vy |
move-object/16 vx,vy | vx = vy |
move-result vx | 将小于等于 32 位的基本类型(int 等)的返回值赋给vx |
move-result-wide vx | 将long 和double 类型的返回值赋给vx |
move-result-object vx | 将对象类型的返回值(的引用)赋给vx |
move-exception vx | 将异常对象(的引用)赋给vx ,只能在throw 之后使用 |
move
系列指令以及move-result
用于处理小于等于 32 位的基本类型。move-wide
系列指令和move-result-wide
用于处理long
和double
类型。move-object
系列指令和move-result-object
用于处理对象引用。
另外不同后缀(无、/from16
、/16
)只影响字节码的位数和寄存器的范围,不影响指令的逻辑。
数据运算
二元运算
二元运算指令格式为<运算类型>-<数据类型> vx,vy,vz
。其中算术运算的type
可以为int
、long
、float
、double
四种(short
、byte
按int
处理),位运算的只支持int
、long
,下同。
指令 | 运算类型 | 含义 |
---|---|---|
算术运算 | ||
add- vx, vy, vz | 加法 | vx = vy + vz |
sub- vx, vy, vz | 减法 | vx = vy - vz |
mul- vx, vy, vz | 乘法 | vx = vy * vz |
div- vx, vy, vz | 除法 | vx = vy / vz |
rem- vx, vy, vz | 取余 | vx = vy % vz |
位运算 | ||
and- vx, vy, vz | 与 | vx = vy & vz |
or- vx, vy, vz | 或 | `vx = vy |
xor- vx, vy, vz | 异或 | vx = vy ^ vz |
shl- vx, vy, vz | 左移 | vx = vy << vz |
shr- vx, vy, vz | 算术右移 | vx = vy >> vz |
ushr- vx, vy, vz | 逻辑右移 | vx = vy >>> vz |
我们可以查看如下代码:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
编译后的代码可能为:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
这里有个特例,当操作数类型是int
,并且第二个操作数是字面值的时候,有一组特化的指令:
指令 | 运算类型 | 含义 |
---|---|---|
算术运算 | ||
add-int/ vx, vy, | 加法 | vx = vy + <litn> |
sub-int/ vx, vy, | 减法 | vx = vy - <litn> |
mul-int/ vx, vy, | 乘法 | vx = vy * <litn> |
div-int/ vx, vy, | 除法 | vx = vy / <litn> |
rem-int/ vx, vy, | 取余 | vx = vy % <litn> |
位运算 | ||
and-int/ vx, vy, | 与 | vx = vy & <litn> |
or-int/ vx, vy, | 或 | `vx = vy |
xor-int/ vx, vy, | 异或 | vx = vy ^ <litn> |
shl-int/ vx, vy, | 左移 | vx = vy << <litn> |
shr-int/ vx, vy, | 算术右移 | vx = vy >> <litn> |
ushr-int/ vx, vy, | 逻辑右移 | vx = vy >>> <litn> |
其中<litn>
可以为lit8
或lit16
,即 8 位或 16 位的整数字面值。比如int a = 0; a += 2;
可能编译为const/4 v0, 0
和add-int/lit8 v0, v0, 0x2
。
二元运算赋值
二元运算赋值指令格式为<运算类型>-<数据类型>/2 vx,vy,vz
。
指令 | 运算类型 | 含义 |
---|---|---|
算术运算 | ||
add-/2addr vx, vy | 加法赋值 | vx += vy |
sub-/2addr vx, vy | 减法赋值 | vx -= vy |
mul-/2addr vx, vy | 乘法赋值 | vx *= vy |
div-/2addr vx, vy | 除法赋值 | vx /= vy |
rem-/2addr vx, vy | 取余赋值 | vx %= vy |
位运算 | ||
and-/2addr vx, vy | 与赋值 | vx &= vy |
or-/2addr vx, vy | 或赋值 | `vx |
xor-/2addr vx, vy | 异或赋值 | vx ^= vy |
shl-/2addr vx, vy | 左移赋值 | vx <<= vy |
shr-/2addr vx, vy | 算术右移赋值 | vx >>= vy |
ushr-/2addr vx, vy | 逻辑右移赋值 | vx >>>= vy |
我们可以查看这段代码:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
可能会编译成:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
一元运算
指令 | 运算类型 | 含义 |
---|---|---|
算术运算 | ||
neg- vx, vy | 取负 | vx = -vy |
位运算 | ||
not- vx, vy | 取补 | vx = ~vy |
简单来说,如果代码为int a = 5, b = -a, c = ~a
,并且变量依次分配给v0, v1, v2
的话,我们会得到const/4 v0, 0x5
、neg-int v1, v0
和not-int v2, v0
。
跳转
无条件
Java 里面没有goto
,但是 Smali 里面有,一般来说和if
以及for
配合的可能性很大,还有一个作用就是用于代码混淆。
指令 | 类型 |
---|---|
goto target | 8 位无条件跳 |
goto/16 target | 16 位无条件跳 |
goto/32 target | 32 位无条件跳 |
target
在 Smali 中是标签,以冒号开头,使用方式是这样:
- 1
- 2
- 3
- 4
- 5
这三个指令在使用形式上都一样,就是位数越大的语句支持的距离也越长。
条件跳转
if
系列指令可用于int
(以及short
、char
、byte
、boolean
甚至是对象引用):
指令 | 含义 |
---|---|
if-eq vx,vy,target | vx == vy 则跳到 target |
if-ne vx,vy,target | vx != vy 则跳到 target |
if-lt vx,vy,target | vx < vy 则跳到 target |
if-ge vx,vy,target | vx >= vy 则跳到 target |
if-gt vx,vy,target | vx > vy 则跳到 target |
if-le vx,vy,target | vx <= vy 则跳到 target |
if-eqz vx,target | vx == 0 则跳到 target |
if-nez vx,target | vx != 0 则跳到 target |
if-ltz vx,target | vx < 0 则跳到 target |
if-gez vx,target | vx >= 0 则跳到 target |
if-gtz vx,target | vx > 0 则跳到 target |
if-lez vx,target | vx <= 0 则跳到 target |
看一下这段代码:
- 1
- 2
- 3
- 4
- 5
可能的编译结果是:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
我们会看到用于比较逻辑是反着的,Java 里是大于,Smali 中就变成了小于等于,这个要注意。也有一些情况下,逻辑不是反着的,但是if
块和else
块会对调。还有,标签不一定是一样的,后面的数字会变,但是多数情况下都是两个标签,一个相对跳一个绝对跳。
如果只有if
:
- 1
- 2
- 3
相对来说就简单一些,只需要在条件不满足时跳过if
块即可:
- 1
- 2
- 3
- 4
比较
对于long
、float
和double
又该如何比较呢?Dalvik 提供了下面这些指令:
指令 | 含义 |
---|---|
cmpl-float vx, vy, vz | vx = -sgn(vy - vz) |
cmpg-float vx, vy, vz | vx = sgn(vy - vz) |
cmp-float vx, vy, vz | cmpg-float 的别名 |
cmpl-double vx, vy, vz | vx = -sgn(vy - vz) |
cmpg-double vx, vy, vz | vx = sgn(vy - vz) |
cmp-double vx, vy, vz | cmpg-double 的别名 |
cmp-long vx, vy, vz | vx = sgn(vy - vz) |
其中sgn(x)
是符号函数,定义为:x > 0
时值为 1,x = 0
时值为 0,x < 0
时值为 -1。
我们把之前例子中的int
改为float
:
- 1
- 2
- 3
- 4
- 5
我们会得到:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
由于cmpg
更类似平时使用的比较器,用起来更加顺手,但是cmpl
也需要了解。
switch
Dalvik 共支持两种switch
,密集和稀疏。先来看密集switch
,密集的意思是case
的序号是挨着的:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
编译为:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
然后是稀疏switch
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
编译为:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
数组操作
数组拥有一套特化的指令。
创建
指令 | 含义 |
---|---|
new-array vx,vy,type | 创建类型为type ,大小为vy 的数组赋给vx |
filled-new-array {params},type_id | 从params 创建数组,结果使用move-result 获取 |
filled-new-array-range {vx..vy},type_id | 从vx 与vy 之间(包含)的所有寄存器创建数组,结果使用move-result 获取 |
对于第一条指令,如果我们这样写:
- 1
就可以使用该指令编译:
- 1
- 2
但如果我们直接使用数组字面值给一个数组赋值:
- 1
- 2
- 3
可以使用第二条指令编写如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
我们这里的寄存器是连续的,实际上不一定是这样,如果寄存器是连续的,还可以改写为第三条指令:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
元素操作
aget
系列指令用于读取数组元素,效果为vx = vy[vz]
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
有两个指令需要说明,aget
用于获取int
和float
,aget-wide
用于获取long
和double
。
同样,aput
系列指令用于写入数组元素,效果为vy[vz] = vx
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
如果我们编写以下代码:
- 1
- 2
- 3
可能会编译成:
- 1
- 2
- 3
- 4
- 5
- 6
对象操作
对象创建
指令 | 含义 |
---|---|
new-instance vx, type | 创建type 的新实例,并赋给vx |
new-instance
用于创建实例,但之后还需要调用构造器<init>
,比如:
- 1
会编译成:
- 1
- 2
方法调用后面再讲。
字段操作
sget
系列指令用于获取静态字段,效果为vx = class.field
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
sput
系列指令用于设置静态字段,效果为class.field = vx
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
我们在这里创建一个类:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
编译之后,我们可以在getStaticField
中找到:
- 1
- 2
在setStaticField
中可以找到:
- 1
- 2
iget
系列指令用于获取实例字段,效果为vx = vy.field
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
iput
系列指令用于设置实例字段,效果为vy.field = vx
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
我们将之前的类修改一下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
反编译之后,我们可以在getInstanceField
中找到:·
- 1
- 2
在setInstanceField
中可以找到:
- 1
- 2
在实例方法中,this
引用永远是p0
。第一个参数从p1
开始。
方法调用
有五类方法调用指令:
指令 | 含义 |
---|---|
invoke-static | 调用静态方法 |
invoke-direct | 调用直接方法 |
invoke-direct-empty | 无参的invoke-direct |
invoke-virtual | 调用虚方法 |
invoke-super | 调用超类的虚方法 |
invoke-interface | 调用接口方法 |
这些指令的格式均为:
- 1
如果需要传递this
引用,将其放置在param
的第一个位置。
那么这些指令有什么不同呢?首先要分辨两个概念,虚方法和直接方法(JVM 里面叫特殊方法)。其实 Java 是没有虚方法这个概念的,但是 DVM 里面有,直接方法是指类的(type
为某个类)所有实例构造器和private
实例方法。反之protected
或者public
方法都叫做虚方法。
invoke-static
比较好分辨,当且仅当调用静态方法时,才会使用它。
invoke-direct
(在 JVM 中叫做invokespecial
)用于调用直接方法,invoke-virtual
用于调用虚方法。除了一种情况,显式使用super
调用超类的虚方法时,使用invoke-super
(直接方法仍然使用invoke-direct
)。
就比如说,每个Activity
的onCreate
中要调用super.onCreate
,该方法属于虚方法,于是我们会看到:
- 1
但是呢,每个Activity
构造器里面要调用super
的无参构造器,它属于直接方法,那么我们会看到:
- 1
invoke-interface
用于调用接口方法,接口方法就是接口的方法,type
一定为某个接口,而不是类。换句话说,类中实现的方法仍然是虚方法。比如我们在某个对象上调用Map.get
,属于接口方法,但是调用HashMap.get
,属于虚方法。这个指令一般在向上转型为接口类型的时候出现。
此外,五类指令中每一个都有对应的invoke-*-range
指令,格式为:
- 1
如果参数所在的寄存器的连续的,可以替换为这条指令。
对象转换
对象转换有自己的一套检测方式,DVM 使用以下指令来实现:
指令 | 含义 |
---|---|
instance-of vx, vy, type | 检验vy 的类型是不是type ,将结果存入vx |
check-cast vx, type | 检验vx 类型是不是type ,不是的话会抛出ClassCastException |
instance-of
指令对应 Java 的instanceof
运算符。如果我们编写:
- 1
- 2
可能会编译为:
- 1
- 2
check-cast
用于对象类型强制转换的情况,如果我们编写:
- 1
- 2
那么就会:
- 1
- 2
- 3
返回
- 1
- 2
- 3
- 4
如果函数无返回值,那么使用return-void
,注意在 Java 中,无返回值函数结尾处的return
可以省,而 Smali 不可以。
如果函数需要返回对象,使用return-object
;需要返回long
或者double
,使用return-wide
;除此之外所有情况都使用return
。
异常指令
异常指令实际上只有一条,但是代码结构相当复杂。
指令 | 含义 |
---|---|
throw vx | 抛出vx (所指向的对象) |
我们需要看看 Smali 如何处理异常。
try-catch
不失一般性,我们构造以下语句:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
可能会编译成这样,这些语句每个都不一样,可以按照特征来定位:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
我们可以看到,:try_start_0
和:try_end_0
之间的语句如果存在异常,则会向下寻找.catch
(或者.catch-all
)语句,符合条件时跳到标签的位置,这里是:catch_0
,结束之后会有个goto
跳回去。
try-finally
- 1
- 2
- 3
- 4
- 5
- 6
- 7
编译之后是这样:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
我们可以看到,编译器把finally
编译成了重新抛出的.catch-all
,这在逻辑上也是说得通的。但是,finally
中的逻辑在无异常情况下也会执行,所以需要复制一份到finally
块的后面。
try-catch-finally
下面看看如果把这两个叠加起来会怎么样。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
我们可以看到,其中同时含有.catch
块和.catchall
块。有一些不同之处在于,finally
块中的语句异常发生时也要执行,并且如果把finally
编译成.catchall
,那么和.catch
就是互斥的,所以要复制一份到catch
块里面。特别是finally
块中的语句一多,就容易乱。
锁
指令 | 含义 |
---|---|
monitor-enter vx | 获得vx 所引用的对象的锁 |
monitor-exit vx | 释放vx 所引用的对象的锁 |
对应 Java 的synchronized
语句。而synchronized
一般是被try-finally
包起来的。
如果你编写:
- 1
- 2
- 3
- 4
就相当于
- 1
- 2
- 3
- 4
- 5
- 6
- 7
此外 Java 中没有与这两条指令相对应的方法,所以这两条指令一定成对出现。
数据转换
整数与浮点以及浮点与浮点
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
因为它们的表示方式不同,所以要保持表示的值不变,重新计算二进制位。如果不转换的话,就相当于二进制位不变,而表示的值改变,结果毫无意义。比如前面的0.1f
如果不转换为直接使用,就会表示0x3dcccccd
。
整数之间的向上转换
这种转换方式相当直接,int
向long
转换,long
的第一个寄存器完全复制,第二个寄存器以int
的最高位填充。除此之外没有其它的指令了,因为比int
小的整数其实都是 32 位表示的,只是有效范围是 8 位或 16 位罢了(见数据定义)。
- 1
整数之间的向下转换
其规则是数据位截断,符号位保留。每个整数的最高位都是符号位,其余是数据位。以int
转short
为例,int
的低 15 位复制给short
,然后int
的最高位(符号位)复制给short
的最高位。其它同理。如果不转换而直接使用的话,会直接截断低 16 位,符号可能不能保留。
- 1
- 2
- 3
- 4
NOP
nop
指令表示无操作。在一些场合下,不能修改二进制代码的字节数和偏移,需要用nop
来填充,但是安卓逆向中几乎用不到。