UDS诊断
UDS 分析里,最常见的错误往往发生在服务处理之前:把 ISO-TP 长度当成 SID,把 CAN ID 当成 DID,或者把不同 ECU 的连续帧拼到了一起。字节看着都对,层次一错,后面的解释就会全部跑偏。
下面从网络层、NRC、DID 和常用服务讲到完整的多帧重组与会话时序。除单独说明外,本文例子限定为 经典 CAN、8 字节数据场、正常寻址。报文均为教学构造,不代表某款 ECU 的服务配置。
网络层协议数据单元 N_PDU
UDS 的服务语义主要由 ISO 14229 定义;在 CAN 上传输较长诊断消息,还要依赖 ISO-TP(ISO 15765-2)。因此抓到下面这条 CAN 数据时:
1 | |
03 是 ISO-TP 单帧长度;22 F1 90 才是 UDS 请求,分别是 SID 和 DID;剩余 55 是本例采用的填充。
N_PDU 的组成与寻址
N_AI、N_PCI、N_Data 分别对应寻址信息、协议控制信息与有效数据。分析时把两个维度分开:
- 物理 / 功能寻址:一次请求面向一个节点,还是一组满足条件的节点。
- 正常 / 扩展 / 混合等地址格式:地址信息由 CAN ID 承载,还是还需要数据场中的地址字节。
“扩展寻址”不等于“29 位 CAN 扩展帧”;正常寻址也可以使用 29 位 CAN ID。正常固定寻址常见的 29 位物理地址形式是 0x18DA<TA><SA>,但源、目标地址和功能寻址约定仍需结合所用配置解释。0x7DF 是常见 11 位 OBD 功能请求 ID,不能推广成所有厂商 UDS 功能请求的统一 ID。python-can-isotp 的地址模式说明可以辅助核对这两种维度。
四种帧与 N_PCI
| 帧 | 高半字节 | 本文配置下的关键字段 |
|---|---|---|
| SF 单帧 | 0 |
低半字节给出长度,可容纳 1~7 字节诊断数据 |
| FF 首帧 | 1 |
本文使用两字节 PCI 中的 12 位总长度,首帧带 6 字节数据 |
| CF 连续帧 | 2 |
低半字节是序号 SN,后面通常最多 7 字节数据 |
| FC 流控帧 | 3 |
Flow Status、Block Size、STmin |
CF 序号从 1 开始,经过 0xF 后回到 0。重组必须验证序号和总长度,而不是去掉每帧第一个字节后无条件拼接。
FC 由正在接收多帧数据的一方发出。如果 ECU 正在回复长消息,FC 就由诊断仪发给 ECU;如果诊断仪正在发送长请求,方向反过来。
30 00 05 表示 Continue To Send、BS=0、STmin=5 ms。BS=0 表示本次传输无需每过固定数量 CF 再等待一个 FC,并不取消其他时序限制。STmin 的 00~7F 对应 0~127 ms,F1~F9 对应 100~900 μs,其他编码不能直接按毫秒解释。Linux ISO-TP 文档给出了相关字段与接口。
CAN FD 的边界
CAN FD 可以使用更长的数据场;相应 ISO-TP 单帧长度编码、容量和填充规则也需要按实际 DLC 与寻址格式处理。扩展长度 FF 也不能套用这里的两字节 PCI 简化模型。
因此下面的离线脚本刻意只处理经典 CAN 正常寻址,不声称兼容所有 CAN FD 报文。实际工程优先使用完整协议栈,避免在一个教学拼包脚本上继续堆协议例外。
ISO-TP 真正需要维护哪些状态
只拼接字节可以解释一条正常报文,却不足以分析一次失败的传输。对于一个正在接收的多帧消息,至少要保存 total_length、已接收长度、期望 SN、当前 block 的 CF 数量、最近一帧时间与流控状态。两条传输若共用这些变量,一遇到交错报文就会互相覆盖。
一个连接的标识应包含通道、寻址格式、请求/响应 ID、必要的扩展地址,以及数据方向。例如 can0 / normal-11 / 7E0 / 7E8 / ECU→Tester。这里的 ID 对从配置或上下文得来,不能对任意厂商报文都机械使用“响应 ID 减 8”。
用 30 字节、BS=2 算一遍
经典 CAN 正常寻址下,FF 带 6 字节,之后每条 CF 最多带 7 字节。总长 30 字节需要:
1 | |
假设接收端返回 30 02 05,表示每个 block 最多接收 2 个 CF,CF 间要求 5 ms 的 STmin。发送端在 FF 后等待第一次 CTS,发完 CF1、CF2 后再等第二次 CTS,随后发 CF3、CF4。本例一共需要 2 个 FC。CF4 已经完成全部负载,不能因为它又凑够了两个 CF,就再等待一个毫无用途的 FC。
BS 统计的是连续帧数量,不包括 FF 或 FC。BS=0 表示不用在中途因块数量再次等待 FC;发送端仍然要在 FF 后等待首次流控,也仍然受 STmin 约束。
CTS、WAIT 与 Overflow
| FC 流状态 | 接收方含义 | 发送方应该怎样处理 |
|---|---|---|
0 CTS |
允许继续 | 使用本次协商的 BS、STmin 推进发送 |
1 WAIT |
暂时不能继续 | 暂停,并受等待次数/时间策略约束 |
2 Overflow |
无法接收本次长度 | 终止传输,向上层报告失败 |
WAIT 不是允许永远占用缓存的承诺。实现通常需要有上限,超出上限就释放本次状态。Overflow 也不能按 CTS 处理后继续灌数据,否则正好打穿接收端声明的容量边界。
同一个协议在具体栈里还有实现限制。例如 Linux 内核 ISO-TP 文档说明其接收端不发送 WAIT,而是在收到 FF 时判断能否接收整条消息。分析代码时应把“协议允许什么”和“所选栈实现了什么”分别核对。内核 ISO-TP 接口与错误语义能直接对应 FC 超时、序号错误、填充错误等失败原因。
SN 回绕、重复与缺帧
CF 的 SN 是 4 bit,因此序列是:
1 | |
每次新的 FF 开始,第一条 CF 又从 1 计数。SN 不是整个 CAN 通道的全局序号,20 也不意味着“无效连续帧”。只在完整传输上下文中,才能判断它是正常回绕还是错误。
重组器若期待 SN=3 却收到 SN=4,不能简单跳过差异继续拼,因为应用负载的字节偏移已经不可信。重复帧也不能无条件追加;这里没有 TCP 那种按绝对字节偏移自动去重的机制。诊断工具应把传输错误和 ECU 返回的 NRC 分开记录:前者可能尚未产生可交给 UDS 的完整消息。
STmin 到底量哪两个时刻
STmin 描述相邻 CF 的最小分离要求,不能把它当作 ECU 的总响应时间。记录仪只给出一个时间戳时,先确认它记录的是帧开始、帧结束还是主机收到回调的时刻。控制器队列、USB 批量上传、驱动调度都会改变后两种观察值。
本文离线附件约定时间戳为帧结束时刻。两个 CF 的结束时刻之差已经小于 STmin 时,可以判定间隔不足;反过来,结束时刻之差大于 STmin,并不能证明“前帧结束到后帧开始”的要求一定满足,因为还没有扣除后帧本身的传输时间。这也是脚本只称为轨迹检查器的原因。
分析微秒级 STmin 时尤需注意这一点:F3 是 300 μs,不是 243 ms。Python sleep(0.0003) 或桌面操作系统中的定时回调,也不能自动承诺 300 μs 精度。python-can-isotp 参数说明分别提供发送间隔、阻塞发送与等待函数等选项,使用时还要检查底层时钟和调度能力。
把流控成本算进传输预算
对本文的长度范围,负载 L 大于 7 时:
1 | |
L=4095 时需要 585 个 CF。取 BS=8,需要 74 个 FC。若 STmin=5 ms,仅 584 个相邻 CF 间隔的最低等待量就是 2.92 s;还没有计入帧本身的传输、总线仲裁和接收端返回流控的时间。
这解释了一个常见误会:ECU 可以及时启动响应,但整条长响应依然耗时数秒。拿 P2=50 ms 去限制整条 ISO-TP 长消息全部收完,容易制造假超时。实际库如果只能向上层返回完整 PDU,就还要核查它如何暴露首帧事件、如何计算接收期间的等待以及总请求超时,不能只依据参数名字推断计时起止。
CAN FD 与长长度 FF:解析器在哪里分支
旧的两字节 FF PCI 只编码 12 bit 长度,不能把 4095 误认为所有现代 ISO-TP 实现的最大负载。较新格式可以在特定 FF 长度标记之后使用更长的长度字段;接收端也应按自身配置限制最大消息,避免对远端声明的巨大长度无条件分配内存。
对 CAN FD,单帧的长度解析需要同时看链路层数据长度与 PCI 编码。常见分支是:适用普通 SF 编码时从低半字节取长度;适用 escape 格式时,首字节的长度半字节为零,后续长度字节再给出有效负载长度。PCI 和可能的地址字节都占用容量,不能简单认为“64 字节 CAN FD 帧就能放 64 字节 UDS”。
工程里应先固定地址模式和 tx_data_length,再按库支持的标准版本选择处理。本文脚本发现超出经典 CAN 正常寻址范围的输入就拒绝,不以“尽量拼出来”的方式冒充兼容 CAN FD。
定时器不能合成一个 timeout
传输层和应用层的超时负责不同阶段。即使软件最后都抛一个 TimeoutError,日志也应该保留发生在哪个阶段。
| 计时概念 | 等待或约束对象 | 典型问题 |
|---|---|---|
| N_As / N_Ar | 网络层帧发送及链路确认相关过程 | 下层队列拥塞、发送无法完成 |
| N_Bs | 发完 FF 或当前 block 后等待 FC | 地址不匹配、流控丢失、接收方停止 |
| N_Cr | 接收方等待后续 CF | 帧丢失、发送端中断、调度延迟 |
| P2 | 请求后的首次响应时限 | ECU 未及时开始响应处理 |
| P2* | 收到 ResponsePending 后的等待 | 长操作迟迟没有后续响应 |
| S3 | 非默认会话空闲维持相关时限 | 长时间无有效诊断活动导致会话退出 |
| 工具总截止时间 | 用户允许一次操作占用的总时长 | 连续 pending 使工具无限等待 |
这不是 ISO-TP 所有计时参数的完整清单,而是抓包排错时最常用的几个位置。具体计时起止、容差和可配置项要回到所用栈与会话层定义。
NRC 78 的等待模型
假设客户端首次响应预算为 80 ms,pending 后预算为 5000 ms,总操作预算为 6000 ms。时间线如下:
1 | |
附件的 PendingRequest 使用显式时间戳模拟这条时间线,得到的截止点依次是 5040、6000。没有真的等待 7 秒,也没有把模拟时钟包装成 ECU 响应时间的实测值。
这里还要验证否定响应中的原请求 SID。等待 31 时收到 7F 22 78,不能据此刷新 31 的计时器。肯定响应同样需要匹配子功能、RID、DID 或块号;只看“首字节等于 SID+40”还不够。
对于 78,客户端继续等最终结果;对于某些 busy/retry 类错误,重试策略、延迟和总次数要另行定义。写操作是否允许重试更不能由一个通用网络重试装饰器决定。udsoncan 的超时配置把首次等待、pending 等待和全局请求上限明确分开,可用来对照自己的客户端实现。
UDS 的请求与响应
请求至少包含一个 SID;后面有没有子功能,由具体服务决定:
1 | |
常见肯定响应的 SID 是请求 SID 加 0x40,随后字段仍按具体服务定义解释。否定响应的 UDS 负载为三个字节:
1 | |
比如对 36 服务的否定响应可以是 7F 36 31。放进单帧之后为 03 7F 36 31 ...,第二个 UDS 字节不能写成肯定响应 SID 76。
常见 NRC
| NRC | 含义 | 分析时重点检查 |
|---|---|---|
10 |
General Reject | 通用拒绝,不等于专指服务未实现 |
11 / 12 |
服务 / 子功能不支持 | SID、子功能与 ECU 能力 |
13 |
长度或格式错误 | 重组、参数长度、字段顺序 |
22 |
前提条件不满足 | ECU 状态与具体服务条件 |
24 |
请求顺序错误 | 是否遗漏前置请求或状态已经变化 |
31 |
请求超出范围 | DID/RID、地址、长度或参数值 |
33 |
安全访问拒绝 | 对应安全级别是否满足 |
35 / 36 / 37 |
错误 Key / 尝试次数超限 / 等待时间未到 | SecurityAccess 的失败与延时状态 |
70 / 71 / 72 / 73 |
上传下载拒绝 / 传输挂起 / 编程失败 / 块计数错误 | 下载流程与块编号 |
78 |
请求已接收,最终响应尚未就绪 | 延长等待,继续关联后续响应 |
7E / 7F |
当前会话不支持子功能 / 服务 | 当前 session |
92 / 93 |
电压过高 / 过低 | 条件是否满足 |
NRC 78 不是最终成功,也不是简单重发原请求的信号。诊断仪应按相应时间参数等待最终响应;如果直接把它当失败重试,可能让同一个操作重复开始。
完整例子:读取 F190 并重组长响应
下面用一个人工构造的 17 字节标识 LTEST123456789012 演示 F190 响应。它不对应真实车辆,本例只讨论传输重组,不进行 VIN 合法性校验。
1 | |
首帧中的 0x014 表示 UDS 负载总长 20 字节:62 F1 90 占 3 字节,后面的标识占 17 字节。首帧先带 6 字节负载,两个 CF 各带 7 字节,正好凑齐 20。
注意 30 00 05 是流控,不属于 UDS 响应内容;两条 CF 也应遵守这里声明的间隔。只保留 ECU→Tester 的数据帧重组后得到:
1 | |
附件 can_decode.py会运行这个例子,并拒绝缺帧、序号错误和非法单帧长度。它只处理已经采集到的教学帧,不生成或处理 FC,也不实现超时与 STmin 调度:
1 | |
1 | |
这是已经按连接、方向筛选后的离线重组。混合抓包还需要先区分 CAN 通道、地址对、寻址格式和每次传输的起止,不能把所有 21 开头的帧归在一起。
会话、安全级别与条件要分别判断
进入 Extended Session 不代表已经解锁;完成某一级 SecurityAccess 也不代表所有服务都能执行。一个服务可能同时要求指定会话、安全级别和车辆状态,例如电压范围、车速或其他先决条件。
服务与会话的支持矩阵、会话跳转顺序和安全恢复行为取决于 ECU 配置,应从诊断描述或实测中确认,不能把别的车型的矩阵直接搬来。
0x10:DiagnosticSessionControl
常见子功能为 01 默认会话、02 编程会话、03 扩展会话。示例:
1 | |
响应中 00 32 表示 P2Server_max=50 ms,01 F4 对应 P2*Server_max=500×10 ms=5 s。它们涉及响应等待,与保持非默认会话的 S3 定时器不是同一个量。
服务基础并不意味着“通常不会否定响应”。不支持的子功能、条件不满足等都可能产生 NRC。具体参数定义可对照 udsoncan 的服务接口。
0x3E:TesterPresent
1 | |
3E 80 中,子功能仍是低 7 位的 00,高位 0x80 是 suppressPosRspMsgIndicationBit,表示抑制肯定响应。它不是独立的“80 号子功能”,也不表示所有错误都必须无响应。
保持频率需要结合 ECU 的 S3 约定及其他诊断请求处理,不能从上面的 P2=50 ms 推出“每 50 ms 发一次 TesterPresent”。
0x27:SecurityAccess
典型流程是奇数子功能请求 Seed,相邻偶数子功能提交对应 Key。Seed 长度、Key 长度、计算方法和失败策略取决于实际实现。
1 | |
这里 BB... 和 CC... 仅表示占位数据,没有定义任何 Seed-to-Key 算法。接受 Key 的响应只有 67 02 两字节,单帧长度应为 02。
分析时记录请求安全级别、Seed 是否随请求改变、失败次数与延时如何作用于会话。不能把某次算法输出正确等同于整个访问控制流程可靠;诊断认证服务等其他机制也可能同时存在。
数据访问与常用 DID
0x22:ReadDataByIdentifier
读取请求是 22 DID_hi DID_lo,DID 为两字节。一次可以请求多个 DID,但回复中的数据通常不是自描述 TLV;必须知道各 DID 的编码和长度,才能正确分开后续记录。
下面的常见 DID 表适合作为查找入口,具体长度、编码和支持情况仍需查询目标 ECU 的诊断描述。
| DID(0x) | Description | 说明 |
|---|---|---|
| F180 | bootSoftwareIdentificationDataIdentifier | BOOT软件ID数据ID |
| F181 | applicationSoftwareIdentificationDataIdentifier | 应用软件ID数据ID |
| F182 | applicationDataIdentificationDataIdentifier | 应用数据ID数据ID |
| F183 | bootSoftwareFingerprintDataIdentifier | BOOT软件指纹数据ID |
| F184 | applicationSoftwareFingerprintDataIdentifier | 应用软件指纹数据ID |
| F185 | applicationDataFingerprintDataIdentifier | 应用数据指纹数据ID |
| F186 | ActiveDiagnosticSessionDataIdentifier | 当前诊断会话数据ID |
| F187 | vehicleManufacturer SparePartNumberDataIdentifier | 主机厂车辆备件数据ID |
| F188 | vehicleManufacturer ECUSoftwareNumberDataIdentifier | 主机厂车辆ECU软件编号数据ID |
| F189 | vehicleManufacturer ECUSoftwareVersionNumberDataIdentifier | 主机厂车辆ECU软件版本编号数据ID |
| F18A | systemSupplierIdentificationDataIdentifier | 系统供应商ID数据ID |
| F18B | ECUManufacturingDateDataIdentifier | ECU制造日期数据ID |
| F18C | ECUSerialNumberDataIdentifier | ECU序列号数据ID |
| F190 | VINDataIdentifier | VIN数据ID |
| F191 | vehicleManufacturer ECUHardwareNumberDataIdentifier | 主机厂ECU硬件编号数据ID |
| F192 | systemSupplier ECUHardwareNumberDataIdentifier | 系统供应商ECU硬件编号数据ID |
| F193 | systemSupplier ECUHardwareVersionNumberDataIdentifier | 系统供应商ECU硬件版本编号数据ID |
| F194 | systemSupplier ECUSoftwareNumberDataIdentifier | 系统供应商ECU软件编号数据ID |
| F195 | systemSupplier ECUSoftwareVersionNumberDataIdentifier | 系统供应商ECU软件版本编号数据ID |
0x2E:WriteDataByIdentifier
请求形如 2E DID_hi DID_lo dataRecord,肯定响应通常回显 DID。能通过 22 读取一个 DID,不代表允许通过 2E 写它。写入条件、长度和持久化行为都需要单独确认。
0x2F:InputOutputControlByIdentifier
这个服务把某个 DID 对应的 I/O 交给诊断控制,参数包含控制方式及相应记录。它与永久写配置不同;分析时要确认控制能否返回 ECU、自身的超时恢复和条件限制,不能只看是否收到 6F。
复位、通信与故障信息
| 服务 | 常见操作 | 分析重点 |
|---|---|---|
11 ECUReset |
01 硬复位、02 key-off/on、03 软复位 |
实际重置范围、响应与复位时序、重启后的会话状态 |
28 CommunicationControl |
控制指定类型消息的收发 | 是否只影响常规通信/网络管理等指定对象,别理解成切断所有物理总线 |
85 ControlDTCSetting |
01 开启、02 停止相应 DTC 设置 |
与清除已经保存的故障不同 |
14 ClearDiagnosticInformation |
请求清除指定 DTC 组信息 | 操作改变诊断记录,不是只读查询 |
19 ReadDTCInformation |
按子功能读取状态、快照、扩展记录等 | 状态掩码、记录格式和支持的子功能 |
11 01 不能直接理解成“恢复出厂并清空全部非易失数据”;具体复位语义需要看实现。
以 19 02 FF 为例,子功能 02 请求按状态掩码报告 DTC。响应中的三字节 DTC 与一个状态字节要分别解释,也不能套用 OBD 两字节 DTC 的解析方式。报文“包含故障码”不意味着其格式统一。
RoutineControl 与下载流程
0x31:RoutineControl
三个常见子功能分别为 01 StartRoutine、02 StopRoutine、03 RequestRoutineResults。后面是两字节 RID,再接 routine 约定的参数。RID 与 DID 属于不同命名空间。
返回 71 只能结合后面的控制类型、RID 和 routineStatusRecord 才能判断含义。有些例程还会用 7F 31 78 告知等待,不能把第一条响应直接当最终完成。
0x34:RequestDownload
请求中的 dataFormatIdentifier 描述压缩、加密格式;addressAndLengthFormatIdentifier 指定地址和大小各占多少字节。比如后者为 44 时,高半字节表示 memorySize 占 4 字节,低半字节表示 memoryAddress 占 4 字节。
肯定响应可返回 maxNumberOfBlockLength。假设响应 UDS 负载为 74 20 04 00,长度字段占 2 字节,允许的 TransferData 请求块长度是 0x0400。在通常的 SID + blockSequenceCounter + data 布局下,数据空间要扣掉前两字节,而不是直接塞 1024 字节数据。
0x36:TransferData
UDS 的 blockSequenceCounter 和 ISO-TP 的 CF 序号是两层独立计数:一个给下载数据块编号,一个给该块拆出来的传输帧编号。
1 | |
超过单帧容量的数据块应先由 ISO-TP 拆分、流控与重组,再交给 UDS 服务处理。实际下载流程还涉及块号检查、总大小、地址范围及编程条件。
0x37:RequestTransferExit
最小请求和响应示例:
1 | |
服务可以根据流程带额外参数或返回记录,因此“没有参数”只能描述这个最小例子。退出传输得到肯定响应,也不能自行推断整套固件已经通过验证或完成激活。
把会话状态放回每一个 ECU
诊断软件可以用一个小表保存每个目标的状态:当前 session、已满足的安全级别、服务器返回的时序参数、当前未完成请求,以及是否存在下载事务。这个表必须按目标分别维护。
如果工具向 ECU A 成功发送 10 03,随后把全局变量 session=extended 用到 ECU B,就会产生一串难以解释的 7F。网关后的两个 ECU 即使使用相同 DID,支持矩阵也可能不同。重连后同样不应把上次连接的安全状态直接当成仍然有效;需要按配置和当前响应重新确认。
服务权限可以抽象为:
1 | |
这个表达式有助于定位失败发生在哪里,但不意味着 ECU 一定按这个顺序检查,也不意味着 NRC 可以反推出其全部内部状态。两个实现对相同无效请求,可能优先报告不同条件。
TesterPresent 与并发请求
S3 保活要与客户端请求调度协调。一个线程正在等待长例程的最终响应,另一个线程随时发 3E 00,若两者从同一个接收队列取消息,就可能互相拿走响应。正确做法是由一个调度器管理目标的请求与响应关联,并按协议栈/OEM 要求安排保活。
3E 80 抑制的是肯定响应,不是关闭全部错误处理。是否能够从“没有收到响应”判断会话仍有效,取决于请求与寻址条件;无响应本身没有携带成功证据。P2 和 S3 也不能互相替代:P2 只有几十毫秒,不代表需要用这个周期发送保活。
SecurityAccess 的状态比算法更值得检查
有了 Seed-to-Key 公式,只能说明能够为某种输入生成一个候选 Key。真正的状态还包括安全级别、Seed 是否仍有效、请求顺序、失败计数、等待窗口以及会话/复位后如何恢复。
例如收到 Seed 后切换会话,再提交旧 Key,服务端应该如何处理,要看目标约定的状态清理规则。只在一条正常流程里得到 67 02,不足以证明跨会话、并发请求和异常断线的行为正确。
分析失败响应时,可以逐项记录:
| 观察 | 待验证的问题 |
|---|---|
7F 27 24 |
是否未请求 Seed、级别不匹配,或中间状态已失效 |
7F 27 35 |
Key 校验失败后是否累计次数,旧 Seed 是否仍可使用 |
7F 27 36 |
超限状态作用于哪个级别、哪个目标,是否进入延时 |
7F 27 37 |
等待窗口如何结束,重连是否错误地绕过窗口 |
Seed/Key 的长度和算法不由这张表决定。本文保留前面的占位报文,不虚构一个通用解锁算法。现代诊断中的 0x29 Authentication 又是另一项服务,不能把它和 0x27 的安全级别简单视作同一个认证过程。
多 DID 响应为什么不能搜索 F1 90 来切分
请求 22 F1 90 F1 8C 后,假设目标的诊断描述规定 F190 为 17 字节、F18C 为 4 字节。这里4 字节序列号是本例配置,不是 F18C 的统一长度。
1 | |
第二个数据记录故意包含 F1 90。如果用字符串搜索 DID 作为分隔符,它会被误判成第三条记录。正确的解析过程是读 DID、从诊断字典取长度、精确消费该长度,然后再读下一个 DID。
1 | |
完整附件还检查 DID 字段本身是否截断、重复 DID 和不合法长度。可变长度记录需要目标定义的专用 codec;“未知 DID 就把剩余字节全部吃掉”只能用于明确允许消费剩余负载的配置,不能作为通用容错策略。ReadDataByIdentifier 实现同样依赖 DID 编解码定义。
服务 22 本身没有 subfunction,所以不能给 DID 高字节随意按上 0x80 来抑制响应。F190 里的 F1 就是 DID 的一部分,这类错误与前面的层次混淆本质相同。
2E:写成功的证据分几步
假设一个教学 DID F1A0 被配置为可写的两字节参数,则请求与响应可以是:
1 | |
这里的 DID 用法与长度都由教学配置指定,不表示任意 ECU 存在这个参数。回显 DID 表明这次服务得到了相应肯定响应,但值的单位、是否立即生效、是否写入非易失存储仍依赖目标定义。
验证写入时,应在同一会话读回、退出/重建会话后再读回;若测试目标允许重启,还应确认重启后的值。这样可以区分运行时缓存更新、延迟持久化和真正保存。不能把一次 6E 扩大成“永久配置已经可靠更新”。
2F:临时接管 I/O 的恢复路径
常用 inputOutputControlParameter 的值包括 00 ReturnControlToECU、01 ResetToDefault、02 FreezeCurrentState、03 ShortTermAdjustment。后面的控制状态记录和可选控制使能掩码由 DID 配置决定,不存在一套所有执行器通用的长度。
这里的风险点不仅是“能不能控制”,还有“控制怎样结束”。诊断工具断线、会话超时、条件变化时,临时控制应如何退回正常逻辑?回包 6F 只能证明服务响应,还要结合状态记录和目标反馈确认实际效果。把 I/O 控制当成永久写参数,会遗漏这一整条恢复路径。InputOutputControlByIdentifier 接口将控制参数、值和掩码分别建模。
0x19:三字节 DTC 与状态掩码
先用一个完整例子看 reportDTCByStatusMask:
1 | |
请求掩码 08 选择 confirmedDTC 状态。这个构造响应中,目标声明支持全部八个状态位;123456 的状态为 2F,ABCDEF 为 08,两条都满足 status & 08 != 0。
| bit | 状态名称 | 阅读抓包时的含义 |
|---|---|---|
| 0 | testFailed | 当前测试结果标记为失败 |
| 1 | testFailedThisOperationCycle | 本操作循环发生过失败 |
| 2 | pendingDTC | pending 状态 |
| 3 | confirmedDTC | confirmed 状态 |
| 4 | testNotCompletedSinceLastClear | 自上次清除以来测试尚未完成 |
| 5 | testFailedSinceLastClear | 自上次清除以来发生过失败 |
| 6 | testNotCompletedThisOperationCycle | 本操作循环测试尚未完成 |
| 7 | warningIndicatorRequested | 请求告警指示 |
过滤关系一般写成 status & requestedMask & availabilityMask != 0。availability 为零的位不能解释成“该状态经过验证为 false”,它也可能是目标不支持报告。掩码包含多个 bit 时,不能误写成要求所有 bit 同时成立。
19 01 mask 读取的是符合条件的数量,响应还包含 DTC 格式标识和两字节计数;不能把它按 19 02 的四字节记录循环解析。读取 snapshot、extended data 的子功能又有各自的记录布局和外部数据定义。源码层面可对照 ReadDTCInformation 与 DTC 状态定义,尤其注意所选标准版本的子功能差异。
这里故意把 DTC 显示为六位十六进制。目标的 DTCFormatIdentifier 与诊断描述确定了它如何映射到显示代码;不应该把三字节随便截成两字节再套 OBD 的 P/C/B/U 公式。
清除记录、停止设置和复位分别改变什么
14 使用三字节 DTC 分组等参数,最小肯定响应为 54;85 控制后续 DTC 设置行为,不能推断它会清掉已有记录;11 是 ECUReset,不能推断它会恢复出厂或擦除全部 NVM。三种请求可能在调试时一起出现,但它们的状态影响需要分别记录。
对 28 CommunicationControl,还要解析 communicationType,区分请求作用于哪些类型的通信。看到 68 后网络流量减少,不等于物理 CAN 控制器彻底关闭;诊断通信是否仍然允许也应按目标配置验证。
0x31:请求启动与例程完成之间的区别
用一个教学例程 RID=F001 举例,它只是为了说明字段,并非通用擦除或校验例程编号:
1 | |
optionRecord、statusRecord、resultRecord 的内容要由该 RID 的定义解释。有的实现收到启动请求后先返回 pending,再给最终结果;有的接受启动后返回状态,稍后通过 RequestRoutineResults 查询。客户端必须按目标约定处理,不能看到任意 71 就在界面标“校验通过”。
服务 31 的顺序错误还可能来自例程正在运行、尚未启动或已经结束。要把时间线和 RID 一起记录,单独摘一条 7F 31 24 无法解释前因后果。
下载流程按字节算:4 KiB 是怎样传完的
下面只讨论传输状态,不向设备写固件。教学模型在 Python 内存里保存数据;真实 ECU 的地址范围、会话、安全访问、擦除例程、签名和激活步骤都要由目标定义。
RequestDownload 的两个半字节
假设请求从地址 0x00100000 开始传输 0x00001000,即 4096 字节:
1 | |
DFI 的高半字节描述压缩方法,低半字节描述加密方法;非零编码的具体算法不能凭编号猜测。ALFI 的高半字节是大小字段字节数,低半字节是地址字段字节数,两者特别容易写反。这个请求的 UDS 负载已经超过 7 字节,需要由 ISO-TP 分帧。
假设肯定响应为:
1 | |
258 是这次约定下一个 TransferData 请求的最大长度。按 36 + blockSequenceCounter + data 布局,两字节属于服务头,实际数据最多 256 字节。因此 4 KiB 恰好分成 16 个满块。原来 74 20 04 00 的例子也一样:最大请求长度 1024,数据容量为 1022,不能直接塞 1024 字节数据。
两套计数器各管哪一层
第一块请求为 36 01 <256 bytes>,第二块为 36 02 <256 bytes>,本例一直到 36 10 <256 bytes>。UDS 的 blockSequenceCounter 为 8 bit,从第一块的 1 开始,超过 FF 后回到 00。
但每一块 258 字节都要重新经历一次 ISO-TP 传输:FF 先带 6 字节,再用 ceil((258−6)/7)=36 个 CF 发完。CF 的 4 bit SN 在这一个块内部就会多次回绕。下一块又从 FF 开始,CF1 的 SN 又是 1。
如果实现把这两个计数器合在一起,一到第 16 个 CF 就容易出错。故障表面上可能是 wrongBlockSequenceCounter 或 ISO-TP sequence mismatch,根因却来自不同层共享了错误的状态。
回应丢了,为什么不能直接发下一块
设客户端发出 36 05 ...,ECU 已经接受并写入,但是 76 05 没有到达客户端。客户端不知道执行结果,此时盲目发 36 06 ... 或重新开启下载事务都会带来额外风险。
一个需要支持重发的实现应识别“最近刚接受的相同块号”,对相同数据重发返回相应确认,避免把同一块追加两次。本文模型还显式要求重发的数据与上一块完全一致;相同块号但数据改变时直接报错。这是模型采用的保护策略,实际 ECU 的具体校验和恢复规则仍需核对目标实现。
更早的块号、未来块号、超过声明总长度的数据,都不属于“正常重发最后一块”。客户端也不能用无上限重试掩盖持续的 73。先确认双方当前事务和最后已确认块号,再决定恢复或重新开始。
本地模型实测
附件依次提交 16 块,每块 256 字节,然后把每一块原样重发一次。模型实际运行后的输出是:
1 | |
测试同时验证最终缓存只有 4096 字节、改变重复块内容会失败、257 字节数据超过这次协商容量、未传完不能 TransferExit,以及块号 FF→00 的回绕。它没有实现真实 Flash 写入、断电恢复、密钥算法或固件签名校验。
TransferExit 之后还没有结束的事
77 可以说明相应退出服务得到肯定响应,不能独自证明固件可启动。真实流程可能还有数据完整性检查、签名验证、软件依赖校验、启动槽切换、复位和软件版本回读。这些步骤的组合与例程编号不是通用答案,应结合 bootloader 与 OEM 流程分析。
这也是安全审计里需要追的路径:允许进入 34 与允许把任意镜像激活,是不同的检查点;传输过程加密和镜像签名也解决不同问题。只证明能发送数据,距离证明 ECU 会信任并运行它还差一整段证据。
把异常轨迹也跑一遍
下载 diagnostic_lab.py 与 diagnostic_trace.jsonl。运行环境为 Python 3.10+,仅使用标准库:
1 | |
脚本覆盖多种长度的重组、两个目标交错、SN 回绕、缺少流控、流控块上限、序号错误、截断、传输超时、DID 边界、pending 总截止点以及下载重发。它按完整轨迹检查,遇到不完整记录就拒绝给出貌似正常的应用负载。
在真实被动分析中,拒绝重组不等于认定发送者违规。抓包起点位于传输中途、分析仪过滤了 FC、时间戳精度不足或采集端丢帧,都可能产生相同现象。保留原始记录、采集配置与错误位置,才有条件区分总线问题、工具问题和 ECU 行为。
如果接真实设备,优先使用成熟传输栈。以 Linux 为例,CAN_ISOTP socket 的用户空间读写对象是完整应用负载;内核负责相应 ISO-TP 处理。不要把已经包含 03、10 xx 的原始帧再写给期待 UDS PDU 的接口。这和 OBD 文章里 ELM 自动格式化的重复封装问题是同一种边界错误。
从抓包到结论
实际分析按这个顺序记录会更稳:先确认通道和地址格式,再按方向重组 ISO-TP,随后解释 SID、DID/RID 和 NRC,最后把报文放回会话、安全级别与操作流程。
每个结论最好能落到一个具体请求、一个完整响应和对应时间。仅凭某条 67、74 或 7F 的出现,很容易把“进入流程”“等待结果”和“操作完成”混为一谈。
参考
- Linux Kernel:ISO 15765-2
- python-can-isotp:寻址格式
- udsoncan:服务参数与响应解析
- NI Automotive Diagnostic Command Set 手册
标准和 ECU 诊断描述是最终依据;开源库文档便于理解与验证接口,不能替代目标车辆的服务配置。
