逆向学习笔记 - 部分场景的 ARM64 汇编指令

本文将对高级语言中常见的基础语句块进行汇编代码调试,如全局变量、局部变量、if / for / while 语句、switch、指针类型等,以加深印象和理解,方便在查看二进制反汇编代码时能快速识别并准确地“翻译”为高级语言伪代码。

全局变量 & 局部变量

下面代码中,全局变量和局部变量是如何取值并参与运算的呢?

1
2
3
4
5
6
7
8
int globalVal = 0xabcdef;
int sumV(int a, int b) {
int tempG = globalVal;
NSArray *arr = [NSArray new];
int c = 0x11;
int d = 0x14;
return a + b + c + tempG;
}

main 中调用 int res = sumV(0x1, 0x2);,在 sumV 首行添加断点,设置 Xode 展示汇编指令,以默认的 Debug 优化级别运行,如下:

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
33
34
35
36
37
38
    0x100dc206c <+0>:   sub    sp, sp, #0x40    ; =0x40 
0x100dc2070 <+4>: stp x29, x30, [sp, #0x30]
0x100dc2074 <+8>: add x29, sp, #0x30 ; =0x30
0x100dc2078 <+12>: stur w0, [x29, #-0x4]
0x100dc207c <+16>: stur w1, [x29, #-0x8]
0x100dc2080 <+20>: adrp x8, 3
0x100dc2084 <+24>: add x8, x8, #0x6a0 ; =0x6a0
-> 0x100dc2088 <+28>: ldr w9, [x8]
0x100dc208c <+32>: stur w9, [x29, #-0xc]
0x100dc2090 <+36>: adrp x8, 3
0x100dc2094 <+40>: add x8, x8, #0x4c8 ; =0x4c8
0x100dc2098 <+44>: ldr x0, [x8]
0x100dc209c <+48>: adrp x8, 3
0x100dc20a0 <+52>: add x8, x8, #0x4a8 ; =0x4a8
0x100dc20a4 <+56>: ldr x1, [x8]
0x100dc20a8 <+60>: bl 0x100dc24f4 ; objc_msgSend
0x100dc20ac <+64>: add x8, sp, #0x18 ; =0x18
0x100dc20b0 <+68>: str x0, [sp, #0x18]
0x100dc20b4 <+72>: mov w9, #0x11
0x100dc20b8 <+76>: str w9, [sp, #0x14]
0x100dc20bc <+80>: mov w9, #0x14
0x100dc20c0 <+84>: str w9, [sp, #0x10]
0x100dc20c4 <+88>: ldur w9, [x29, #-0x4]
0x100dc20c8 <+92>: ldur w10, [x29, #-0x8]
0x100dc20cc <+96>: add w9, w9, w10
0x100dc20d0 <+100>: ldr w10, [sp, #0x14]
0x100dc20d4 <+104>: add w9, w9, w10
0x100dc20d8 <+108>: ldur w10, [x29, #-0xc]
0x100dc20dc <+112>: add w0, w9, w10
0x100dc20e0 <+116>: str w0, [sp, #0xc]
0x100dc20e4 <+120>: mov x0, x8
0x100dc20e8 <+124>: mov x8, #0x0
0x100dc20ec <+128>: mov x1, x8
0x100dc20f0 <+132>: bl 0x100dc2524 ; objc_storeStrong
0x100dc20f4 <+136>: ldr w0, [sp, #0xc]
0x100dc20f8 <+140>: ldp x29, x30, [sp, #0x30]
0x100dc20fc <+144>: add sp, sp, #0x40 ; =0x40
0x100dc2100 <+148>: ret
  1. 前三行开辟栈空间 -> 保存上一个函数栈底信息和自己的返回地址 -> 保存自己的栈底地址
  2. 第 4 行的 stur 等同于 str,但操作数为负数,结果是将两个形参放到栈底开始的位置
  3. adrp x8, 3adrp 以页为单位的大范围的地址读取指令。将 3 左移 12 位得到的数与清空了低 12 位的 pc 寄存器的值相加,结果放入 x8 中,这是某个 Page 的起始地址,配合接下来的偏移操作 add x8, x8, #0x6a0,x8 中存放的是一个特定的地址。如果基址偏移为 0x100dbc000,可以计算出来 x8 中的地址在二进制中的偏移为:0x100dc2000 + (3 << 12) + 0x6a0 - 0x100dbc000 = 0x00000000000096a0,如下图,高 8 位即为 globalVal 的值: 接下来将它存到了栈底偏移 0xc
  4. 第 16 行是一个 objc_msgSend 调用,上面分别通过 adrp 指令准备了参数 x0 和 x1,同上,计算可知它们的值分别来自于二进制文件偏移量 0x00000000000094c80x00000000000094a8 处,根据 objc_msgSend 调用规则,x0、x1 分别是 NSArray.classnew,接下来将返回值存到了栈顶偏移 0x18 处:
  5. 接下来是两个基本类型的临时变量,是立即数通过寄存器中转存到了栈空间中,等到需要做加法运算时,再取到寄存器中做运算
  6. 最后 4 行将返回值存入 x0 ,恢复主调函数调用环境退栈并 ret
  7. 在函数即将退栈时,第 34 行以 x8 和 nil 为形参调用了 objc_storeStrong,x8 是 arr 变量的指针,目的是在离开作用域时对指针置空,引用计数 -1,以期待下一个 RunLoop 时实例对象的内存能被 AutoReleasePool 自动释放。objc_storeStrong 的实现:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void objc_storeStrong(id *location, id obj)
    {
    id prev = *location;
    if (obj == prev) {
    return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
    }

总结

  • 全局变量存放于数据区,和代码区距离较远,使用 adrp 来加载
  • 局部变量如果是基本类型,将会以立即数的形式加载;如果是对象类型,将加载指向堆区的指针
  • 由于 NASrray 来自于动态库,其地址(或者说偏移值)在二进制 rebase 阶段才会被决定,所以上图中只能展示为 0x0

条件、循环语句

forifwhiledo-while 语句的汇编相差不大。参考以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
void looptest(void) {
int i;
for(i = 0xab;i < 0xffff;i ++) {
if (i > 0xad) {
printf("enough!\n");
break;
}
}
while (i < 0xff) {
printf("Go on\n");
i ++;
}
}

按上述方法编译成汇编得到:
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
33
    0x1008e6064 <+0>:   sub    sp, sp, #0x20    ; =0x20 
0x1008e6068 <+4>: stp x29, x30, [sp, #0x10]
0x1008e606c <+8>: add x29, sp, #0x10 ; =0x10
0x1008e6070 <+12>: mov w8, #0xab
0x1008e6074 <+16>: stur w8, [x29, #-0x4]
0x1008e6078 <+20>: ldur w8, [x29, #-0x4]
0x1008e607c <+24>: mov w9, #0xffff
0x1008e6080 <+28>: cmp w8, w9
0x1008e6084 <+32>: b.ge 0x1008e60b4 ; <+80> at main.m:29:12
0x1008e6088 <+36>: ldur w8, [x29, #-0x4]
0x1008e608c <+40>: cmp w8, #0xad ; =0xad
0x1008e6090 <+44>: b.le 0x1008e60a4 ; <+64> at main.m:23:31
0x1008e6094 <+48>: adrp x0, 0
0x1008e6098 <+52>: add x0, x0, #0x66a ; =0x66a
-> 0x1008e609c <+56>: bl 0x1008e6514 ; symbol stub for: printf
0x1008e60a0 <+60>: b 0x1008e60b4 ; <+80> at main.m:29:12
0x1008e60a4 <+64>: ldur w8, [x29, #-0x4]
0x1008e60a8 <+68>: add w8, w8, #0x1 ; =0x1
0x1008e60ac <+72>: stur w8, [x29, #-0x4]
0x1008e60b0 <+76>: b 0x1008e6078 ; <+20> at main.m:23:18
0x1008e60b4 <+80>: ldur w8, [x29, #-0x4]
0x1008e60b8 <+84>: cmp w8, #0xff ; =0xff
0x1008e60bc <+88>: b.ge 0x1008e60dc ; <+120> at main.m:33:1
0x1008e60c0 <+92>: adrp x0, 0
0x1008e60c4 <+96>: add x0, x0, #0x673 ; =0x673
0x1008e60c8 <+100>: bl 0x1008e6514 ; symbol stub for: printf
0x1008e60cc <+104>: ldur w8, [x29, #-0x4]
0x1008e60d0 <+108>: add w8, w8, #0x1 ; =0x1
0x1008e60d4 <+112>: stur w8, [x29, #-0x4]
0x1008e60d8 <+116>: b 0x1008e60b4 ; <+80> at main.m:29:12
0x1008e60dc <+120>: ldp x29, x30, [sp, #0x10]
0x1008e60e0 <+124>: add sp, sp, #0x20 ; =0x20
0x1008e60e4 <+128>: ret

  1. cmp label1, label2 实际是执行 label1 和 label2 相减的操作,其结果会影响 cpsr 寄存器,后续的 b.geb.le 根据 cpsr 寄存器的标记为决定是否跳转到指定地址执行:
    1. b.ge label: 结果是大于等于时跳转到 label 执行
    2. b.le label: 结果是小于等于时跳转到 lebal 执行
  2. break 语句编译为了一个无条件跳转指令b label,跳转到循环体的外面第一行指令处

switch 语句

从 C 的角度来看,switchif-else 是可以等价变换的,但是在汇编底层角度它们有什么不同呢?

case 分支小于 4 个

将下面两个函数编译成汇编代码:

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
void switchTest(int a) {
switch (a) {
case 3:
printf("3");
break;
case 4:
printf("4");
break;
case 7:
printf("7");
break;
default:
printf("miss matched");
break;
}
}

void ifelseTest(int a) {
if (a == 3) {
printf("3");
} else if (a == 4) {
printf("4");
} else if (a == 7) {
printf("7");
}
}

可以看到它们几乎没有区别,都是通过 cmpb 语句来控制流程走向。

case 分支跨度过大

再来看看 switch case 超过 4 个,但 case 最小值和最大值超过 50 的情况:


汇编层面同样是通过 cmpb 语句来进行控制的

正常情况的 switch

case 分支多于 4 个且最大值和最小值差值不超过 50 时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void switchTest(int a) {
switch (a) {
case 2:
printf("3");
break;
case 4:
printf("4");
break;
case 12:
printf("12");
break;
case 7:
printf("7");
break;
case 8:
printf("8");
break;
default:
printf("miss matched");
break;
}
}

形参传入 10 ,Xcode 运行后的汇编代码:
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
33
34
35
36
37
38
39
40
41
42
43
    0x100b01f94 <+0>:   sub    sp, sp, #0x20    ; =0x20 
0x100b01f98 <+4>: stp x29, x30, [sp, #0x10]
0x100b01f9c <+8>: add x29, sp, #0x10 ; =0x10
0x100b01fa0 <+12>: stur w0, [x29, #-0x4]
0x100b01fa4 <+16>: ldur w8, [x29, #-0x4]
0x100b01fa8 <+20>: subs w8, w8, #0x2 ; =0x2
0x100b01fac <+24>: mov x9, x8
0x100b01fb0 <+28>: ubfx x9, x9, #0, #32
-> 0x100b01fb4 <+32>: cmp x9, #0xa ; =0xa
0x100b01fb8 <+36>: str x9, [sp]
0x100b01fbc <+40>: b.hi 0x100b02028 ; <+148> at main.m
0x100b01fc0 <+44>: adrp x8, 1
0x100b01fc4 <+48>: add x8, x8, #0x40 ; =0x40
0x100b01fc8 <+52>: ldr x11, [sp]
0x100b01fcc <+56>: ldrsw x10, [x8, x11, lsl #2]
0x100b01fd0 <+60>: add x9, x8, x10
0x100b01fd4 <+64>: br x9
0x100b01fd8 <+68>: adrp x0, 1
0x100b01fdc <+72>: add x0, x0, #0x662 ; =0x662
0x100b01fe0 <+76>: bl 0x100b0250c ; symbol stub for: printf
0x100b01fe4 <+80>: b 0x100b02034 ; <+160> at main.m:56:1
0x100b01fe8 <+84>: adrp x0, 1
0x100b01fec <+88>: add x0, x0, #0x664 ; =0x664
0x100b01ff0 <+92>: bl 0x100b0250c ; symbol stub for: printf
0x100b01ff4 <+96>: b 0x100b02034 ; <+160> at main.m:56:1
0x100b01ff8 <+100>: adrp x0, 1
0x100b01ffc <+104>: add x0, x0, #0x666 ; =0x666
0x100b02000 <+108>: bl 0x100b0250c ; symbol stub for: printf
0x100b02004 <+112>: b 0x100b02034 ; <+160> at main.m:56:1
0x100b02008 <+116>: adrp x0, 0
0x100b0200c <+120>: add x0, x0, #0x669 ; =0x669
0x100b02010 <+124>: bl 0x100b0250c ; symbol stub for: printf
0x100b02014 <+128>: b 0x100b02034 ; <+160> at main.m:56:1
0x100b02018 <+132>: adrp x0, 0
0x100b0201c <+136>: add x0, x0, #0x66b ; =0x66b
0x100b02020 <+140>: bl 0x100b0250c ; symbol stub for: printf
0x100b02024 <+144>: b 0x100b02034 ; <+160> at main.m:56:1
0x100b02028 <+148>: adrp x0, 0
0x100b0202c <+152>: add x0, x0, #0x66d ; =0x66d
0x100b02030 <+156>: bl 0x100b0250c ; symbol stub for: printf
0x100b02034 <+160>: ldp x29, x30, [sp, #0x10]
0x100b02038 <+164>: add sp, sp, #0x20 ; =0x20
0x100b0203c <+168>: ret

  1. 第 6-7 行表示让 w8 和 0x2 相减,结果存入 w8 并影响 cpsr 寄存器。w8 保存的是形参 10,与 case 最小值 2 相减,结果存入 x9。计这个差值为 D1
  2. 第 8 - 11 行,首先通过 ubfx x9, x9, #0, #32 清空 x9 高 32 位,只保留低 32 位的值,这里即是 D1,让 D1 与case 的最大最小差值(此处为 0xa,计这个差值为 D2)比较,如果最终无符号大于,表明 D1 跨越的范围大于 case 的最大最小值,说明不可能匹配成功,直接跳转到 default 分支或 switch 语句块的下一行执行,此处形参传入的是 10,在 case 最大值和最小值之间,所以会进入 switch 特有的查表跳转流程
  3. 第 12 - 17 行,先通过 page 偏移找到一个地址存入 x8,然后将之前存到栈区的只保留低 32 位的值放入 x11,后面的 ldrsw x10, [x8, x11, lsl #2] 表示将 x11 左移两位,再加上 x8,把结果作为地址寻址,将得到的值存入 x10,执行完毕后,x10 的值为 0xffffffffffffffe8,看起来像是一个负数,去掉最高位逐位取反后加 1,为 -24。x8 保存的是一个起始地址,把偏移量了 -24 后的地址放入 x9 中,接下来 br 语句以 x9 里面的值作为地址跳转。相比较 if-else ,这里只需要这一次跳转即可,无需逐个比较 case 的值
  4. 传入的 value 经过 x8 加上一个偏移量后,就可以得到真正要执行的地址。偏移量是传入的 value 决定的,那么 x8 处是什么呢?查看 0x0000000100b02040 处的内存数据,可以看到类似的负数有 11 个, case 最大值和最小值之差 10,再加上一个 default case 刚好就是 11。这个偏移值数组需要占用内存空间,case 分支差值跨越越大耗费的内存空间也大,编译器会决定 switch 编译后的汇编指令是查表跳转还是 if-else 形式的逐个 cmp+b
    1
    2
    3
    4
    5
    (lldb) x/16 0x0000000100b02040
    0x100b02040: 0xffffff98 0xffffffe8 0xffffffa8 0xffffffe8
    0x100b02050: 0xffffffe8 0xffffffc8 0xffffffd8 0xffffffe8
    0x100b02060: 0xffffffe8 0xffffffe8 0xffffffb8 0xd10083ff
    0x100b02070: 0xa9017bfd 0x910043fd 0xb81fc3a0 0xb85fc3a8

总结

  • case 分支少于 4 个时,switch 的效率和 if-else 语句一样
  • case 分支多余 4 个且分支最大值和最小值差值合理( arm64 时是少于等于 50 个,可能和具体的架构与编译器版本、代码优化级别有关)时,会有一个内存表来辅助计算跳转到分支的地址,得到地址后直接跳转,不在逐个比较,效率最高
  • case 分支最大值和最小值差值太大时,switch 会退化成 if-else 语句
  • 对效率要求敏感的代码,可将代码结构优化成符合 switch 的要求,提高代码执行效率

指针

Objective-C 里面少不了和各种各样的指针打交道,如果从底层汇编角度来看,指针到底代表什么呢?对指针类型做偏移操作,偏移值如何计算?

在 Objective-C 中,对象是指一块能存储数据并具有某种类型的内存空间,一个对象 a 它有值和地址 &a,运行程序时,计算机会为该对象分配存储空间,存储该对象的值,我们通过该对象的地址,来访问存储空间中的值。指针 p 也是对象,它同样有地址 &p 和存储的值 p,只不过,p 存储的数据类型是数据的地址。如果我们要以 p 中存储的数据为地址,来访问对象的值,则要在 p 前加解引用操作符 *,即 *p

指针偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int *a;
a = (int *)100;
a = a + 3;
/*
👆 指针的加减和它指向的数据的宽度有关,加上 3 个 4 字节后,a 的值为 112
*/
int *b;
b = (int *)200;
int x = b - a;
/*
👆 a 和 b 之间间隔 88 字节,转换成 int 宽度为单位后 x 的值为 22
*/

int p[4] = {1,2,3,4};
int val = *p + 3;
printf("val = %d",val);
/*
👆 数组连续存放,p 的宽度为 4,偏移 3 个宽度后的值为 4
*/

多级指针

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
33
int **a;  
a = (int **)100;
a = a + 3;
/*
👆 a 是指针类型了,宽度为 8,所以加 3 后,a 的值为 124
*/

char ** p1;
p1 = (char **)100;
char c = *(p1 + 2) + 2;
/*
👆 p1 是 char ** 类型,是指针,宽度为 8,(p1 + 2) 后,p1 为 116。
*(p1 + 2) 解引用,得到指向 char 类型的指针,宽度为 1,p1 为 118。
这里的解引用可能会 crash,因为 p1 指向的区域并不合法
*/

int p1[4][3] = {{1,2,3,4},
{5,6,7,8},
{9,'a','b','c'}};
int **pp = p1;
/*
👆 pp 可以看做存放了三个元素的数组,数组中的每个元素是一维数组(指针)
*/
int *temp1 = pp + 2;
/*
👆 pp 指向指针,宽度为 8,加 2 后相当于偏移了两个指针的宽度,
指向了 {9,'a','b','c'} 这一行首位置
*/
int *temp2 = *pp + 2;
/*
👆 *pp 解引用后,得到指向了指针(数组)类型,加 2,相当于偏移了两个 int 宽度,
指向了二维数组第一行的 3
*/