32位

一、寄存器、内存和、栈

在高级语言里面,操作的是变量。在ARM汇编里面,操作的是寄存器(register)、内存和栈(stack)。

1.1寄存器

寄存器可以看成CPU自带的变量,他们的数量一般是很有限的,当需要更多变量时,就可以把他们放在内存中;

1.2栈

其实也是一片内存区域,但它具有内存的特点:先进后出。ARM的栈是满递减(Full Descending)的,向下增长,也就是开口朝下,新的变量被存放到栈底的位置;越靠近栈底,内存地址越小。
一个名为stack pointer(简称SP)的寄存器保存栈的栈底地址,称为栈地址;可以把一个变量给入(push)栈以保存它的值,也可以让它出(pop)栈,恢复变量的原始值。在实际操作中,栈地址会不断变化;但是在执行一块代码的前后,栈地址应该是不变的,不然程序就要出问题了。举个例子

1
2
3
4
5
6
7
8
static int global_var0;
static int global_var1;

void foo(void)
{
bar();
// 其他操作;
}

在上面4行代码中,假设函数foo()用到了A、B、C、D四个寄存器;foo()内部调用了bar(),假设bar()用到了A、B、C三个寄存器。因为2个不同的函数用到了3个相同的寄存器,所以bar()在开始执行前需要将3个寄存器中原来的值入栈以保存其原始值,在结束执行前将它们出栈以恢复其原始值,保证foo()能够正常执行。用伪汇编代码表示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// foo()函数
foo:
// 将A、B、C、D入栈,保存它们的原始值
入栈 {A, B, C, D}
// 使用A ~ D
移动 A, #1 // A = 1
移动 B, #2 // B = 2
移动 C, #3 // 你猜猜这行是什么意思?
调用 bar
移动 D, global_var0
// global_var1 = A + B + C + D
相加 A, B // A = A + B,注意此处A的值
相加 A, C // A = A + C,还要注意此处A的值
相加 A, D // 你再猜猜这行是什么意思?
移动 global_var1, A
// 将A、B、C、D出栈,恢复它们的原始值
出栈 {A-D}
返回
// bar()函数
bar:
// 将A、B、C入栈,保存它们的原始值A == 1,B == 2,C == 3
入栈 {A-C}
// 使用A ~ C
移动 A, #2 // 还需要注释吗?
移动 B, #5
移动 C, A
相加 C, B // C = 7
// global_var0 = A + B + C (== 2 * C)
相加 C, C
移动 global_var0, C // A = 2,B = 5,C = 14
// 现在你知道入栈和出栈的重要意义了吗?
出栈 {A-C}
返回

二、部分特殊用途的寄存器

这里有比较详细的特殊寄存器用途

  • R0-R3 传递参数与返回值
  • R7 (FP寄存器也叫BP)帧指针,指向母函数与被调用子函数在栈中的交界。 也就是当前函数的栈底,上面是母函数的栈空间。
  • R9 在iOS 3.0以前被系统保留
  • R12 内部过程调用寄存器,dynamic linker会用到它
  • R13 SP寄存器,栈指针,指向当前函数的栈顶
  • R14 LR寄存器,保存函数返回地址(也就是执行完子函数的下一条指令地址)
  • R15 PC寄存器,下一条要执行的指令地址寄存器

记住,不断的压栈的时候,栈地址是不断变小的。栈底的地址最大

三、分支跳转与条件判断

处理器中PC(program counter)的寄存器用于存放下一条指令的地址。
在ARM汇编中,分支的条件一般有4种:

  • 操作结果为0或者不为0
  • 操作结果为负数
  • 操作结果有进位
  • 运算溢出

这些条件的判断准则(flag)放在程序状态寄存器(PSR)中,数据处理相关指令会改变这些flag,分支指令再绝地是否跳转。

四、ARM/THUMB指令解读

ARM处理器用到的指令集分为ARM和THUMB两种;ARM指令长度均为32bit,THUMB指令的长度均为16bit。所有指令可大致分为3大类,分别是数据操作指令内存操作指令分支指令

4.1数据操作指令

