三:Switch底层原理和指针基础

三: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]));
}
image-20210402143803282

情况二:超过三个分支结果,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   

分析:

image-20210402144902023

当前传入的常量值5,减去了5

疑问:为什么要减去一个5?

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

image-20210402152251617

刚刚的常量值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,提交效率,一次判断即可,牛!

image-20210402152536349

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

image-20210402153519780
image-20210402153705643

第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跳转,不是更快更方便吗?

答:

  1. 地址占8个字节,从空间来讲,占用内存比较大
  2. 虚拟地址都是有aslr保护的,aslr是为了安全,在虚拟内存头中的一段随机值,如果保存的是地址,拿出来之后还是要和aslr进行运算,才能获取到br要跳转的地址,所以最方便的还是直接放偏移值,不管地址怎么变, 最终都是通过地址加上这个便宜值,获取到br地址

我们看看x10的值是多少,x8的地址是0x104726258,所以x10的值是:0xffffffffffffff94

image-20210402162441052
image-20210402162504353
image-20210402162542060

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

第19行:跳转x9的地址,x9的值是0x00000001047261ec

image-20210402163307037

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

image-20210402163452066

结束!

情况三:超过三个分支结果,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]));
}
image-20210402144411516

总结

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
img

指向指针的指针

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

img
#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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容