判断
cmp
:(Compare
)比较指令
cmp
把一个寄存器的内容和另一个寄存器的内容或立即数进行比较。但不存储结果,只是正确的更改标志一般
cmp
做完判断后会进行跳转,后面通常会跟上b
指令有符号数
b.lt 标号
:比价结果是小于(less than
),执行标号,否则不跳转b.le 标号
:比较结果是小于等于(less than or qeual to
),执行标号,否则不跳转b.gt 标号
:比较结果是大于(greater than
),执行标号,否则不跳转b.ge 标号
:比较结果是大于等于(greater than or equal to
),执行标号,否则不跳转b.eq 标号
:比较结果是等于(equal to
),执行标号,否则不跳转b.ne 标号
:比较结果是不等于(not equal to
),执行标号,否则不跳转无符号数
b.lo 标号
:比较结果是小于,执行标号,否则不跳转b.ls 标号
:比较结果是小于等于,执行标号,否则不跳转b.hi 标号
:比较结果是大于,执行标号,否则不跳转b.hs 标号
:比较结果是大于等于,执行标号,否则不跳转案例:
编译器对
if
判断的识别打开
ViewController.m
文件,写入以下代码:```
import “ViewController.h”
int g = 12;
void func(int a, int b){ if (a > b) { g = a; } else { g = b; } }
@implementation ViewController
- (void)viewDidLoad { // [super viewDidLoad]; func(1, 2); }
@end
> 使用真机编译项目,将`Mach-O`文件拖到`Hopper`中,找到`func`函数<br />

> - `func`函数在`Hopper`中被分为`四段`
> `func`函数,`第一段`
>
_func:
0000000100005f0c sub sp, sp, #0x10 ; CODE XREF=-[ViewController viewDidLoad]+28 0000000100005f10 str w0, [sp, #0xc] 0000000100005f14 str w1, [sp, #0x8] 0000000100005f18 ldr w8, [sp, #0xc] 0000000100005f1c ldr w9, [sp, #0x8] 0000000100005f20 cmp w8, w9 0000000100005f24 b.le loc_100005f3c
> - 开辟栈空间,现场保护
> - `cmp w8, w9`:比较`w8`、`w9`寄存器,相当于比较`参数1`和`参数2`。`cmp`指令相当于做减法,但只会影响状态寄存器,不会影响目标寄存器
> - `b.le loc_100005f3c`:如果`参数1 - 参数2`的结果`<= 0`,相当于触发`else`代码块,执行标号跳转到`第三段`。否则触发`if`代码块,向下执行`第二段`
> - 所以`cmp`指令之后的跳转,进入的是`else`代码分支
> `func`函数,`第二段`。如果`参数1 - 参数2 > 0`执行此段代码
>
0000000100005f28 ldr w8, [sp, #0xc] 0000000100005f2c adrp x9, #0x10000d000 0000000100005f30 add x9, x9, #0x490 ; _g 0000000100005f34 str w8, x9 0000000100005f38 b loc_100005f4c
> - 此段代码,相当于`if`中的代码块
> - `w8`为`参数1`,`x9`为全局变量`g`,将`w8`写入`x9`
> - `b`指令,执行标号跳转到`第四段`
> `func`函数,`第三段`。如果`参数1 - 参数2 <= 0`执行此段代码
>
loc_100005f3c:
0000000100005f3c ldr w8, [sp, #0x8] ; CODE XREF=_func+24 0000000100005f40 adrp x9, #0x10000d000 0000000100005f44 add x9, x9, #0x490 ; _g 0000000100005f48 str w8, x9
> - 此段代码,相当于`else`中的代码块
> - `w8`为`参数2`,`x9`为全局变量`g`,将`w8`写入`x9`
> - 继续向下执行`第四段`
> `func`函数,`第四段`
>
loc_100005f4c:
0000000100005f4c add sp, sp, #0x10 ; CODE XREF=_func+44 0000000100005f50 ret ; endp
> - 恢复栈平衡,返回
#####循环
> 不同的循环方式:
> - `do...while`
> - `while`
> - `for`
> 案例1:
> 编译器对`do...while`循环的识别
> 打开`ViewController.m`文件,写入以下代码:
>
import “ViewController.h”
void funcA(){
int sum = 0; int i = 0;
do { sum += 1; i++; } while (i < 100); }
@implementation ViewController
- (void)viewDidLoad { // [super viewDidLoad]; funcA(); }
@end
> 使用真机编译项目,将`Mach-O`文件拖到`Hopper`中,找到`funcA`函数<br />

> - `funcA`函数在`Hopper`中被分为三段
> `funcA`函数,`第一段`
>
_funcA:
0000000100005f24 sub sp, sp, #0x10 ; CODE XREF=-[ViewController viewDidLoad]+20 0000000100005f28 str wzr, [sp, #0xc] 0000000100005f2c str wzr, [sp, #0x8]
> - 开辟栈空间,现场保护。将`wzr`寄存器的值,分别在`sp + 0xc`和`sp + 0x8`地址入栈
> - 继续向下执行`第二段`
> `funcA`函数,`第二段`
>
loc_100005f30:
0000000100005f30 ldr w8, [sp, #0xc] ; CODE XREF=_funcA+44 0000000100005f34 add w8, w8, #0x1 0000000100005f38 str w8, [sp, #0xc] 0000000100005f3c ldr w8, [sp, #0x8] 0000000100005f40 add w8, w8, #0x1 0000000100005f44 str w8, [sp, #0x8] 0000000100005f48 ldr w8, [sp, #0x8] 0000000100005f4c cmp w8, #0x64 0000000100005f50 b.lt loc_100005f30
> - 因为使用`do...while`循环,所以`cmp`和`b.lt`指令在最下面。这意味着此代码块,至少也会执行一次
> - 读取`sp + 0xc`地址的值,写入`w8`,进行`w8 += 1`操作,将结果再次入栈
> - 读取`sp + 0x8`地址的值,写入`w8`,进行`w8 += 1`操作,将结果再次入栈
> - 再次将`sp + 0x8`地址的值,写入`w8`
> - `cmp`指令,将`w8`的值和`#0x64`,即`10进制`的`100`进行比较
> - `b.lt`指令,如果`w8 - 100 < 0`,执行标号跳转到`第二段`,相当于递归执行。否则向下执行`第三段`,相当于跳出循环
> `funcA`函数,`第三段`
>
0000000100005f54 add sp, sp, #0x10 0000000100005f58 ret ; endp
> - 恢复栈平衡,返回
> 案例2:
> 编译器对`while`循环的识别
> 打开`ViewController.m`文件,写入以下代码:
>
import “ViewController.h”
void funcA(){
int sum = 0; int i = 0;
while (i < 100) { sum += 1; i++; } }
@implementation ViewController
- (void)viewDidLoad { // [super viewDidLoad]; funcA(); }
@end
> 使用真机编译项目,将`Mach-O`文件拖到`Hopper`中,找到`funcA`函数<br />

> - `funcA`函数在`Hopper`中被分为`四段`
> `funcA`函数,`第一段`
>
_funcA:
0000000100005f20 sub sp, sp, #0x10 ; CODE XREF=-[ViewController viewDidLoad]+20 0000000100005f24 str wzr, [sp, #0xc] 0000000100005f28 str wzr, [sp, #0x8]
> - 开辟栈空间,现场保护。将`wzr`寄存器的值,分别在`sp + 0xc`和`sp + 0x8`地址入栈
> - 继续向下执行`第二段`
> `funcA`函数,`第二段`
>
loc_100005f2c:
0000000100005f2c ldr w8, [sp, #0x8] ; CODE XREF=_funcA+48 0000000100005f30 cmp w8, #0x64 0000000100005f34 b.ge loc_100005f54
> - 读取`sp + 0x8`地址的值,写入`w8`
> - `cmp`指令,将`w8`的值和`#0x64`,即`10进制`的`100`进行比较
> - `b.lt`指令,如果`w8 - 100 >= 0`,执行标号跳转到`第四段`,相当于跳出循环。否则向下执行`第三段`,执行循环内部的代码块
> `funcA`函数,`第三段`
>
0000000100005f38 ldr w8, [sp, #0xc] 0000000100005f3c add w8, w8, #0x1 0000000100005f40 str w8, [sp, #0xc] 0000000100005f44 ldr w8, [sp, #0x8] 0000000100005f48 add w8, w8, #0x1 0000000100005f4c str w8, [sp, #0x8] 0000000100005f50 b loc_100005f2c
> - 读取`sp + 0xc`地址的值,写入`w8`,进行`w8 += 1`操作,将结果再次入栈
> - 读取`sp + 0x8`地址的值,写入`w8`,进行`w8 += 1`操作,将结果再次入栈
> - `b`指令,执行标号跳转到`第二段`,继续验证进入循环的条件限制,决定之后的代码流程
> `funcA`函数,`第四段`
>
loc_100005f54:
0000000100005f54 add sp, sp, #0x10 ; CODE XREF=_funcA+20 0000000100005f58 ret ; endp
> - 恢复栈平衡,返回
> 案例3:
> 编译器对`for`循环的识别
> 打开`ViewController.m`文件,写入以下代码:
>
import “ViewController.h”
void funcA(){
int sum = 0;
for (int i = 0; i < 100; i++) { sum += 1; } }
@implementation ViewController
- (void)viewDidLoad { // [super viewDidLoad]; funcA(); }
@end
> 使用真机编译项目,将`Mach-O`文件拖到`Hopper`中,找到`funcA`函数<br />

> - `funcA`函数在`Hopper`中被分为四段
> - 代码流程和`while`循环完全一致
#####选择
> `switch`语句:当满足条件时,执行某些操作。可以使用`if-else`来实现,也可以使用`switch`来实现
> - 假设`switch`语句的分支比较少的时候,例如:少于`4个`的时候,没有必要使用此结构,此时`switch`的执行方式相当于`if-else`
> - 在分支比较多的时候,编译时会生成一张数据表。表中每个偏移值占`四字节`,通过查表找到指定代码的标号地址,无需遍历。以空间换取时间,提高效率
> - 各个分支常量的差值较大的时候,编译器会在效率和内存中进行取舍,这个时候编译器有可能会编译成类似于`if-else`的结构
> 案例1:
> 当`switch`语句的分支比较少的时候,编译器会如何处理?
> 打开`ViewController.m`文件,写入以下代码:
>
import “ViewController.h”
void funcA(int a){ switch (a) { case 1: printf(“打坐”); break; case 2: printf(“加红”); break; case 3: printf(“加蓝”); break; default: printf(“待命”); break; } }
@implementation ViewController
- (void)viewDidLoad { // [super viewDidLoad]; funcA(1); }
@end
> 使用真机编译项目,将`Mach-O`文件拖到`Hopper`中,找到`funcA`函数<br />

> - `funcA`函数在`Hopper`中被分为`十一段`
> `funcA`函数,`第一段`
>
_funcA:
0000000100005e9c sub sp, sp, #0x20 ; CODE XREF=-[ViewController viewDidLoad]+24 0000000100005ea0 stp x29, x30, [sp, #0x10] 0000000100005ea4 add x29, sp, #0x10 0000000100005ea8 stur w0, [x29, #-0x4] 0000000100005eac ldur w8, [x29, #-0x4] 0000000100005eb0 cmp w8, #0x1 0000000100005eb4 str w8, [sp, #0x8] 0000000100005eb8 b.eq loc_100005ee0
> - 开辟栈空间,现场保护
> - 将`w0`寄存器的值,在`x29 - 0x4`地址入栈
> - 读取`x29 - 0x4`地址的值,写入`w8`
> - `cmp`指令,将`w8`的值和`#0x1`,即`10进制`的`1`进行比较
> - 将`w8`寄存器的值,在`sp + 0x8`地址入栈
> - `b.eq`指令,如果`w8 - 1 == 0`,执行标号跳转到`第七段`,相当于进入`case 1`的代码块。否则向下执行`第二段`
> `funcA`函数,`第二段`
>
0000000100005ebc b loc_100005ec0
> - `b`指令,执行标号跳转到`第三段`
> `funcA`函数,`第三段`
>
loc_100005ec0:
0000000100005ec0 ldr w8, [sp, #0x8] ; CODE XREF=_funcA+32 0000000100005ec4 cmp w8, #0x2 0000000100005ec8 b.eq loc_100005ef0
> - 读取`sp + 0x8`地址的值,写入`w8`
> - `cmp`指令,将`w8`的值和`#0x2`,即`10进制`的`2`进行比较
> - `b.eq`指令,如果`w8 - 2 == 0`,执行标号跳转到`第八段`,相当于进入`case 2`的代码块。否则向下执行`第四段`
> `funcA`函数,`第四段`
>
0000000100005ecc b loc_100005ed0
> - `b`指令,执行标号跳转到`第五段`
> `funcA`函数,`第五段`
>
loc_100005ed0:
0000000100005ed0 ldr w8, [sp, #0x8] ; CODE >XREF=_funcA+48 0000000100005ed4 cmp w8, #0x3 0000000100005ed8 b.eq loc_100005f00
> - 读取`sp + 0x8`地址的值,写入`w8`
> - `cmp`指令,将`w8`的值和`#0x3`,即`10进制`的`3`进行比较
> - `b.eq`指令,如果`w8 - 3 == 0`,执行标号跳转到`第九段`,相当于进入`case 3`的代码块。否则向下执行`第六段`
> `funcA`函数,`第六段`
>
0000000100005edc b loc_100005f10
> - `b`指令,执行标号跳转到`第十段`,相当于进入`default`的代码块
> `funcA`函数,`第七段`。即:`case 1`的代码块
>
loc_100005ee0:
0000000100005ee0 adrp x0, #0x100006000 ; argument #1 for method imp_stubsprintf, CODE XREF=_funcA+28 0000000100005ee4 add x0, x0, #0x634 ; “\xE6\x89\x93\xE5\x9D\x90” 0000000100005ee8 bl imp_stubsprintf 0000000100005eec b loc_100005f1c
> - 在常量区拿到字符串`打坐`,写入`x0`
> - 调用`printf`函数
> - `b`指令,执行标号跳转到`第十一段`。即:`funcA`函数的结尾处
> `funcA`函数,`第八段`。即:`case 2`的代码块
>
loc_100005ef0:
0000000100005ef0 adrp x0, #0x100006000 ; argument #1 for method imp_stubsprintf, CODE XREF=_funcA+44 0000000100005ef4 add x0, x0, #0x63b ; “\xE5\x8A\xA0\xE7\xBA\xA2” 0000000100005ef8 bl imp_stubsprintf 0000000100005efc b loc_100005f1c
> - 在常量区拿到字符串`加红`,写入`x0`
> - 调用`printf`函数
> - `b`指令,执行标号跳转到`第十一段`。即:`funcA`函数的结尾处
> `funcA`函数,`第九段`。即:`case 3`的代码块
>
loc_100005f00:
0000000100005f00 adrp x0, #0x100006000 ; argument #1 for method imp_stubsprintf, CODE XREF=_funcA+60 0000000100005f04 add x0, x0, #0x642 ; “\xE5\x8A\xA0\xE8\x93\x9D” 0000000100005f08 bl imp_stubsprintf 0000000100005f0c b loc_100005f1c
> - 在常量区拿到字符串`加蓝`,写入`x0`
> - 调用`printf`函数
> - `b`指令,执行标号跳转到`第十一段`。即:`funcA`函数的结尾处
> `funcA`函数,`第十段`。即:`default`的代码块
>
loc_100005f10:
0000000100005f10 adrp x0, #0x100006000 ; argument #1 for method imp_stubsprintf, CODE XREF=_funcA+64 0000000100005f14 add x0, x0, #0x649 ; “\xE5\xBE\x85\xE5\x91\xBD” 0000000100005f18 bl imp_stubsprintf
> - 在常量区拿到字符串`待命`,写入`x0`
> - 调用`printf`函数
> - 继续向下执行`第十一段`
> `funcA`函数,`第十一段`
>
loc_100005f1c:
0000000100005f1c ldp x29, x30, [sp, #0x10] ; CODE XREF=_funcA+80, _funcA+96, _funcA+112 0000000100005f20 add sp, sp, #0x20 0000000100005f24 ret ; endp
> - 还原`x29`、`x30`的值
> - 恢复栈平衡
> - 返回
> 上述案例中,每个`case`都会检查一次,当所有`case`都不满足时,跳转`default`。从代码流程上,感觉和使用`if-else`实现没有区别
> `funcA`函数,使用`if-else`实现:
>
void funcA(int a){ if(a == 1){ printf(“打坐”); } else if (a == 2){ printf(“加红”); } else if (a == 3){ printf(“加蓝”); } else{ printf(“待命”); } }
> 使用真机编译项目,将`Mach-O`文件拖到`Hopper`中,找到`funcA`函数<br />

> - 从流程上看,比`switch`语句少了一些b指令的中间跳转,但也是每个条件分支都会检查一次,当所有`if`都不满足时,跳转`else`
> 结论:当`switch`语句的分支比较少的时候,例如:少于`4个`的时候,没有必要使用此结构,此时`switch`的执行方式相当于`if-else`
> 案例2:
> 在分支比较多的时候,如果`case`的值是连续的,或者间隔较小,编译时会生成一张数据表
> 打开`ViewController.m`文件,写入以下代码:
>
import “ViewController.h”
void funcA(int a){ switch (a) { case 1: printf(“打坐”); break; case 2: printf(“加红”); break; case 3: printf(“加蓝”); break; case 4: printf(“打怪”); break; default: printf(“待命”); break; } }
@implementation ViewController
- (void)viewDidLoad { // [super viewDidLoad]; funcA(4); }
@end
> 真机运行项目,来到`func`方法<br />

> - 将`funcA`函数的汇编代码分为`八段`
> `funcA`函数,`第一段`
>
0x102269ec4 <+0>: sub sp, sp, #0x20 ; =0x20 0x102269ec8 <+4>: stp x29, x30, [sp, #0x10] 0x102269ecc <+8>: add x29, sp, #0x10 ; =0x10 0x102269ed0 <+12>: stur w0, [x29, #-0x4] 0x102269ed4 <+16>: ldur w8, [x29, #-0x4] 0x102269ed8 <+20>: subs w8, w8, #0x1 ; =0x1 0x102269edc <+24>: mov x9, x8 0x102269ee0 <+28>: ubfx x9, x9, #0, #32 0x102269ee4 <+32>: cmp x9, #0x3 ; =0x3 0x102269ee8 <+36>: str x9, [sp] 0x102269eec <+40>: b.hi 0x102269f48 ; <+132> at ViewController.m
> - 开辟栈空间,现场保护
> - 将`w0`在`x29 - 0x4`地址入栈,然后再寻址写入`w8`
> - `subs w8, w8, #0x1`:`w8`为当前变量的值,减去`#0x1`,写入`w8`寄存器<br />
`w8`:`viewDidLoad`方法中,调用`funcA`函数,传入的参数为`4`<br />
`#0x1`:最小`case`的值,转为`10进制`为`1`<br />
`w8 -= 1`,即:`4 - 1` ,得到`参数区间值`为`3`<br />
使用`subs`指令,影响目标寄存器,同时影响状态寄存器
> - 将`x8`写入`x9`
> - `ubfx x9, x9, #0, #32`:保留`x9`寄存器的`低32位`,剩余高位用`0`填充<br />
`ubfx`指令:从`x9`寄存器的`第0位`开始,提取`32位`到`x9`寄存器,剩余高位用`0`填充<br />
例如:`x9`为`0x0123456789abcdef`,此指令执行后,`x9`为`0x0000000089abcdef`
> - `cmp x9, #0x3`:将`x9`的值和`#0x3`进行比较<br />
`x9`:`参数区间值`<br />
`#0x3`:最大`case`值减去最小`case`值,得到`case区间值`为`3`
> - 将`x9`在`sp`所在位置入栈
> - `b.hi 0x104105f48`:如果`参数区间值 - case区间值 > 0`,执行标号跳转到`第七段`,相当于进入`default`的代码块。否则向下执行`第二段`
> `funcA`函数,`第二段`。通过查表,获取跳转到指定`case`或`default`的代码标号
>
0x102269ef0 <+44>: adrp x8, 0 0x102269ef4 <+48>: add x8, x8, #0xf60 ; =0xf60 0x102269ef8 <+52>: ldr x11, [sp] 0x102269efc <+56>: ldrsw x10, [x8, x11, lsl #2] 0x102269f00 <+60>: add x9, x8, x10 0x102269f04 <+64>: br x9
> - `adrp x8, 0` ~ `add x8, x8, #0xf60`:将`0x102269f60`地址,写入`x8`<br />
`0x102269f60`:数据表的首地址,存储在`funcA`函数地址之后<br />
`funcA`函数最后指令地址:`0x102269f5c`,向后偏移`4字节`:`0x102269f60`<br />

> - 读取`sp`地址的值,写入`x11`
> - `ldrsw x10, [x8, x11, lsl #2]`:<br />
`ldrsw`指令:通过`x8 + x11左移2位`计算地址,读取该地址的值,赋值给`x10`<br />
`x8`:值为`0x102269f60`。数据表首地址<br />
`x11`:值为`3`。`参数 - 最小case值`,得到的`参数区间值`<br />
`x11`左移`2位`:`3 << 2 = 12`,即:`16进制`为`0xc`<br />
数据表首地址加上偏移地址:`0x102269f60 + 0xc = 0x102269f6c`<br />
读取`0x102269f6c`地址的值`0xffffffffffffffd8`,写入`x10`<br />
<br />

> - `add x9, x8, x10`:将`x8`和`x10`相加,写入`x9`<br />
`x8`:值为`0x102269f60`。数据表首地址<br />
`x10`:值为`0xffffffffffffffd8`,即:`-40`,`16进制`为`0x-28`。从数据表中获取距代码标号的偏移值<br />
`0x102269f60 + 0x-28 = 0x102269f38`,写入`x9`
> - `br x9`:执行`x9`寄存器所存储的标号<br />
`x9`:值为`0x102269f38`。执行标号跳转到`第六段`,相当于进入`case 4`的代码块
> `funcA`函数,`第三段`。即:`case 1`的代码块
>
0x102269f08 <+68>: adrp x0, 1 0x102269f0c <+72>: add x0, x0, #0x634 ; =0x634 0x102269f10 <+76>: bl 0x10226a598 ; symbol stub for: printf 0x102269f14 <+80>: b 0x102269f54 ; <+144> at ViewController.m:28:1
> - 在常量区拿到字符串`打坐`,写入`x0`
> - 调用`printf`函数
> - `b`指令,执行标号跳转到`第八段`。即:`funcA`函数的结尾处
> `funcA`函数,`第四段`。即:`case 2`的代码块
>
0x102269f18 <+84>: adrp x0, 1 0x102269f1c <+88>: add x0, x0, #0x63b ; =0x63b 0x102269f20 <+92>: bl 0x10226a598 ; symbol stub for: printf 0x102269f24 <+96>: b 0x102269f54 ; <+144> at ViewController.m:28:1
> - 在常量区拿到字符串`加红`,写入`x0`
> - 调用`printf`函数
> - `b`指令,执行标号跳转到`第八段`。即:`funcA`函数的结尾处
> `funcA`函数,`第五段`。即:`case 3`的代码块
>
0x102269f28 <+100>: adrp x0, 1 0x102269f2c <+104>: add x0, x0, #0x642 ; =0x642 0x102269f30 <+108>: bl 0x10226a598 ; symbol stub for: printf 0x102269f34 <+112>: b 0x102269f54 ; <+144> at ViewController.m:28:1
> - 在常量区拿到字符串`加蓝`,写入`x0`
> - 调用`printf`函数
> - `b`指令,执行标号跳转到`第八段`。即:`funcA`函数的结尾处
> `funcA`函数,`第六段`。即:`case 4`的代码块
>
0x102269f38 <+116>: adrp x0, 1 0x102269f3c <+120>: add x0, x0, #0x649 ; =0x649 0x102269f40 <+124>: bl 0x10226a598 ; symbol stub for: printf 0x102269f44 <+128>: b 0x102269f54 ; <+144> at ViewController.m:28:1
> - 在常量区拿到字符串`打怪`,写入`x0`
> - 调用`printf`函数
> - `b`指令,执行标号跳转到`第八段`。即:`funcA`函数的结尾处
> `funcA`函数,`第七段`。即:`default`的代码块
>
0x102269f48 <+132>: adrp x0, 1 0x102269f4c <+136>: add x0, x0, #0x650 ; =0x650 0x102269f50 <+140>: bl 0x10226a598 ; symbol stub for: printf
> - 在常量区拿到字符串`待命`,写入`x0`
> - 调用`printf`函数
> - 继续向下执行`第八段`
> `funcA`函数,`第八段`
>
0x102269f54 <+144>: ldp x29, x30, [sp, #0x10] 0x102269f58 <+148>: add sp, sp, #0x20 ; =0x20 0x102269f5c <+152>: ret
> - 还原`x29`、`x30`的值
> - 恢复栈平衡
> - 返回
> 上述案例中,`switch`语句生成的汇编代码逻辑
> 【第一步】:判断参数值是否在`case`值范围之内
> - 通过`参数值 - 最小case值`,得到`参数区间值`
> - 通过`最大case值 - 最小case值`,得到`case区间值`
> - 将`参数区间值`和`case区间值`进行比较
> - 如果`参数区间值 > case区间值`,表示超出`case`范围,直接跳转到`default`代码标号。否则进入`【第二步】`
> 【第二步】:通过查表,获取跳转到指定`case`或`default`的代码标号
> - 获取`数据表首地址`
> - 计算`数据表首地址`加上`参数区间值`偏移`x`位后的地址
> - 通过计算后地址,从数据表中获取距代码标号的偏移值
> - 通过`数据表首地址 + 16进制(偏移值)`,计算出指定`case`或`default`的代码标号
> - 虽然参数在`case`值范围之内,也有可能进入`default`分支,因为`case`值不一定是连续的
> - 表中存储的数据量,是`case区间值 + default`的总数。所以`case`的值间隔越大,需要存储的数据量就会越多
> - 表中存储的数据,以表头地址为参照,计算出跳转到各分支代码的偏移值。它们在编译时已经被编译器计算好并存储在表中,因为`switch`的条件分支代码是连续的,因此可以这样计算
> - 数据表中每`四字节`存储一个偏移值,所以在`ldrsw`指令中有`左移2位`的计算
> - 由于`ASLR`技术(`Address Spce Layout Randomization`),翻译过来就是地址空间布局随机化。数据表中不直接存储地址,而是存储偏移值
> 结论:在`switch`语句分支比较多的时候,如果`case`的值是连续的,或间隔较小,编译器会建立一张数据表。通过查表直接获取跳转到指定代码的标号地址,无需遍历每一个`case`分支,以空间换取时间
> 案例3:
> 当`case`常量的差值较大时,编译器会如何处理?
> 打开`ViewController.m`文件,写入以下代码:
>
void funcA(int a){ switch (a) { case 1: printf(“打坐”); break; case 20: printf(“加红”); break; case 40: printf(“加蓝”); break; case 80: printf(“打怪”); break; default: printf(“待命”); break; } } ```
使用真机编译项目,将
Mach-O
文件拖到Hopper
中,找到funcA
函数
- 如果
case
值的间隔较大,编译器会权衡是否还要以空间换取时间。上述案例中,如果建立数据表需要存储大量数据,耗费大量空间,此时编译器会选择使用if-else
的执行方式结论:各个分支常量的差值较大的时候,编译器会在效率和内存中进行取舍,这个时候编译器有可能会编译成类似于
if-else
的结构
总结
判断(
if
)
cmp
:比较指令,其实在做减法。不影响目标寄存器,只影响状态寄存器循环
do…while
循环:判断条件在后面,满足循环条件往外跳while
循环:判断条件在前面,满足循环条件往里跳for
循环:和while
循环逻辑一致选择
- 分支少于
4个
,switch
底层代码和if-else
一样- 在分支比较多的时候,编译时会生成一张数据表,通过查表进行代码调转,无需遍历。以空间换取时间,提高效率
- 各个分支常量差值较大,底层代码可能使用数据表,也可能使用
if-else
。因为编译器会在效率和内存中进行取舍