Introduction to ARM64v8
Last updated
Last updated
学习和实践 AWS 黑客技术:HackTricks 培训 AWS 红队专家 (ARTE) 学习和实践 GCP 黑客技术:HackTricks 培训 GCP 红队专家 (GRTE)
在 ARMv8 架构中,执行级别称为异常级别(ELs),定义了执行环境的特权级别和能力。共有四个异常级别,从 EL0 到 EL3,每个级别有不同的用途:
EL0 - 用户模式:
这是特权级别最低的级别,用于执行常规应用程序代码。
在 EL0 运行的应用程序相互隔离,并与系统软件隔离,从而增强安全性和稳定性。
EL1 - 操作系统内核模式:
大多数操作系统内核在此级别运行。
EL1 的特权高于 EL0,可以访问系统资源,但有一些限制以确保系统完整性。
EL2 - 虚拟机监控器模式:
此级别用于虚拟化。在 EL2 运行的虚拟机监控器可以管理多个操作系统(每个操作系统在自己的 EL1 中)在同一物理硬件上运行。
EL2 提供了隔离和控制虚拟化环境的功能。
EL3 - 安全监控模式:
这是特权级别最高的级别,通常用于安全启动和受信执行环境。
EL3 可以管理和控制安全状态与非安全状态之间的访问(例如安全启动、受信操作系统等)。
使用这些级别可以以结构化和安全的方式管理系统的不同方面,从用户应用程序到最特权的系统软件。ARMv8 对特权级别的处理有助于有效隔离不同的系统组件,从而增强系统的安全性和稳健性。
ARM64 有 31 个通用寄存器,标记为 x0
到 x30
。每个寄存器可以存储一个 64 位(8 字节)值。对于只需要 32 位值的操作,可以使用 w0 到 w30 的名称以 32 位模式访问相同的寄存器。
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 位 版本。
此外,还有另外 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 包含多个进程组件,序列化到操作系统可见的 SPSR_ELx
特殊寄存器中,X 是触发异常的 权限 级别(这允许在异常结束时恢复进程状态)。
这些是可访问的字段:
N
、Z
、C
和 V
条件标志:
N
表示操作产生了负结果
Z
表示操作产生了零
C
表示操作产生了进位
V
表示操作产生了有符号溢出:
两个正数的和产生负结果。
两个负数的和产生正结果。
在减法中,当从较小的正数中减去较大的负数(或反之),且结果无法在给定位大小的范围内表示时。
显然,处理器不知道操作是有符号的还是无符号的,因此它将在操作中检查 C 和 V,并在发生进位的情况下指示。
并非所有指令都会更新这些标志。一些指令如 CMP
或 TST
会更新,其他带有 s 后缀的指令如 ADDS
也会更新。
当前 寄存器宽度 (nRW
) 标志:如果标志的值为 0,则程序在恢复后将以 AArch64 执行状态运行。
当前 异常级别 (EL
):在 EL0 中运行的常规程序将具有值 0。
单步执行 标志 (SS
):由调试器使用,通过在异常中将 SS 标志设置为 1 来单步执行。程序将执行一步并发出单步异常。
非法异常 状态标志 (IL
):用于标记特权软件执行无效异常级别转移时,此标志设置为 1,处理器触发非法状态异常。
DAIF
标志:这些标志允许特权程序选择性地屏蔽某些外部异常。
如果 A
为 1,则表示将触发 异步中止。I
配置为响应外部硬件 中断请求(IRQ)。F 与 快速中断请求(FIR)相关。
堆栈指针选择 标志 (SPS
):在 EL1 及以上运行的特权程序可以在使用自己的堆栈指针寄存器和用户模型之间切换(例如,在 SP_EL1
和 EL0
之间)。此切换通过写入 SPSel
特殊寄存器执行。此操作无法从 EL0 完成。
ARM64 调用约定规定,前八个参数通过寄存器 x0
到 x7
传递。额外参数通过 堆栈 传递。返回值通过寄存器 x0
返回,或者在 x1
中返回 如果其长度为 128 位。x19
到 x30
和 sp
寄存器必须在函数调用之间 保留。
在阅读汇编中的函数时,查找 函数序言和尾声。序言 通常涉及 保存帧指针(x29
)、设置 新的 帧指针 和 分配堆栈空间。尾声 通常涉及 恢复保存的帧指针 和 从函数返回。
Swift 有其自己的 调用约定,可以在 https://github.com/apple/swift/blob/main/docs/ABI/CallConvSummary.rst#arm64 中找到。
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。
预索引模式:这将对原始值应用计算,获取结果并将新原始值存储在原始值中。
ldr x2, [x1, #8]!
,这将加载 x1 + 8
到 x2
并将结果存储在 x1
中。
str lr, [sp, #-4]!
,将链接寄存器存储在 sp 中并更新寄存器 sp。
后索引模式:这类似于前一个,但内存地址被访问,然后计算并存储偏移。
ldr x0, [x1], #8
,加载 x1
到 x0
并用 x1 + 8
更新 x1
。
PC 相对寻址:在这种情况下,加载的地址相对于 PC 寄存器计算。
ldr x1, =_start
,这将加载 _start
符号开始的地址到 x1
,与当前 PC 相关。
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+16
更新 sp
。
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 0000。
adds
这执行一个 add
并更新标志。
sub
:减去两个寄存器的值并将结果存储在一个寄存器中。
检查 add
语法。
示例:sub x0, x1, x2
— 这将从 x1
中减去 x2
的值并将结果存储在 x0
中。
subs
这类似于减法,但更新标志。
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
从 X1 的第 3 位插入 X2 的 4 位。
BFXIL X1, X2, #3, #4
从 X2 的第 3 位提取 4 位并复制到 X1。
SBFIZ X1, X2, #3, #4
从 X2 中提取 4 位并插入到 X1,从第 3 位开始,右侧位清零。
SBFX X1, X2, #3, #4
从 X2 的第 3 位提取 4 位,进行符号扩展,并将结果放入 X1。
UBFIZ X1, X2, #3, #4
从 X2 中提取 4 位并插入到 X1,从第 3 位开始,右侧位清零。
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 的字节中,以填充 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
与 cmn
)。
tst
:检查比较的值是否都为 1(它的工作方式类似于不存储结果的 ANDS)。用于检查寄存器与值的比较,并检查寄存器中指示的任何位是否为 1。
示例:tst X1, #7
检查 X1 的最后 3 位是否有 1。
teq
:XOR 操作,丢弃结果。
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
:如果不相等则分支。此指令检查条件标志(由先前的比较指令设置),如果比较的值不相等,则分支到标签或地址。
示例:在 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 = X2。
csinc Xd, Xn, Xm, cond
-> 如果为真,Xd = Xn,如果为假,Xd = Xm + 1。
cinc Xd, Xn, cond
-> 如果为真,Xd = Xn + 1,如果为假,Xd = Xn。
csinv Xd, Xn, Xm, cond
-> 如果为真,Xd = Xn,如果为假,Xd = NOT(Xm)。
cinv Xd, Xn, cond
-> 如果为真,Xd = NOT(Xn),如果为假,Xd = Xn。
csneg Xd, Xn, Xm, cond
-> 如果为真,Xd = Xn,如果为假,Xd = - Xm。
cneg Xd, Xn, cond
-> 如果为真,Xd = - Xn,如果为假,Xd = Xn。
cset Xd, Xn, Xm, cond
-> 如果为真,Xd = 1,如果为假,Xd = 0。
csetm Xd, Xn, Xm, cond
-> 如果为真,Xd = <全 1>,如果为假,Xd = 0。
adrp
:计算 符号的页地址 并将其存储在寄存器中。
示例:adrp x0, symbol
— 这计算 symbol
的页地址并将其存储在 x0
中。
ldrsw
:加载一个有符号 32 位 值从内存并 符号扩展到 64 位。
示例:ldrsw x0, [x1]
— 这将从 x1
指向的内存位置加载一个有符号 32 位值,符号扩展到 64 位,并存储在 x0
中。
stur
:将寄存器值存储到内存位置,使用来自另一个寄存器的偏移。
示例:stur x0, [x1, #4]
— 这将值存储在 x0
中到比当前在 x1
中的地址大 4 字节的内存地址。
svc
: 进行 系统调用。它代表“监督调用”。当处理器执行此指令时,它 从用户模式切换到内核模式 并跳转到内存中 内核的系统调用处理 代码所在的特定位置。
示例:
将链接寄存器和帧指针保存到堆栈:
设置新的帧指针: mov x29, sp
(为当前函数设置新的帧指针)
在栈上为局部变量分配空间(如果需要): sub sp, sp, <size>
(其中 <size>
是所需的字节数)
释放局部变量(如果有分配的话): add sp, sp, <size>
恢复链接寄存器和帧指针:
返回: ret
(使用链接寄存器中的地址将控制权返回给调用者)
Armv8-A 支持执行 32 位程序。AArch32 可以在 两种指令集 中运行:A32
和 T32
,并可以通过 互操作
在它们之间切换。
特权 64 位程序可以通过执行异常级别转移到较低特权的 32 位程序来调度 32 位 程序的执行。
请注意,从 64 位到 32 位的过渡发生在异常级别降低时(例如,EL1 中的 64 位程序触发 EL0 中的程序)。这是通过在 AArch32
进程线程准备执行时将 SPSR_ELx
特殊寄存器的 第 4 位 设置为 1 来完成的,其余的 SPSR_ELx
存储 AArch32
程序的 CPSR。然后,特权进程调用 ERET
指令,以便处理器过渡到 AArch32
,根据 CPSR 进入 A32 或 T32。**
互操作
通过 CPSR 的 J 和 T 位发生。J=0
和 T=0
表示 A32
,而 J=0
和 T=1
表示 T32。这基本上意味着将 最低位设置为 1 以指示指令集为 T32。
这在 互操作分支指令 中设置,但也可以在 PC 设置为目标寄存器时通过其他指令直接设置。示例:
另一个示例:
有16个32位寄存器(r0-r15)。从r0到r14可以用于任何操作,但是其中一些通常是保留的:
r15
:程序计数器(始终)。包含下一条指令的地址。在A32中为当前 + 8,在T32中为当前 + 4。
r11
:帧指针
r12
:过程内调用寄存器
r13
:栈指针
r14
:链接寄存器
此外,寄存器在**银行寄存器
中备份。这些是存储寄存器值的地方,允许在异常处理和特权操作中执行快速上下文切换**,以避免每次都手动保存和恢复寄存器。
这是通过**将处理器状态从CPSR
保存到处理器模式的SPSR
来完成的。在异常返回时,CPSR
从SPSR
**恢复。
在AArch32中,CPSR的工作方式类似于**PSTATE
在AArch64中,并且在发生异常时也存储在SPSR_ELx
**中,以便稍后恢复执行:
字段分为几个组:
应用程序状态寄存器(APSR):算术标志,并可从EL0访问
执行状态寄存器:进程行为(由操作系统管理)。
N
、Z
、C
、V
标志(与AArch64相同)
Q
标志:在执行专用饱和算术指令期间,每当整数饱和发生时设置为1。一旦设置为**1
**,它将保持该值,直到手动设置为0。此外,没有任何指令隐式检查其值,必须手动读取。
GE
(大于或等于)标志:用于SIMD(单指令,多数据)操作,例如“并行加法”和“并行减法”。这些操作允许在单个指令中处理多个数据点。
例如,UADD8
指令并行添加四对字节(来自两个32位操作数),并将结果存储在32位寄存器中。然后根据这些结果设置GE
标志在APSR
中。每个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
配置为响应外部硬件中断请求(IRQ)。F与快速中断请求(FIR)相关。
查看syscalls.master。BSD系统调用将具有x16 > 0。
查看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)系统调用:
注意,Ida 和 Ghidra 也可以通过传递缓存来反编译 特定的 dylibs。
有时检查 libsystem_kernel.dylib
的 反编译 代码比检查 源代码 更容易,因为几个系统调用(BSD 和 Mach)的代码是通过脚本生成的(查看源代码中的注释),而在 dylib 中你可以找到被调用的内容。
XNU 支持另一种称为机器依赖的调用。这些调用的数量取决于架构,且调用或数量都不保证保持不变。
这是一个内核拥有的内存页面,映射到每个用户进程的地址空间中。它旨在使从用户模式到内核空间的转换比使用系统调用更快,因为这些内核服务的使用频率很高,这样的转换会非常低效。
例如,调用 gettimeofdate
直接从 comm 页面读取 timeval
的值。
在 Objective-C 或 Swift 程序中,找到这个函数是非常常见的。这个函数允许调用一个 Objective-C 对象的方法。
参数(文档中更多信息):
x0: self -> 指向实例的指针
x1: op -> 方法的选择器
x2... -> 被调用方法的其余参数
因此,如果在调用此函数的分支之前设置断点,你可以很容易地在 lldb 中找到被调用的内容(在这个例子中,对象调用了一个来自 NSConcreteTask
的对象,该对象将运行一个命令):
设置环境变量 NSObjCMessageLoggingEnabled=1
可以记录此函数在文件 /tmp/msgSends-pid
中被调用的情况。
此外,设置 OBJC_HELP=1
并调用任何二进制文件,您可以看到其他环境变量,您可以使用这些变量来 log 某些 Objc-C 操作发生时的情况。
当调用此函数时,需要找到所指实例的调用方法,为此进行不同的搜索:
执行乐观缓存查找:
如果成功,完成
获取 runtimeLock(读取)
如果 (realize && !cls->realized) 实现类
如果 (initialize && !cls->initialized) 初始化类
尝试类自己的缓存:
如果成功,完成
尝试类方法列表:
如果找到,填充缓存并完成
尝试超类缓存:
如果成功,完成
尝试超类方法列表:
如果找到,填充缓存并完成
如果 (resolver) 尝试方法解析器,并从类查找重复
如果仍然在这里(= 所有其他都失败了)尝试转发器
编译:
提取字节:
对于较新的 macOS:
学习与实践 AWS 黑客技术:HackTricks 培训 AWS 红队专家 (ARTE) 学习与实践 GCP 黑客技术:HackTricks 培训 GCP 红队专家 (GRTE)