[关闭]
@qidiandasheng 2020-07-20T21:58:08.000000Z 字数 6471 阅读 1542

汇编

技术


来历

最早的时候,编写程序就是手写二进制指令,然后通过各种开关输入计算机,比如要做加法了,就按一下加法开关。后来,发明了纸带打孔机,通过在纸带上打孔,将二进制指令自动输入计算机。

为了解决二进制指令的可读性问题,工程师将那些指令写成了八进制。二进制转八进制是轻而易举的,但是八进制的可读性也不行。很自然地,最后还是用文字表达,加法指令写成 ADD。内存地址也不再直接引用,而是用标签表示。

这样的话,就多出一个步骤,要把这些文字指令翻译成二进制,这个步骤就称为 assembling,完成这个步骤的程序就叫做 assembler。它处理的文本,自然就叫做 aseembly code。标准化以后,称为 assembly language,缩写为 asm,中文译为汇编语言。

每一种 CPU 的机器指令都是不一样的,因此对应的汇编语言也不一样。

学习汇编语言,首先必须了解两个知识点:寄存器和内存模型。

内存模型:Heap

寄存器只能存放很少量的数据,大多数时候,CPU 要指挥寄存器,直接跟内存交换数据。所以,除了寄存器,还必须了解内存怎么储存数据。

程序运行的时候,操作系统会给它分配一段内存,用来储存程序和运行产生的数据。这段内存有起始地址和结束地址,比如从0x10000x8000,起始地址是较小的那个地址,结束地址是较大的那个地址。

bg2018012208.png-7.5kB

程序运行过程中,对于动态的内存占用请求(比如新建对象,或者使用malloc命令),系统就会从预先分配好的那段内存之中,划出一部分给用户,具体规则是从起始地址开始划分(实际上,起始地址会有一段静态数据,这里忽略)。举例来说,用户要求得到10个字节内存,那么从起始地址0x1000开始给他分配,一直分配到地址0x100A,如果再要求得到22个字节,那么就分配到0x1020

bg2018012209.png-5kB

这种因为用户主动请求而划分出来的内存区域,叫做 Heap(堆)。它由起始地址开始,从低位(地址)向高位(地址)增长。Heap 的一个重要特点就是不会自动消失,必须手动释放,或者由垃圾回收机制来回收。

内存模型:Stack

除了 Heap 以外,其他的内存占用叫做 Stack(栈)。简单说,Stack 是由于函数运行而临时占用的内存区域。

bg2018012210.png-6.6kB

请看下面的例子。

  1. int main() {
  2. int a = 2;
  3. int b = 3;
  4. }

上面代码中,系统开始执行main函数时,会为它在内存里面建立一个帧(frame),所有main的内部变量(比如a和b)都保存在这个帧里面。main函数执行结束后,该帧就会被回收,释放所有的内部变量,不再占用空间。

bg2018012211.png-7.4kB

如果函数内部调用了其他函数,会发生什么情况?

  1. int main() {
  2. int a = 2;
  3. int b = 3;
  4. return add_a_and_b(a, b);
  5. }

上面代码中,main函数内部调用了add_a_and_b函数。执行到这一行的时候,系统也会为add_a_and_b新建一个帧,用来储存它的内部变量。也就是说,此时同时存在两个帧:mainadd_a_and_b。一般来说,调用栈有多少层,就有多少帧。

bg2018012212.png-9.6kB

等到add_a_and_b运行结束,它的帧就会被回收,系统会回到函数main刚才中断执行的地方,继续往下执行。通过这种机制,就实现了函数的层层调用,并且每一层都能使用自己的本地变量。

所有的帧都存放在Stack,由于帧是一层层叠加的,所以 Stack 叫做栈。生成新的帧,叫做"入栈",英文是 push;栈的回收叫做"出栈",英文是 pop。Stack 的特点就是,最晚入栈的帧最早出栈(因为最内层的函数调用,最先结束运行),这就叫做"后进先出"的数据结构。每一次函数执行结束,就自动释放一个帧,所有函数执行结束,整个 Stack 就都释放了。

bg2018012213.jpg-13kB

bg2018012214.jpg-13.4kB

Stack 是由内存区域的结束地址开始,从高位(地址)向低位(地址)分配。比如,内存区域的结束地址是0x8000,第一帧假定是16字节,那么下一次分配的地址就会从0x7FF0开始;第二帧假定需要64字节,那么地址就会移动到0x7FB0

bg2018012215.png-10.5kB

寄存器

CPU 本身只负责运算,不负责储存数据。数据一般都储存在内存之中,CPU 要用的时候就去内存读写数据。但是,CPU 的运算速度远高于内存的读写速度,为了避免被拖慢,CPU 都自带一级缓存和二级缓存。基本上,CPU 缓存可以看作是读写速度较快的内存。

但是,CPU 缓存还是不够快,另外数据在缓存里面的地址是不固定的,CPU 每次读写都要寻址也会拖慢速度。因此,除了缓存之外,CPU 还自带了寄存器(register),用来储存最常用的数据。也就是说,那些最频繁读写的数据(比如循环变量),都会放在寄存器里面,CPU 优先读写寄存器,再由寄存器跟内存交换数据。

