0. CMP汇编指令&B指令扩展
-
CMP指令
CMP w0, w1
CMP 把一个寄存器的内容和另一个寄存器的内容或立即数进行比较。但不存储结果,只是正确的更改标志(更改cpsr标识寄存器相应位的值)。
一般CMP做完判断后会进行跳转,后面通常会跟上B指令! -
B指令扩展
BL 标号
:跳转到标号处,同时更新lr寄存器的值B.GT 标号
:比较结果是大于(greater than),执行标号,否则不跳转B.GE 标号
:比较结果是大于等于(greater than or equal to),执行标号,否则不跳转B.EQ 标号
:比较结果是等于,执行标号,否则不跳转B.LE 标号
:比较结果是小于等于,执行标号,否则不跳转B.HI 标号
:比较结果是无符号大于,执行标号,否则不跳转... 类似有一些指令,后面都是根据cpsr的标记位进行判断是否跳转
1. if选择分支语句
-
1.1 ida汇编示例
-
1.2 汇编逆向
// 在main汇编中判断,函数名+参数个数,参数类型不太确定,暂定int int _global = 20; // 类型与值无法确定的(需要lldb),这里大概估计 void func (int a, int b) { // 汇编:var_8 var_4 int var_8, var_4; // 和参数类型保持一致 // 汇编:STR W0, [SP,#0x10+var_4],函数参数 var_4 = a; // 汇编:STR W1, [SP,#0x10+var_8] var_8 = b; // 汇编:LDR W0, [SP,#0x10+var_4] int w0 = var_4; // 汇编:LDR W1, [SP,#0x10+var_8] int w1 = var_8; // 汇编:CMP W0, W1 // 汇编:B.LE loc_1000047B0 // cmp比较两个数值,b.le 小于等于跳转,转成高级代码反过来, 大于的情况会执行后面的汇编代码 if (a > b) { // 47bc汇编:ADRP X8, #__global@PAGE // ADD X8, X8, #__global@PAGEOFF // x8放的是 _global 的地址,_global全局变量 int * x8 = &_global; // 47a4汇编:LDR W9, [SP,#0x10+var_4] int w9 = var_4; // 47a8汇编:STR W9, [X8] *x8 = w9; } else { // b.le跳转代码 // 47b0汇编:ADRP X8, #__global@PAGE // ADD X8, X8, #__global@PAGEOFF int * x8 = &_global; // 47b8汇编:LDR W9, [SP,#0x10+var_8] int w9 = var_8; // 47bc汇编:STR W9, [X8] *x8 = w9; } // 47c0汇编:ADD SP, SP, #0x10 // RET // 函数返回,没有mov x0, x8类似的代码,表示没有返回值 // 函数返回值存储在x0 return; } 复制代码
整理后的代码
int _global = 20; // 类型与值??操作global是用w9,int 可以满足,但是不确定 void func(int a, int b) { // 参数类型??w0、w1存储参数,所以int可以满足 if (a > b) { _global = a; } else { _global = b; } return; } 复制代码
-
1.3 小结
if-else语句的汇编是通过 先 CMP 然后 B.LE 之类的跳转指令跳转到某条汇编指令地址执行,多个if-elseif也是这样。
2. 循环语句
-
2.1 do-while
int main(int argc, char * argv[]) { int i = 0; do { printf("hello"); i++; } while ( i < 100); return 0; } 复制代码
-
2.2 while
int main(int argc, char * argv[]) { int i = 0; while (i<100) { printf("hello"); i++; } return 0; } 复制代码
-
2.3 for
int main(int argc, char * argv[]) { for (int i = 0; i < 100; i++) { printf("hello"); } return 0; } 复制代码
-
2.4 小结:
熟悉do-while循环的汇编样式:
CMP x1,x2; B.LT loc_1000047B0
比较完成后,根据结果向前跳转
while与for循环一样,CMP x1,x2 B.GE loc_1000047d8
, 比较完成后,根据结果跳出循环; 一次循环完成后,B loc_1000047AC
向前跳转,无需判断注意:上面的汇编截图是ida工具汇编静态分析界面,xcode汇编是不存在loc_1000047d8这类东西的,需要我们自己去判断循环的起始指令位置,这就是ida的方便之处
3. switch分支语句
-
3.1 switch汇编如何查找分支(分支数<=3)
void funcB (int a) { switch (a) { case 3: printf("33"); break; case 2: printf("22"); break; case 4: printf("44"); break; default: printf("default"); break; } return; } 复制代码
汇编3.1 汇编3.1续 小结:case分支数 <=3 (default另算), 此时,汇编使用b.eq对case值一个一个进行判断
-
3.2 switch汇编如何查找分支(分支数>3 && case值连续)
void funcB (int a) { switch (a) { case 2: printf("22"); break; case 3: printf("33"); break; case 4: printf("44"); break; case 5: printf("55"); break; default: printf("default"); break; } return; } 复制代码
汇编3.2 汇编3.2续 汇编分析
-
第一块(2~12行)
- 第7行:与 最小的case值0x2 进行差值比较
- 第8行:存储比较的差值(后面用来做 索引 )
- 第9行:#0x3 是
最大的case值与最小case值之间的差值
- 第12行:判断比较结果 higher 那么跳转到 后面去执行 default的代码
- 主要做:计算传入的case值的索引(相对于最小的case值),判断出入的case值是否在default,是的话直接跳转执行,不是的话通过索引查表然后计算跳转的指令地址
- 举例:如果传入a=4,那么x8存入的索引值就是2,这个索引值在后面非常有用
-
第二块(13~18行)
- 第13~14行:计算索引表的地址(memory read 地址:查看存储的数据, 或者debug-debug flow-view memory查看)
计算过程: 0x1000b0718低12位置0,得0x1000b0000, 加0x79c,得0x1000b079c
我们通过计算可以发现地址刚刚好是ret指令后的地址,也就是0x1000b0798开始4个字节存储ret指令,后面就是我们switch的索引表
索引表是什么???(通过后续汇编理解,现在先打印存储内容)
lldb: memory read 0x1000b079c 0x1000b079c: 94 ff ff ff a8 ff ff ff bc ff ff ff d0 ff ff ff ................ 0x1000b07ac: ff 83 00 d1 fd 7b 01 a9 fd 43 00 91 e8 07 1f 32 .....{...C.....2 复制代码
- 第15行:取出之前存储的索引值,同样,如果a=4,取出来的就是2
- 第16行:
ldrsw x10, [x8, x9, lsl #2]
x9寄存器的值左移2位,然后与x8寄存器的值相加,得到的结果当成地址值,从这个地址值开始,读取1word的数据存入寄存器x10
地址值计算:之前x8=0x1000b079c
如果a=4,x9为索引值2,得到地址是0x1000b079c+2*4
取出数据:利用上面索引表的打印,我们可以得到里面的数据是0xffffffbc(当做有符号数:-1-(0xffffffff-0xffffffbc)=-0x44)
为什么左移2位?why?? 每一个地址偏移量是4Byte,因此左移2位(左移动2位相当于*4) - 第17行:计算要跳转的case指令地址:0x1000b079c-0x44=0x1000b0758==>即为当传入a=4时候,要跳转执行的case指令地址
- 第18行:
br x8
将x8寄存器中的值当做地址进行跳转
- 第13~14行:计算索引表的地址(memory read 地址:查看存储的数据, 或者debug-debug flow-view memory查看)
-
第三块(19~38行)
- 第19~23行:switch代码中第1个case执行,取出常量的地址0x1000b3cd7,lldb调试
p (char*)0x1000b3cd7
, 即可打印printf要输出的常量值,进行验证是哪个case,是否判断一致;b 指令跳转,如果代码不加break,不会生成b指令,因此造成case穿透效果。。。 - 第24~28行:switch代码中第2个case
- 第29~33行:switch代码中第3个case
- 第34~38行:switch代码中第4个case
- 第19~23行:switch代码中第1个case执行,取出常量的地址0x1000b3cd7,lldb调试
-
第四块(39~41)
switch代码中的default分支
小结
- switch(a) 首先计算相对于最小case值的差值(或者偏移,在查表时候用到),然后判断是否超出一个范围 (最大case值与最小case值的范围),超出就执行default分支,未超出就去查表,计算相应的case的指令执行地址,进行跳转执行
- 针对switch代码,编译器会生成一张地址偏移表,内部存储的是switch中每个case指令的执行地址相对于表的开始地址的偏移量(例如:case2 执行地址:0x1000b0730, 表的地址:函数ret地址+4=0x1000b079c, 所以第一个偏移量是-0x6c,转成二进制存储0xffffff94)
- 编译器会对case值进行从小到大的排序,switch代码中case值最小的在表的最前面(例如:如果代码是从case5~case2,但是在生成表的时候还是和case2~case5一样的表,可以验证),但是case汇编地址还是按照switch中case的先后生成。====> 逆序写case的情况
- switch相对于if效率高的原因:以空间换时间。。不在是逐一比较
-
-
3.3 switch汇编如何查找分支(分支数>3 && case 值不连续)
- 情况1(case值不连续,case的差值不是很大)
void funcB (int a) { switch (a) { case 2: printf("22"); break; case 3: printf("33"); break; case 4: printf("44"); break; case 6: printf("66"); break; default: printf("default"); break; } return; } 复制代码
汇编分析1 汇编分析2 偏移索引表分析 - 情况2(case值不连续,case的差值很大)
void funcB (int a) { switch (a) { case 2: printf("22"); break; case 3: printf("33"); break; case 4: printf("44"); break; case 100: printf("100"); break; default: printf("default"); break; } return; } 复制代码
多个case之间差值过大,switch在进行分支判断的时候,使用b.eq逐一判断,和if-else没有区别
-
3.4 总结( switch使用注意)
- 使用switch,分支数大于3才有意义
- 使用switch,分支case值的差值不要过大,否则没有意义(无法提高效率,汇编层次提高效率的思路利用空间换取时间)
- 使用switch,case值尽量连续(断续会造成索引表空间的浪费)
- switch中,case执行频率高的,尽量放在default分支去(省去查表计算的时间)
switch相对于if-else效率高是有条件的,如果switch代码书写不当,并没有提高效率
4. Xcode编译器的优化
之前我们看到的汇编,有很多指令是没有必要的,例如:stur w0, [x29, #-0x4]
ldur w0, [x29, #-0x4]
。 原因是我们使用的是debug模式编译生成汇编
举例1
int sum(int a, int b) {
return a + b;
}
int main(int argc, char * argv[]) {
NSLog(@"haha"); // 加log是为了能够打断点
sum(1,2);
return 0;
}
复制代码
举例2
int sum(int a, int b) {
return a + b;
}
int main(int argc, char * argv[]) {
int c = sum(1,2);
NSLog(@"%d", c);
return 0;
}
复制代码
直接将sum函数优化掉,结果给c
注意:c函数在编译的时候可以确定,但OC方法是动态发送消息的,因此方法调用编译器是无法优化的