三:Switch底层原理和指针基础
cmp(Compare)比较指令
CMP 把一个寄存器的内容和另一个寄存器的内容或立即数进行比较。但不存储结果,只是正确的更改标志。
一般CMP做完判断后会进行跳转,后面通常会跟上B指令!
- BL 标号:跳转到标号处执行
- 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.LS 标号:比较结果是无符号小于等于,执行标号,否则不跳转
- B.LO 标号:比较结果是无符号小于,执行标号,否则不跳转
- B.HI 标号:比较结果是无符号大于,执行标号,否则不跳转
- B.HS 标号:比较结果是无符号大于等于,执行标号,否则不跳转
Switch
情况一:只有三个分支结果,底层是怎么实现的?
void func(int a){
switch (a) {//
case 5:
printf("1");
break;
case 400:
printf("2");
break;
case 800:
printf("3");
break;
}
}
int main(int argc, char * argv[]) {
func(5);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}

情况二:超过三个分支结果,case常量的差值较小的情况,底层是怎么实现的?
void func(int a){
switch (a) {//
case 5:
printf("5");
break;
case 6:
printf("6");
break;
case 7:
printf("7");
break;
case 8:
printf("8");
break;
}
}
int main(int argc, char * argv[]) {
func(5);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
func函数完整汇编代码如下:
0x1047261a4 <+0>: sub sp, sp, #0x50 ; =0x50
0x1047261a8 <+4>: stp x29, x30, [sp, #0x40]
0x1047261ac <+8>: add x29, sp, #0x40 ; =0x40
0x1047261b0 <+12>: stur w0, [x29, #-0x4]
0x1047261b4 <+16>: ldur w0, [x29, #-0x4]
0x1047261b8 <+20>: subs w0, w0, #0x5 ; =0x5
0x1047261bc <+24>: mov x8, x0
0x1047261c0 <+28>: subs w0, w0, #0x3 ; =0x3
0x1047261c4 <+32>: stur x8, [x29, #-0x10]
0x1047261c8 <+36>: stur w0, [x29, #-0x14]
0x1047261cc <+40>: b.hi 0x10472623c ; <+152> at main.m:26:13
-> 0x1047261d0 <+44>: adrp x8, 0
0x1047261d4 <+48>: add x8, x8, #0x258 ; =0x258
0x1047261d8 <+52>: ldur x11, [x29, #-0x10]
0x1047261dc <+56>: ldrsw x10, [x8, x11, lsl #2]
0x1047261e0 <+60>: add x9, x8, x10
0x1047261e4 <+64>: str x10, [sp, #0x20]
0x1047261e8 <+68>: br x9
0x1047261ec <+72>: adrp x0, 1
0x1047261f0 <+76>: add x0, x0, #0xf82 ; =0xf82
0x1047261f4 <+80>: bl 0x1047265f4 ; symbol stub for: printf
0x1047261f8 <+84>: str w0, [sp, #0x1c]
0x1047261fc <+88>: b 0x10472624c ; <+168> at main.m:29:1
0x104726200 <+92>: adrp x0, 1
0x104726204 <+96>: add x0, x0, #0xf84 ; =0xf84
0x104726208 <+100>: bl 0x1047265f4 ; symbol stub for: printf
0x10472620c <+104>: str w0, [sp, #0x18]
0x104726210 <+108>: b 0x10472624c ; <+168> at main.m:29:1
0x104726214 <+112>: adrp x0, 1
0x104726218 <+116>: add x0, x0, #0xf86 ; =0xf86
0x10472621c <+120>: bl 0x1047265f4 ; symbol stub for: printf
0x104726220 <+124>: str w0, [sp, #0x14]
0x104726224 <+128>: b 0x10472624c ; <+168> at main.m:29:1
0x104726228 <+132>: adrp x0, 1
0x10472622c <+136>: add x0, x0, #0xf88 ; =0xf88
0x104726230 <+140>: bl 0x1047265f4 ; symbol stub for: printf
0x104726234 <+144>: str w0, [sp, #0x10]
0x104726238 <+148>: b 0x10472624c ; <+168> at main.m:29:1
0x10472623c <+152>: adrp x0, 1
0x104726240 <+156>: add x0, x0, #0xf8a ; =0xf8a
0x104726244 <+160>: bl 0x1047265f4 ; symbol stub for: printf
0x104726248 <+164>: str w0, [sp, #0xc]
0x10472624c <+168>: ldp x29, x30, [sp, #0x40]
0x104726250 <+172>: add sp, sp, #0x50 ; =0x50
0x104726254 <+176>: ret
分析:

当前传入的常量值5,减去了5
疑问:为什么要减去一个5?
答:分支case的常量值有:5,6,7,8,5是case中最小的常量,传入的常量减去最小的常量可以获取到一个差值,这个差值后续有用,后续底层会根据这个差值,逐步得到对应case的执行语句,下面会提到,请继续往下看

刚刚的常量值5减去5等于0,0再和3进行比较,如果前者是无符号大于,直接进标号0x10472623c进行地址跳转,有default:break实现,就到default:break执行语句,没有就释放函数,否则继续往下单步执行
疑问:为什么和3进行比较,这个3哪里来的呢?
答:这个3是case分支的最大值减去最小值得来的,这个例子中,最大值是8,最小值是5,所以8-5=3,那么为什么要和3进行比较呢?5-5=0,8-5=3,如果0<=3,那么一定存在一个值,在5~8之间,否则,直接跳到default:break,提交效率,一次判断即可,牛!

数据表,数据表一般都放在当前函数栈的底部,如下图


第15行:取出[x29, #-0x10]的值,刚刚x8就把计算的差值存放在这里,所以x11的值就是刚刚x8存放进去的值,就是0x0
第16行:x11的值0x0座左移2位,二进制表现形式就是0x000,还是0x0,然后读取[x8, #0x0]存放的值给x10,相当于[x8]的值给x10
疑问:
- 为什么要左移2位?
答:例如一个差值1,左移两位二进制表现形式就是100,十进制就是4,差值为2,左移两位二进制表现形式就是1000,十进制表现形式就是8,所以,相当于差值乘以4,然后获取到存放的值,数据表存放的都是4个字节有符号的整型int
- 这个存放的值的作用是什么?
答:这个值是一个偏移值,方便根据表头[x8]加上偏移值,直接获取到标号,就是br要跳转的地址,也就是case分支对应的执行语句
- 为什么要放偏移值呢,按你这样说,直接放地址,获取到地址,直接br跳转,不是更快更方便吗?
答:
- 地址占8个字节,从空间来讲,占用内存比较大
- 虚拟地址都是有aslr保护的,aslr是为了安全,在虚拟内存头中的一段随机值,如果保存的是地址,拿出来之后还是要和aslr进行运算,才能获取到br要跳转的地址,所以最方便的还是直接放偏移值,不管地址怎么变, 最终都是通过地址加上这个便宜值,获取到br地址
我们看看x10的值是多少,x8的地址是0x104726258,所以x10的值是:0xffffffffffffff94



第17行:x8+x10,值给x9保存,x8=0x104726258,x10=0xffffffffffffff94,x10是一个有符号整型,转换为整型是-108,相加需要转成十六进制,十六进制表现形式是-6C,所以最终的结果为:0x00000001047261ec
第19行:跳转x9的地址,x9的值是0x00000001047261ec

一系列操作,调用printf函数,保存w0,b跳转0x10472624c

结束!
情况三:超过三个分支结果,case常量的差值又比较大的情况,底层是怎么实现的?
void func(int a){
switch (a) {//
case 5:
printf("5");
break;
case 400:
printf("400");
break;
case 800:
printf("800");
break;
case 4:
printf("4");
break;
}
}
int main(int argc, char * argv[]) {
func(5);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}

总结
1、假设switch语句的分支比较少的时候(例如3个,少于4的时候没有意义)没有必要使用此结构,相当于if。
2、各个分支常量的差值较大的时候,编译器会在效率还是内存进行取舍,这个时候编译器还是会编译成类似于if,else的结构。
3、在分支比较多的时候且差值较小的时候:在编译的时候会生成一个数据表(跳转表每个地址四个字节)。
指针
什么是指针?
指针也就是内存地址,指针变量是用来存放内存地址的变量。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:
type *var-name;
在这里,type 是指针的基类型,它必须是一个有效的 C 数据类型,var-name 是指针变量的名称。用来声明指针的星号*与乘法中使用的星号是相同的。但是,在这个语句中,星号是用来指定一个变量是指针。以下是有效的指针声明:
int *ip; /* 一个整型的指针 */
double *dp; /* 一个 double 型的指针 */
float *fp; /* 一个浮点型的指针 */
char *ch; /* 一个字符型的指针 */
#include <stdio.h>
int main ()
{
int var_runoob = 10;
int *p; // 定义指针变量
p = &var_runoob;
printf("var_runoob 变量的地址: %p\n", p);
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
var_runoob 变量的地址: 0x7ffeeaae08d8

指向指针的指针
指向指针的指针是一种多级间接寻址的形式,或者说是一个指针链。通常,一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。

#include <stdio.h>
int main ()
{
int V;
int *Pt1;
int **Pt2;
V = 100;
/* 获取 V 的地址 */
Pt1 = &V;
/* 使用运算符 & 获取 Pt1 的地址 */
Pt2 = &Pt1;
/* 使用 pptr 获取值 */
printf("var = %d\n", V );
printf("Pt1 = %p\n", Pt1 );
printf("*Pt1 = %d\n", *Pt1 );
printf("Pt2 = %p\n", Pt2 );
printf("**Pt2 = %d\n", **Pt2);
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
var = 100
Pt1 = 0x7ffee2d5e8d8
*Pt1 = 100
Pt2 = 0x7ffee2d5e8d0
**Pt2 = 100
补充知识:
adrp
0x104d32224 <+40>: adrp x8, 1
0x104d1a228 <+44>: add x8, x8, #0x30
0x104d32224:当前代码段地址
adrp:1二进制表现形式往左偏移12位,十六进制形式表现就是0x1000,以当前代码段地址作为参照0x104d32224,低三位清零,即0x104d32000,然后0x104d32000+0x1000=0x104d33000,就拿到该页的头,因为pigeSize都是按0x1000来进行划分的,所以就先找到当前代码段地址的分页头,然后根据add的偏移值,0x104d33000+0x30=0x104d33030就是具体的值
0x1000=4096=4k=mac pageSize
0x4000=4*4k=iOS pageSize
大小端模式
- 名词解释
- 大端模式:是指数据的高字节保存在内存的低地址中,而低子节数据保存在内存的高地址中。
- 小端模式:是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。
- 为什么会有大小端模式?
处理器(例如32位或者64位的cpu)的发展。
ARM芯片(iPhone)默认采用小端。
例子
int num = 0x12123678; // 十进制为305419896
char a = num & 0xff; // 取(0 ~ 7位)一个子节
char b = num >> 8 & 0xff; // 取(8 ~15位)一个子节
char c = num >> 16 & 0xff; // 取(16~23位)一个子节
char d = num >> 24 & 0xff; // 取(24~31位)一个子节
printf("%x, %x, %x, %x", a, b, c, d);
输出结果:
78, 36, 12, 12