bg2018012206.png-4kB

寄存器不依靠地址区分数据,而依靠名称。每一个寄存器都有自己的名称,我们告诉 CPU 去具体的哪一个寄存器拿数据,这样的速度是最快的。有人比喻寄存器是 CPU 的零级缓存。

寄存器有对应的大小,比如ARMv7的寄存器为32位,ARMv8的寄存器为64位。

32位可以组成不重复的0,1组合数就是2^32 次方种,可以代表2^32 种地址(一种地址一个字节),所以说可以寻址4GB(2^32/1024/1024/1024)的内存。

ARM寄存器和x86寄存器

ARM32和x86:

下面这张表将ARM的寄存器(32位)和x86寄存器做了一个简单类比:

v2-3f9a9c8d737a7be48015018d7969cfce_1440w.png-12.2kB

ARM64:

自从ARMv8出现以后,ARM的寄存器就全面进入了64位时代,通用寄存器的数量从13个(R0-R12)变成了31个(X0-X30) ,其名称中的"R"也被"X"所取代了。

为了保持和32位系统的兼容性,每个ARMv8/ARM64通用寄存器都可被当做2个32位寄存器来使用,这样的32位寄存器用"Wn"来表示。当使用 x0 - x30 访问时,它就是一个64位的数。当使用 w0 - w30 访问时,访问的是这些寄存器的低32位,如图:

1117042-e3fabfec65c187f5.png-14.3kB

v2-7e58efe3debfca0db563d938523272b0_1440w.jpg-23.3kB

寄存器 位数 描述
x0-x30 64bit 通用寄存器,如果有需要可以当做32bit使用:WO-W30
FP(x29) 64bit (Frame Pointer)为栈基址寄存,用于保存栈底地址
LR(x30) 64bit (Link Register) 通常称X30为程序链接寄存器,保存子程序结束后需要执行的下一条指令
ZR(x31) 64bit (Zero Register),xzr/wzr分别代表 64/32 位,其作用就是 0,写进去代表丢弃结果,读出来是 0;
SP 64bit (Stack Pointer),栈顶寄存器,用于保存栈顶地址
PC 64bit 程序计数器,俗称PC指针,保存将要执行的指令的地址(指向即将要执行的下一条指令),在arm64中,软件是不能改写PC寄存器的
CPSR 32bit 状态寄存器

通用寄存器

ARM32:

R0~R12是通用寄存器(R12已经不完全是了),它们可以在常规操作中使用,来存储临时变量或地址。习惯上,R0常在算数运算中作为累加器,或者存储函数的返回地址。R7常用于存储系统调用号。R11常作为栈帧指针来标记函数栈帧的边界。此外,ARM的函数调用约定规定,函数的前四个参数存储在寄存器r0~r3中。

ARM64:

X0~X30是通用寄存器,它们可以在常规操作中使用,来存储临时变量或地址。ARM64的函数调用约定规定,函数的前八个参数存储在寄存器x0~x7中,如果参数个数超过了8个,多余的参数会存在栈上,新方法会通过栈来读取。
方法的返回值一般都在 x0 上;如果方法返回值是一个较大的数据结构时,结果会存在 x8 执行的地址上。

特殊寄存器

每个函数都有自己的一块操作空间,我们称其为“栈帧(stack frame)”。寄存器 fpsp 的值是栈帧范围的唯一标识。我们前面说过调用栈有多少层,就有多少帧。

状态寄存器 CPSR

其他寄存器是用来存放数据的,都是整个寄存器具有一个含义;而 CPSR 寄存器是按位起作用的,即,每一位都有专门的含义,记录特定的信息;如下图
导出图片Thu Jul 09 2020 07_11_53 GMT+0800 (中国标准时间).png-63.8kB
注: CPSR 寄存器是 32 位的。

指令

