Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

2.17 按地址写内存(0x3D)——直接注射

当DID体系成了一道墙

你在做一件DID框架干不了的事。某个ECU的Flash中0xA0004000地址处,存放着一组工厂校准参数——这组参数没有被分配任何DID。为什么?因为它太“底层“了——它不是“发动机转速“,不是“冷却液温度“——它是CPU读取ADC原始通道后使用的线性化系数。它存在于硬件层面,但在诊断语义层面上没有名字。

你需要改它。发动机的某个传感器换了供应商,新传感器在2.5V的输出对应的是85°C而不是原来老传感器的83°C——不改这个系数,ECU读到的温度就永远是偏的。

你只有裸地址。没有DID。你怎么写?

这条命令就是0x3D WriteMemoryByAddress——UDS里最“原始“也最“危险“的写入手段。它完全绕过DID映射层,直接按物理地址打入内存——就像医生不通过病历记录,直接把一管药注射到血管里。


三个不得不绕过DID的真实场景

场景一:工厂标定站

产线末端的EOL(End of Line,生产线终端)工位上,一辆新车的发动机刚刚跑完首次热试。标定软件读取了以下数据:喷油器1在2.0ms喷射脉冲下的实际流量是0.042g——比标称值0.040g偏高5%。ECU软件需要把这个偏差补偿值写进NvM的标定区——但标定区没有DID映射,因为它是在ECU软件开发完成后才确定的、由产线动态生成的数据。标定工程师只能按地址写。

场景二:安全研究/渗透测试

安全研究员正在验证某个ECU的内存保护机制。“按照AUTOSAR规范,安全等级为0的会话不应该能写入校准区——让我们来试试。“他发出一条0x3D请求,目标地址正好落在NvM校准块的地址范围。如果ECU直接接受了——这就是一个安全漏洞——意味着不需要解锁安全访问就能篡改标定数据。渗透测试团队需要0x3D作为验证工具。

场景三:ODX数据库不一致时的紧急修复

一辆待交付的新车在最终质检测试时发现某ECU的一个校准参数错误。服务站查询ODX数据库试图用0x2E写该参数对应的DID——但发现ODX数据库版本陈旧,没有包含这个DID的定义。等新数据库发布需要两周——车不能等。工程团队通过调试工具查到了该参数的物理地址——用0x3D直接写入。

这三个场景有一个共同点:你操作的不是“参数“,是“内存“。 DID体系是语义层——0x3D是物理层。这是UDS最底层的写入手术。


addressAndLengthFormatIdentifier深度解析

0x3D和0x23(按地址读)以及0x34(下载)共享同一个编码机制——用一个字节同时描述地址宽度和长度宽度:

addressAndLengthFormatIdentifier (1字节):
┌───────────┬───────────┐
│ Bit 7 ~ 4 │ Bit 3 ~ 0 │
├───────────┼───────────┤
│memorySize │memoryAddr │
│  Length   │  Length   │
└───────────┴───────────┘

memoryAddressLength = 该字段实际值 (多少字节来编码地址)
  0x1 = 1字节地址 (最大256B空间——仅适用于小MCU)
  0x2 = 2字节地址 (最大64KB空间)
  0x4 = 4字节地址 (最大4GB空间——32位处理器)
  0x8 = 8字节地址 (64位处理器)

memorySizeLength = 该字段实际值 (多少字节来编码"写入多少字节")
  0x1 = 1字节 (最多写入255字节)
  0x2 = 2字节 (最多写入65535字节)
  0x4 = 4字节 (最多写入约4GB)

为什么用半字节而不是各自独立字节? 如果你已经读过0x34下载服务的章节,你会记得相同的问题和相同的答案——节省帧头开销。CAN单帧只能携带7字节的有效载荷——每省下一字节就是8个bit。用一个字节编码两种信息(地址宽度+长度宽度)而不是两个字节——这是ISO 15765-2传输层极限带宽约束下的设计。诊断协议的设计者们在每一个bit上都精打细算。

核心洞察:addressAndLengthFormatIdentifier不是数据——它是元数据。它告诉ECU“接下来几字节是地址、接下来几字节是长度“——让报文解析器不需要额外的配置就能理解变长字段。这是UDS协议自描述性的一个经典案例——报文自己携带着如何被解析的指令。


完整请求与响应解剖

请求

┌─────┬──────────────────────────────────┬──────────────────────┬──────────────────────┐
│ 0x3D│  addressAndLengthFormatIdentifier│  memoryAddress[ ]     │  memorySize[ ]      │  dataRecord[ ]
├─────┼──────────────────────────────────┼──────────────────────┼──────────────────────┼──────────────────────┤
│ SID │ 1 byte                           │ n bytes              │ m bytes              │ k ytes               │
└─────┴──────────────────────────────────┴──────────────────────┴──────────────────────┴──────────────────────┘
      n = memoryAddressLength
      m = memorySizeLength
      k = memorySize的值 (即实际要写入的数据的字节数)

