二进制漏洞分析-35.Samsung NPU的Reversing与 Exploiting (第一部分上)
2024-1-7 09:22:12 Author: 安全狗的自我修养(查看原文) 阅读量:10 收藏

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-G980Fdmesg

三星在他们开发的组件中提供调试信息的情况并不少见,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驱动加载

在我们开始分析我们最喜欢的反汇编程序中的固件之前,我们首先需要找到它。本节介绍内核如何启动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 驱动程序电源管理和固件加载

前面提到的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.binnpu/NPU.binsystem->fw_npu_memory_buffer->vaddr

固件的 iomem 区域在 arch/arm64/boot/dts/exynos/exynos9830.dts 中定义,init_iomem_area在驱动程序初始化期间对其进行解析。在 NPU 的地址空间中,此区域从物理地址开始,大小为 。内核中对应的地址是动态分配的。FW_DRAM0x500000000xe0000

注意:IOMMU 分配在内核和 NPU 之间共享资源一节中有更详细的说明。

最后,调用 npu_system_soc_resume 以使用 npu_cpu_on 启动 NPU。

NPU 在打开时启动,在关闭时停止。打开设备时,您应该会看到类似于以下内容的日志:/dev/vertex10dmesg

[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-G980FG980FXXU5CTL1

以下修补程序可以应用于内核以修复这些问题:

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 代码,因为其中大部分都是注释的。

NPU固件初始化

由于我们处理的是 32 位 Cortex-A CPU,因此异常向量基于以下格式:

抵消处理器
0x00重置
0x04未定义的指令
0x08主管电话
0x0c预取中止
0x10数据中止
0x14未使用
0x18IRQ 中断
0x1cFIQ 中断

当 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_tablesTTBR0

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系列美联社BCS
简短的描述0x000000000x500000000x0001d000NNN在 PL0 处写入会生成权限错误YYN
简短的描述0x0001d0000x5001d0000x00003000NNN在 PL0 处写入会生成权限错误YYN
简短的描述0x000200000x500200000x0000c000NNN在 PL0 处写入会生成权限错误YYN
简短的描述0x0002c0000x5002c0000x00004000NNN在 PL0 处写入会生成权限错误YYN
简短的描述0x000300000x500300000x00001000NNN在 PL0 处写入会生成权限错误YYN
简短的描述0x000310000x500310000x00002800NNN完全访问权限YYN
简短的描述0x000338000x500338000x00001000NNN完全访问权限YYN
简短的描述0x000348000x500348000x00001000NNN完全访问权限YYN
简短的描述0x000358000x500358000x00001000NNN完全访问权限YYN
简短的描述0x000368000x500368000x00001000NNN完全访问权限YYN
简短的描述0x000378000x500378000x00005000NNN完全访问权限YYN
简短的描述0x0003c8000x5003c8000x0002b800NNN完全访问权限YYN
简短的描述0x000680000x500680000x00018000NNN完全访问权限NNN
简短的描述0x000800000x500800000x00060000NNN完全访问权限YYN
部分0x100000000x100000000x10000000NYN在 PL0 处写入会生成权限错误YNN
部分0x401000000x401000000x00100000NYN完全访问权限YNN
部分0x403000000x403000000x00100000NNN完全访问权限YYN
部分0x406000000x406000000x00100000NYN完全访问权限NNN
部分0x402000000x402000000x00100000NYN完全访问权限YNN
部分0x403000000x403000000x00100000NNN完全访问权限YYN
部分0x407000000x407000000x00100000NYN完全访问权限NNN
部分0x501000000x501000000x00200000NNN完全访问权限NNN
部分0x500000000x500000000x000e0000NNN完全访问权限NNN
部分0x404000000x404000000x00100000NNN完全访问权限NNN
部分0x400000000x400000000x00100000NYN完全访问权限YNN
部分0x800000000x800000000x60000000NNN完全访问权限NNN

正如你所看到的,很少有部分使用软件缓解措施,当我们在下一篇文章中利用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

完成此初始化步骤后,现在可以使用 和 等函数动态管理内存。mallocfree

为了从堆中分配内存,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_funcarm_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+系列0x00R4型0
SP+系列0x04R5型0
SP+系列0x08R6型0
SP+系列0x0cR7型0
SP+系列0x10R8型0
SP+系列0x14R9型0
SP+系列0x18R10型0
SP+系列0x1CR11型0
SP+系列0x20r00
SP+系列0x24R1型0
SP+系列0x28R2型0
SP+系列0x2cR3型0
SP+系列0x30R12型0
SP+系列0x34LR型0
SP+系列0x38个人电脑run_task
SP+系列0x3cCPSR的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_stoppedforbid_scheduling

优先组

NPU 中实现的调度器基于任务创建期间与任务关联的优先级。对于 NPU OS,应区分任务的优先级值和其实际优先级,因为它们是倒置的:高优先级任务具有低优先级值。因此,查找要计划的下一个任务等同于查找准备运行的优先级最低的任务。

准备执行的任务列表存储在调度程序的全局状态结构中,每个优先级值都有一个就绪列表(从 到 )。0x000xff

#define TASK_MAX_PRIORITY 0x100

struct 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;

如果我们的优先级为 ,或二进制,则其值将按如下方式拆分:770b01001101

然后,这些值用于在 的三个位域中设置位。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

  • ()


文章来源: http://mp.weixin.qq.com/s?__biz=MzkwOTE5MDY5NA==&mid=2247491091&idx=1&sn=9331aa80e6ffdef99a260f7c8acec73b&chksm=c0c4fc4aa1f462bf926fadcb9a8251d3560f75dfd8864cbb0784e56cccf17b4b68139bd9a665&scene=0&xtrack=1#rd
如有侵权请联系:admin#unsafe.sh