ARM64经常用到的汇编指令

  1. MOV X1X0 ;将寄存器X0的值传送到寄存器X1
  2. ADD X0X1X2 ;寄存器X1X2的值相加后传送到X0
  3. SUB X0X1X2 ;寄存器X1X2的值相减后传送到X0
  4. AND X0X0,#0xF ; X0的值与0xF相位与后的值传送到X0
  5. ORR X0X0,#9 ; X0的值与9相位或后的值传送到X0
  6. EOR X0X0,#0xF ; X0的值与0xF相异或后的值传送到X0
  7. LDR X5,[X6,#0x08] ldload; X6寄存器加0x08的和的地址值内的数据传送到X5
  8. LDP x29, x30, [sp, #0x10] ; ldp :load pair ; 一对寄存器, 从内存读取数据到寄存器
  9. STR X0, [SP, #0x8] ;st:store,str:往内存中写数据(偏移值为正); X0寄存器的数据传送到SP+0x8地址值指向的存储空间
  10. STUR w0, [x29, #-0x8] ;往内存中写数据(偏移值为负)
  11. STP x29, x30, [sp, #0x10] ;store pair,存放一对数据, 入栈指令
  12. CBZ ;比较(Compare),如果结果为零(Zero)就转移(只能跳到后面的指令)
  13. CBNZ ;比较,如果结果非零(Non Zero)就转移(只能跳到后面的指令)
  14. CMP ;比较指令,相当于SUBS,影响程序状态寄存器CPSR
  15. B ;跳转指令,可带条件跳转与cmp配合使用
  16. BL ;带返回的跳转指令, 返回地址保存到LRX30
  17. BLR ; 带返回的跳转指令,跳转到指令后边跟随寄存器中保存的地址(例:blr x8 ;跳转到x8保存的地址中去执行)
  18. RET ;子程序返回指令,返回地址默认保存在LRX30

STR/STP

str (store register) :将寄存器中的值写入到内存中,如:

  1. str w9, [sp, #0x8] ; 将寄存器 w9 中的值保存到栈内存 [sp + 0x8] 处

strb: (store register byte) 将寄存器中的值写入到内存中(只存储一个字节),如:

  1. strb w8, [sp, #7] ; 将寄存器 w8 中的低 1 字节的值保存到栈内存 [sp + 7] 处

stp:入栈指令(str 的变种指令,可以同时操作两个寄存器),如:

  1. stp x29, x30, [sp, #0x10] ; 将 x29, x30 的值存入 sp 偏移 16 个字节的位置

LDR/STR

在x86架构中,不管是寄存器之间,还是寄存器和内存之间,都可以使用MOV指令,并且直接操作内存单元上的数据是被允许的。

在ARM架构中,寄存器间传送数据的指令依然是MOV,比如"MOV Ra Rb" 就是把Rb里存放的数据传送给Ra,但内存单元上的数据不允许被直接操作,而是必须先放到寄存器中,为此就有了把内存的内容传送到寄存器的指令LDR(Load),以及把寄存器的内容传送回内存的指令STR(Store)

传送的时候,内存单元的地址存放在一个寄存器中(比如R1),用[R1]表示,"[]"在这里就对应C语言里的"*",表示取地址里的内容。假设R1里存放的是0x200,内存中地址0x200处的内容是0x5,那么"ldr r0, [r1]"就是将0x5放入r0中。

v2-d1b29773f373687176b55ff7a7dd4bfa_r.jpg-26.1kB

通用寄存器的数量一共就那么多,直接用寄存器的值来获取内存地址的数量实在太有限了,更多的时候,是通过寄存器的值(基址)加上一个偏移/索引(offset/index)来指向内存对应的单元,索引的大小可以由立即数提供,也可以由寄存器存储的值提供:

  1. STR R0,[R1, #12] // R0 --> [R1+12]
  2. LDR R4,[R5, R6] // R4 <-- [R5+R6]

如果索引对基址的更改发生在数据传输之前,则称为"预索引"(pre-index),传输前后寄存器R1的值都不会改变。

v2-6ce5e5c5c616ff3f58b4a0948f9f1df8_1440w.jpg-25.9kB

如果索引对基址的更改发生在数据传输之后(注意下图"[]"位置的改变),则称为"后索引"(post-index),传输后寄存器R1的内容将变为加上其原来的值加上索引后的值。"后索引"其实算是一种二合一的指令,比如"str r0, [r1], #12"就等同于"str r0, [r1]"加上"r1 = r1+12"。

v2-a41e98af2706f1761e062730efc8c03a_1440w.jpg-15.6kB

子程序返回

子程序返回的三种方法:

  1. 1.MOV PCLR
  2. 2.BL LR
  3. 3.在子程序入口处使用以下指令将R14存入堆栈
  4. STMFD SP!, {<Regs>,LR}
  5. 对应的,使用以下指令可以完成子程序的返回
  6. LDMFD SP!, {<Regs>,LR}

lr就是连接寄存器(Link Register, LR),在ARM体系结构中LR的特殊用途有两种:一是用来保存子程序返回地址;二是当异常发生时,LR中保存的值等于异常发生时PC的值减4(或者减2),因此在各种异常模式下可以根据LR的值返回到异常发生前的相应位置继续执行。

当通过BLBLX指令调用子程序时,硬件自动将子程序返回地址保存在R14(LR)寄存器中。在子程序返回时,把LR的值复制到程序计数器PC即可实现子程序返回。

示例解析

  1. int add_a_and_b(int a, int b) {
  2. return a + b;
  3. }
  4. int main() {
  5. return add_a_and_b(2, 3);
  6. }

x86汇编示例:汇编语言入门教程

xcode查看汇编代码:Debug->Debug WorkFlow-> Always Show Disassembly

在要查看汇编代码的代码中放置断点.然后,当代码到达该断点时,您可以查看汇编代码.

汇编Hook objc_msgSend

从汇编角度分析objc_msgSend的hook过程
Hook objc_msgSend -- 从 0.5 到 1

参考

汇编语言入门教程
ARM汇编语言 - 简介 [一]
arm64 架构之入栈/出栈操作
ARM64 汇编——寄存器和指令

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注