数据操作指令有以下2条规则:
1) 所有操作数均为32bit;
2) 所有结果均为32bit,且只能放在寄存器中。

总的来说,数据操作指令的基本格式是:

1
op{cond}{s} Rd,Rn,Op2

其中conds是两个可选后缀;cond的作用是指定指令op在什么条件下执行,共有下面17种条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
EQ              结果为0(EQual to 0)	 
NE 结果不为0(Not Equal to 0)
CS 有进位或借位(Carry Set)
HS 同CS(unsigned Higher or Same)
CC 没有进位或借位(Carry clear)
LO 同CC(unsigned LOwer)
MI 结果小于0(MInus)
PL 结果大于等于0(PLus)
VS 溢出(oVerflow Set)
VC 无溢出(oVerflow Clear)
HI 无符号比较大于(unsigned HIgher)
LS 无符号比较小于等于(unsigned Lower or Same)
GE 有符号比较大于等于(signed Greater than or Equal)
LT 有符号比较小于(signed Less Than)
GT 有符号比较大于(signed Greater Than)
LE 无符号比较小于等于(signed Less than or Equal)
AL 无条件(ALways,默认)

用法例子:

1
2
3
比较 R0, R1
移动 GE R2, R0
移动 LT R2, R1

比较R0和R1的值,如果R0大于等于R1,则R2=R0;否则R2=R1。

“s”的作用是指定指令”op”是否设置flag,共有下面4种flag:

1
2
3
4
5
6
7
8
N(Negative)
如果结果小于0则置1,否则置0;
Z(Zero)
如果结果是0则置1,否则置0;
C(Carry)
对于加操作(包括CMN)来说,如果产生进位则置1,否则置0;对于减操作(包括CMP)来说,Carry相当于Not-Borrow,如果产生借位则置0,否则置1;对于有移位操作的非加/减操作来说,C置移出值的最后一位;对于其他的非加/减操作来说,C的值一般不变;
V(oVerflow)
如果操作导致溢出,则置1,否则置0。

需要注意的一点,C flag表示无符号数运算结果是否溢出;V flag表示有符号数运算结果是否溢出。

数据操作指令可以大致分为以下4类:

4.2算术操作

1
2
3
4
5
6
7
ADD R0,R1				; R0=R0+R1;
ADD R0, R1, R2 ; R0 = R1 + R2
ADC R0, R1, R2 ; R0 = R1 + R2 + C(arry)
SUB R0, R1, R2 ; R0 = R1 - R2
SBC R0, R1, R2 ; R0 = R1 - R2 - !C
RSB R0, R1, R2 ; R0 = R2 - R1
RSC R0, R1, R2 ; R0 = R2 - R1 - !C

算术操作中,ADD和SUB为基础操作,其他均为两者的变种。RSB是“Reverse SuB”的缩写,仅仅是把SUB的两个操作数调换了位置而已;以“C”(即Carry)结尾的变种代表有进位和借位的加减法,当产生进位或没有借位时,将Carry flag置1

4.3逻辑操作

1
2
3
4
5
6
AND R0, R1, R2          ; R0 = R1 & R2
ORR R0, R1, R2 ; R0 = R1 | R2
EOR R0, R1, R2 ; R0 = R1 ^ R2
BIC R0, R1, R2 ; R0 = R1 &~ R2
MOV R0, R2 ; R0 = R2
MVN R0, R2 ; R0 = ~R2

逻辑操作指令没什么多说的,它们的作用都已经用C操作符表示出来了,大家应该很熟悉;但是C操作符里的移位操作并没有对应的逻辑操作指令,因为ARM采用了桶式移位,共有以下4种指令:

1
2
3
4
LSL 逻辑左移 (右边补0)
LSR 逻辑右移 (左边补0)
ASR 算术右移 (左边最后一位是什么就补什么)
ROR 循环右移 (最右边的一位补到最左边的一位)

4.4比较操作

1
2
3
4
CMP R1, R2  ;执行R1 - R2并依结果设置flag
CMN R1, R2 ;执行R1 + R2并依结果设置flag
TST R1, R2 ;执行R1 & R2并依结果设置flag
TEQ R1, R2 ;执行R1 ^ R2并依结果设置flag

