让你理解ARM64汇编语言

咱们本篇文章讲的语法不多,因为语法已经有很多文章可以参考学习,本篇主要讲的是怎么去理解汇编。

首先了解计算机结构

  • 总的来说计算机分为CPU、内存、硬盘、外设。因为咱们是前端开发,可以忽略外设,所以结构就如下图。手机的运行内存比较常见的是4G、8G,但是相对动辄就128G、256G的硬盘来说,还是很小的。为啥内存要那么小呢,和硬盘有啥区别呢

    1. CPU的处理速度比硬盘的读取速度快很多,比方说CPU一秒可以处理1000个数据,但是硬盘1秒只能读出10个数据,造成CPU的性能发挥不出来
    2. 这时候内存就出来了,因为内存的读取速度比硬盘快很多,所以可以先把硬盘的部分数据加载到内存,让CPU直接从内存读数据处理,这样性能就会好很多
    3. 因为内存的制造成本高,从手机分级就能看出来,比如低配4G内存 + 128G硬盘,但是高配就可以到8G内存 + 256G硬盘,差几百,硬盘可以升级那么多,内存就升级一点。正因为价格高,所以内存一般不大
CPU、内存、硬盘
  • CPU的介绍,CPU由3部分组成,分别是运算器,控制器,寄存器。
    1. 运算器,顾名思义,就是处理数据的,比如运算1+1
    2. 控制器,就是控制着把数据从内存加载到CPU,并且解析这个数据是干啥的,比如是做加法的话就送到运算器进行处理
    3. 寄存器,重点来了咱们上面说了内存的存在是为了缩小和CPU处理速度的,但是很不幸的是,虽然内存的速度虽然比硬盘快,但是还是跟不上CPU的速度,所以,在CPU里也有存储数据的地方,他叫寄存器,作用呢和内存一样就是存储数据的,但是他的读取速度比内存更快,当然成本也更高。让CPU里的运算器直接从寄存器加载数据可以最大限度的发挥CPU的作用
      cpu1.png

