Introduction to ARM64v8
异常级别 - EL(ARM64v8)
在ARMv8架构中,执行级别称为异常级别(ELs),定义了执行环境的特权级别和功能。有四个异常级别,从EL0到EL3,每个都有不同的目的:
EL0 - 用户模式:
这是最低特权级别,用于执行常规应用程序代码。
在EL0上运行的应用程序彼此之间以及与系统软件隔离,增强安全性和稳定性。
EL1 - 操作系统内核模式:
大多数操作系统内核在此级别运行。
EL1比EL0具有更多特权,并且可以访问系统资源,但受一些限制以确保系统完整性。
EL2 - 虚拟化模式:
此级别用于虚拟化。在EL2上运行的虚拟机监视程序可以管理在同一物理硬件上运行的多个操作系统(每个操作系统在其自己的EL1中)。
EL2提供了用于隔离和控制虚拟化环境的功能。
EL3 - 安全监视器模式:
这是最高特权级别,通常用于安全引导和可信执行环境。
EL3可以管理和控制安全和非安全状态之间的访问(例如安全引导、可信操作系统等)。
使用这些级别可以以结构化和安全的方式管理系统的不同方面,从用户应用程序到最高特权系统软件。ARMv8对特权级别的处理有助于有效隔离不同系统组件,从而增强系统的安全性和稳健性。
寄存器(ARM64v8)
ARM64有31个通用寄存器,标记为x0
到x30
。每个寄存器可以存储64位(8字节)值。对于需要仅使用32位值的操作,可以使用相同的寄存器以32位模式访问,名称为w0到w30。
x0
到x7
- 这些通常用作临时寄存器和用于向子例程传递参数。
x0
还携带函数的返回数据
x8
- 在Linux内核中,x8
用作svc
指令的系统调用号。在macOS中使用x16!x9
到x15
- 更多临时寄存器,通常用于局部变量。x16
和x17
- 过程内调用寄存器。用于立即值的临时寄存器。它们还用于间接函数调用和PLT(过程链接表)存根。
x16
在macOS中用作**svc
指令的系统调用号**。
x18
- 平台寄存器。它可以用作通用寄存器,但在某些平台上,此寄存器保留用于特定于平台的用途:在Windows中用作指向当前线程环境块的指针,在Linux内核中用于指向当前执行任务结构。x19
到x28
- 这些是被调用者保存的寄存器。函数必须保留这些寄存器的值供调用者使用,因此它们存储在堆栈中,并在返回给调用者之前恢复。x29
- 帧指针 用于跟踪堆栈帧。当由于调用函数而创建新的堆栈帧时,x29
寄存器被存储在堆栈中,并且新的帧指针地址(sp
地址)被存储在此寄存器中。
尽管通常用作局部变量的引用,但此寄存器也可以用作通用寄存器。
x30
或lr
- 链接寄存器。在执行BL
(带链接的分支)或BLR
(带链接到寄存器的分支)指令时,通过将**pc
值存储在此寄存器中来保存返回地址**。
它也可以像其他寄存器一样使用。
如果当前函数将调用新函数,因此覆盖
lr
,它将在堆栈顶部存储它,这是尾声(stp x29, x30 , [sp, #-48]; mov x29, sp
-> 存储fp
和lr
,生成空间并获取新的fp
),并在最后恢复它,这是序言(ldp x29, x30, [sp], #48; ret
-> 恢复fp
和lr
并返回)。
sp
- 堆栈指针,用于跟踪堆栈顶部。
sp
值应始终保持至少四字对齐,否则可能会发生对齐异常。
pc
- 程序计数器,指向下一条指令。此寄存器只能通过异常生成、异常返回和分支更新。唯一可以读取此寄存器的普通指令是带链接的分支指令(BL、BLR),以将**pc
地址存储在lr
**(链接寄存器)中。xzr
- 零寄存器。在其32位寄存器形式中也称为**wzr
。可用于轻松获取零值(常见操作)或使用subs
执行比较,例如subs XZR, Xn, #10
将结果数据存储在任何地方(在xzr
**中)。
Wn
寄存器是Xn
寄存器的32位版本。
SIMD和浮点寄存器
此外,还有另外32个长度为128位的寄存器,可用于优化的单指令多数据(SIMD)操作和执行浮点运算。这些称为Vn寄存器,尽管它们也可以在64位、32位、16位和8位上操作,然后称为**Qn
、Dn
、Sn
、Hn
和Bn
**。
系统寄存器
有数百个系统寄存器,也称为特殊目的寄存器(SPRs),用于监视和控制 处理器的行为。
它们只能使用专用的特殊指令**mrs
和msr
**来读取或设置。
特殊寄存器**TPIDR_EL0
和TPIDDR_EL0
在逆向工程中经常被发现。EL0
后缀表示可以访问寄存器的最小异常**(在这种情况下,EL0是常规程序运行的异常(特权)级别)。
它们通常用于存储内存中线程本地存储区域的基地址。通常第一个对于在EL0中运行的程序是可读写的,但第二个可以从EL0中读取并从EL1(如内核)中写入。
mrs x0, TPIDR_EL0 ; 将 TPIDR_EL0 读入 x0
msr TPIDR_EL0, X0 ; 将 x0 写入 TPIDR_EL0
PSTATE
PSTATE 包含几个进程组件序列化到操作系统可见的**SPSR_ELx
特殊寄存器中,其中 X 是触发的异常的权限** **级别(这允许在异常结束时恢复进程状态)。
这些是可访问的字段:
N
、Z
、C
和V
条件标志:N
表示操作产生了负结果Z
表示操作产生了零C
表示操作进行了进位V
表示操作产生了有符号溢出:两个正数的和产生负结果。
两个负数的和产生正结果。
在减法中,当从较小的正数(或反之)中减去一个较大的负数,并且结果无法在给定位大小的范围内表示时。
显然,处理器不知道操作是有符号的还是无符号的,因此它将在操作中检查 C 和 V,并指示是否发生了进位。
并非所有指令都会更新这些标志。一些像**CMP
或TST
的指令会更新,而像ADDS
**这样带有 s 后缀的指令也会更新。
当前的寄存器宽度 (
nRW
) 标志:如果标志的值为 0,则程序在恢复后将在 AArch64 执行状态下运行。当前的异常级别(
EL
):在 EL0 中运行的常规程序将具有值 0单步执行标志(
SS
):调试器使用此标志进行单步执行,通过异常将 SS 标志设置为 1。程序将运行一步并发出单步执行异常。非法异常状态标志(
IL
):用于标记特权软件执行无效的异常级别转移时,此标志设置为 1,处理器触发非法状态异常。DAIF
标志:这些标志允许特权程序有选择地屏蔽某些外部异常。如果
A
为 1,则会触发异步中止。I
配置为响应外部硬件中断请求(IRQs)。而 F 与快速中断请求(FIRs)有关。堆栈指针选择标志(
SPS
):在 EL1 及以上运行的特权程序可以在使用自己的堆栈指针寄存器和用户模型之间进行切换(例如,在SP_EL1
和EL0
之间)。这种切换是通过写入**SPSel
**特殊寄存器来执行的。无法从 EL0 中执行此操作。
调用约定(ARM64v8)
ARM64 调用约定指定函数的前八个参数通过寄存器**x0
到 x7
传递。额外的参数通过堆栈传递。返回值通过寄存器x0
传回,如果其长度为 128 位,则也可以在x1
中传回。必须在函数调用之间保留x19
到x30
和sp
**寄存器。
在汇编中阅读函数时,查找函数序言和尾声。序言通常涉及保存帧指针(x29
),设置新的帧指针,以及分配堆栈空间。尾声通常涉及恢复保存的帧指针和从函数返回。
Swift 中的调用约定
Swift 有其自己的调用约定,可以在https://github.com/apple/swift/blob/main/docs/ABI/CallConvSummary.rst#arm64中找到。
常见指令(ARM64v8)
ARM64 指令通常具有格式 opcode dst, src1, src2
,其中**opcode
是要执行的操作**(如 add
、sub
、mov
等),dst
是将存储结果的目标寄存器,src1
和 src2
是源寄存器。也可以使用立即值代替源寄存器。
mov
:将一个值从一个寄存器移动到另一个寄存器。示例:
mov x0, x1
— 这将从x1
移动的值到x0
。ldr
:将内存中的值加载到寄存器中。示例:
ldr x0, [x1]
— 这将从x1
指向的内存位置加载的值到x0
。偏移模式:指示影响原始指针的偏移量,例如:
ldr x2, [x1, #8]
,这将在 x1 + 8 中加载 x2 的值ldr x2, [x0, x1, lsl #2]
,这将在数组 x0 中的位置 x1(索引)* 4 中加载 x2 的对象预索引模式:这将对原始进行计算,获取结果并将新原始存储在原始中。
ldr x2, [x1, #8]!
,这将在x1 + 8
中加载x2
并将x1 + 8
的结果存储在x1
中str lr, [sp, #-4]!
,将链接寄存器存储在 sp 中并更新寄存器 sp后索引模式:与前一个类似,但是首先访问内存地址,然后计算并存储偏移量。
ldr x0, [x1], #8
,加载x1
到x0
并更新x1
为x1 + 8
相对于 PC 的寻址:在这种情况下,相对于 PC 寄存器计算要加载的地址
ldr x1, =_start
,这将加载_start
符号开始的地址与当前 PC 相关的 x1 中。str
:将寄存器中的值存储到内存中。示例:
str x0, [x1]
— 这将x0
中的值存储到x1
指向的内存位置。ldp
:加载一对寄存器。此指令从连续内存位置加载两个寄存器。内存地址通常是通过将偏移量添加到另一个寄存器中形成的。示例:
ldp x0, x1, [x2]
— 这从x2
和x2 + 8
处的内存位置分别加载x0
和x1
。stp
:存储一对寄存器。此指令将两个寄存器存储到连续内存位置。内存地址通常是通过将偏移量添加到另一个寄存器中形成的。示例:
stp x0, x1, [sp]
— 这将x0
和x1
存储到sp
和sp + 8
处的内存位置。stp x0, x1, [sp, #16]!
— 这将x0
和x1
存储到sp+16
和sp + 24
处的内存位置,并将sp
更新为sp+16
。add
:将两个寄存器的值相加并将结果存储在一个寄存器中。语法:add(s) Xn1, Xn2, Xn3 | #imm, [shift #N | RRX]
Xn1 -> 目的地
Xn2 -> 操作数1
Xn3 | #imm -> 操作数2(寄存器或立即数)
[shift #N | RRX] -> 执行移位或调用RRX
示例:
add x0, x1, x2
— 这将把x1
和x2
的值相加,并将结果存储在x0
中。add x5, x5, #1, lsl #12
— 这等于4096(1左移12位) -> 1 0000 0000 0000 0000adds
这执行一个add
并更新标志位sub
:从两个寄存器中减去值并将结果存储在一个寄存器中。检查**
add
的语法**。示例:
sub x0, x1, x2
— 这将从x1
中减去x2
的值,并将结果存储在x0
中。subs
这类似于sub但会更新标志位mul
:将两个寄存器的值相乘并将结果存储在一个寄存器中。示例:
mul x0, x1, x2
— 这将把x1
和x2
的值相乘,并将结果存储在x0
中。div
:将一个寄存器的值除以另一个寄存器的值,并将结果存储在一个寄存器中。示例:
div x0, x1, x2
— 这将把x1
的值除以x2
的值,并将结果存储在x0
中。lsl
、lsr
、asr
、ror
、rrx
:逻辑左移:从末尾添加0,将其他位向前移动(乘以n次2)
逻辑右移:在开始添加1,将其他位向后移动(在无符号情况下除以n次2)
算术右移:类似于**
lsr
**,但如果最高有效位是1,则添加1(在有符号情况下除以n次2)右旋转:类似于**
lsr
**,但从右侧移除的内容会添加到左侧带扩展的右旋转:类似于**
ror
**,但将进位标志作为“最高有效位”。因此,将进位标志移动到第31位,将移除的位移动到进位标志。bfm
:位字段移动,这些操作将从一个值中复制位0...n
并将其放置在位置m..m+n
。**#s
指定最左边的位位置,#r
**指定右旋转量。位字段移动:
BFM Xd, Xn, #r
有符号位字段移动:
SBFM Xd, Xn, #r, #s
无符号位字段移动:
UBFM Xd, Xn, #r, #s
位字段提取和插入:从一个寄存器中复制位字段并将其复制到另一个寄存器中。
BFI X1, X2, #3, #4
从X2的第3位插入4位到X1BFXIL X1, X2, #3, #4
从X2的第3位提取四位并将其复制到X1SBFIZ X1, X2, #3, #4
从X2中扩展4位并从第3位开始插入X1,将右侧位清零SBFX X1, X2, #3, #4
从X2的第3位开始提取4位,进行符号扩展,并将结果放入X1UBFIZ X1, X2, #3, #4
从X2中扩展4位并从第3位开始插入X1,将右侧位清零UBFX X1, X2, #3, #4
从X2的第3位开始提取4位,并将零扩展的结果放入X1。符号扩展至X:扩展值的符号(或在无符号版本中仅添加0)以便对其进行操作:
SXTB X1, W2
从W2扩展一个字节的符号到X1(W2
是X2
的一半)以填充64位SXTH X1, W2
从W2扩展一个16位数的符号到X1以填充64位SXTW X1, W2
从W2扩展一个字节的符号到X1以填充64位UXTB X1, W2
添加0(无符号)到W2的一个字节到X1以填充64位extr
:从指定的连接的一对寄存器中提取位。示例:
EXTR W3, W2, W1, #3
这将连接W1+W2并从W2的第3位到W1的第3位获取并将其存储在W3中。cmp
:比较两个寄存器并设置条件标志。它是subs
的别名,将目标寄存器设置为零寄存器。用于判断m == n
。支持与
subs
相同的语法示例:
cmp x0, x1
— 这将比较x0
和x1
的值,并相应地设置条件标志。cmn
:比较负数操作数。在这种情况下,它是adds
的别名并支持相同的语法。用于判断m == -n
。ccmp
:条件比较,仅在先前的比较为真时执行比较,并明确设置nzcv位。cmp x1, x2; ccmp x3, x4, 0, NE; blt _func
-> 如果x1 != x2且x3 < x4,则跳转到func这是因为**
ccmp
仅在先前的cmp
为NE
时执行**,如果不是,则位nzcv
将设置为0(不满足blt
比较)。这也可以用作
ccmn
(与cmp
相同,但是负数,类似于cmp
与cmn
)。tst
:检查比较的值是否都为1(类似于ANDS,但不会在任何地方存储结果)。用于检查一个寄存器与一个值,并检查值中指定的寄存器的任何位是否为1。示例:
tst X1, #7
检查X1的最后3位中是否有任何位为1teq
:执行异或操作并丢弃结果b
:无条件跳转示例:
b myFunction
请注意,这不会将链接寄存器填充为返回地址(不适用于需要返回的子程序调用)
bl
:带链接的分支,用于调用子程序。将返回地址存储在x30
中。示例:
bl myFunction
— 这将调用函数myFunction
并将返回地址存储在x30
中。请注意,这不会将链接寄存器填充为返回地址(不适用于需要返回的子程序调用)
blr
:带链接到寄存器的分支,用于调用寄存器中指定的目标的子程序。将返回地址存储在x30
中。(这是示例:
blr x1
— 这将调用地址包含在x1
中的函数,并将返回地址存储在x30
中。ret
:从子程序返回,通常使用**x30
**中的地址。示例:
ret
— 这将使用x30
中的返回地址从当前子程序返回。b.<cond>
:条件分支b.eq
:如果相等则跳转,基于先前的cmp
指令。示例:
b.eq label
— 如果先前的cmp
指令找到两个相等的值,则跳转到label
。b.ne
: Branch if Not Equal. 这个指令检查条件标志(由先前的比较指令设置),如果比较的值不相等,则跳转到一个标签或地址。示例:在
cmp x0, x1
指令之后,b.ne label
— 如果x0
和x1
中的值不相等,则跳转到label
。cbz
: 比较并在零时跳转。这个指令将一个寄存器与零进行比较,如果它们相等,则跳转到一个标签或地址。示例:
cbz x0, label
— 如果x0
中的值为零,则跳转到label
。cbnz
: 比较并在非零时跳转。这个指令将一个寄存器与零进行比较,如果它们不相等,则跳转到一个标签或地址。示例:
cbnz x0, label
— 如果x0
中的值为非零,则跳转到label
。tbnz
: 测试位并在非零时跳转示例:
tbnz x0, #8, label
tbz
: 测试位并在零时跳转示例:
tbz x0, #8, label
条件选择操作:这些是根据条件位不同而行为不同的操作。
csel Xd, Xn, Xm, cond
->csel X0, X1, X2, EQ
-> 如果为真,X0 = X1,如果为假,X0 = X2csinc Xd, Xn, Xm, cond
-> 如果为真,Xd = Xn,如果为假,Xd = Xm + 1cinc Xd, Xn, cond
-> 如果为真,Xd = Xn + 1,如果为假,Xd = Xncsinv Xd, Xn, Xm, cond
-> 如果为真,Xd = Xn,如果为假,Xd = NOT(Xm)cinv Xd, Xn, cond
-> 如果为真,Xd = NOT(Xn),如果为假,Xd = Xncsneg Xd, Xn, Xm, cond
-> 如果为真,Xd = Xn,如果为假,Xd = - Xmcneg Xd, Xn, cond
-> 如果为真,Xd = - Xn,如果为假,Xd = Xncset Xd, Xn, Xm, cond
-> 如果为真,Xd = 1,如果为假,Xd = 0csetm Xd, Xn, Xm, cond
-> 如果为真,Xd = <all 1>,如果为假,Xd = 0adrp
: 计算一个符号的页地址并将其存储在一个寄存器中。示例:
adrp x0, symbol
— 这将计算symbol
的页地址并将其存储在x0
中。ldrsw
: 从内存中加载一个带符号的32位值并将其符号扩展为64位。示例:
ldrsw x0, [x1]
— 这将从由x1
指向的内存位置加载一个带符号的32位值,将其符号扩展为64位,并将其存储在x0
中。stur
: 将寄存器值存储到内存位置,使用另一个寄存器的偏移量。示例:
stur x0, [x1, #4]
— 这将把x0
中的值存储到比x1
当前地址大4个字节的内存地址中。svc
:进行一个系统调用。它代表"Supervisor Call"。当处理器执行这个指令时,它会从用户模式切换到内核模式,并跳转到内存中内核系统调用处理代码所在的特定位置。示例:
函数序言
将链接寄存器和帧指针保存到堆栈中:
设置新的帧指针:
mov x29, sp
(为当前函数设置新的帧指针)为局部变量在栈上分配空间(如果需要):
sub sp, sp, <size>
(其中<size>
是所需字节数)
函数尾声
释放局部变量(如果有分配的):
add sp, sp, <size>
恢复链接寄存器和帧指针:
返回:
ret
(使用链接寄存器中的地址将控制返回给调用者)
AARCH32 执行状态
Armv8-A 支持执行 32 位程序。AArch32 可以在 两种指令集之一中运行:A32
和 T32
,并可以通过 interworking
在它们之间切换。
特权的 64 位程序可以通过执行将执行权转移给较低特权的 32 位程序的例外级别转移来调度 执行 32 位 程序。
请注意,从 64 位到 32 位的过渡发生在较低的异常级别(例如,EL1 中的 64 位程序触发 EL0 中的程序)。这是通过在 AArch32
进程线程准备执行时将 SPSR_ELx
特殊寄存器的 第 4 位设置为 1 来完成的,而 SPSR_ELx
的其余部分存储了 AArch32
程序的 CPSR。然后,特权进程调用 ERET
指令,使处理器转换到 AArch32
进入 A32 或 T32 取决于 CPSR**。**
interworking
使用 CPSR 的 J 和 T 位。J=0
和 T=0
表示 A32
,J=0
和 T=1
表示 T32。这基本上意味着将 最低位设置为 1 以指示指令集为 T32。
这是在 interworking 分支指令 中设置的,但也可以直接使用其他指令设置,当 PC 被设置为目标寄存器时。示例:
另一个示例:
寄存器
有16个32位寄存器(r0-r15)。从r0到r14它们可以用于任何操作,但其中一些通常被保留:
r15
:程序计数器(始终)。包含下一条指令的地址。在A32中为当前 + 8,在T32中为当前 + 4。r11
:帧指针r12
:函数内调用寄存器r13
:堆栈指针r14
:链接寄存器
此外,寄存器在**banked registries
中备份。这些地方存储寄存器的值,允许在异常处理和特权操作中执行快速上下文切换**,避免每次都需要手动保存和恢复寄存器。
这是通过**将处理器状态从CPSR
保存到所采取的处理器模式的SPSR
**来完成的。在异常返回时,从SPSR
恢复CPSR
。
CPSR - 当前程序状态寄存器
在AArch32中,CPSR的工作方式类似于AArch64中的**PSTATE
,当发生异常时也存储在SPSR_ELx
**中以便稍后恢复执行:
这些字段分为一些组:
应用程序状态寄存器(APSR):算术标志,可从EL0访问
执行状态寄存器:进程行为(由操作系统管理)。
应用程序状态寄存器(APSR)
N
,Z
,C
,**V
**标志(就像在AArch64中一样)Q
标志:在执行专门的饱和算术指令时,整数饱和发生时将其设置为1。一旦设置为1
,它将保持该值,直到手动设置为0。此外,没有任何隐式检查其值的指令,必须通过手动读取来完成。GE
(大于或等于)标志:用于SIMD(单指令,多数据)操作,例如“并行加法”和“并行减法”。这些操作允许在单个指令中处理多个数据点。
例如,UADD8
指令并行添加四对字节(来自两个32位操作数),并将结果存储在32位寄存器中。然后,基于这些结果,它在APSR
中设置GE
标志。每个GE标志对应于一个字节加法,指示该字节对的加法是否溢出。
**SEL
**指令使用这些GE标志执行条件操作。
执行状态寄存器
**
J
和T
位:J
应为0,如果T
**为0,则使用指令集A32,如果为1,则使用T32。IT块状态寄存器(
ITSTATE
):这些是位10-15和25-26。它们存储**IT
**前缀组内指令的条件。E
位:指示字节序。模式和异常掩码位(0-4):它们确定当前的执行状态。第5个指示程序是否以32位(1)或64位(0)运行。其他4个表示当前正在使用的异常模式(当发生异常并正在处理时)。设置的数字表示在处理此异常时触发另一个异常时的当前优先级。
AIF
:可以使用位**A
,I
,F
禁用某些异常。如果A
为1,则表示将触发异步中止**。I
配置为响应外部硬件中断请求(IRQs)。F与快速中断请求(FIRs)有关。
macOS
BSD系统调用
查看syscalls.master。BSD系统调用将具有x16 > 0。
Mach陷阱
在syscall_sw.c中查看mach_trap_table
,在mach_traps.h中查看原型。Mach陷阱的最大数量是MACH_TRAP_TABLE_COUNT
= 128。Mach陷阱将具有x16 < 0,因此您需要使用负号调用前一个列表中的数字:_kernelrpc_mach_vm_allocate_trap
是-10
。
您还可以在反汇编器中检查**libsystem_kernel.dylib
**,以找出如何调用这些(以及BSD)系统调用:
有时候检查来自libsystem_kernel.dylib
的反编译代码比检查源代码更容易,因为几个系统调用(BSD和Mach)的代码是通过脚本生成的(请检查源代码中的注释),而在dylib中,您可以找到正在调用的内容。
machdep 调用
XNU支持另一种称为机器相关调用的调用类型。这些调用的数量取决于架构,调用或数量都不保证保持恒定。
comm page
这是一个内核所有者内存页面,映射到每个用户进程的地址空间中。它旨在使从用户模式到内核空间的转换比使用内核服务的系统调用更快,因为这些服务被使用得如此频繁,以至于这种转换将非常低效。
例如,调用gettimeofdate
直接从comm页面读取timeval
的值。
objc_msgSend
在Objective-C或Swift程序中经常会找到此函数的使用。此函数允许调用Objective-C对象的方法。
参数(更多信息请参阅文档):
x0:self -> 实例指针
x1:op -> 方法的选择器
x2... -> 调用方法的其余参数
因此,如果在调用此函数之前设置断点,您可以轻松地在lldb中找到调用的内容(在此示例中,对象调用NSConcreteTask
中的对象,该对象将运行一个命令)。
Shellcodes
编译:
提取字节:
对于更新的 macOS:
最后更新于