比较操作其实就是改变flag的算术操作或逻辑操作,只是操作结果不保留在寄存器里而已。flag存放在 PSR(状态寄存器里面)

4.5乘法操作

1
2
MUL R4,R3,R2    ; R4 = R3*R2
MLA R4,R3,R2,R1 ; R4 = R3*R2+R1

乘法操作的操作数必须来自寄存器。

4.6内存操作指令

内存操作指令的基本格式是:

1
op {cond}{type} Rd,[Rn,?Op2]

其中Rn是基址寄存器,用于存放基地址; “cond”的作用与数据操作指令相同;”type”指定指令”op”操作的数据类型,共有4种:

1
2
3
4
5
6
7
8
9
B(unsigned Byte)
无符号byte(执行时扩展到32bit,以0填充);
SB(Signed Byte)
有符号byte(仅用于LDR指令;执行时扩展到32bit,以符号位填充);
H(unsigned Halfword)
无符号halfword(执行时扩展到32bit,以0填充);
SH(Signed Halfword)
有符号halfword(仅用于LDR指令;执行时扩展到32bit,以符号位填
充)。

如果不指定”type”,则默认数据类型是word。

ARM内存操作的基本指令只有两个:LDR (LoaD Register)将数据从内存中读出来,存到寄存器中;STR(Store Register)将数据从寄存器中读出来,存到内存中。两个指令的使用情况如下:

  • LDR
