免責聲明
这项工作是在我们在 Longterm Security 工作时完成的,他们友好地允许我们在公司博客上镜像原始文章。
Samsung NPU的Reversing与 Exploiting (第一部分上)
免責聲明
这项工作是在我们在 Longterm Security 工作时完成的,他们友好地允许我们在公司博客上镜像原始文章。
本系列博客文章旨在描述和解释三星片上系统最近增加的内部结构,即其神经处理单元。第一部分深入探讨了 NPU 的内部结构,第二部分重点介绍了我们在实现中发现的一些漏洞的利用。如果您有兴趣逆转最小的操作系统,想要了解 Android 如何与外围设备交互并像 2000 年代初一样进行利用,那么本系列可能适合您。
目录
介绍
环境
NPU驱动初始化和NPU固件加载
NPU驱动加载
NPU 驱动程序电源管理和固件加载
固件提取和逆向工程
NPU操作系统
优先组
调度算法
使用调度程序
准备清单
延迟名单
待定名单
NPU固件初始化
主要功能
堆
缓存、异常和中断
任务
调度
定时器
事件
信号灯
沟通渠道
运行系统
与NPU交互
邮箱控件
向下邮箱:接收来自 AP 的邮件
向上邮箱:向 AP 发送消息
在内核和NPU之间共享资源
从内核到 NPU
在 NPU 中处理命令
从 NPU 回到内核
结论
引用
神经处理器或神经处理单元 (NPU) 是一种专用电路,用于实现执行机器学习算法所需的所有必要控制和算术逻辑,通常通过对人工神经网络 (ANN) 或随机森林 (RF) 等预测模型进行操作。
来源:https://en.wikichip.org/wiki/neural_processor
在撰写本文时,三星已经发布了四款嵌入 NPU 的 SoC:
Exynos 9820
埃克西诺斯 9825
Exynos 980 系列
埃克西诺斯 990
本文主要关注Galaxy S990设备上的Exynos 20;虽然,这里讨论的大多数概念也适用于其他 SoC。
SoC 上的额外芯片通常意味着专用固件,因此攻击面更大。Google Project Zero 披露并利用了影响三星 NPU 驱动程序的漏洞,并指出可以通过 SELinux 上下文访问该漏洞。这些问题现已得到修补,可以访问 NPU 的 SELinux 上下文更加严格。untrusted_app
但是,目前没有关于实际的 NPU 固件实现、它如何工作以及它如何与系统其余部分交互的信息。这篇博文试图回答其中的一些问题。
此分析是使用固件G980FXXS5CTL5在已扎根的三星 Galaxy S20 上进行的。对于设备进行root操作很重要,因为我们希望访问NPU驱动程序(自Project Zero披露以来,在最新版本的shell中无法做到这一点)。SM-G980F
dmesg
三星在他们开发的组件中提供调试信息的情况并不少见,NPU 也不例外。NPU 驱动程序和固件的登录都非常详细,如下所示。dmesg
x1s:/ # sysctl -w kernel.kptr_restrict=1
x1s:/ # dmesg -w | grep "NPU:"
[102.037911] [Exynos][NPU][NOTICE]: NPU:[*]npu_debug_open(221):start in npu_debug open
[102.037928] [Exynos][NPU][NOTICE]: NPU:[*]npu_debug_open(222):complete in npu_debug open
[102.037936] [Exynos][NPU][NOTICE]: NPU:[*]npu_log_open(1335):start in npu_log_open
[102.037943] [Exynos][NPU][NOTICE]: NPU:[*]npu_log_open(1336):complete in npu_log_open
[102.037951] [Exynos][NPU][NOTICE]: NPU:[*]npu_util_memdump_open(319):start in npu_util_memdump_open
[102.037958] [Exynos][NPU][NOTICE]: NPU:[*]npu_util_memdump_open(344):complete in npu_util_memdump_open
[102.037966] [Exynos][NPU][NOTICE]: NPU:[*]npu_scheduler_open(1458):done
[102.039801] [Exynos][NPU][NOTICE]: NPU:[*]npu_system_resume(387):wake_lock, now(1)
[102.039813] [Exynos][NPU][NOTICE]: NPU:[*]npu_system_alloc_fw_dram_log_buf(93):start: initialization.
[102.040957] [Exynos][NPU][NOTICE]: NPU:[*]npu_system_alloc_fw_dram_log_buf(103):DRAM log buffer for kernel: size(2097152) / dv(0x0000000080000000) / kv(ffffff802ca85000)
还有一些条目可用于检索有关 NPU 的信息(NPU 驱动程序必须至少打开一次才能显示所有条目)。debugfs
x1s:/ # ls -la /d/npu/
total 0
drwxr-xr-x 2 root root 0 2021-01-30 18:18 .
drwxr-xr-x 63 system system 0 1970-01-01 01:00 ..
-rw------- 1 root root 0 2021-01-30 18:21 SRAM-IDP
-rw------- 1 root root 0 2021-01-30 18:21 SRAM-TCU
-r-------- 1 root root 0 2021-01-30 18:18 dev-log
-r-------- 1 root root 0 2021-01-30 18:21 fw-log-SRAM
-r-------- 1 root root 0 2021-01-30 18:18 fw-profile
-r-------- 1 root root 0 2021-01-30 18:18 fw-report
-rw------- 1 root root 0 2021-01-30 18:18 idiot
-r-------- 1 root root 0 2021-01-30 18:18 proto-drv-dump
-r-------- 1 root root 0 2021-01-30 18:18 result-golden-match
--w------- 1 root root 0 2021-01-30 18:18 set-golden-desc
在尝试对NPU进行逆向工程并理解基本概念时,所有这些信息都非常有帮助。
在我们开始分析我们最喜欢的反汇编程序中的固件之前,我们首先需要找到它。本节介绍内核如何启动NPU驱动,以及内核在专用芯片上加载固件时执行的操作。
初始化在文件 drivers/vision/npu/core/npu-device.c 中开始,并在内核引导时调用 npu_device_init
。
static int __init npu_device_init(void)
{
int ret = platform_driver_register(&npu_driver); /* [...] */
}
/* [...] */
late_initcall(npu_device_init);
npu_device_init
调用以下结构并将其作为参数传递:platform_driver_register
static struct platform_driver npu_driver = {
.probe = npu_device_probe,
.remove = npu_device_remove,
.driver = {
.name = "exynos-npu",
.owner = THIS_MODULE,
.pm = &npu_pm_ops,
.of_match_table = of_match_ptr(exynos_npu_match),
},
};
当内核加载模块时,将调用函数 npu_device_probe
(为清楚起见,已从以下代码片段中删除了错误检查)。
static int npu_device_probe(struct platform_device *pdev)
{
int ret = 0;
struct device *dev;
struct npu_device *device; dev = &pdev->dev;
device = devm_kzalloc(dev, sizeof(*device), GFP_KERNEL);
device->dev = dev;
ret = npu_system_probe(&device->system, pdev);
ret = npu_debug_probe(device);
ret = npu_log_probe(device);
ret = npu_vertex_probe(&device->vertex, dev);
ret = proto_drv_probe(device);
ret = npu_sessionmgr_probe(&device->sessionmgr);
#ifdef CONFIG_NPU_GOLDEN_MATCH
ret = register_golden_matcher(dev);
#endif
#ifdef CONFIG_NPU_LOOPBACK
ret = mailbox_mgr_mock_probe(device);
#endif
ret = npu_profile_probe(&device->system);
ret = iovmm_activate(dev);
iovmm_set_fault_handler(dev, npu_iommu_fault_handler, device);
dev_set_drvdata(dev, device);
ret = 0;
probe_info("complete in %s\n", __func__);
goto ok_exit;
err_exit:
probe_err("error on %s ret(%d)\n", __func__, ret);
ok_exit:
return ret;
}
实质上,初始化以下组件:npu_device_probe
中断(使用相应的 DTS 文件)
共享内存映射
相关 IO 设备映射
NPU 接口和邮箱
固件二进制路径
/data/NPU.bin
和/vendor/firmware/NPU.bin
如果这两个路径不存在,设备将尝试从 加载它,该路径嵌入在内核映像中npu/NPU.bin
debugfs 条目
顶点对象
文件操作:npu_vertex_fops
Ioctl 处理程序:npu_vertex_ioctl_ops
除其他事项外,它为设备设置文件操作和 ioctl 处理程序:
会话管理器
IOVMM的
目前了解这些操作的细节还不太重要。当它们变得相关时,本文稍后将解释其中的一些内容。现在,让我们看一下NPU是如何加载到芯片上并由内核启动的。
前面提到的npu_driver
结构还将 npu_pm_ops
注册为其电源管理操作处理程序。
static const struct dev_pm_ops npu_pm_ops = {
SET_SYSTEM_SLEEP_PM_OPS(npu_device_suspend, npu_device_resume)
SET_RUNTIME_PM_OPS(npu_device_runtime_suspend, npu_device_runtime_resume, NULL)
};
当设备需要启动 NPU 时,它会触发对电源管理系统中 npu_device_runtime_resume
的调用,然后调用 npu_system_resume
(为清楚起见,下面的代码片段进行了简化)。
int npu_system_resume(struct npu_system *system, u32 mode)
{
/* [...] */ /* Loads the firmware in memory from the filesystem */
ret = npu_firmware_load(system);
/* Starts the NPU firmware */
ret = npu_system_soc_resume(system, mode);
/* Opens an interface to the NPU */
ret = npu_interface_open(system);
/* [...] */
return ret;
}
npu_firmware_load
调用尝试从 或 读取固件的 npu_firmware_file_read
。如果这些文件都不存在,则尝试从内核文件系统 中读取它。然后,将文件的内容复制到位于 的 iomem 区域。/data/NPU.bin
/vendor/firmware/NPU.bin
npu/NPU.bin
system->fw_npu_memory_buffer->vaddr
固件的 iomem 区域在 arch/arm64/boot/dts/exynos/exynos9830.dts
中定义,init_iomem_area
在驱动程序初始化期间对其进行解析。在 NPU 的地址空间中,此区域从物理地址开始,大小为 。内核中对应的地址是动态分配的。FW_DRAM
0x50000000
0xe0000
注意:IOMMU 分配在内核和 NPU 之间共享资源一节中有更详细的说明。
最后,调用 npu_system_soc_resume
以使用 npu_cpu_on
启动 NPU。
NPU 在打开时启动,在关闭时停止。打开设备时,您应该会看到类似于以下内容的日志:/dev/vertex10
dmesg
[123.007254] NPU:[*]npu_debug_open(221):start in npu_debug open
[123.007264] NPU:[*]npu_debug_open(222):complete in npu_debug open
[123.007269] NPU:[*]npu_log_open(1152):start in npu_log_open
[123.007274] NPU:[*]npu_log_open(1153):complete in npu_log_open
[123.007279] NPU:[*]npu_util_memdump_open(317):start in npu_util_memdump_open
[123.007282] NPU:[*]npu_util_memdump_open(342):complete in npu_util_memdump_open
[123.007820] NPU:[*]npu_system_resume(346):wake_lock, now(1)
[123.007827] NPU:[*]npu_system_alloc_fw_dram_log_buf(93):start: initialization.
[123.009277] NPU:[*]npu_system_alloc_fw_dram_log_buf(103):DRAM log buffer for firmware: size(2097152) / dv(0x0000000080000000) / kv(ffffff803db75000)
[123.009293] NPU:[*]npu_store_log_init(216):Store log memory initialized : ffffff803db75000[Len = 2097152]
[123.009303] NPU:[*]npu_fw_test_initialize(290):fw_test : initialized.
[123.009309] NPU:[*]npu_system_alloc_fw_dram_log_buf(125):complete : initialization.
[123.009315] NPU:[*]npu_firmware_load(540):Firmware load : Start
[123.023161] NPU:[*]__npu_binary_read(215):success of binay(npu/NPU.bin, 475349) apply.
[123.023196] NPU:[*]print_fw_signature(111):NPU Firmware signature : 009:094 2019/04/25 14:56:44
[123.023210] NPU:[*]npu_firmware_load(572):complete in npu_firmware_load
[123.023233] NPU:[*]print_iomem_area(466):\x01c(TCU_SRAM) Phy(0x19200000)-(0x19280000) Virt(ffffff802b900000) Size(524288)
[123.023243] NPU:[*]print_iomem_area(466):\x01c(IDP_SRAM) Phy(0x19300000)-(0x19400000) Virt(ffffff802ba00000) Size(1048576)
[123.023251] NPU:[*]print_iomem_area(466):\x01c(SFR_NPU0) Phy(0x17900000)-(0x17a00000) Virt(ffffff802bc00000) Size(1048576)
[123.023259] NPU:[*]print_iomem_area(466):\x01c(SFR_NPU1) Phy(0x17a00000)-(0x17af0000) Virt(ffffff802be00000) Size(983040)
[123.023270] NPU:[*]print_iomem_area(466):\x01c( PMU_NPU) Phy(0x15861d00)-(0x15861e00) Virt(ffffff8010eedd00) Size(256)
[123.023279] NPU:[*]print_iomem_area(466):\x01c(PMU_NCPU) Phy(0x15862f00)-(0x15863000) Virt(ffffff8010ef5f00) Size(256)
[123.023288] NPU:[*]print_iomem_area(466):\x01c(MBOX_SFR) Phy(0x178b0000)-(0x178b017c) Virt(ffffff8010efd000) Size(380)
[123.023367] NPU:[*]npu_cpu_on(729):start in npu_cpu_on
[123.023420] NPU:[*]npu_cpu_on(736):complete in npu_cpu_on
[123.023445] NPU:[*]npu_system_soc_resume(513):CLKGate1_DRCG_EN_write_enable
[123.023451] NPU:[*]CLKGate4_IP_HWACG_qch_disable(261):start CLKGate4_IP_HWACG_qch_disable
[123.024797] NPU log sync [60544]
[123.024894] NPU:[*]npu_system_soc_resume(525):CLKGate5_IP_DRCG_EN_write_enable
[123.025842] NPU:[*]mailbox_init(46):mailbox initialize: start, header base at ffffff802b97ff7c
[123.025852] NPU:[*]mailbox_init(47):mailbox initialize: wait for firmware boot signature.
[123.036810] NPU:[*]mailbox_init(53):header signature \x09: C0FFEE0
[123.036821] NPU:[*]mailbox_init(76):header version \x09: 00060004
[123.036826] NPU:[*]mailbox_init(83):init. success in NPU mailbox
[123.036831] NPU:[*]npu_device_runtime_resume(582):npu_device_runtime_resume():0
在这个阶段,我们知道NPU是如何启动的,固件在哪里。下一步是从设备中提取二进制文件。
有两种不同类型的固件,具体取决于用于 NPU 的 CPU 类型。
Exynos 9820 SoC(Galaxy S10 型号)使用 ARMv7 Cortex-M 内核。
Exynos 990 SoC(Galaxy S20 型号)使用 ARMv7 Cortex-A 内核。
这两个固件在实现上非常相似,但仍然存在差异,尤其是在初始化阶段。在本文中,我们将重点介绍 ARMv7-A 实现。
如上一节所述,固件可以在三个可能的位置找到:
/data/NPU.bin
/vendor/firmware/NPU.bin
npu/NPU.bin
(摘自内核镜像)
在 Galaxy S20 上,NPU 固件嵌入在内核映像中。可以使用以下工具从有根设备中提取它:npu_firmware_extractor.py并传递标志。--cortex-a
$ python3 npu_firmware_extractor.py -d . --cortex-a
[+] Connection to the device using ADB.
[+] Pulling the kernel from the device to the host.
[+] Extracting the firmware.
[+] Done.
$ ll
-rw-r--r-- 1 lyte staff 464K 4 jan 16:30 NPU.bin
-rw-r--r-- 1 lyte staff 55M 4 jan 16:30 boot.img
-rw-r--r-- 1 lyte staff 3,6K 4 jan 16:29 npu_firmware_extractor.py
还有可能转储分配给NPU固件的SRAM存储器范围。之前,我们已经展示了不同的 debugfs 条目。其中,有一个可用于在运行时转储 NPU 的代码和数据。虽然此文件在三星 S10 上运行,但如果您尝试在三星 S20 上打开它,内核将崩溃。SRAM-TCU
可以通过重新编译新版本的内核来解决此问题。三星的内核源代码可以通过以下方式下载: 此链接,搜索并下载该版本的源代码。SM-G980F
G980FXXU5CTL1
以下修补程序可以应用于内核以修复这些问题:
diff --git a/drivers/vision/npu/core/npu-util-memdump.c b/drivers/vision/npu/core/npu-util-memdump.c
index 5711bbb..8749701 100755
--- a/drivers/vision/npu/core/npu-util-memdump.c
+++ b/drivers/vision/npu/core/npu-util-memdump.c
@@ -109,12 +109,13 @@ int ram_dump_fault_listner(struct npu_device *npu)
{
int ret = 0;
struct npu_system *system = &npu->system;
- u32 *tcu_dump_addr = kzalloc(system->tcu_sram.size, GFP_ATOMIC);
+ u32 *tcu_dump_addr = kzalloc(system->fw_npu_memory_buffer->size, GFP_ATOMIC);
u32 *idp_dump_addr = kzalloc(system->idp_sram.size, GFP_ATOMIC); if (tcu_dump_addr) {
- memcpy_fromio(tcu_dump_addr, system->tcu_sram.vaddr, system->tcu_sram.size);
- pr_err("NPU TCU SRAM dump - %pK / %paB\n", tcu_dump_addr, &system->tcu_sram.size);
+ memcpy_fromio(tcu_dump_addr, system->fw_npu_memory_buffer->vaddr,
+ system->fw_npu_memory_buffer->size);
+ pr_err("NPU TCU SRAM dump - %pK / %paB\n", tcu_dump_addr, &system->fw_npu_memory_buffer->size);
} else {
pr_err("tcu_dump_addr is NULL\n");
ret= -ENOMEM;
@@ -281,20 +282,22 @@ DECLARE_NPU_SRAM_DUMP(idp);
int npu_util_memdump_probe(struct npu_system *system)
{
BUG_ON(!system);
- BUG_ON(!system->tcu_sram.vaddr);
+ BUG_ON(!system->fw_npu_memory_buffer->vaddr);
#ifdef CONFIG_NPU_LOOPBACK
return 0;
#endif
atomic_set(&npu_memdump.registered, 0);
- npu_memdump.tcu_sram = system->tcu_sram;
+ npu_memdump.tcu_sram.vaddr = system->fw_npu_memory_buffer->vaddr;
+ npu_memdump.tcu_sram.paddr = system->fw_npu_memory_buffer->paddr;
+ npu_memdump.tcu_sram.size = system->fw_npu_memory_buffer->size;
npu_memdump.idp_sram = system->idp_sram;
- probe_info("%s: paddr = %08x\n", FW_MEM_LOG_NAME,
- system->tcu_sram.paddr + MEM_LOG_OFFSET
+ probe_info("%s: paddr = %08llx\n", FW_MEM_LOG_NAME,
+ system->fw_npu_memory_buffer->paddr + MEM_LOG_OFFSET
);
#ifdef CONFIG_EXYNOS_NPU_DEBUG_SRAM_DUMP
- probe_info("%s: paddr = %08x\n", TCU_SRAM_DUMP_SYSFS_NAME,
- system->tcu_sram.paddr);
- tcu_sram_dump_size = system->tcu_sram.size;
+ probe_info("%s: paddr = %08llx\n", TCU_SRAM_DUMP_SYSFS_NAME,
+ system->fw_npu_memory_buffer->paddr);
+ tcu_sram_dump_size = system->fw_npu_memory_buffer->size;
probe_info("%s: paddr = %08x\n", IDP_SRAM_DUMP_SYSFS_NAME,
system->idp_sram.paddr);
idp_sram_dump_size = system->idp_sram.size;
重新编译并启动内核后,可以使用 npu_sram_dumper 转储 NPU 的地址空间。
$ make run
这些二进制文件现在可以加载到 IDA 或任何其他反汇编程序中。对于 Cortex-M 和 Cortex-A,基址均为 。如果要继续操作,下面是本文中分析的二进制文件的链接:npu_s20_binary.bin。但是,这只是普通固件,如果您还希望在运行时初始化一些值,您可以查看npu_s20_dump.bin。0
NPU 固件实现了一个最小的操作系统,能够处理来自内核的不同请求并发回结果。本部分概述了组成操作系统的不同组件,并尝试突出显示它们的交互。
本节中介绍的很大一部分代码是逆向工程的,可在此处获得。所有这些代码片段都试图尽可能地保持对实现的原始逻辑的真实性,但为了简单起见,可能会有一些偏差(例如,删除关键部分包装器,这将使函数数量增加近两个)。此外,尽管本节解释了相关组件的内部工作原理,但鼓励对实际实现感兴趣的读者阅读等效的 C 代码,因为其中大部分都是注释的。
由于我们处理的是 32 位 Cortex-A CPU,因此异常向量基于以下格式:
抵消 | 处理器 |
---|---|
0x00 | 重置 |
0x04 | 未定义的指令 |
0x08 | 主管电话 |
0x0c | 预取中止 |
0x10 | 数据中止 |
0x14 | 未使用 |
0x18 | IRQ 中断 |
0x1c | FIQ 中断 |
当 NPU 启动时,在偏移量 0 处执行的第一条指令跳转到函数 reset_handler
。
基本上,这个函数将:
启用 NEON 指令;
确保我们在主管模式下运行;
初始化页表并激活MPU;
为不同的 CPU 模式(例如 abort、FIQ 等)设置堆栈指针;
调用函数。main
在跳到NPU的main功能之前,我们先来看看固件是如何设置页表的,以获得内存映射的概述。
负责这些操作的函数是init_memory_management
。如果查看代码,可能会注意到它将 SCR
设置为 0,这意味着 NPU 可能会访问安全内存。不幸的是,对于攻击者来说,此组件在 AXI 总线上未配置为安全组件,这意味着访问安全内存将导致硬件异常。
int init_memory_management() {
/* [...] */ /*
* SCR - Secure Configuration Register
*
* NS=0b0: Secure mode enabled.
* ...
*/
write_scr(0);
return init_page_tables();
}
然后,该函数继续调用 init_page_tables
。此方法调用 SetTransTable
以实际创建级别 1 和级别 2 页表条目。 然后将 L1 页表地址写入并清理缓存。init_page_tables
TTBR0
int init_page_tables() {
/* [...] */ SetTransTable(0, 0x50000000, 0x1D000, 0, 0x180D);
SetTransTable(0x1D000, 0x5001D000, 0x3000, 0, 0x180D);
SetTransTable(0x20000, 0x50020000, 0xC000, 0, 0x180D);
SetTransTable(0x2C000, 0x5002C000, 0x4000, 0, 0x180D);
SetTransTable(0x30000, 0x50030000, 0x1000, 0, 0x180D);
SetTransTable(0x31000, 0x50031000, 0x2800, 0, 0x1C0D);
SetTransTable(0x33800, 0x50033800, 0x1000, 0, 0x1C0D);
SetTransTable(0x34800, 0x50034800, 0x1000, 0, 0x1C0D);
SetTransTable(0x35800, 0x50035800, 0x1000, 0, 0x1C0D);
SetTransTable(0x36800, 0x50036800, 0x1000, 0, 0x1C0D);
SetTransTable(0x37800, 0x50037800, 0x5000, 0, 0x1C0D);
SetTransTable(0x3C800, 0x5003C800, 0x2B800, 0, 0x1C0D);
SetTransTable(0x68000, 0x50068000, 0x18000, 0, 0x1C01);
SetTransTable(0x80000, 0x50080000, 0x60000, 0, 0x1C0D);
SetTransTable(0x10000000, 0x10000000, 0x10000000, 0, 0x816);
SetTransTable(0x40100000, 0x40100000, 0x100000, 0, 0xC16);
SetTransTable(0x40300000, 0x40300000, 0x100000, 0, 0x1C0E);
SetTransTable(0x40600000, 0x40600000, 0x100000, 0, 0x1C12);
SetTransTable(0x40200000, 0x40200000, 0x100000, 0, 0xC16);
SetTransTable(0x40300000, 0x40300000, 0x100000, 0, 0x1C0E);
SetTransTable(0x40700000, 0x40700000, 0x100000, 0, 0x1C12);
SetTransTable(0x50100000, 0x50100000, 0x200000, 0, 0x1C02);
SetTransTable(0x50000000, 0x50000000, 0xE0000, 0, 0x1C02);
SetTransTable(0x40400000, 0x40400000, 0x100000, 0, 0x1C02);
SetTransTable(0x40000000, 0x40000000, 0x100000, 0, 0xC16);
SetTransTable(0x80000000, 0x80000000, 0x60000000, 0, 0x1C02);
/* [...] */
注意:此链接提供了有关页表在 ARMv7 上如何工作的良好复习,并且对于理解 中执行的操作很有用。SetTransTable
反转后,现在可以很容易地检索 NPU 内存映射的所有详细信息,如下表所示。SetTransTable
类型 | 虚拟地址 | 物理地址 | 大小 | PXN系列 | XN型 | NS系列 | 美联社 | B | C | S |
---|---|---|---|---|---|---|---|---|---|---|
简短的描述 | 0x00000000 | 0x50000000 | 0x0001d000 | N | N | N | 在 PL0 处写入会生成权限错误 | Y | Y | N |
简短的描述 | 0x0001d000 | 0x5001d000 | 0x00003000 | N | N | N | 在 PL0 处写入会生成权限错误 | Y | Y | N |
简短的描述 | 0x00020000 | 0x50020000 | 0x0000c000 | N | N | N | 在 PL0 处写入会生成权限错误 | Y | Y | N |
简短的描述 | 0x0002c000 | 0x5002c000 | 0x00004000 | N | N | N | 在 PL0 处写入会生成权限错误 | Y | Y | N |
简短的描述 | 0x00030000 | 0x50030000 | 0x00001000 | N | N | N | 在 PL0 处写入会生成权限错误 | Y | Y | N |
简短的描述 | 0x00031000 | 0x50031000 | 0x00002800 | N | N | N | 完全访问权限 | Y | Y | N |
简短的描述 | 0x00033800 | 0x50033800 | 0x00001000 | N | N | N | 完全访问权限 | Y | Y | N |
简短的描述 | 0x00034800 | 0x50034800 | 0x00001000 | N | N | N | 完全访问权限 | Y | Y | N |
简短的描述 | 0x00035800 | 0x50035800 | 0x00001000 | N | N | N | 完全访问权限 | Y | Y | N |
简短的描述 | 0x00036800 | 0x50036800 | 0x00001000 | N | N | N | 完全访问权限 | Y | Y | N |
简短的描述 | 0x00037800 | 0x50037800 | 0x00005000 | N | N | N | 完全访问权限 | Y | Y | N |
简短的描述 | 0x0003c800 | 0x5003c800 | 0x0002b800 | N | N | N | 完全访问权限 | Y | Y | N |
简短的描述 | 0x00068000 | 0x50068000 | 0x00018000 | N | N | N | 完全访问权限 | N | N | N |
简短的描述 | 0x00080000 | 0x50080000 | 0x00060000 | N | N | N | 完全访问权限 | Y | Y | N |
部分 | 0x10000000 | 0x10000000 | 0x10000000 | N | Y | N | 在 PL0 处写入会生成权限错误 | Y | N | N |
部分 | 0x40100000 | 0x40100000 | 0x00100000 | N | Y | N | 完全访问权限 | Y | N | N |
部分 | 0x40300000 | 0x40300000 | 0x00100000 | N | N | N | 完全访问权限 | Y | Y | N |
部分 | 0x40600000 | 0x40600000 | 0x00100000 | N | Y | N | 完全访问权限 | N | N | N |
部分 | 0x40200000 | 0x40200000 | 0x00100000 | N | Y | N | 完全访问权限 | Y | N | N |
部分 | 0x40300000 | 0x40300000 | 0x00100000 | N | N | N | 完全访问权限 | Y | Y | N |
部分 | 0x40700000 | 0x40700000 | 0x00100000 | N | Y | N | 完全访问权限 | N | N | N |
部分 | 0x50100000 | 0x50100000 | 0x00200000 | N | N | N | 完全访问权限 | N | N | N |
部分 | 0x50000000 | 0x50000000 | 0x000e0000 | N | N | N | 完全访问权限 | N | N | N |
部分 | 0x40400000 | 0x40400000 | 0x00100000 | N | N | N | 完全访问权限 | N | N | N |
部分 | 0x40000000 | 0x40000000 | 0x00100000 | N | Y | N | 完全访问权限 | Y | N | N |
部分 | 0x80000000 | 0x80000000 | 0x60000000 | N | N | N | 完全访问权限 | N | N | N |
正如你所看到的,很少有部分使用软件缓解措施,当我们在下一篇文章中利用NPU时,这将派上用场。
在配置了NPU的地址空间和其他CPU相关设置之后,我们来分析一下操作系统的启动过程,从.main
void main() {
heap_init();
arm_init();
timers_init();
events_init();
semaphores_init();
scheduler_init();
comm_channels_init();
run_native_tasks(0x37800); /* Should not be reached */
abort();
}
main
除了调用所有初始化例程来配置堆、计时器等之外,它本身并没有做太多事情。在以下各节中,我们将介绍这些函数中的每一个,以及它们初始化的子系统。
堆是在函数heap_init
中设置的。堆初始化背后的想法非常简单。为了能够从此内存区域分配内存,首先需要将其标记为已释放。为此,操作系统将整个堆定义为一个块。堆块基于以下结构:
struct heap_chunk {
u32 size;
struct heap_chunk *next;
};
然后,操作系统将第一个块的大小设置为整个堆的大小(即 ),然后最终释放此块。这个过程如下图所示。HEAP_END_ADDR - HEAP_START_ADDR = 0x60000
完成此初始化步骤后,现在可以使用 和 等函数动态管理内存。malloc
free
为了从堆中分配内存,malloc
遍历单链接自由列表,以找到第一个足够大的块来满足分配的大小约束。块使用其地址进行排序,如果找到的块大于操作系统请求的块,则将其一分为二,返回具有请求大小的块,并使用剩余的内容创建一个新块。
例如,如果操作系统调用,并且在 freelist 中找到大小的块,则会创建两个新块:malloc(0x50)
0x80
返回给操作系统的一块字节;0x50
一个字节块链接回 freelist。0x30
当操作系统要求释放内存时,将执行相反的过程。free
遍历 freelist 并查找地址低于我们要插入的地址的第一个块,以保持列表按块地址排序。如果两个块相邻,它们就会合并。
堆初始化后,下一个调用的函数是 arm_init
。
arm_init
首先通过调用 init_caches
初始化 CPU 缓存。它基本上只是检索有关 CPU 缓存的一些信息,并使不同的内存区域失效以从干净状态开始。
arm_init
然后在 init_exception
中初始化异常。ARM 异常向量表引用的异常处理程序只是使用函数指针调用实际处理程序的包装程序。而且,正如您可能已经猜到的那样,这些函数指针是在 中设置的。init_exception
最后,在 init_interrupt
中初始化中断。基本上,它配置 ARM 全局中断控制器并重置所有挂起的中断。在初始化过程的其余部分,操作系统使用函数 request_irq
注册并启用多个中断处理程序。现在,当中断发生时,它会通过相关的异常处理程序并到达irq_fiq_handler
。然后,此函数调用检索中断 ID 并调用关联的处理程序的 handle_isr_func
。arm_init
注意:关于与 ARM 的 GIC 的交互,省略了很多细节,这些细节可以在反转函数的注释中找到。
在本节中,我们稍微偏离了函数的初始化顺序。原因是堆以外的所有组件都直接链接到任务,这就是首先解释它们的原因。main
NPU 任务基于以下结构:
struct task {
u32 magic;
void *stack_ptr;
void *stack_start;
void *stack_end;
u32 stack_size;
u32 state;
u32 unknown;
u32 priority;
void (*handler)(void *);
u32 max_sched_slices;
u32 total_sched_slices;
u32 remaining_sched_slices;
u32 delay;
void *args;
char *name;
struct list_head tasks_list_entry;
struct list_head ready_list_entry;
struct list_head delayed_list_entry;
struct list_head pending_list_entry;
struct workqueue* wait_queue;
char unknown2[60];
};
注意:本文中描述的所有列表(堆块除外)都是双向链表。
所有任务共享内核的地址空间,有自己的专用堆栈,其执行时间由调度程序管理(将在下一节中解释)。
使用函数 create_task
创建任务。它将任务添加到全局任务列表,并配置多个属性,例如:
它的名字;
其优先次序;
它将运行的函数;
其状态(最初设置为暂停);
其堆栈的大小、起始地址和结束地址;
各种日程安排设置。
create_task
还使用 初始化写入堆栈的值。当操作系统计划任务以设置寄存器、CPSR 并恢复执行时,将使用这些值。初始值如下:init_task_stack
抵消 | 名字 | 价值 |
---|---|---|
SP+系列0x00 | R4型 | 0 |
SP+系列0x04 | R5型 | 0 |
SP+系列0x08 | R6型 | 0 |
SP+系列0x0c | R7型 | 0 |
SP+系列0x10 | R8型 | 0 |
SP+系列0x14 | R9型 | 0 |
SP+系列0x18 | R10型 | 0 |
SP+系列0x1C | R11型 | 0 |
SP+系列0x20 | r0 | 0 |
SP+系列0x24 | R1型 | 0 |
SP+系列0x28 | R2型 | 0 |
SP+系列0x2c | R3型 | 0 |
SP+系列0x30 | R12型 | 0 |
SP+系列0x34 | LR型 | 0 |
SP+系列0x38 | 个人电脑 | run_task |
SP+系列0x3c | CPSR的 | 0x153 |
首次计划任务时,其入口点为run_task
。此函数是一个状态机,它调用任务的处理程序,在完成后挂起它并再次循环回来。
任务运行后,可以恢复或暂停它。
通过调用 __suspend_task
来执行挂起任务。根据任务的当前状态(即就绪/正在运行、睡眠、挂起),它会从当前所在的列表中删除(即就绪、延迟、挂起),并将其状态设置为 。任务可以属于的列表类型将在以下专门针对计划程序的部分中进行说明。TASK_SUSPENDED
通过调用 __resume_task
来恢复任务。此函数只需将列表添加到就绪列表,并将其状态设置为 。TASK_READY
与大多数操作系统一样,NPU 操作系统可以处理多任务处理,并使用调度程序决定运行哪个任务。调度程序在函数scheduler_init
中初始化,并使用以下结构跟踪其状态:
struct scheduler_state_t {
u32 scheduler_stopped;
u32 forbid_scheduling;
u8 prio_grp1[4];
u8 prio_grp2[4][8];
u8 prio_grp0;
struct list_head tasks_list;
struct list_head delayed_list;
struct list_head ready_list[TASK_MAX_PRIORITY];
u32 unknown;
u32 nb_tasks;
u32 count_sched_slices;
};
scheduler_init
只需初始化任务列表、优先级组值和设置 / 以向操作系统表示它暂时不应安排任务。但是,在我们进一步讨论之前,我们需要解释什么是优先级组、现成列表以及更一般的调度算法。scheduler_stopped
forbid_scheduling
NPU 中实现的调度器基于任务创建期间与任务关联的优先级。对于 NPU OS,应区分任务的优先级值和其实际优先级,因为它们是倒置的:高优先级任务具有低优先级值。因此,查找要计划的下一个任务等同于查找准备运行的优先级最低的任务。
准备执行的任务列表存储在调度程序的全局状态结构中,每个优先级值都有一个就绪列表(从 到 )。0x00
0xff
#define TASK_MAX_PRIORITY 0x100struct scheduler_state_t {
/* [...] */
struct list_head ready_list[TASK_MAX_PRIORITY];
/* [...] */
};
现在,为了能够找到优先级最低的任务所在的列表,可以使用不同的解决方案。一种幼稚的方法是遍历所有这些列表,找到第一个非空列表,然后返回其中的第一个任务。三星使用的实现略有不同。为了理解它,让我们看一下将任务添加到其就绪列表中的函数:__add_to_ready_list
。
__add_to_ready_list
将要添加到就绪列表中的任务的优先级,并将其分为三组:
/* Computes the priority group values based on the task's priority */
u8 grp0_val = priority >> 6;
u8 grp1_val = (priority >> 3) & 7;
u8 grp2_val = priority & 7;
如果我们的优先级为 ,或二进制,则其值将按如下方式拆分:77
0b01001101
然后,这些值用于在 的三个位域中设置位。g_scheduler_state
/* Adds the current task's priority to the priority group values */
g_scheduler_state.prio_grp0 |= 1 << grp0_val;
g_scheduler_state.prio_grp1[grp0_val] |= 1 << grp1_val;
g_scheduler_state.prio_grp2[grp0_val][grp1_val] |= 1 << grp2_val;
下面直观地表示了在添加优先级为 77 的任务,然后添加优先级为 153 的任务时如何修改这些位域。
现在已经清楚了调度程序如何引用任务优先级,让我们详细介绍用于查找这些位域中编码的最低优先级的算法。
继续请看下部分
二进制漏洞课程(更新中)
其它课程
windows网络安全防火墙与虚拟网卡(更新完成)
windows文件过滤(更新完成)
USB过滤(更新完成)
游戏安全(更新中)
ios逆向
windbg
恶意软件开发(更新中)
还有很多免费教程(限学员)
更多详细内容添加作者微信