咱们本篇文章讲的语法不多,因为语法已经有很多文章可以参考学习,本篇主要讲的是怎么去理解汇编。
首先了解计算机结构
-
总的来说计算机分为CPU、内存、硬盘、外设。因为咱们是前端开发,可以忽略外设,所以结构就如下图。手机的运行内存比较常见的是4G、8G,但是相对动辄就128G、256G的硬盘来说,还是很小的。为啥内存要那么小呢,和硬盘有啥区别呢
- CPU的处理速度比硬盘的读取速度快很多,比方说CPU一秒可以处理1000个数据,但是硬盘1秒只能读出10个数据,造成CPU的性能发挥不出来
- 这时候内存就出来了,因为内存的读取速度比硬盘快很多,所以可以先把硬盘的部分数据加载到内存,让CPU直接从内存读数据处理,这样性能就会好很多
- 因为内存的制造成本高,从手机分级就能看出来,比如低配4G内存 + 128G硬盘,但是高配就可以到8G内存 + 256G硬盘,差几百,硬盘可以升级那么多,内存就升级一点。正因为价格高,所以内存一般不大
- CPU的介绍,CPU由3部分组成,分别是运算器,控制器,寄存器。
- 运算器,顾名思义,就是处理数据的,比如运算1+1
- 控制器,就是控制着把数据从内存加载到CPU,并且解析这个数据是干啥的,比如是做加法的话就送到运算器进行处理
-
寄存器,重点来了咱们上面说了内存的存在是为了缩小和CPU处理速度的,但是很不幸的是,虽然内存的速度虽然比硬盘快,但是还是跟不上CPU的速度,所以,在CPU里也有存储数据的地方,他叫寄存器,作用呢和内存一样就是存储数据的,但是他的读取速度比内存更快,当然成本也更高。让CPU里的运算器直接从寄存器加载数据可以最大限度的发挥CPU的作用
汇编要来了
-
汇编为啥是底层语言
咱们知道,一台计算机或者手机可以工作,最核心的东西就是CPU
-
咱们平时的编程语言直接操作CPU了吗?答案是并没有。比如说用Swift、Java或者C语言定义了一个变量a = 10,b = 20,并且计算a+b,或者创建了一个对象,或者获取一个变量的地址,咱们脑海里想的都是在内存上开辟了一块空间,放了个变量a,或者在内存上创建了一个dog对象,或者获取内存区域某个变量的地址。我把平时的高级编程语言抽象为“面向内存编程”,因为咱们脑海里都想的是在内存上怎么着怎么着,如下图
-
咱们上面说了,为了不让内存拖CPU的后腿,CPU里自带内存,也就是寄存器,汇编就是可以直接操作CPU里的寄存器和内存的语言,所以汇编是面向底层的语言。ARM64 有很多个寄存器,包括X0~X28、LR、SP、PC、CPSR,咱们列举几个,以下图六个寄存器X1、X2、X3、SP、PC、LR为例讲解汇编
-
汇编开始啦
寄存器本身就是用于存储数据的,但是寄存器是在CPU内部
-
寄存器和寄存器之间传数据
- 如果我想把寄存器X2的赋值给X1怎么操作呢,汇编的写法就是:MOV X1,X2
- 如果我想把X2寄存器里的值和X3里的加起来放到X1里怎么做呢,汇编的写法就是 ADD X1,X2,X3
- 如果我想用X3寄存器里的值减掉X2里的值放到X1里怎么做呢,汇编的写法就是 SUB X1,X3,X2
-
寄存器和内存之间传数据
以上图为例,比如我想把地址是FFF0的内存单元的10取出来存放到寄存器X1怎么做呢,汇编的写法就是 LDR X1,[FFF0],但是平时见到的没有直接在中括号里写地址的,一般都是先把要取得内存的地址放到另一个寄存器,比如 MOV X2, FFF0,然后再
LDR X1,[X2]。也就是先把地址放到一个寄存器里,再根据寄存器寻址以上图为例,比如我想把寄存器X3的值写入到内存是FFF1的地址怎么写呢,先把地址放到另一个寄存器中 MOV X2, FFF0
然后再用存储命令STR X3,[X2]。简单的总结:知道为啥汇编语言更底层了吧,因为这种语言可以直接操作CPU,咱们普通语言是做不到的。语法规则就是除了从CPU往内存里存数据用的STR相关的命令是从左往右读以外,其余的基本是和咱们高级语言一样,从右往左
-
指令的加载
-
假如我定义了一个整型变量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
-
比如咱们开发了一个90M的软件,在运行的时候,CPU就开始处理,但是CPU需要从哪里开始执行呢,咱们90M的包里包含代码,也包含一些全局变量的数据等,代码是可执行的,数据是用来参与运算的。假如说 MOV X1,X0 对应的二进制是 0110 0100 , 恰巧可执行文件的第一行代码就是0110 0100, CPU怎么知道这行代码是命令还是数据,如果当成命令,CPU做的就是把寄存器X0的值赋值给X1,如果当成数据,那就代表十进制的100。其实咱们写的代码在编译链接后生成可执行文件时,就已经把这90M的包分好了,哪块是代码段,哪块是数据段。这样CPU处理的时候就不会混乱了,下图是用xcode编译生成的可执行文件,可以看出分的很详细,有代码段有数据段
- 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 ;
- PC寄存器要出现了,PC寄存器俗称PC指针,CPU运行哪条指令取决于PC寄存器存的是哪条指令的地址,也就是说,PC寄存器指向哪,程序就运行哪。程序刚开始运行的时候PC寄存器会存储代码段的第一行所在内存的地址,后续每执行一条指令,PC寄存器会默认加4指向下一条指令(加4是因为ARM64汇编的每条指令占4个字节)。比如下面的程序,刚开始运行的时候,PC寄存器存储着地址是0x10004e1e0,当开始执行第一条指令时,会默认指向下一条指令的地址0x10004e1e4
-
-
指令的跳转
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 }
- 跳转指令BL
咱们之前说,当开始执行一条指令的时候,PC寄存器会默认加4,指向下一条指令的地址。但是这说的是默认,如果遇到函数跳转就不会了,比如咱们当前执行的指令地址是0x10004e10,要跳转的函数地址是0x10004e1e怎么办,可以直接把PC寄存的值改成0x10004e1e就可以了。想法是对的,只是ARM64汇编不允许直接修改PC寄存器,但是可以通过跳转指令BL,比如 BL 0x10004e1e,这样就会把PC寄存器改成0x10004e1e,并且开始执行0x10004e1e处的指令 - 函数返回
上面说了函数调用的跳转指令BL,就是直接拿到被调用函数的地址,然后跳过去。但跳过去,函数执行完了以后,怎么回去呢,还是以上面的代码为例,当执行第2行时,会跳到第6行,执行完第8行后,应该回到第3行,可是计算机怎么知道回到哪呢,如果不做特殊处理,按照之前的说法PC寄存器会默认执行第9行的代码。这时LR寄存器出场了,LR(x30)通常称X30为程序链接寄存器,保存子程序结束后需要执行的下一条指令,其实咱们在执行跳转指令BL时,CPU除了将PC寄存器里的值修改成要跳转的地址以外,还会存储跳转回来以后要执行的指令的地址(以上面的程序为例,就是保存第3行的地址),存到哪呢,就是存到了LR寄存器。当函数结束以后,就把PC寄存器的值,修改为LR寄存器的值,这样就相当于回到调用的地方了
- 跳转指令BL
-
参数问题
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
- 那调到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和内存的语言