1
2
3
LDR Rt, [Rn {, #offset}]        ; Rt = *(Rn {+ offset}),{}代表可选
LDR Rt, [Rn, #offset]! ; Rt = *(Rn + offset); Rn = Rn + offset
LDR Rt, [Rn], #offset ; Rt = *Rn; Rn = Rn + offset

LDR 与 SDR的区别

1
2
3
4
5
6
7
8

ldr r0, _start
adr r0, _start
ldr r0, =_start
nop
mov pc, lr
_start:
nop

编译的时候设置 RO 为 0x0c008000

1
2
3
4
5
6
7
8
9
10
11
12
13

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

0c008000 <_start-0x14>:
c008000: e59f000c ldr r0, [pc, #12] ; c008014 <_start>
c008004: e28f0008 add r0, pc, #8 ; 0x8
c008008: e59f0008 ldr r0, [pc, #8] ; c008018 <_start+0x4>
c00800c: e1a00000 nop (mov r0,r0)
c008010: e1a0f00e mov pc, lr

0c008014 <_start>:
c008014: e1a00000 nop (mov r0,r0)
c008018: 0c008014 stceq 0, cr8, [r0], -#80

分析:

ldr r0, _start

从内存地址 _start 的地方把值读入。执行这个后,r0 = 0xe1a00000

adr r0, _start

取 得 _start 的地址到 r0,但是请看反编译的结果,它是与位置无关的。其实取得的时相对的位置。例如这段代码在 0x0c008000 运行,那么 adr r0, _start 得到 r0 = 0x0c008014;如果在地址 0 运行,就是 0x00000014 了。

ldr r0, =_start

这 个取得标号 _start 的绝对地址。这个绝对地址是在 link 的时候确定的。看上去这只是一个指令,但是它要占用 2 个 32bit 的空间,一条是指令,另一条是 _start 的数据(因为在编译的时候不能确定 _start 的值,而且也不能用 mov 指令来给 r0 赋一个 32bit 的常量,所以需要多出一个空间存放 _start 的真正数据,在这里就是 0x0c008014)。
因此可以看出,这个是绝对的寻址,不管这段代码在什么地方运行,它的结果都是 r0 = 0x0c008014

1
2
3
4
5
6

对于第一条指令,ldr r0, _start,最终生成的代码是将pc的值加上12得到一个地址,然后将这个地址的值赋给R0。12应该是这么算出来的:执行当前指令时,pc的值为当前值加8,即0x0c008008,而_start的地址是c08014,二者的差换算成十进制就是12。

第二条指令很好理解,只是需要知道adr是一个伪指令,需要汇编器把它进行转换。

第三条指令用来取得_start的实际地址,现在可以把_start视作一个地址,这也是它的本质。
  • STR
1
2
3
STR Rt, [Rn {, #offset}]      ; *(Rn {+ offset}) = Rt
STR Rt, [Rn, #offset]! ; *(Rn {+ offset}) = Rt; Rn = Rn + offset
STR Rt, [Rn], #offset ; *Rn = Rt; Rn = Rn + offset

此外,LDR和SRT的变种LDRD和STRD还可以操作双字,即一次性操作2个寄存器,其基本格式如下:

1
op{cond} Rt,Rt2,[Rn{,#offset}]

其用法与原型类似,如下:

  • STRD
1
STRD R4, R5,[R9,#offset]    ;*(R9 + offset) = R4; *(R9 + offset + 4) = R5
  • LDSR
1
LDSR R4,R5,[R9,$offset]     ; R4 = *(R9 + offset); R5=*(R9+offset+4)

除了LDR和STR外,还可以通过LDM(Load Mutiple)STM (Store Mutiple)进行块传输,一次性操作多个寄存器。块传输指令的基本格式是:

1
op{cond}{mode} Rd{!},reglist

其中Rd是基址寄存器,可选的”!”指定Rd变化后的值是否写回Rd; reglist是一系列寄存器,用大括号括起来,它们之间可以用“,”分隔,也可以用“-”表示一个范围,比如,{R4–R6,R8}表示寄存器R4、R5、R6、R8;这些寄存器的顺序是按照自身的编号由小到大排列的,与大括号内的排列顺序无关。

需要特别注意的是,LDM和STM的操作方向与LDR和STR完全相反:LDM是把从Rd开始,地址连续的内存数据存入reglist中,STM是把reglist中的值存入从Rd开始,地址连续的内存中。此处特别容易混淆,大家一定要注意!

“cond”的作用与数据操作指令相同。“mode”指定Rd值的4种变化规律,如下所示:

1
2
3
4
5
6
7
8
IA(Increment After)
每次传输后增加Rd的值;
IB(Increment Before)
每次传输前增加Rd的值;
DA(Decrement After)
每次传输后减少Rd的值;
DB(Decrement Before)
每次传输前减少Rd的值。

例如:
内存中存储着这些值。 1,2,3,4,5,6,7,8 。 R0指向5。执行以下指令得到的结果为

1
2
3
4
5
foo():
LDMIA R0, {R4 – R6} ; R4 = 5, R5 = 6, R6 = 7
LDMIB R0, {R4 – R6} ; R4 = 6, R5 = 7, R6 = 8
LDMDA R0, {R4 – R6} ; R4 = 5, R5 = 4, R6 = 3
LDMDB R0, {R4 – R6} ; R4 = 4, R5 = 3, R6 = 2”

4.7分支指令

分支指令可以分为无条件和条件分支两种

  • 4.7.1 无条件分支
1
2
3
B Label	;PC = Label
BL Label ;LR = PC - 4; PC = Label
BX Rd ;PC = Rd 并切换指令集

举个例子

1
2
3
4
5
foo():
B Label ; 跳转到Label处往下执行
....
Label:
....
B指令是最简单的跳转指令

B指令格式B{条件} 目标地址

B 指令是最简单的跳转指令。一旦遇到一个 B 指令,ARM 处理器将立即跳转到给定的目标地址,从那里继
续执行。注意存储在跳转指令中的实际值是相对当前PC 值的一个偏移量,而不是一个绝对地址,它的值由汇编器来计算(参考寻址方式中的相对寻址)。它是 24 位有符号数,左移两位后有符号扩展为 32 位,表示的有效偏移为 26 位(前后32MB 的地址空间)。以下指令:

1
2
3
B Label ;程序无条件跳转到标号 Label 处执行
CMP R1 ,# 0 ;当 CPSR(状态寄存器) 寄存器中的 Z 条件码置位时,程序跳转到标号 Label 处执行
BEQ Label
BL 指令

BL 指令格式:BL{条件} 目标地址
BL 是另一个跳转指令,但跳转之前,会在寄存器R14 中保存PC 的当前内容,因此,可以通过将R14 的内容重新加载到PC 中,来返回到跳转指令之后的那个指令处执行。该指令是实现子程序调用的一个基本但常用的手段。以下指令:

1
BL Label ;当程序无条件跳转到标号 Label 处执行时,同时将当前的 PC 值保存到 R14 中
BLX指令

BLX 指令从ARM 指令集跳转到指令中所指定的目标地址,并将处理器的工作状态由ARM 状态切换到Thumb 状态,该指令同时将PC 的当前内容保存到寄存器R14 中。因此,当子程序使用Thumb 指令集,而调用者使用ARM 指令集时,可以通过BLX 指令实现子程序的调用和处理器工作状态的切换。
同时,子程序的返回可以通过将寄存器R14 值复制到PC 中来完成。

  • 4.7.2 条件分支

条件分支的cond跟上面提到的4中flag来判断的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cond            flag
EQ Z = 1
NE Z = 0
CS C = 1
HS C = 1
CC C = 0
LO C = 0
MI N = 1
PL N = 0
VS V = 1
VC V = 0
HI C = 1 & Z = 0
LS C = 0 | Z = 1
GE N = V
LT N != V
GT Z = 0 & N = V
LE Z = 1 | N != V

在条件分支指令前会有一条数据操作指令来设置flag,分支指令根据flag的值来决定代码走向,举个例子:

1
2
3
4
Label:
LDR R0, [R1], #4
CMP R0, 0 ; 如果R0 == 0,Z = 1;否则Z = 0
BNE Label ; Z == 0则跳转

ARM 调用规则

1、前言与后记

当一个函数调用另外一个函数时,常常需要传递参数和返回值;如何传递这些数据,称为ARM汇编的调用规则。在执行一块代码时,其前后栈地址应该是不变的。这个操作是通过被执行代码块的前言(prologs)和后记(epilogs)完成的。前言所做的工作主要有:

  • 将LR入栈;
  • 将R7入栈;
  • R7=SP;
  • 将需要保留的寄存器原始值入栈;
  • 为本地变量开辟空间;

后记工作:
后记所做的主要工作跟前言正好相反:

  • 释放本地变量占用的空间;
  • 将需要保留的寄存器原始值出栈;
  • 将R7出栈;
  • 将LR出栈,PC=LR。

前言和后记中的这些工作并不是必须的,如果这块代码压根儿就没有用到栈,就不需要”保留寄存器原始值”这一步了。

2、传递参数与返回值

“函数的前4个参数存放在R0到R3中,其他参数存放在栈中;返回值放在R0中”

分析一个例子

1
2
3
4
5
6
7
8
// clang -arch armv7 -isysroot `xcrun --sdk iphoneos --show-
sdk-path` -o MainBinary main.m
#include <stdio.h>
int main(int argc, char **argv)
{
printf("%d, %d, %d, %d, %d", 1, 2, 3, 4, 5);
return 6;
}

上面的代码拉到IDA,展示出来的汇编代码是下面这样的

arm assemble

BLX_printf”执行printf函数,它的6个参数分别存放在R0、R1、R2、R3、[SP,#0x20+var_20]和[SP,#0x20+var_1C]中,返回值存放在R0里,其中var_20=-0x20,var_1C=-0x1C,因此栈上的2个参数分别位于[SP]和[SP,#0x4]。

伪指令

在 ARM 汇编语言程序里,有一些特殊的助记符,这些助记符与指令系统的助记符不同,没有相对应的操作码,这些特殊指令助记符被称为伪指令,他们所完成的操作称为伪操作。伪指令在源程序中的作用是为完成汇编程序作各种准备工作的,这些伪指令仅在汇编过程中起作用,一旦汇编结束,伪指令的使命就完成。

在ARM的汇编程序中,有如下几种伪指令:符号定义伪指令、数据定义伪指令、汇编控制伪指令、宏指令以及其他伪指令。

DCB

该汇编器指令用于分配一个或者多个字节内存,并且可以指定这些内存中的初始值

  • Syntax(语法): {label} DCB expr{,expr}
1
2
3
exp:1、数值表达式(整数,范围-128到255)(注:此处有疑问)

2、字符串,字符串中字符按顺序存储在连续的字节空间中
  • Usage(用法):
1
2
3
4
5
如果有指令在DCB汇编器指令后面,则应该在该指令前使用ALIGN汇编器指令来保证该指令是对齐的                 (DCB用在代码段中)。
"=" 和"DCB"是同义的。

例如: label1 DCB 6 和 label1 = 6 是一样的
DCB 8 和 = 8 是一样的
  • Example(例子):
1
2
3
4
5
ARM汇编语言字符串和C语言字符串是不同的,不是以NULL结尾的。可以用下面这个例子构造类似C语言中的以NULL结尾的字符串:

C_string DCB "C_string",0

总共分配了9个字节的内存,最后一个字节值为0即NULL。

thumb 指令 IT

先来看个例子,IT 后面经常跟了个 EQ,例如

1
2
3
CMP R5,#0
IT EQ
MOVEQ R2,R3

如果符合EQ,则执行 MOVEQ R2,R3。 IT指定了EQ作用域后面跟的一条语句

详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
IT (If-Then) 指令由四条后续条件指令(IT 块)句组成。 这些条件可以完全相同,也可以互为逻辑反。
IT 块中的指令(包括跳转)还必须在语法的 {cond} 部分中指定条件。
无需在代码中编写 IT 指令,因为汇编器会根据在后续指令中指定的条件为您自动生成这些指令。 不过,如果确实 需要编写 IT 指令,则汇编器会根据后续指令中指定的条件对 IT 中指定的条件进行验证。
编写 IT 指令可确保您会考虑如何在代码设计中放置条件指令以及选择条件。
在汇编为 ARM 代码时,汇编器会执行相同的检查,但是不会生成任何 IT 指令。


限制
不允许在 IT 块中使用下面的指令:
IT
CBZ 和 CBNZ
TBB 和 TBH
CPS、CPSID 和 CPSIE
SETEND
使用 IT 块时的其他限制有:
跳转指令或修改 pc 的任何指令只能是 IT 块中的最后一个指令。
无法跳转到 IT 块中的任何指令,除非在从异常处理程序返回时。
不能在 IT 块中使用任何汇编器指令

再来个例子:

1
2
3
4
5
6
7
8
9
10
if (R4 == R5)
{
R7 = R8 + R9;
R7 /= 2;
}
else
{
R7 = R10 + R11;
R7 *= 2;
}

汇编如下,下面的代码除了第一个指令,其余组成一个IT块:

1
2
3
4
5
6
CMP R4, R5
ITTEE EQ
ADDEQ R7, R8, R9 ; if R4 = R5, R7 = R8 + R9
ASREQ R7, R7, #1 ; if R4 = R5, R7 /= 2
ADDNE R7, R10, R11 ; if R4 != R5, R7 = R10 + R11
LSLNE R7, R7, #1 ; if R4 != R5, R7 *=2

IT语法:

1
2
3
4
5
语法
IT{x{y{z}}} {cond}

IT后面最多可以跟4条指令,其中xyz只可以用T或者E,T就是Then,条件为真时执行,E是Else,条件为假时执行。
ITT后面就是两条指令,ITTEE就是四条指令。IT后面就是跟一条指令

64位

64位上的特殊寄存器

1
2
3
4
5
6
7
__uint64_t __x[29];	/* General purpose registers x0-x28 */
__uint64_t __fp; /* Frame pointer x29 */
__uint64_t __lr; /* Link register x30 */
__uint64_t __sp; /* Stack pointer x31 */
__uint64_t __pc; /* Program counter */
__uint32_t __cpsr; /* Current program status register */
__uint32_t __pad; /* Same size for 32-bit or 64-bit clients */

32为与64位的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

32位的ldm,stm,push,pop指令在64位中不存在.
ldm,stm本来的作用是批量出栈进栈.//详细:http://blog.chinaunix.net/uid-25100840-id-340479.html
例子:
LDMFD SP! , {R0, R1, R2} //把sp指向的地址和后2个,就是总共3个连续内存数据拷到3个寄存器中.
STMFD SP!, {R0} //把r0拷到sp指向的地址(堆栈)中.如果2个或2个以上寄存器,就是拷到后连续地址这样,总之和ldm对应.
取而代之的是ldp,stp也是这个作用.而且使用很频繁.一般使用在函数开头使用代替push.
例子:
stp x29, x30, [sp, #0xfffffff0]!
ldp x29, x30, [sp], #0x10

感叹号的意思是先运算修改寄存器的值,再使用寄存器.
没有感叹号就像下面这个例子.每次使用完sp的值后加上0x10.这样就可以省略armv7里add sp,#0x40的操作了.
ffffff8012ae5770 ldp x24, x23, [sp], #0x10
ffffff8012ae5774 ldp x22, x21, [sp], #0x10
ffffff8012ae5778 ldp x20, x19, [sp], #0x10
ffffff8012ae577c ldp x29, x30, [sp], #0x10

从可用寄存器角度来说,程序员可以完全使用31个通用寄存器(X0-X30或W0-W30),而堆栈指针寄存器(SP或WSP)以及指令指针寄存器IP都是独立的,这个与32位的不同(R13为堆栈指针寄存器,R15作为指令指针寄存器)。对于ARM官方提供的调用约定,参数可以用8个寄存器(X0-X7或W0-W7)。这意味着64位下,即便传8个参数都能进寄存器,呵呵。而需要被当前例程所保护的通用寄存器是从X18到X30。不过少了原来的push/pop指令,原来的push能一次将几乎所有通用寄存器保存到栈上。现在如果要保存通用寄存器到栈上的话一般使用LDP/STP指令对SP操作,这样可以同时加载/存储两个64位寄存器。用这对指令同时也能确保栈地址始终能16字节对齐。 而对于SIMD寄存器以及浮点寄存器来说,除了标量单精度与双精度寄存器的数量不变以为(它俩仍然与SIMD寄存器共享),SIMD寄存器由原先32位下的16个扩充到了32个。在32位下,需要被保护的SIMD寄存器是Q4-Q7这四个,而64位下,需要被当前例程所保护的SIMD寄存器是V8-V15。

从ISA角度上来说,原本32位下有很强悍的几乎每条指令都带条件操作的特性完全木有了~留下几条含有前缀C的比较简单常用的操作,比如CCMP、CSEL、CINC、CINV等。不过像算术逻辑操作仍然有不改变当前标志位与改变当前标志位两种版本,这点还是很不错的。在64位下,Thumb指令集全都没有了,所有指令都是32位宽。因此立即数与ARMv7比起来,除了移位还算正常,其它的都显得有些奇葩

简单点:传参寄存器增加到8个,push,pop用stp,ldp.看代码要注意些小心些.

arm结构 寄存器:arm64有32个64bit长度的通用寄存器x0~x30,sp,可以只使用其中的32bit w0~w30,arm32只有16个32bit的通用寄存器r0~r12, lr, pc, sp. arm64有32个128bitSIMD寄存器v0~v31,arm32有16个128bitSIMD寄存器Q0~Q15,又可细分为32个64bitSIMD寄存器D0~D31 函数调用:arm64前面8个参数都是通过寄存器来传递x0~x7, arm32前面4个参数通过寄存器来传递r0~r3,其他通过栈传递

arm结构 寄存器:arm64有32个64bit长度的通用寄存器x0~x30,sp,可以只使用其中的32bit w0~w30,arm32只有16个32bit的通用寄存器r0~r12, lr, pc, sp. arm64有32个128bitSIMD寄存器v0~v31,arm32有16个128bitSIMD寄存器Q0~Q15,又可细分为32个64bitSIMD寄存器D0~D31 函数调用:arm64前面8个参数都是通过寄存器来传递x0~x7, arm32前面4个参数通过寄存器来传递r0~r3,其他通过栈传递

指令集:arm64和arm32是两套不同的指令集,尤其是SIMD指令集完全不同。

iOS 附录

IDA的F5中出现 j_objc_msgSend 这个情况是要去看汇编了