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 22 F1 90 55 55 55 55

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

经典 CAN 正常寻址下的 SF、FF、CF 和 FC 字段

帧 高半字节 本文配置下的关键字段
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 下的流控,以及接收端需要保存的状态

用 30 字节、BS=2 算一遍

经典 CAN 正常寻址下,FF 带 6 字节,之后每条 CF 最多带 7 字节。总长 30 字节需要:

1
2
3
4
5
FF:6 字节
CF1:7 字节,累计 13
CF2:7 字节,累计 20
CF3:7 字节,累计 27
CF4:3 字节,累计 30;数据场剩余部分为填充

假设接收端返回 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
1, 2, ... E, F, 0, 1, 2, ...

每次新的 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
2
3
CF 数量 = ceil((L − 6) / 7)
若 BS > 0:FC 数量 = ceil(CF 数量 / BS)
若 BS = 0:FC 数量 = 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
2
3
4
5
0 ms     发送 31 01 F0 01
40 ms 收到 7F 31 78,下一截止点为 5040 ms
4040 ms 再收到 7F 31 78,按 P2* 可等到 9040 ms
但总截止点仍然是 6000 ms
7000 ms 即使最终 71 到达,对这个操作预算也已经超时

P2、P2* 和总截止时间共同决定何时停止等待

附件的 PendingRequest 使用显式时间戳模拟这条时间线,得到的截止点依次是 5040、6000。没有真的等待 7 秒,也没有把模拟时钟包装成 ECU 响应时间的实测值。

这里还要验证否定响应中的原请求 SID。等待 31 时收到 7F 22 78,不能据此刷新 31 的计时器。肯定响应同样需要匹配子功能、RID、DID 或块号;只看“首字节等于 SID+40”还不够。

对于 78,客户端继续等最终结果;对于某些 busy/retry 类错误,重试策略、延迟和总次数要另行定义。写操作是否允许重试更不能由一个通用网络重试装饰器决定。udsoncan 的超时配置把首次等待、pending 等待和全局请求上限明确分开,可用来对照自己的客户端实现。

UDS 的请求与响应

请求至少包含一个 SID;后面有没有子功能,由具体服务决定:

1
2
10 03          SID=10,子功能=03
22 F1 90 SID=22,DID=F190;F1 不是子功能

常见肯定响应的 SID 是请求 SID 加 0x40,随后字段仍按具体服务定义解释。否定响应的 UDS 负载为三个字节:

1
7F | 原请求 SID | NRC

比如对 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
2
3
4
5
6
方向          CAN ID    数据
Tester→ECU 7E0 03 22 F1 90 55 55 55 55
ECU→Tester 7E8 10 14 62 F1 90 4C 54 45
Tester→ECU 7E0 30 00 05 55 55 55 55 55
ECU→Tester 7E8 21 53 54 31 32 33 34 35
ECU→Tester 7E8 22 36 37 38 39 30 31 32

UDS 读取长数据时 FF、FC、CF 的完整方向与重组结果

首帧中的 0x014 表示 UDS 负载总长 20 字节:62 F1 90 占 3 字节,后面的标识占 17 字节。首帧先带 6 字节负载,两个 CF 各带 7 字节,正好凑齐 20。

注意 30 00 05 是流控,不属于 UDS 响应内容;两条 CF 也应遵守这里声明的间隔。只保留 ECU→Tester 的数据帧重组后得到:

1
62 F1 90 4C 54 45 53 54 31 32 33 34 35 36 37 38 39 30 31 32

附件 can_decode.py会运行这个例子,并拒绝缺帧、序号错误和非法单帧长度。它只处理已经采集到的教学帧,不生成或处理 FC,也不实现超时与 STmin 调度:

1
python3 can_decode.py
1
2
3
4
UDS: length=20, DID=f190, data=LTEST123456789012
truncated: rejected (incomplete payload)
wrong sequence: rejected (consecutive-frame sequence mismatch)
invalid SF length: rejected (invalid single frame)

这是已经按连接、方向筛选后的离线重组。混合抓包还需要先区分 CAN 通道、地址对、寻址格式和每次传输的起止,不能把所有 21 开头的帧归在一起。

会话、安全级别与条件要分别判断

UDS 服务访问由会话、安全级别和车辆状态共同决定