正响应

┌─────┬──────────────────────────────────┬──────────────────────┬──────────────────────┐
│ 0x7D│  addressAndLengthFormatIdentifier│  memoryAddress[ ]     │  memorySize[ ]      │
├─────┼──────────────────────────────────┼──────────────────────┼──────────────────────┤
│SID+ │ 回声                              │ 回声                  │ 回声                 │
│0x40 │                                  │                       │                     │
└─────┴──────────────────────────────────┴──────────────────────┴──────────────────────┘

具体实例——向地址0xA0004000写入4字节系数0x3F800000(浮点1.0):

请求:
  0x3D 0x44 0xA0 0x00 0x40 0x00 0x00 0x00 0x00 0x04 0x3F 0x80 0x00 0x00
  │SID│ │memSizeLen=4 │──────memoryAddress=0xA0004000───────│ │mSize=4│ │data=1.0f│
       │memAddrLen=4                        (4 bytes)          (4 bytes)  (4 bytes)
        ↓
  0x44 = 高位半字节0x4(memSizeLen=4) + 低位半字节0x4(memAddrLen=4)

正响应:
  0x7D 0x44 0xA0 0x00 0x40 0x00 0x00 0x00 0x00 0x04
  │   │同印│──────地址回声──────│ │─长度回声─│

注意:正响应不返回写入的数据——和0x2E一样的设计哲学——诊断仪持有自己发送的数据。但正响应返回了地址和长度——因为同一个ECU可能支持不同的地址宽度(比如0x2和0x4并行),回声机制确保诊断仪知道ECU是按哪个地址宽度解析的请求。


0x3D vs 0x34→0x36→0x37——注射vs手术

这是UDS里两条写入物理内存的路径,但设计哲学截然不同:

维度0x3D (按地址写)0x34→0x36→0x37 (下载)
步数1步——一次性完成3步——RequestDownload→TransferData→RequestTransferExit
最大数据量受传输层MTU限制——CAN上约4KB不限——可传输完整固件镜像(数MB)
地址解析每次请求携带完整地址地址只在RequestDownload中指定一次
完整性校验——不校验写入是否完整有——TransferData携带BlockSequenceCounter
并发性无并发保护阻塞式——下载进行中拒绝其他写入
撤销能力无——写入即生效有——RequestTransferExit可以报错阻止全量写入
安全等级要求高——通常Level 3高——编程会话+Level 3
医院类比门诊注射——一针下去,即刻生效住院手术——麻醉→手术→复苏,全流程管理

什么时候用0x3D而不是下载流程?

  • 修改单个参数值(校准系数、DID之外的阈值)
  • 数据量小——几十到几百字节
  • 需要立即生效——不希望走“请求→传输→提交“的三步流程
  • 目标地址已知且不变

什么时候用下载流程?

  • 固件升级——传输完整的新版软件镜像
  • 大批量配置——几百KB的标定数据表
  • 需要传输完整性保证——BlockSequenceCounter校验每个数据块
  • 需要失败回滚——如果传输中断,下载未完成则旧固件保留

风险——为什么0x3D是UDS里最危险的服务之一

0x3D是UDS协议里少数的几条“后果自负“指令。它绕过了DID框架所有的语义检查——DID检查给了你什么?会话校验、安全等级校验、数据长度校验、值范围校验。0x3D只保留了会话和安全等级——其他的要靠你自己。

风险清单:

1. 地址错误 → Flash写入到错误区域 → 覆盖了Bootloader或固件 → ECU变砖
2. 长度错误 → 写到了目标区域之外的相邻数据 → 破坏其它配置
3. 没有原子性 → 中途CAN总线错误导致报文丢弃 → 部分数据写入、部分是旧值
4. 绕过值范围校验 → 写了非法值 → ECU后续读取时产生除零/溢出 → Crash
5. NvM未同步 → 写到RAM但NvM还未更新 → 下电后修改丢失

这就是为什么大多数ECU对0x3D的实施条件极其苛刻:

  • 必须是编程会话(0x10 0x02)——大多数OEM实现将其限制在编程会话,默认和扩展会话下通常不可用
  • 必须解锁最高安全等级——通常是Level 3(非对称密钥算法——包含私钥计算)
  • 地址范围白名单——ECU维护一份允许0x3D写入的地址范围表,落在白名单之外的地址返回NRC 0x31
  • 写入后校验——ECU在写入完成后将数据回读并与请求对比——不匹配就触发安全复位