汇编要来了

  • 汇编为啥是底层语言

    1. 咱们知道,一台计算机或者手机可以工作,最核心的东西就是CPU

    2. 咱们平时的编程语言直接操作CPU了吗?答案是并没有。比如说用Swift、Java或者C语言定义了一个变量a = 10,b = 20,并且计算a+b,或者创建了一个对象,或者获取一个变量的地址,咱们脑海里想的都是在内存上开辟了一块空间,放了个变量a,或者在内存上创建了一个dog对象,或者获取内存区域某个变量的地址。我把平时的高级编程语言抽象为“面向内存编程”,因为咱们脑海里都想的是在内存上怎么着怎么着,如下图


      CPU、内存、硬盘.png
    3. 咱们上面说了,为了不让内存拖CPU的后腿,CPU里自带内存,也就是寄存器,汇编就是可以直接操作CPU里的寄存器和内存的语言,所以汇编是面向底层的语言。ARM64 有很多个寄存器,包括X0~X28、LR、SP、PC、CPSR,咱们列举几个,以下图六个寄存器X1、X2、X3、SP、PC、LR为例讲解汇编


      寄存器内存2.png
  • 汇编开始啦

    • 寄存器本身就是用于存储数据的,但是寄存器是在CPU内部

    • 寄存器和寄存器之间传数据

      1. 如果我想把寄存器X2的赋值给X1怎么操作呢,汇编的写法就是:MOV X1,X2
      2. 如果我想把X2寄存器里的值和X3里的加起来放到X1里怎么做呢,汇编的写法就是 ADD X1,X2,X3
      3. 如果我想用X3寄存器里的值减掉X2里的值放到X1里怎么做呢,汇编的写法就是 SUB X1,X3,X2
    • 寄存器和内存之间传数据

      1. 以上图为例,比如我想把地址是FFF0的内存单元的10取出来存放到寄存器X1怎么做呢,汇编的写法就是 LDR X1,[FFF0],但是平时见到的没有直接在中括号里写地址的,一般都是先把要取得内存的地址放到另一个寄存器,比如 MOV X2, FFF0,然后再
        LDR X1,[X2]。也就是先把地址放到一个寄存器里,再根据寄存器寻址

      2. 以上图为例,比如我想把寄存器X3的值写入到内存是FFF1的地址怎么写呢,先把地址放到另一个寄存器中 MOV X2, FFF0
        然后再用存储命令STR X3,[X2]

      3. 简单的总结:知道为啥汇编语言更底层了吧,因为这种语言可以直接操作CPU,咱们普通语言是做不到的。语法规则就是除了从CPU往内存里存数据用的STR相关的命令是从左往右读以外,其余的基本是和咱们高级语言一样,从右往左

    • 指令的加载

      1. 假如我定义了一个整型变量int a = 65,那么a在内存里的数据就是0100 0001(十进制就是65),如果我定义了一个字符变量 char a = 'A',那在内存里存的数是啥,因为计算机只能存储二进制0和1,所以需要先把'A'转成对应的ASCII码65,所以实际上,'A'在计算机上存的也是0100 0001(十进制就是65)。所有数据在内存上都是以0和1存储的,内存不知道他是啥类型,就看你把他当成啥类型处理,如下面。

           NSInteger a = 65;
           NSLog(@"%ld",a);
           NSLog(@"%c",a);
            -----------------打印结果如下-----------------------            
           OCTest[35482:8821755] 65    
           OCTest[35482:8821755] A
        
      2. 比如咱们开发了一个90M的软件,在运行的时候,CPU就开始处理,但是CPU需要从哪里开始执行呢,咱们90M的包里包含代码,也包含一些全局变量的数据等,代码是可执行的,数据是用来参与运算的。假如说 MOV X1,X0 对应的二进制是 0110 0100 , 恰巧可执行文件的第一行代码就是0110 0100, CPU怎么知道这行代码是命令还是数据,如果当成命令,CPU做的就是把寄存器X0的值赋值给X1,如果当成数据,那就代表十进制的100。其实咱们写的代码在编译链接后生成可执行文件时,就已经把这90M的包分好了,哪块是代码段,哪块是数据段。这样CPU处理的时候就不会混乱了,下图是用xcode编译生成的可执行文件,可以看出分的很详细,有代码段有数据段


        可执行文件2.png
        1. PC寄存器要出现了,PC寄存器俗称PC指针,CPU运行哪条指令取决于PC寄存器存的是哪条指令的地址,也就是说,PC寄存器指向哪,程序就运行哪。程序刚开始运行的时候PC寄存器会存储代码段的第一行所在内存的地址,后续每执行一条指令,PC寄存器会默认加4指向下一条指令(加4是因为ARM64汇编的每条指令占4个字节)。比如下面的程序,刚开始运行的时候,PC寄存器存储着地址是0x10004e1e0,当开始执行第一条指令时,会默认指向下一条指令的地址0x10004e1e4
          0x10004e1e0:  sub    sp, sp, #0x20             ; =0x20 
          0x10004e1e4:  stp    x29, x30, [sp, #0x10]
          0x10004e1e8:  add    x29, sp, #0x10            ; =0x10 
          0x10004e1ec:  adrp   x8, 3
          0x10004e1f0:  add    x8, x8, #0x3d0            ;   
          
    • 指令的跳转
      1. 函数的调用:看下面的OC代码,咱们都知道当执行到第二行时,会跳到另一个函数test3WithParamB,也就是第6行,当执行完第8行以后,会回到上面的第三行继续执行,也就是函数跳转。这个在汇编层面是怎么做到的呢。咱们上面不是刚说PC寄存器,执行完一条指令会默认加4。现在怎么还会跳了呢。

       1     - (void)test2WithParamA:(NSInteger)a b:(NSInteger)b c:(NSInteger)c {
       2          [self test3WithParamB:103 b:104 c:105];
       3          NSInteger total = a + b + c;
       4     }
       5
       6      - (void)test3WithParamB:(NSInteger)a b:(NSInteger)b c:            (NSInteger)c {
       7           NSInteger total = a + b + c;
       8      }
      
      1. 跳转指令BL
        咱们之前说,当开始执行一条指令的时候,PC寄存器会默认加4,指向下一条指令的地址。但是这说的是默认,如果遇到函数跳转就不会了,比如咱们当前执行的指令地址是0x10004e10,要跳转的函数地址是0x10004e1e怎么办,可以直接把PC寄存的值改成0x10004e1e就可以了。想法是对的,只是ARM64汇编不允许直接修改PC寄存器,但是可以通过跳转指令BL,比如 BL 0x10004e1e,这样就会把PC寄存器改成0x10004e1e,并且开始执行0x10004e1e处的指令
      2. 函数返回
        上面说了函数调用的跳转指令BL,就是直接拿到被调用函数的地址,然后跳过去。但跳过去,函数执行完了以后,怎么回去呢,还是以上面的代码为例,当执行第2行时,会跳到第6行,执行完第8行后,应该回到第3行,可是计算机怎么知道回到哪呢,如果不做特殊处理,按照之前的说法PC寄存器会默认执行第9行的代码。这时LR寄存器出场了,LR(x30)通常称X30为程序链接寄存器,保存子程序结束后需要执行的下一条指令,其实咱们在执行跳转指令BL时,CPU除了将PC寄存器里的值修改成要跳转的地址以外,还会存储跳转回来以后要执行的指令的地址(以上面的程序为例,就是保存第3行的地址),存到哪呢,就是存到了LR寄存器。当函数结束以后,就把PC寄存器的值,修改为LR寄存器的值,这样就相当于回到调用的地方了
    • 参数问题
      1.函数调用,在汇编层面就是修改PC寄存器的值达到跳转的目的,但是如果调用的函数需要传参,参数放哪呢?答案还是寄存器,只不过用的是普通的寄存器x0 ~ x7: 用于子程序调用时的参数传递,以下面的程序为例,调用test2Wit时,需要传参,参数分别是100、101、102,可以看地址是0x10001e1c0的相邻的三个指令,就是把0x64(十进制的100)、0x65、0x66存到X2、X3、X4。然后就调用 bl 0x10001e5d4跳走了

         - (void)test1 {
             [self test2WithParamA:100 b:101 c:102];
         }
           --------对应的汇编如下-------------
         OCTest`-[AppDelegate test1]:
         0x10001e19c <+0>:  sub    sp, sp, #0x20             ; =0x20 
         0x10001e1a0 <+4>:  stp    x29, x30, [sp, #0x10]
         0x10001e1a4 <+8>:  add    x29, sp, #0x10            ; =0x10 
         0x10001e1a8 <+12>: adrp   x8, 3
         0x10001e1ac <+16>: add    x8, x8, #0x3c0            ; =0x3c0 
         0x10001e1b0 <+20>: str    x0, [sp, #0x8]
         0x10001e1b4 <+24>: str    x1, [sp]
         0x10001e1b8 <+28>: ldr    x0, [sp, #0x8]
         0x10001e1bc <+32>: ldr    x1, [x8]
         0x10001e1c0 <+36>: mov    x2, #0x64
         0x10001e1c4 <+40>: mov    x3, #0x65
         0x10001e1c8 <+44>: mov    x4, #0x66
         0x10001e1cc <+48>: bl     0x10001e5d4               ; symbol stub for: objc_msgSend
         0x10001e1d0 <+52>: ldp    x29, x30, [sp, #0x10]
         0x10001e1d4 <+56>: add    sp, sp, #0x20             ; =0x20 
         0x10001e1d8 <+60>: ret 
      
      1. 那调到test2WithParamA后,怎么取参数的呢,看下图0x1000a2260地址对应的指令,会把X2、X3、X4的值先存到内存里,然后再从对应的内存地址取来来做加法,虽然没直接用X2、X3、X4做加法,但是用的值,是从最初的X2、X3、X4里的值。咱们发现每个函数结束后,都会有个ret指令,比如下面0x1000a2288对应的指令,这个ret的作用就是告诉CPU我这个函数结束了,如果需要返回到调用函数的地方,就把PC寄存器的值修改为LR寄存器里值,这样就可以调回去了
 - (void)test2WithParama:(NSInteger)a b:(NSInteger)b c:(NSInteger)c {
      NSInteger total = a + b + c;
 }
 -----------对应的汇编如下---------------
 OCTest`-[AppDelegate test2WithParama:b:c:]:
 0x1000a2254 <+0>:  sub    sp, sp, #0x30             ; =0x30 
 0x1000a2258 <+4>:  str    x0, [sp, #0x28]
 0x1000a225c <+8>:  str    x1, [sp, #0x20]
 0x1000a2260 <+12>: str    x2, [sp, #0x18]
 0x1000a2264 <+16>: str    x3, [sp, #0x10]
 0x1000a2268 <+20>: str    x4, [sp, #0x8]
 0x1000a226c <+24>: ldr    x0, [sp, #0x18]
 0x1000a2270 <+28>: ldr    x1, [sp, #0x10]
 0x1000a2274 <+32>: add    x0, x0, x1
 0x1000a2278 <+36>: ldr    x1, [sp, #0x8]
 0x1000a227c <+40>: add    x0, x0, x1
 0x1000a2280 <+44>: str    x0, [sp]
 0x1000a2284 <+48>: add    sp, sp, #0x30             ; =0x30 
 0x1000a2288 <+52>: ret    
  • SP寄存器
    在上面的汇编代码里,经常看到类似[sp, #0x18]的写法,这个咱们大概说一下,只要带着中括号[]的一般就是表示内存的某个地址。而SP指向的是内存中的栈顶,内存分为代码段和数据段,数据段里有一个栈的部分,就是先进后出的数据结构的内存。这种先进后出,后进先出的数据结构就是为了方便临时存储数据,比如函数跳转,参数太多的话,就可以先存到栈上,用完就销毁。比如函数跳转,如果连续跳转,比如A函数调到B,还没回到A呢,继续调到B,那LR寄存器就一个怎么办,没法保存好几个返回地址,那就可以先保存到内存的栈里,等用到的时候,再取出来。

总结

汇编语言是一门可以直接操作CPU和内存的语言

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342