进入 Extended Session 不代表已经解锁;完成某一级 SecurityAccess 也不代表所有服务都能执行。一个服务可能同时要求指定会话、安全级别和车辆状态,例如电压范围、车速或其他先决条件。

服务与会话的支持矩阵、会话跳转顺序和安全恢复行为取决于 ECU 配置,应从诊断描述或实测中确认,不能把别的车型的矩阵直接搬来。

0x10:DiagnosticSessionControl

常见子功能为 01 默认会话、02 编程会话、03 扩展会话。示例:

1
2
请求:02 10 03 55 55 55 55 55
响应:06 50 03 00 32 01 F4 55

响应中 00 32 表示 P2Server_max=50 ms,01 F4 对应 P2*Server_max=500×10 ms=5 s。它们涉及响应等待,与保持非默认会话的 S3 定时器不是同一个量。

服务基础并不意味着“通常不会否定响应”。不支持的子功能、条件不满足等都可能产生 NRC。具体参数定义可对照 udsoncan 的服务接口。

0x3E:TesterPresent

1
2
请求:02 3E 00 55 55 55 55 55
响应:02 7E 00 55 55 55 55 55

3E 80 中,子功能仍是低 7 位的 00,高位 0x80 是 suppressPosRspMsgIndicationBit,表示抑制肯定响应。它不是独立的“80 号子功能”,也不表示所有错误都必须无响应。

保持频率需要结合 ECU 的 S3 约定及其他诊断请求处理,不能从上面的 P2=50 ms 推出“每 50 ms 发一次 TesterPresent”。

0x27:SecurityAccess

典型流程是奇数子功能请求 Seed,相邻偶数子功能提交对应 Key。Seed 长度、Key 长度、计算方法和失败策略取决于实际实现。

1
2
3
4
5
请求 Seed:02 27 01 55 55 55 55 55
返回 Seed:06 67 01 BB BB BB BB 55
提交 Key :06 27 02 CC CC CC CC 55
接受 Key :02 67 02 55 55 55 55 55
拒绝 Key :03 7F 27 35 55 55 55 55

这里 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
2
3
请求:05 36 01 AA BB CC 55 55
成功:02 76 01 55 55 55 55 55
失败:03 7F 36 31 55 55 55 55

超过单帧容量的数据块应先由 ISO-TP 拆分、流控与重组,再交给 UDS 服务处理。实际下载流程还涉及块号检查、总大小、地址范围及编程条件。

0x37:RequestTransferExit

最小请求和响应示例:

1
2
3
请求:01 37 55 55 55 55 55 55
成功:01 77 55 55 55 55 55 55
失败:03 7F 37 24 55 55 55 55

服务可以根据流程带额外参数或返回记录,因此“没有参数”只能描述这个最小例子。退出传输得到肯定响应,也不能自行推断整套固件已经通过验证或完成激活。

把会话状态放回每一个 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
2
62 | F1 90 | 4C 54 45 53 54 31 32 33 34 35 36 37 38 39 30 31 32
| F1 8C | F1 90 00 01

第二个数据记录故意包含 F1 90。如果用字符串搜索 DID 作为分隔符,它会被误判成第三条记录。正确的解析过程是读 DID、从诊断字典取长度、精确消费该长度,然后再读下一个 DID。

1
2
3
4
5
6
7
8
9
lengths = {0xF190: 17, 0xF18C: 4}  # 仅对应这个教学目标
pos = 1 # 跳过 62
while pos < len(response):
did = int.from_bytes(response[pos:pos + 2], "big")
size = lengths[did] # 未知 DID 必须停止或交给专用 codec
record = response[pos + 2:pos + 2 + size]
if len(record) != size:
raise ValueError("truncated DID data")
pos += 2 + size

完整附件还检查 DID 字段本身是否截断、重复 DID 和不合法长度。可变长度记录需要目标定义的专用 codec;“未知 DID 就把剩余字节全部吃掉”只能用于明确允许消费剩余负载的配置,不能作为通用容错策略。ReadDataByIdentifier 实现同样依赖 DID 编解码定义。

服务 22 本身没有 subfunction,所以不能给 DID 高字节随意按上 0x80 来抑制响应。F190 里的 F1 就是 DID 的一部分,这类错误与前面的层次混淆本质相同。

2E:写成功的证据分几步

假设一个教学 DID F1A0 被配置为可写的两字节参数,则请求与响应可以是:

1
2
2E F1 A0 00 64
6E F1 A0