基本ECU端校验逻辑

FUNC(Std_ReturnType, DCM_CODE) Dsp_Memory_WriteByAddress(
    P2CONST(uint8, AUTOMATIC, DCM_APPL_DATA) pReqData,
    uint16 reqLen)
{
    uint8 addr_len, size_len;
    uint32 mem_addr = 0, mem_size = 0;
    uint16 header_len, total_len;

    /* 1. 解析地址/长度宽度 */
    addr_len = pReqData[0] & 0x0F;    /* 低半字节 */
    size_len = (pReqData[0] >> 4) & 0x0F; /* 高半字节 */

    /* 2. 最短长度检查 */
    header_len = 1 + addr_len + size_len;  /* SID+addrLen已在之前减掉1 */
    if (reqLen < header_len) {
        Dcm_SendNrc(0x3D, NRC_IMLOIF);
        return E_NOT_OK;
    }

    /* 3. 解析地址(支持1/2/4字节) */
    for (uint8 i = 0; i < addr_len; i++) {
        mem_addr = (mem_addr << 8) | pReqData[1 + i];
    }

    /* 4. 解析长度(支持1/2/4字节) */
    for (uint8 i = 0; i < size_len; i++) {
        mem_size = (mem_size << 8) | pReqData[1 + addr_len + i];
    }

    /* 5. 总长度一致性验证 */
    total_len = header_len + (uint16)mem_size;
    if (reqLen != total_len) {
        Dcm_SendNrc(0x3D, NRC_IMLOIF);
        return E_NOT_OK;
    }

    /* 6. 地址范围白名单检查 */
    if (!MemIf_IsWritableAddress(mem_addr, mem_size)) {
        Dcm_SendNrc(0x3D, NRC_REQUEST_OUT_OF_RANGE);
        return E_NOT_OK;
    }

    /* 7. Flash对齐检查 */
    if ((mem_addr % FLASH_WRITE_ALIGNMENT) != 0) {
        Dcm_SendNrc(0x3D, NRC_GENERAL_REJECT);
        return E_NOT_OK;
    }

    /* 8. 执行写入 */
    P2CONST(uint8, AUTOMATIC, DCM_APPL_DATA) pData = &pReqData[header_len];
    Std_ReturnType ret = FlashIf_Write(mem_addr, pData, (uint32)mem_size);
    if (ret != E_OK) {
        Dcm_SendNrc(0x3D, NRC_GENERAL_REJECT);
        return E_NOT_OK;
    }

    /* 9. 正响应 */
    {
        uint8 resp[1 + 1 + 4 + 4]; /* SID+0x40 + addrLenFormat + addr + size */
        uint8 idx = 0;
        resp[idx++] = 0x7D;
        resp[idx++] = pReqData[0]; /* 地址长度格式回声 */

        for (uint8 i = 0; i < addr_len; i++) {
            resp[idx++] = pReqData[1 + i];
        }
        for (uint8 i = 0; i < size_len; i++) {
            resp[idx++] = pReqData[1 + addr_len + i];
        }

        Dcm_SendPositiveResponse(resp, idx);
    }

    return E_OK;
}

注意第6步的白名单检查——这是ECU防御0x3D误用的最后一道屏障。一个典型的白名单可能只包含NvM的校准区和配置区——再多的就不会开放。任何试图写入代码区、Bootloader区、或RAM中堆栈/堆的请求都将被拒绝。


本篇小结

  1. 0x3D WriteMemoryByAddress 是UDS中最底层的写入手段——完全绕过DID语义层,直接按物理地址操作内存。
  2. addressAndLengthFormatIdentifier用一个字节的半字节编码分别表示地址宽度和长度宽度——是CAN带宽约束下节省帧头的精密设计。
  3. 0x3D和0x34下载流程是“注射vs手术“的关系——前者一步完成、适合小数据、无完整性保证;后者三步流程、适合大文件传输、有BlockSequenceCounter校验。
  4. 0x3D的风险极高——地址错误可致ECU变砖——因此大多数ECU将其限制在编程会话+最高安全等级+地址白名单三重门禁之内。
  5. 主要使用场景:工厂EOL标定、安全渗透测试、ODX数据库缺失时的紧急参数修复——都是开发/产线/安全场景而非售后常规诊断。

【下集预告】:常用服务你已经读完了——但还有最后一个主角,它不是一个SID,而是一个影子角色——负响应码(NRC)。一个简单的’不行’要附带上20多种不同的’为什么不行’——NRC是把沉默变成指南的编码系统。