调试器的谎言

Hacker News Top 新闻

摘要

作者讨论了在调试Nordic的nRF54L系列时遇到的意外内存值问题,解释了调试器与内部SoC组件的交互如何可能导致此类问题。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/09/24 13:06

# 当调试器撒谎时 来源:https://danielmangum.com/posts/when-the-debugger-lies/ 过去几周,我一直致力于Nordic Semiconductor(https://www.nordicsemi.com/)的nRF54L系列(https://www.nordicsemi.com/Products/nRF54LM20B)安全架构工作(顺便一提,我最近加入了Nordic(https://danielmangum.com/posts/farewell-golioth-hello-nordic/)!)。在此过程中,我使用了自己典型的低级学习技巧:不编写固件,而是通过调试器手动探查寄存器。几天前,我在使用密钥管理单元时,观察到内存中出现了意料之外的值。结果发现这是一个常见问题,但需要理解片上系统(SoC)的内部组件以及调试器如何与它们交互才能诊断。 先简单介绍一下背景:nRF54L系列拥有一套相当先进的安全功能,主要包括Cortex-M33内核(https://danielmangum.com/posts/arm-cortex-m-security-state-gdb/)中的Arm TrustZone(https://www.arm.com/technologies/trustzone-for-cortex-m)支持、CRACEN加密加速器(https://docs.nordicsemi.com/r/bundle/ps_nrf54lm20a/page/cracen.html)以及密钥管理单元(KMU)(https://docs.nordicsemi.com/r/bundle/ps_nrf54lm20a/page/kmu.html)。KMU用于在安全信息配置区域(SICR)(https://docs.nordicsemi.com/r/bundle/ps_nrf54lm20a/page/sicr.html)中存储敏感数据,例如密钥种子和相关元数据。SICR被划分为多个槽位。这些槽位通过向KMU发出任务来寻址,而KMU只能在安全模式下访问。通常,应用程序固件不会直接与KMU交互。相反,PSA驱动(http://danielmangum.com/posts/psa-crypto-portability/)被实现出来,以抽象密钥的生成、存储和使用。例如,如果调用`psa_generate_key()`(https://arm-software.github.io/psa-api/crypto/1.1/api/keys/management.html#c.psa_generate_key),该操作最终会调用CRACEN PSA驱动(https://github.com/nrfconnect/sdk-nrf/tree/b19bcf262862cb85140282dfd44dbe542d34cbc9/subsys/nrf_security/src/drivers/cracen/cracenpsa)中的`import_key_for_kmu()`(https://github.com/nrfconnect/sdk-nrf/blob/b19bcf262862cb85140282dfd44dbe542d34cbc9/subsys/nrf_security/src/drivers/cracen/cracenpsa/src/cracen_psa_key_management.c#L90-L124)。 ```c static psa_status_t import_key_for_kmu(const psa_key_attributes_t *attributes, const uint8_t *data, size_t data_length, uint8_t *key_buffer, size_t key_buffer_size, size_t *key_buffer_length, size_t *key_bits) { size_t opaque_key_size; psa_status_t status = PSA_ERROR_CORRUPTION_DETECTED; int slot_id = CRACEN_PSA_GET_KMU_SLOT(MBEDTLS_SVC_KEY_ID_GET_KEY_ID(psa_get_key_id(attributes))); psa_key_attributes_t stored_attributes; status = cracen_get_opaque_size(attributes, &opaque_key_size); if (status != PSA_SUCCESS) { return status; } if (key_buffer_size < opaque_key_size) { return PSA_ERROR_BUFFER_TOO_SMALL; } status = cracen_kmu_provision(attributes, slot_id, data, data_length); if (status != PSA_SUCCESS) { return status; } status = cracen_kmu_get_builtin_key(slot_id, &stored_attributes, key_buffer, key_buffer_size, key_buffer_length); if (status != PSA_SUCCESS) { return status; } *key_bits = psa_get_key_bits(&stored_attributes); return status; } ``` 一个槽位可以是已擦除、已配置或已撤销状态。数据手册(https://docs.nordicsemi.com/r/bundle/ps_nrf54lm20a/page/kmu.html-concept_key_slot_states)包含一个有用的状态机图示。 [调试器撒谎-0] 槽位有ID(`0`-`249`),可以存储元数据(32位)、目标地址(32位)、值(128位)和撤销策略(2位)。后者决定了当密钥处于预配置状态且向KMU发出引用其槽位ID的各种任务时,状态如何变化。在实验KMU时,最简单的做法是使用`ROTATING`撤销策略,它规定当发出撤销任务时,槽位会转换回已擦除状态。当发出`PUSH`任务时,槽位中的值会被写入密钥预配置时指定的目标地址。配置过程在数据手册(https://docs.nordicsemi.com/r/bundle/ps_nrf54lm20a/page/kmu.html-concept_provision)中有文档说明,但也可以在`cracen_kmu_key_slot_provision()`实现(https://github.com/nrfconnect/sdk-nrf/blob/b19bcf262862cb85140282dfd44dbe542d34cbc9/subsys/nrf_security/src/drivers/cracen/cracenpsa/src/cracen_psa_kmu.c#L267-L279)中看到: ```c static int cracen_kmu_key_slot_provision(const nrfx_kmu_key_slot_data_t *key_slot_data, uint32_t slot_id) { int kmu_status; uint8_t orig_write_buf_size; cracen_kmu_key_slot_provision_write_enable_set(true, &orig_write_buf_size); kmu_status = nrfx_kmu_key_slot_provision(key_slot_data, slot_id); cracen_kmu_key_slot_provision_write_enable_set(false, &orig_write_buf_size); return kmu_status; } ``` `nrfx_kmu_key_slot_data_t`的定义可以在Nordic Zephyr硬件抽象层(HAL)(https://github.com/zephyrproject-rtos/hal_nordic/blob/4387c79cebd31927fb1ea7d64bee11728ae8041f/nrfx/drivers/include/nrfx_kmu.h#L75-L88)中找到。 ```c typedef struct __PACKED { uint32_t keyslot_value[KEY_SLOT_WORDS_COUNT]; ///< 待配置的密钥数据 #if NRF_KMU_HAS_REVOKE_POLICY || defined(__NRFX_DOXYGEN__) uint32_t revoke_policy; /**< 密钥撤销策略。 * @ref nrfx_kmu_rpolicy_t * 包含可能的值。 */ #endif uint32_t keyslot_dest; /**< 执行密钥推送时的 * 密钥槽目标地址。 */ #if NRF_KMU_HAS_METADATA || defined(__NRFX_DOXYGEN__) nrfx_kmu_key_slot_metadata_t metadata; ///< 写入密钥槽的元数据 #endif } nrfx_kmu_key_slot_data_t; ``` 你可以编写一些相当简单的固件,甚至使用CRACEN KMU示例(https://github.com/nrfconnect/sdk-nrf/tree/b19bcf262862cb85140282dfd44dbe542d34cbc9/samples/crypto/kmu_cracen_usage),构建它,然后将其刷写到开发套件上,以轻松地向KMU配置一个密钥。但是,如果使用受支持的驱动程序(你应该这样做),那么在配置密钥槽位时,你能使用的值会受到额外的限制。例如,虽然KMU支持任何32位元数据值,但PSA驱动程序为每个位分配了特定含义(https://github.com/nrfconnect/sdk-nrf/blob/b19bcf262862cb85140282dfd44dbe542d34cbc9/subsys/nrf_security/src/drivers/cracen/cracenpsa/src/cracen_psa_kmu.c#L77-L85)。 ```c typedef struct kmu_metadata { uint32_t metadata_version: 4; uint32_t key_usage_scheme: 2; uint32_t reserved: 8; uint32_t algorithm: 6; uint32_t size: 3; uint32_t rpolicy: 2; uint32_t usage_flags: 7; } kmu_metadata; ``` 同样,对于你可以写入的值以及特定类型密钥被推送到的目标地址也有限制。如果手动与KMU交互,元数据、值和目标地址的灵活性要大得多。但是,如果你在KMU的槽位中配置了不符合规范的数据,然后尝试使用受支持的驱动程序与其交互,那将会非常麻烦。 了解这些风险,并且知道我可以使用控制访问端口(CTRL-AP)(https://docs.nordicsemi.com/r/bundle/ps_nrf54lm20a/page/ctrl-ap.html)上的`ERASEALL`(https://docs.nordicsemi.com/r/bundle/ps_nrf54lm20a/page/ctrl-ap.html-ctrlap_erase)操作在nRF54LM20 DK(https://www.nordicsemi.com/Products/Development-hardware/nRF54LM20-DK)上恢复SICR后,我给开发板上电并连接了GDB(https://en.wikipedia.org/wiki/GNU_Debugger)。如前所述,KMU只能在安全模式下访问。但是,当访问端口保护未启用(https://docs.nordicsemi.com/r/bundle/ps_nrf54lm20a/page/debug.html-access_port_protection)时,安全特权侵入式调试使能(SPIDEN)(https://support.arm.com/documentation/ddi0309/f/Debug-Architecture/External-debug-interface/SPIDEN)信号被拉高,板载J-Link调试器(J-Link OB)(https://www.segger.com/products/debug-probes/j-link/models/j-link-ob/)可以以安全特权运行。 CPU暂停后,我测试了能够访问KMU,并通过读取`STATUS`寄存器(https://docs.nordicsemi.com/r/bundle/ps_nrf54lm20a/page/kmu.html-register.status)(`0x50049400`)确定它已准备好进行操作。为了测试实际功能,我按照数据手册中描述的配置(https://docs.nordicsemi.com/r/bundle/ps_nrf54lm20a/page/kmu.html-concept_provision)和推送(https://docs.nordicsemi.com/r/bundle/ps_nrf54lm20a/page/kmu.html-concept_push)步骤进行操作。第一步是在内存中构建`SRC`数据结构,其格式符合打包的`nrfx_kmu_key_slot_data_t`结构定义。为了更方便地测试多个值,我编写了一个小型Python脚本来构建该结构。 ```python import struct open("kmu_src.bin", "wb").write( bytes.fromhex("abc123abc123abc123abc123abc123ab") # 值 + struct.pack("<II", 0x20000000, 0x01) # 目标地址和撤销策略 + struct.pack("<I", 0x00000002) # 元数据(密钥大小=128位,算法=?) ) ``` 在GDB中,我加载该文件并将其写入内存,然后按照手册中描述的方式向KMU发出配置任务。 ``` (gdb) dump binary memory kmu_src.bin 0x20000000 0x20000020 (gdb) monitor exec SetReg.KMU.TASKS_PROVISION = 0 (gdb) monitor SetReg.KMU.SRC = 0x20000000 (gdb) monitor exec SetReg.KMU.TASKS_PROVISION = 1 ``` 配置完成后,我将密钥值推送到目标地址。 ``` (gdb) monitor exec SetReg.KMU.TASKS_PUSH = 0 (gdb) monitor exec SetReg.KMU.TASKS_PUSH = 1 ``` 此时,我预期在`0x20000000`处会看到配置的值(`0xab`重复)。使用调试器的内存检查命令查看地址,但值仍然是之前的值。在检查了多次并确认没有错误后,我感到有些困惑。我决定使用`info registers`命令检查KMU寄存器,并注意到`STATUS`寄存器中的`PUSHADDR`字段指示目标地址为`0x20000000`。我再次读取该内存地址,值依然没变。 为了确认问题是否出在调试器上,我决定编写一个小程序来读取内存地址。我创建了一个最小的固件,它会立即读取`0x20000000`并将值通过串口输出。构建并刷写后,串口输出确认了新值确实存在于内存中。这表明调试器报告的值是过时的。 为了进一步调查,我查阅了J-Link文档。经过搜索,我发现了J-Link内存缓存相关的命令`SetEnableMemCache`。文档中提到,默认情况下,为了提高性能,内存缓存是启用的。文档还指出,此命令可能不被任何支持的IDE用来默认禁用内存缓存机制。它仅允许特定客户用于需要禁用缓存机制的非常特定的测试案例。 渴望观察禁用内存缓存是否确实会在发出检查命令时返回新值,我再次擦除了设备并连接了GDB。在执行任何操作之前,我禁用了内存缓存。 ``` (gdb) monitor exec SetEnableMemCache = 0 ``` 对第一个槽位执行配置和推送操作后,我像之前一样观察到了预期值。然后在第二次配置和推送时,检查命令终于返回了更新后的值。 ``` 0x20000000: 0xde56f4de 0xf4de56f4 0x56f4de56 0xde56f4de ``` 然而,正如J-Link文档所述,你通常不希望关闭缓存。在这种情况下,`JLinkArm.dll`内存缓存返回过时值的原因是CPU暂停了,我们正在尝试读取已经访问过且CPU未向前推进的内存地址。启用内存缓存时,向前推进几条CPU指令(`stepi`)会导致缓存被清除,并在下次读取时返回新值。 除了像nRF54LM20的KMU这样的外设具有直接内存访问(DMA)(https://en.wikipedia.org/wiki/Direct_memory_access)能力且能在核心暂停时写入数据这样的用例外,你通常不会遇到调试器内存缓存值过时的问题。如果确实遇到,理解底层总线架构以及如何通过直接从访问端口读取来绕过缓存会很有帮助。

相似文章

微控制器电路调试冒险记

Hacker News Top

作者详细描述了Floppy Emu微控制器板神秘故障的故障排除过程,调查了潜在原因,如不良芯片或组装缺陷。

NX位不仅仅关乎安全

Lobsters Hottest

一位开发者描述了在postmarketOS的ARM64裸机虚拟机管理程序中调试一个复杂bug的过程,涉及指令缓存一致性问题和与NX位相关的硬件特定行为。

整数无故发生神秘变化,而本应无代码生成影响

The Old New Thing (Raymond Chen)

一位开发者发现,交换两个等效宏竟导致无关函数中出现意外的整数变化,这篇博客文章深入探究了这一谜团,并对某个大语言模型(LLM)关于控制流保护的解释提出了质疑。