这里的 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
2
3
4
5
6
请求:19 02 08
响应:59 02 FF 12 34 56 2F AB CD EF 08
│ │ └───────┬───────┘
│ │ 两条 DTC + status 记录
│ └─ statusAvailabilityMask
└──── subfunction echo

请求掩码 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
2
3
4
31 01 F0 01 <optionRecord>    请求启动
71 01 F0 01 <statusRecord> 启动请求的肯定响应
31 03 F0 01 请求结果(若该例程支持)
71 03 F0 01 <resultRecord> 结果记录

optionRecord、statusRecord、resultRecord 的内容要由该 RID 的定义解释。有的实现收到启动请求后先返回 pending,再给最终结果;有的接受启动后返回状态,稍后通过 RequestRoutineResults 查询。客户端必须按目标约定处理,不能看到任意 71 就在界面标“校验通过”。

服务 31 的顺序错误还可能来自例程正在运行、尚未启动或已经结束。要把时间线和 RID 一起记录,单独摘一条 7F 31 24 无法解释前因后果。

下载流程按字节算:4 KiB 是怎样传完的

下面只讨论传输状态,不向设备写固件。教学模型在 Python 内存里保存数据;真实 ECU 的地址范围、会话、安全访问、擦除例程、签名和激活步骤都要由目标定义。

RequestDownload 的两个半字节

假设请求从地址 0x00100000 开始传输 0x00001000,即 4096 字节:

1
2
3
4
5
6
34 00 44 00 10 00 00 00 00 10 00
│ │ │ └────┬────┘ └────┬────┘
│ │ │ address size
│ │ └─ ALFI:大小占4字节,地址占4字节
│ └──── DFI:本例不压缩、不加密
└─────── SID

DFI 的高半字节描述压缩方法,低半字节描述加密方法;非零编码的具体算法不能凭编号猜测。ALFI 的高半字节是大小字段字节数,低半字节是地址字段字节数,两者特别容易写反。这个请求的 UDS 负载已经超过 7 字节,需要由 ISO-TP 分帧。

假设肯定响应为:

1
2
3
4
74 20 01 02
│ └───┘
│ maxNumberOfBlockLength = 0x0102 = 258
└─ 长度数值占2字节

258 是这次约定下一个 TransferData 请求的最大长度。按 36 + blockSequenceCounter + data 布局,两字节属于服务头,实际数据最多 256 字节。因此 4 KiB 恰好分成 16 个满块。原来 74 20 04 00 的例子也一样:最大请求长度 1024,数据容量为 1022,不能直接塞 1024 字节数据。

UDS 的块计数与每个块内部的 ISO-TP 连续帧计数独立推进

两套计数器各管哪一层

第一块请求为 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
download: 4096 bytes, 16 blocks, 16 duplicate acknowledgements, exit=77

测试同时验证最终缓存只有 4096 字节、改变重复块内容会失败、257 字节数据超过这次协商容量、未传完不能 TransferExit,以及块号 FF→00 的回绕。它没有实现真实 Flash 写入、断电恢复、密钥算法或固件签名校验。

TransferExit 之后还没有结束的事

77 可以说明相应退出服务得到肯定响应,不能独自证明固件可启动。真实流程可能还有数据完整性检查、签名验证、软件依赖校验、启动槽切换、复位和软件版本回读。这些步骤的组合与例程编号不是通用答案,应结合 bootloader 与 OEM 流程分析。

这也是安全审计里需要追的路径:允许进入 34 与允许把任意镜像激活,是不同的检查点;传输过程加密和镜像签名也解决不同问题。只证明能发送数据,距离证明 ECU 会信任并运行它还差一整段证据。

把异常轨迹也跑一遍

下载 diagnostic_lab.py 与 diagnostic_trace.jsonl。运行环境为 Python 3.10+,仅使用标准库:

1
2
python3 diagnostic_lab.py --test
python3 diagnostic_lab.py --trace diagnostic_trace.jsonl

脚本覆盖多种长度的重组、两个目标交错、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 的出现,很容易把“进入流程”“等待结果”和“操作完成”混为一谈。

参考

标准和 ECU 诊断描述是最终依据;开源库文档便于理解与验证接口,不能替代目标车辆的服务配置。


UDS诊断
https://g1at.github.io/2024/06/10/UDS/
作者
g0at
发布于
2024年6月10日
许可协议