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中堆栈/堆的请求都将被拒绝。
本篇小结
- 0x3D WriteMemoryByAddress 是UDS中最底层的写入手段——完全绕过DID语义层,直接按物理地址操作内存。
- addressAndLengthFormatIdentifier用一个字节的半字节编码分别表示地址宽度和长度宽度——是CAN带宽约束下节省帧头的精密设计。
- 0x3D和0x34下载流程是“注射vs手术“的关系——前者一步完成、适合小数据、无完整性保证;后者三步流程、适合大文件传输、有BlockSequenceCounter校验。
- 0x3D的风险极高——地址错误可致ECU变砖——因此大多数ECU将其限制在编程会话+最高安全等级+地址白名单三重门禁之内。
- 主要使用场景:工厂EOL标定、安全渗透测试、ODX数据库缺失时的紧急参数修复——都是开发/产线/安全场景而非售后常规诊断。
【下集预告】:常用服务你已经读完了——但还有最后一个主角,它不是一个SID,而是一个影子角色——负响应码(NRC)。一个简单的’不行’要附带上20多种不同的’为什么不行’——NRC是把沉默变成指南的编码系统。