Skip to content

Latest commit

 

History

History
441 lines (255 loc) · 16.6 KB

arm汇编.md

File metadata and controls

441 lines (255 loc) · 16.6 KB

汇编

一些前情提要的知识

  • 一个字节是两个十六进制的数字 FF 也就是八位
  • 一个指令是四个字节,也就是 32位(在 ARM32 当中,有ARM32 指令 32位,thumb 指令是两个字节 16位)在ARM64 当中已经被淘汰了
  • 并且指令是小端存储的,
  • ARM32 有 16 个 32 位的通用寄存器,而 ARM64 则有 31 个 64 位的通用寄存器
  • ARM32(也称为 ARM 或 AArch32)和 ARM64(也称为 AArch64)
  • Thumb 指令集的存在意义在于它提供了一种更高效的方式来编写和运行代码,特别是对于内存和存储空间有限,以及需要节省电力的系统
  • 在ARM 当中只有寄存器和内存的概念,有的地方会介绍存储器,存储器是一个宽泛的概念,任何存储数据的介质都可以称之为存储器,它既可以是指的是内存,也可以指的是寄存器
  • 一个字 32 位(32位的ARM)

总是记不住一个字节,一个字,一条指令多少位的可以看下面这个代码,帮助你记忆

  • 两个十六进制的数字排在一起就是一个字节,比方说:ff,要表示一个 f 我们知道要四个1 也就是 1111,那么两个 ff 就是 1111 1111,所以,一个字节就是八位
  • 四个字节就是一条指令,所以,一条指令就是四个字节,也就是32位
  • 在 ARM32 当中,一个寄存器是 32 位,CPU一次性能够处理一个寄存器,也就是说,ARM32 的一个字是32位,也就是,四个字节
  • 在 ARM64 当中,一个寄存器是 64 位,CPU一次性能够处理一个寄存器,也就是说,ARM64 的一个字是64位,也就是,八个字节
        00119bec ff 03 04 d1     sub        sp,sp,#0x100
        00119bf0 fd 7b 0f a9     stp        x29,x30,[sp, #local_10]
        00119bf4 fd c3 03 91     add        x29,sp,#0xf0
        00119bf8 a8 a3 01 d1     sub        x8,x29,#0x68
        00119bfc ea 04 80 92     mov        x10,#-0x28
        00119c00 eb 03 00 91     mov        x11,sp

B-branch(和跳转相关)

B 指令的格式为:

B{条件} 偏移地址

但是一般都是 B label

释义:B 指令是最简单的跳转指令。一旦遇到一个 B 指令,ARM 处理器将立即跳转到给定的目标地址,从那里继续执行。

注意:存储在跳转指令中的值是相对当前PC 值的一个偏移量,而不是一个绝对地址,它的值由汇编器来计算(参考寻址方式中的相对寻址)。它是 24 位有符号数,左移两位后有符号扩展为 32 位,表示的有效偏移为 26 位(前后32MB 的地址空间)。

关于这个的解释

首先他不是一个绝对的地址,这个很好理解,他只是一个针对 PC 指针的一个偏移

它是 24 位有符号数:关于这个解释是,一个指令比方说 BL 0xff 这是一条指令,他会占据一个字的空间。但是前面的 BL 会占据一个字节,那么留给后面的,数据的就只剩下了 3 个字节,也就是24位

为什么要左移两位:因为为了更大的寻址空间,左移两位就相当于 -> 原来的值 * 2二次方也就是扩大了四倍的寻址空。BL 后面的这个能够表示更多的相对地址。所以实际的表达空间的二进制的数字是 26 位,其中第一位(最左边)是符号位,也就是表达数字的是25位,大小是:33,554,432 B == 32768KB == 32MB

为什么左移两位相当于乘以4,在二进制系统中,向左移位相当于乘以2的幂。就像在十进制中,向左移位相当于乘以10的幂一样。例如,把十进制数123左移一位就变成了1230,相当于乘以10

左移两位之后,最右边的数值始终是 0 ,为什么能够表达连续的 -32MB ~32MB 的空间:因为我们 ARM 给地址的时候,根本就不是一个字节一个字节给地址的,而是一个字一个字的给地址,还记得吗,我们的 ARM32 一口气能够处理一个寄存器,一个寄存器就是32位,四个字节,第一个地址是 00 ,第二个地址就是 04 了详情请看下面的表格代码

0x0FF8: 44 33 22 11
0x0FFC: F0 DE BC 9A
0x1000: 78 56 34 12

例如以下指令:

B Label ;程序无条件跳转到标号 Label 处执行,所以一般后面跟的是一个,标签而不是一个具体的地址

BL 指令

BL 指令的格式为:

BL{条件} 目标地址

释义:BL 是另一个跳转指令,但跳转之前,会在寄存器R14 中保存 PC寄存器(程序计数器) 的当前内容,一般就是 BL 指令的下一条指令,因此,可以通过将R14 的内容重新加载到PC 中,来返回到跳转指令之后的那个指令处执行。该指令是实现子程序调用的一个基本但常用的手段。以下指令:

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

BX 指令

BX 指令的格式为:

BX{条件} 目标地址

释义:BX 指令跳转到指令中所指定的目标地址,目标地址处的指令既可以是ARM 指令,也可以是Thumb指令。也就是 arm 指令和 Thumb 指令的相互转换

BLX 指令

BLX 指令的格式为:

BLX 目标地址

释义:BLX 指令从ARM 指令集跳转到指令中所指定的目标地址,并将处理器的工作状态有ARM 状态(或者是从 Thumb 指令转换成 ARM 指令)切换到Thumb 状态,该指令同时将PC 的当前内容保存到寄存器R14 中。因此,当子程序使用Thumb 指令集,而调用者使用ARM 指令集时,可以通过BLX 指令实现子程序的调用和处理器工作状态的切换。

同时,子程序的返回可以通过将寄存器R14 值复制到PC 中来完成


LDR,STR(加载,存储)

LDR Rd, <address>
STR Rd, <address>

加载(load )就是从 存储器 加载到 寄存器

存储(store)就是从 寄存器 取值到 存储器

寄存器是比内存更加靠近 cpu 的存储介质,加载表示的是,马上要使用该数据,要将该数据存储在寄存器当中等待使用

存储器是用来存储数据处理的结果,自然就是寄存器到存储器

LDR:从内存当中加载数据到寄存器 ← Load from memory into a registor

LDR R8,[R9,#4] R8为待加载数据的寄存器,加载值为R9+0x4所指向的存储单元 R8=(R9+4)

LDR PC,[SP+4+var_4] 就是将 SP+4+var_4 所指向得数据 丢进 PC 的寄存器当中

R1 ← [R2]

将 R2 地址所指向的存储器得值 给到 R1 寄存器

  • LDR R0, [R1]:从内存地址 R1 加载一个字到 R0
  • LDR R0, [R1, #4]:从内存地址 R1+4 加载一个字到 R0
  • LDR R0, =0x12345678:将立即数 0x12345678 加载到 R0

对于 ARM64,指令可能稍有不同,例如使用 Xn 寄存器代替 Rn,或者使用 LDR 的变种如 LDRBLDRHLDRD 等来加载不同大小的数据。

  1. LDRB (Load Register Byte):该指令用于从指定的内存地址读取一个字节(8位)的数据,并将其加载到寄存器中。如果读取的字节是一个有符号数,它会被符号扩展到32位;如果是无符号数,它会被零扩展到32位。例如:LDRB R0, [R1] 会将 R1 寄存器存储的地址处的一个字节加载到 R0 寄存器。
  2. LDRH (Load Register Halfword):该指令用于从指定的内存地址读取一个半字(16位)的数据,并将其加载到寄存器中。就像 LDRB,读取的半字可以是有符号的,也可以是无符号的,它会被相应地扩展到32位。例如:LDRH R0, [R1] 会将 R1 寄存器存储的地址处的两个字节(半字)加载到 R0 寄存器。
  3. LDRD (Load Register Doubleword):该指令用于从指定的内存地址读取一个双字(64位)的数据,并将其加载到寄存器中。这在 ARM64 架构中特别有用,因为它支持64位寄存器。例如:LDRD X0, [X1] 会将 X1 寄存器存储的地址处的八个字节(双字)加载到 X0 寄存器。

STR:将寄存器的数据存储到内存当中 → Store from a register into memory

STR R8,[R9,#4] 将R8寄存器的数据存储到R9+0x4指向的存储单元 *(R9+4)=R8

R1 → [R2]

将 R1 寄存器的值 给到 存储器 R2 这个地址上

  1. STR R0, [R1]:这条指令会将 R0 寄存器的内容存储到由 R1 寄存器指定的内存地址。例如,如果 R0 中的值为 0x12345678R1 中的值为 0x1000,那么这条指令将会将 0x12345678 这个值存储到内存地址 0x1000 处。
  2. STR R3, [R4, #4]:这条指令会将 R3 寄存器的内容存储到 R4 寄存器指定的内存地址加上偏移量 4 的地方。例如,如果 R3 中的值为 0x87654321R4 中的值为 0x2000,那么这条指令将会将 0x87654321 这个值存储到内存地址 0x2004 处。


LDM,STM

LDM:将内存的数据依次加载到一个寄存器列表 → Load from memory into register

LDM R0,{R1-R3}将R0指向的存储单元的数据依次加载到R1,R2,R3寄存器

千万要注意,这个指令运行的方向和LDR是不一样的,是从左到右运行的

LDM <address>, Rd

其实就是是出栈操作;其中堆栈指针一般对应于SP,注意SP是寄存器R13,实际用到的却是R13中的内存地址,只是该指令没有写为[R13],同时,LDM指令中寄存器和内存地址的位置相对于前面两条指令改变了,下面的例子:

LDMFD SP! , {R0, R1, R2}

实际上可以理解为: LDMFD [SP]!, {R0, R1, R2}

(sp后面的!,作用是指命令执行完后,对应的地址值赋给sp,对于例程的SDM,是说最后sp的值应该是sp+3*4=sp+12)

注意这里的 LDMFD 和 LDMIA 都是说的,在执行完这个指令之后 R0 这个寄存器的内容会发生改变,为什么会发生改变,因为这样可以顺利的指向下一个数组的的内容。

R0! 的 !其实也是在表达同样的意思

如果你执行 LDMIA R0!, {R1-R3} 这个指令,并且 R0 存储的地址是 0x1000,那么我们可以设想内存中的数据如下:

地址     数据
-----------------
0x1000   0x11
0x1001   0x22
0x1002   0x33
0x1003   0x44

0x1004   0x55
0x1005   0x66
0x1006   0x77
0x1007   0x88

0x1008   0x99
0x1009   0xAA
0x100A   0xBB
0x100B   0xCC

执行 LDMIA R0!, {R1-R3} 指令之后,寄存器的内容会是这样的:

R1 = 0x44332211
R2 = 0x88776655
R3 = 0xCCBBAA99

你可以看到,每个寄存器中的数据都是按照小端方式从内存中加载的。同时,R0 的值也会增加,成为 0x100C,也就是指向了下一块未被加载的内存

but 在 ARM64 当中这一条指令被删除掉了,取而代之的是 LDP 指令

LDP :Load Pair of Registers

Pair 英语当中的就是 一对,一双 的意思

LDP X0, X1, [X2] ; 从内存地址 X2 处加载两个字(16字节)到寄存器 X0 和 X1

不过需要注意的是,LDP 指令一次只能加载两个寄存器,而 LDM 可以一次加载多个寄存器

如果需要加载多个,那么就需要写多个 LDP

STM:将一个寄存器列表的数据存储到指定的内存

STMFD SP!, R0

同样的,该指令也可理解为: STMFD [SP]!, R0

意思是:把R0保存到堆栈(sp指向的地址)中

比方说:STMIA SP!, {R0-R2}"

在开始执行指令之前,假设我们有以下的设置:

R0 = 0x12345678
R1 = 0x9ABCDEF0
R2 = 0x11223344
SP = 0x1000

首先,R0的值(0x12345678)被写入到SP指向的地址(0x1000)。SP被减小4变为0x0FFC。

内存:
0x0FFC: ?? ?? ?? ??
0x1000: 78 56 34 12

然后,R1的值(0x9ABCDEF0)被写入到新的SP地址(0x0FFC)。SP再次被减小4变为0x0FF8。

内存:
0x0FF8: ?? ?? ?? ??
0x0FFC: F0 DE BC 9A
0x1000: 78 56 34 12

最后,R2的值(0x11223344)被写入到再次更新的SP地址(0x0FF8)。SP最后被减小4变为0x0FF4。

内存:
0x0FF4: ?? ?? ?? ??
0x0FF8: 44 33 22 11
0x0FFC: F0 DE BC 9A
0x1000: 78 56 34 12

所以在执行完这个指令之后,SP的值变为了0x0FF4,内存的布局也被更新。注意这里的内存数据是以16进制并且按照小端顺序表示的 值得注意的是 :这个是向下生长的栈,才会出现,压栈之后。sp 的值变小。如果是向上生长的栈,则是相反的。至于这个是向哪个方向生长,这个主要是,系统决定的。android 是向下生长

但是,这个指令在 ARM64 当中同样被删除了,取而代之的,也是 STP ,他同样的只能够处理两个寄存器

STMIA R0!, {R1, R2, R3}

等效于 =》

STP R1, R2, [R0], #16
STR R3, [R0], #8

第一条指令会将R1和R2的内容存入R0指向的内存,然后将R0增加16(因为每个寄存器在ARM64中占8字节)。然后,第二条指令将R3的内容存入新的R0指向的内存,并将R0增加8。

为什么这里是增加 16 和 8 ,因为,这个ARM64

还有一种写法

STP Rt1, Rt2, [Rn, #imm]!

给出两个例子,感受一下他们的区别

STP R1, R2, [R0, #16]! 这个指令是一个 pre-indexed 指令,表示先将基址寄存器 R0 的值加上偏移量 #16,然后把 R1R2 的内容存储在新的 R0 所指向的地址。感叹号 "!" 在这里表示的就是先进行地址的更新,然后再进行存储。

这是这个操作的一个具体的例子:

  1. 开始时: R0 = 0x1000 R1 = 0x123456789ABCDEF0 R2 = 0x0FEDCBA987654321

  2. 执行 STP R1, R2, [R0, #16]! 之后:

    假设这是一个64位系统,那么每个寄存器的大小为8字节。

    地址 数据(每个数据项为8字节,存储按照小端方式)
    0x1000 不确定
    0x1008 不确定
    0x1010 F0 DE BC 9A 78 56 34 12 (R1 的内容,小端模式)
    0x1018 21 43 65 87 A9 CB ED 0F (R2 的内容,小端模式)

    同时,R0 的值也会被更新为新的地址 0x1010(即原始的 R0 值加上偏移量 16)。

STP R1, R2, [R0], #16 这个指令是一个 post-indexed 指令,表示先将 R1R2 的内容存储在 R0 所指向的地址,然后再将基址寄存器 R0 的值加上偏移量 #16。逗号后的 "#16" 表示的就是存储后更新地址。

这是这个操作的一个具体的例子:

  1. 开始时: R0 = 0x1000 R1 = 0x123456789ABCDEF0 R2 = 0x0FEDCBA987654321

  2. 执行 STP R1, R2, [R0], #16 之后:

    假设这是一个64位系统,那么每个寄存器的大小为8字节。

    地址 数据(每个数据项为8字节,存储按照小端方式)
    0x1000 F0 DE BC 9A 78 56 34 12 (R1 的内容,小端模式)
    0x1008 21 43 65 87 A9 CB ED 0F (R2 的内容,小端模式)
    0x1010 不确定
    0x1018 不确定

    同时,R0 的值也会被更新为新的地址 0x1010(即原始的 R0 值加上偏移量 16)。

这样你就可以看到,这两种指令的主要区别在于地址更新的时机,一个是先更新地址再存储,一个是先存储再更新地址


PUSH,POP

push 寄存器:将一个寄存器中的数据入栈

pop 寄存器:出栈一个寄存器接收数据

MOVE

MOV:将立即数或寄存器的数据传送到目标寄存器 ← MOV R0, #8 R0=8

算术运算符

ADD,SUB,MUL,DIV 有符号,无符号运算;带进位运算

逻辑运算符

与:AND 全1出1 或:ORR 有1出1 异或:EOR 相同为0,不同为1 移位:实质是乘,除,类似于小数点移位,但相反。小数点左移,数变小;右移变大。 但逻辑移位,左移变大,右移变小,且按2的倍数进行,因为是2进制。 LSL:逻辑左移← LSR:逻辑右移←

比较指令

CMP:比较 CMP R0 #0 R0寄存器中的值与0比较 标志位:如z位,这个都可以在动态调试时,寄存器窗口看到

存储器

存储数据用的,小数据用寄存器,大数据用堆栈

例子: PUSH { $R_0$ ~ $R_7$ ,LR} ; 将低寄存器 $R_0$ ~ $R_7$ 全部入栈,LR也入栈

POP { $R_0$ ~ $R_7$ ,PC} ; 将堆栈中的数据弹出到低寄存器 $R_0$ ~ $R_7$ 以及 PC 当中