ARM汇编一些基础知识
32位
一、寄存器、内存和、栈
在高级语言里面,操作的是变量。在ARM汇编里面,操作的是寄存器(register)、内存和栈(stack)。
1.1寄存器
寄存器
可以看成CPU自带的变量,他们的数量一般是很有限的,当需要更多变量时,就可以把他们放在内存
中;
1.2栈
栈
其实也是一片内存区域,但它具有内存的特点:先进后出。ARM的栈是满递减(Full Descending)
的,向下增长
,也就是开口朝下
,新的变量被存放到栈底的位置;越靠近栈底,内存地址越小。
一个名为stack pointer
(简称SP)的寄存器保存栈的栈底地址,称为栈地址;可以把一个变量给入(push)栈以保存它的值,也可以让它出(pop)栈,恢复变量的原始值。在实际操作中,栈地址会不断变化;但是在执行一块代码的前后,栈地址应该是不变的,不然程序就要出问题了。举个例子
1 | static int global_var0; |
在上面4行代码中,假设函数foo()用到了A、B、C、D四个寄存器;foo()内部调用了bar(),假设bar()用到了A、B、C三个寄存器。因为2个不同的函数用到了3个相同的寄存器,所以bar()在开始执行前需要将3个寄存器中原来的值入栈以保存其原始值,在结束执行前将它们出栈以恢复其原始值,保证foo()能够正常执行。用伪汇编代码表示如下:
1 | // foo()函数 |
二、部分特殊用途的寄存器
- 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 |
其中cond
和s
是两个可选后缀;cond
的作用是指定指令op
在什么条件下执行,共有下面17种条件:
1 | EQ 结果为0(EQual to 0) |
用法例子:
1 | 比较 R0, R1 |
比较R0和R1的值,如果R0大于等于R1,则R2=R0;否则R2=R1。
“s”的作用是指定指令”op”是否设置flag,共有下面4种flag:
1 | N(Negative) |
需要注意的一点,C flag表示无符号数运算结果是否溢出;V flag表示有符号数运算结果是否溢出。
数据操作指令可以大致分为以下4类:
4.2算术操作
1 | ADD R0,R1 ; R0=R0+R1; |
算术操作中,ADD和SUB为基础操作,其他均为两者的变种。RSB是“Reverse SuB”的缩写,仅仅是把SUB的两个操作数调换了位置而已;以“C”(即Carry)结尾的变种代表有进位和借位的加减法,当产生进位或没有借位时,将Carry flag置1
。
4.3逻辑操作
1 | AND R0, R1, R2 ; R0 = R1 & R2 |
逻辑操作指令没什么多说的,它们的作用都已经用C操作符表示出来了,大家应该很熟悉;但是C操作符里的移位操作并没有对应的逻辑操作指令,因为ARM采用了桶式移位,共有以下4种指令:
1 | LSL 逻辑左移 (右边补0) |
4.4比较操作
1 | CMP R1, R2 ;执行R1 - R2并依结果设置flag |
比较操作其实就是改变flag的算术操作或逻辑操作,只是操作结果不保留在寄存器里而已。flag存放在 PSR(状态寄存器里面)
4.5乘法操作
1 | MUL R4,R3,R2 ; R4 = R3*R2 |
乘法操作的操作数必须来自寄存器。
4.6内存操作指令
内存操作指令的基本格式是:
1 | op {cond}{type} Rd,[Rn,?Op2] |
其中Rn是基址寄存器,用于存放基地址; “cond”的作用与数据操作指令相同;”type”指定指令”op”操作的数据类型,共有4种:
1 | B(unsigned Byte) |
如果不指定”type”,则默认数据类型是word。
ARM内存操作的基本指令只有两个:LDR (LoaD Register)将数据从内存中读出来,存到寄存器中;STR(Store Register)将数据从寄存器中读出来,存到内存中。两个指令的使用情况如下:
- LDR
1 | LDR Rt, [Rn {, #offset}] ; Rt = *(Rn {+ offset}),{}代表可选 |
LDR 与 SDR的区别
1 |
|
编译的时候设置 RO 为 0x0c008000
1 |
|
分析:
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 |
|
- STR
1 | STR Rt, [Rn {, #offset}] ; *(Rn {+ offset}) = Rt |
此外,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 | IA(Increment After) |
例如:
内存中存储着这些值。 1,2,3,4,5,6,7,8
。 R0指向5。执行以下指令得到的结果为
1 | foo(): |
4.7分支指令
分支指令可以分为无条件和条件分支两种
- 4.7.1 无条件分支
1 | B Label ;PC = Label |
举个例子
1 | foo(): |
B指令是最简单的跳转指令
B指令格式B{条件} 目标地址
B 指令是最简单的跳转指令。一旦遇到一个 B 指令,ARM 处理器将立即跳转到给定的目标地址,从那里继
续执行。注意存储在跳转指令中的实际值是相对当前PC 值的一个偏移量,而不是一个绝对地址,它的值由汇编器来计算(参考寻址方式中的相对寻址)
。它是 24 位有符号数,左移两位后有符号扩展为 32 位,表示的有效偏移为 26 位(前后32MB 的地址空间)。以下指令:
1 | B Label ;程序无条件跳转到标号 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 | cond flag |
在条件分支指令前会有一条数据操作指令来设置flag,分支指令根据flag的值来决定代码走向,举个例子:
1 | Label: |
ARM 调用规则
1、前言与后记
当一个函数调用另外一个函数时,常常需要传递参数和返回值;如何传递这些数据,称为ARM汇编的调用规则。在执行一块代码时,其前后栈地址应该是不变的
。这个操作是通过被执行代码块的前言(prologs)和后记(epilogs)完成的。前言所做的工作主要有:
- 将LR入栈;
- 将R7入栈;
- R7=SP;
- 将需要保留的寄存器原始值入栈;
- 为本地变量开辟空间;
后记工作:
后记所做的主要工作跟前言正好相反:
- 释放本地变量占用的空间;
- 将需要保留的寄存器原始值出栈;
- 将R7出栈;
- 将LR出栈,PC=LR。
前言和后记中的这些工作并不是必须的,如果这块代码压根儿就没有用到栈,就不需要”保留寄存器原始值”这一步了。
2、传递参数与返回值
“函数的前4个参数存放在R0到R3中,其他参数存放在栈中;返回值放在R0中”
分析一个例子
1 | // clang -arch armv7 -isysroot `xcrun --sdk iphoneos --show- |
上面的代码拉到IDA,展示出来的汇编代码是下面这样的
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 | exp:1、数值表达式(整数,范围-128到255)(注:此处有疑问) |
- Usage(用法):
1 | 如果有指令在DCB汇编器指令后面,则应该在该指令前使用ALIGN汇编器指令来保证该指令是对齐的 (DCB用在代码段中)。 |
- Example(例子):
1 | ARM汇编语言字符串和C语言字符串是不同的,不是以NULL结尾的。可以用下面这个例子构造类似C语言中的以NULL结尾的字符串: |
thumb 指令 IT
先来看个例子,IT 后面经常跟了个 EQ,例如
1 | CMP R5,#0 |
如果符合EQ,则执行 MOVEQ R2,R3。 IT指定了EQ作用域后面跟的一条语句
详解
1 | IT (If-Then) 指令由四条后续条件指令(IT 块)句组成。 这些条件可以完全相同,也可以互为逻辑反。 |
再来个例子:
1 | if (R4 == R5) |
汇编如下,下面的代码除了第一个指令,其余组成一个IT块:
1 | CMP R4, R5 |
IT语法:
1 | 语法 |
64位
64位上的特殊寄存器
1 | __uint64_t __x[29]; /* General purpose registers x0-x28 */ |
32为与64位的区别
1 |
|
从可用寄存器角度来说,程序员可以完全使用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 这个情况是要去看汇编了