UDS 36服务详解:从报文格式到刷写实战,一次讲清TransferData
发布时间:2026/9/25 1:34:53来源:尧图网络
做UDS诊断开发的朋友应该都有过这种经历别人问你在刷写流程里哪个服务最不起眼你可能脱口而出“36服务”。它不像10服务那样负责切换会话也不像27服务那样跟密钥较劲更不像34服务那样要小心翼翼地计算地址和长度。但真到了跑刷写的时候你才会发现整个流程里调用次数最多、最容易出问题、也最需要抠细节的恰恰就是这个看起来平平无奇的36服务——TransferData。这个服务说白了就是干一件事把固件数据一块一块地搬进ECU。搬得快慢决定了整包固件刷写要花多长时间搬得对不对直接决定ECU刷完能不能正常启动。我见过不少项目前面27服务、34服务都调得顺顺当当最后卡在36服务上一刷就报错一查全是块序列号、块大小、超时节奏这些细节。这篇文章就把36服务从报文格式到传输机制再到实战代码完整拆开讲一遍同时会提一嘴刷写链路里的安全威胁和防御思路希望对正在做UDS刷写或诊断协议栈开发的朋友有帮助。1. 36服务在UDS协议族里的真实地位1.1 一次完整刷写里36服务站在哪UDSUnified Diagnostic Services统一诊断服务是ISO 14229定义的一套汽车ECU诊断规则。在刷写场景下我们通常说的“刷写流程”是一个多服务配合的时序过程36服务正好卡在中间最繁忙的位置。一个典型的Bootloader刷写时序是这样的顺序服务请求示例作用110 0210 02切换到扩展诊断会话227 0527 05请求安全种子327 0627 06 Key发送安全密钥解锁刷写权限43434 00 44 地址 长度请求下载协商数据块大小53636 01 数据传输第一块数据63636 02 数据传输第二块数据73636 03 数据循环传输直到所有数据发完83737请求退出传输911 0111 01ECU复位启动新程序注意看这个时序36服务在中间出现了一次又一次。固件镜像越大36服务被调用的次数就越多。比如一个2MB的固件按每块256字节来切需要调用8192次36服务。刷写耗时主要就是这8192次请求的时间累加。所以做刷写性能优化核心就是在36服务上做文章。1.2 和34、37服务怎么配合经常有人把34、36、37三个服务搞混觉得它们都是干“传输”的。其实这三个服务的分工非常明确一句话就能说清34服务是“报备”36服务是“干活”37服务是“收尾”。34服务RequestDownload负责向ECU声明我准备往某个内存地址写入一段数据这段数据有多长。ECU收到34请求后会检查这个地址是否在自己的可编程Flash范围内长度是否合法然后返回一个它允许的最大数据块长度。这个返回值非常关键后面36服务的块大小不能超过这个值。36服务TransferData才是真正传输数据的服务。它接收一个块序列号和一段数据把数据写入34协商好的地址区间。每发一块地址往后偏移一块的长度。37服务RequestTransferExit表示数据发完了请ECU结束这次传输会话并校验一下整体数据是否完整。37成功后ECU内部才会认为这次刷写的数据传输阶段正式结束。这三个服务的关系有点像去银行存钱34是跟柜员说“我要存5万”36是分几次把现金递进去37是说“钱给完了给我回执”。1.3 为什么说36服务是“搬运工”19服务读DTC、31服务执行例程这些属于管理类操作对时序和性能的要求没那么苛刻。但36服务不一样它承载的是真实的固件字节流一个字一个字地搬搬得快慢直接决定刷写时长搬得对不对直接决定固件能不能用。我见过不少项目组刷写速度快慢不一最后对比下来差距就出在36服务的实现上。有的实现每发一块都要等完整的响应超时再发下一块有的实现则把ISO-TP流控参数调得很激进吞吐差距能到两三倍。后面第4章我会给出一个可参考的代码实现把节奏控制好。2. 36服务的报文格式一个字节都不能含糊2.1 请求报文结构36服务的请求报文结构非常紧凑一共三个部分字节位置名称说明第1字节SID固定为0x36表示TransferData第2字节blockSequenceCounter块序列计数器0x01~0xFF循环第3字节及之后transferRequestParameterRecord实际的传输数据比如发送第一个数据块时请求可能是36 01 11 22 33 44 55 ...其中0x36是服务ID0x01是块序列号后面的0x11 0x22 0x33 0x44 0x55就是固件数据的前几个字节。这里有一个很多人忽略的细节如果走经典CAN总线一个CAN帧的数据场只有8字节。ISO-TP协议ISO 15765-2的单帧格式会占用1个字节作为协议控制信息PCI剩下7字节给UDS应用层。这7字节里UDS报文本身要占掉SID1字节和blockSequenceCounter1字节真正能留给固件数据的只有5字节。这就是为什么经典CAN上刷写如果采用单帧传输数据块大小通常不会超过5字节。如果你收到的34响应里maxNumberOfBlockLength居然是64、128甚至更大那说明ECU要求用ISO-TP多帧来传输一个数据块也就是把一个36请求分成多个CAN帧发送。在这种情况下单个36请求的数据部分可以很大但每个CAN帧还是要遵守ISO-TP的分帧规则。我建议初次接触36服务的人先把单帧的情形吃透再去碰多帧。单帧场景下报文格式简单抓包也直观不容易迷茫。2.2 肯定响应与否定响应36服务的肯定响应格式也比较简单字节位置名称说明第1字节SID0x76即0x36 0x40第2字节blockSequenceCounter回显请求中的块序列号后续可选字节maxNumberOfBlockLength某些ECU会在响应中捎带下次允许的最大块长度最常见的肯定响应就是两字节比如收到请求36 01 11 22 33 44 55ECU如果处理成功会回复76 01。这个回显的块序列号非常有用主机可以用它判断ECU是否真的在处理当前块而不是把上一块的响应又发了一遍。如果ECU处理失败会返回否定响应格式为固定的7F 36 NRC。第一个字节0x7F表示否定响应第二个字节0x36表示是36服务被拒绝第三个字节是具体的否定响应码Negative Response CodeNRC。比如7F 36 73就是块序列号错误。第5章我会整理一张常见的NRC速查表到那是可以直接查的。2.3 blockSequenceCounter的循环规则blockSequenceCounter是36服务最容易出错的地方值得单独拿出来讲清楚。计数器从0x01开始每成功发送一个数据块就加1。当计数达到0xFF后下一个块应该回到0x01继续使用形成一个循环0x01 - 0x02 - 0x03 - ... - 0xFE - 0xFF - 0x01 - 0x02 - ...注意0x00这个值不能作为正常的块序列号使用。理论上如果从0xFF回卷下一个值应该是0x01而不是0x00。这个约定在ISO 14229里写得很明确0x00保留不用于计数。为什么这样设计因为固件通常很大而块序列号只有8位。如果固件按256字节一块切切出2MB固件会产生8192个块远超过255。这时候就必须允许计数器回卷否则没法传大固件。代码里的回卷处理我个人建议写成seq (seq % 0xFF) 1这条式子当seq为0xFF时得到的是1完美跳过0。不信可以自己推一遍0xFF % 0xFF 0再加1等于1正确。如果写成简单的seq 1并让uint8_t自然溢出就会从0xFF变成0x00这就是很多刷写失败案例的根源。3. 数据传输机制的底层设计逻辑3.1 为什么非要有blockSequenceCounter从应用层看blockSequenceCounter其实是一个轻量级的序列号机制。你可以把它理解成快递运单号。主机连续发送多个数据块如果没有序号ECU收到一块数据后根本不知道它是第几块。如果传输过程中丢了一块或者CAN总线上有两个节点同时往ECU发数据ECU可能把顺序搞乱。固件数据一旦乱序写入Flash轻则校验失败重则ECU变砖。有了blockSequenceCounterECU每收到一个块都可以检查它的序号是否等于自己期望的下一个序号。如果期望的是0x05收到的却是0x07说明中间至少丢了一块或者序列错乱了ECU可以立刻回复0x73并终止传输会话防止错误数据继续写入。这个机制的本质是“尽力保证数据顺序和完整性”但它只做顺序校验不做数据内容校验。数据内容有没有被篡改需要靠31服务执行的CRC校验或者MAC认证来兜底这一点在第6章会展开。3.2 块大小的限制CAN帧、ISO-TP和maxNumberOfBlockLength36服务的块大小不是随便定的它要同时受三方面约束。第一物理链路约束。经典CAN单帧数据场8字节减去ISO-TP的PCI开销后应用层最多7字节。再减去36服务的SID和blockSequenceCounter单帧能携带的有效数据最多5字节。CAN FD单帧数据场64字节应用层最多63字节再减去SID和计数器单帧最多携带61字节有效数据。如果块大小超过这个值就必须通过ISO-TP多帧传输。第二ECU接收能力约束。34服务的肯定响应里会返回maxNumberOfBlockLength这是ECU能接受的最大数据块长度。主机切分固件时块大小绝不能超过这个值否则大概率会收到0x31请求超出范围或0x13长度错误的否定响应。第三流控约束。多帧传输时接收方通过流控帧FC控制发送节奏流控帧里包含BlockSize和STmin两个参数。BlockSize表示连续收到多少个帧后需要暂停等待流控STmin表示相邻两个连续帧之间的最小间隔。实际项目中刷写吞吐率的高低往往取决于这两个参数的设置。如果你发得多但STmin太小接收方缓冲区溢出整个36请求会被ECU丢弃反而更慢。所以做刷写性能调优时不是盲目调大块大小就完事要先确认34响应里ECU支持的最大块长度再看ISO-TP的流控节奏是否匹配。3.3 超时机制与刷写节奏UDS诊断有一个基础超时概念P2Server和P2StarServer。P2Server是ECU处理服务请求的默认最大时间通常是50msP2StarServer是增强超时时间通常为5000ms用于ECU需要长时间处理的情况。36服务在刷写过程中有一个特殊点ECU在写入Flash时CPU可能正忙着操作Flash控制器甚至关闭了中断响应时间会超过普通的P2Server。这时候主机不能等50ms就判定超时而是要按P2StarServer来等待。如果P2StarServer超时了还没响应再去考虑重发或者检查错误。另外块与块之间的间隔不能太长。ECU刷写时内部有一个传输会话计时器如果超过一定时间没有收到新的数据块它会认为传输会话挂起主动退出传输状态。这时候你再发36通常会收到0x24请求顺序错误或者ECU直接跳回默认会话连响应都不同了。所以主机发完一个36块后不能无限期地等下去要监控好节奏。我习惯的做法是发完一个块后先等P2Server时长50ms如果没响应就继续等待直到P2StarServer5000ms超时。如果这个块是Flash写操作密集的块比如擦除阶段我会有意识地做一次状态查询比如用22服务读一下ECU当前会话状态判断它是卡住还是正常处理中。4. 实战手写一个36服务数据发送函数4.1 完整刷写时序回顾在写代码前先把一个具体的例子完整摆出来后面代码就围绕这个例子展开。假设目标ECU的Flash起始地址为0x08000000固件长度为0x400016384字节34服务协商出的maxNumberOfBlockLength为256字节。那么固件需要切分为总块数 16384 / 256 64块块序列号从0x01开始一直用到0x40即十进制的64刚好在一个回卷周期内不需要回卷。如果固件更大跨过0xFF后才会用到回卷逻辑。完整的请求列表前面已经给过这里补一个更细的局部时序示例发送: 34 00 44 08 00 00 00 00 00 00 40 00 接收: 74 00 44 01 00 发送: 36 01 [256字节数据] 接收: 76 01 发送: 36 02 [256字节数据] 接收: 76 02 ... 发送: 36 40 [最后256字节数据] 接收: 76 40 发送: 37 接收: 7734请求里0x00是数据格式标识无压缩无加密0x44表示地址用4字节表示、长度用4字节表示随后是地址0x08000000和长度0x00004000。34响应74 00 44 01 00中的最后两个字节0x01 0x00如果按大端解读就是256表示ECU允许的最大块长度是256字节。4.2 用Python模拟36服务数据块发送下面这个函数实现了一个36服务的发送和响应校验。这里的iso_tp是一个抽象接口实际项目中你可以替换成CANoe、PCAN或SocketCAN的ISO-TP封装。def send_transfer_data(iso_tp, data: bytes, seq: int) - bytes: request bytes([0x36, seq]) data iso_tp.send(request) response iso_tp.receive(timeout5.0) if response[0] 0x76: return response elif response[0] 0x7F: if len(response) 3: nrc response[2] raise Exception(TransferData rejected, NRC0x%02X % nrc) else: raise Exception(Negative response with invalid length) else: raise Exception(Unexpected response: 0x%02X % response[0])然后是完整的分块发送逻辑def download_firmware(iso_tp, base_addr: int, firmware: bytes, block_size: int): total_len len(firmware) # 发送34请求这里简化为固定格式实际要按ECU要求组装 req_download bytes([0x34, 0x00, 0x44, (base_addr 24) 0xFF, (base_addr 16) 0xFF, (base_addr 8) 0xFF, base_addr 0xFF, (total_len 24) 0xFF, (total_len 16) 0xFF, (total_len 8) 0xFF, total_len 0xFF]) iso_tp.send(req_download) max_block_len parse_max_block_length(iso_tp.receive(timeout5.0)) if block_size max_block_len: raise Exception(block_size exceeds ECU limit) seq 0x01 for offset in range(0, total_len, block_size): chunk firmware[offset:offset block_size] resp send_transfer_data(iso_tp, chunk, seq) # 校验响应中回显的块序列号必须与请求一致 if resp[1] ! seq: raise Exception(Block sequence mismatch: sent%d, echoed%d % (seq, resp[1])) seq (seq % 0xFF) 1end of snippet. 这里有几个关键点需要强调。第一个响应中的块序列号一定要校验。曾经在项目里遇到过ECU响应缓存没刷新连续两块都回76 01的情况如果主机不回显校验就会误以为每块都成功实际上第二块数据可能根本没写入正确位置。第二个最后一块的处理。如果固件长度不是block_size的整数倍最后一块就是剩余的数据代码里firmware[offset:offset block_size]自然切出剩余部分不要手动补0。补0会导致写入的固件比实际长度多出一截可能触发ECU的0x31否定响应。第三个enexpect the response for each block before sending the next。上述代码是串行等待模式每发一块等一块响应稳定性最好。如果你想提高刷写速度可以做流水线式的发送但要确保ISO-TP底层缓冲足够并且ECU支持这种连续接收。一般量产刷写工具会做多帧流水线但对初学者我不建议上来就玩并发先串行跑通了再说。4.3 上位机脚本常见的坑第一个坑是发36之前没有确认34协商结果。有的上位机脚本直接硬编码块大小也不管34响应里ECU支持的上限结果ECU回0x13或者0x31。这种情况在更换ECU型号后特别常见因为不同ECU的Flash控制器能力不一样允许的块大小也千差万别。第二个坑是忽略会话状态就发36。36服务通常要求当前处于非默认会话且已经通过安全访问。如果没做27服务或者安全访问已超时ECU直接回0x33安全访问被拒绝或者0x24请求顺序错误。排查的时候先确认自己是不是处于正确的会话和安全等级。第三个坑是块与块之间间隔太短。串行模式下主机收到0x76后立刻发下一块通常没问题但如果是多帧传输ECU可能还在处理上一块的Flash写操作此时新块发过来可能被缓存溢出丢掉。遇到这种问题适当加一点小延时或者根据ECU的流控帧参数调整发送节奏比无脑重发更有效。第四个坑是不做完整性和最终校验就复位ECU。36服务返回0x76只代表ECU接收了这一块数据不代表数据真正写入成功。Flash写入过程中可能因为电压跌落或者Flash控制器异常产生坏块但ECU可能不报错。所以刷写完成后必须通过31服务执行一次CRC校验或者在37退出后读取Flash内容与原始固件比对确认无误再走11复位。5. 36服务常见问题与NRC排查速查5.1 最常踩的坑blockSequenceCounter不连续收到7F 36 73是我见过最多的一类失败。73这个NRC对应的含义就是wrongBlockSequenceCounter块序列号错误。最常见的引发原因不是ECU问题而是主机端的计数器实现错了。用C语言写代码如果定义了一个uint8_t变量来做计数器然后每次发完一块就让它自然加1当计数从0xFF再往上加时会溢出成0x00。0x00被ISO 14229明确排除在合法块序列号之外ECU收到0x00就会回0x73。排查这个问题的思路很简单抓包看主机发出的36请求序列逐条检查计数器是否严格按照0x01, 0x02, ..., 0xFF, 0x01, ...的顺序排列。只要中间出现0x00或者跳号就能定位到主机代码问题。另外有一些ECU对块序列号有更严格的要求它们期望计数回卷从0x01重新开始同时也期望响应中的回显值与请求值一致。如果ECU实现有缺陷响应回显可能不更新这种情况下主机端校验逻辑要加一些容错避免因为响应缓存问题导致刷写流程被误判失败。5.2 NRC速查表36服务涉及的否定响应码比较多我把常见的整理成一张速查表方便收藏。NRC名称常见触发场景0x13incorrectMessageLengthOrInvalidFormat请求中只有SID和计数器没有数据或数据长度与34协商不一致0x24requestSequenceError未先执行34服务就发36传输会话已结束37之后再发360x31requestOutOfRange数据块地址超出34协商范围块大小超过ECU限制0x33securityAccessDenied未通过27服务安全访问或安全访问已超时0x72generalProgrammingFailureFlash擦写失败、电源异常、地址异常等通用编程错误0x73wrongBlockSequenceCounter块序列号不连续或使用了0x00作为序列号有几个NRC容易混淆。0x13和0x31的区分点在于0x13强调的是报文本身长度不对而0x31强调的是长度合法但地址或参数超出ECU支持范围。举个实际例子34协商块大小上限是256字节你发了一个300字节的数据块如果ECU无法解析这个长度可能回0x13如果ECU能解析出长度、但发现300字节超出了写入范围就可能回0x31。不同ECU的实现有差异遇到具体问题还是要抓包看上下文。0x24和0x33也容易搞混。0x24更偏向流程顺序问题比如没有先发340x33更偏向安全等级不够。建议排查时先看会话状态和安全访问状态再看传输会话是否存在。5.3 时序相关的细节有一次量产现场刷写偶发失败复现率不高但一出现就是整包校验不过。后来抓CAN总线日志才发现问题出在块与块之间的间隔太长ECU内部的传输会话计时器超时退出导致后续的36请求全部被回0x24。这种问题在刷写大固件时尤其隐蔽。上位机每发一块数据要等ECU写Flash完成再回0x76。如果Flash写操作本身耗时较长或者上位机在两次36之间额外做了日志记录、界面刷新之类的操作很容易把间隔拖到几百毫秒以上。一旦超过ECU内部传输会话超时时间整个刷写就断了。解决办法有两个方向。一个是主机侧优化减少两次36服务之间的非必要操作保证发送节奏紧凑。另一个是ECU侧延长传输超时时间但这涉及ECU内部实现主机侧改不了。所以实际项目中主要还是靠主机侧控节奏。还有一种特殊情况ECU在写某一块Flash时耗时特别久比如擦除扇区阶段响应时间可能超过P2Server。这时候主机不能因为超过50ms就判定失败重发否则会打乱ECU的工作节奏。正确做法是按P2StarServer等待或者先通过22服务查询状态再决定是否重发。这个经验对于做刷写工具链的朋友非常重要因为市面上不少通用诊断工具在P2超时后立刻重发反而把原本正常的刷写流程搞挂了。6. 从36服务看UDS刷写安全威胁与防御6.1 刷写链路面临的主要威胁最近几年车辆网络安全被提到很高的优先级。36服务作为承载固件数据的核心服务自然成为攻击面最大的地方。攻击者一旦能操作36服务本质上就能往ECU里灌任意数据这比读几个DTC、清几个故障码严重得多。第一种典型威胁是未授权刷写。攻击者物理接入OBD口直接向ECU发送诊断请求。如果ECU没有安全访问保护或者安全访问算法被破解攻击者就能利用36服务灌入恶意固件。这也是为什么工程上特别强调27服务安全访问的种子要足够随机、密钥算法不能硬编码在公开资料里。第二种威胁是重放攻击。攻击者不需要真的破解安全密钥只要在合法的刷写过程中抓取总线报文把完整的刷写序列27服务、34服务、36服务、37服务原样记录下来然后找个时间重放一遍。如果ECU没有做重放保护它根本分辨不出这是不是合法诊断仪发来的照样会执行刷写。所以刷写频率限制、时间戳校验、一次性随机数这些机制在实际量产里非常有必要。第三种威胁是中间人篡改。攻击者在总线上串联一个设备把36服务携带的固件字节流悄悄改掉。ECU以为是正常刷写实际上写入的已经是攻击者构造的数据。如果固件本身没有签名机制ECU很难发现。这类攻击的隐蔽性很强因为整个流程看起来全部成功了但最终固件行为已经被篡改。第四种威胁是拒绝服务。攻击者不需要成功刷写只要持续往CAN总线上发送高优先级报文干扰正常的36服务传输就能让刷写流程反复超时失败。这种攻击不需要很高的技术门槛但对量产下线影响很大因为会拉低生产节拍甚至导致设备处于半刷写状态。6.2 工程上怎么落地防御针对上述威胁工程上的防御手段是分层的不是靠某一个措施就能全部堵住。第一层是安全访问强化。27服务不能只是走形式种子要使用真随机数密钥算法不要使用对称密钥硬编码的方式更不能把所有ECU都用同一个密钥。要限制安全访问的失败尝试次数比如连续失败5次后锁定一段时间。这一层能挡住大多数外围尝试但挡不住密钥被逆向之后的攻击。第二层是数据完整性校验。刷写完成后用31服务执行一次CRC或校验和计算对比ECU内部计算结果和主机计算结果。这个措施能发现数据被篡改但它是事后校验。更严格的做法是在刷写过程中给每个36数据块附加一个消息认证码MACECU逐块验证验证通过才写入。MAC的密钥只有合法工具链和ECU知道攻击者即使篡改了数据也生成不了合法MAC。第三层是固件签名和加密。固件镜像在生成时用私钥签名ECU在刷写后验签签名合法才允许启动新固件。这个方案能有效防止中间人篡改因为攻击者没有私钥就无法生成合法签名。固件加密能防止攻击者从抓包中直接分析出固件内容保护知识产权。第四层是传输链路保护。DoIP场景下可以使用TLS建立安全通道CAN场景下可以使用SecOC或者类SecOC机制对诊断报文做认证。这类机制在量产车型上越来越普遍但对Bootloader的资源占用有要求需要提前规划。第五层是最小化暴露。ECU正常工作时尽可能禁止非必要诊断服务刷写Bootloader里只开放刷写必需的服务其他UDS服务一律不响应。同时要记录安全访问、刷写时间、数据哈希日志一旦发生异常可以追溯到具体操作。防御手段再多也不能完全消灭风险。工程上讲究的是把攻击成本提高到攻击者觉得不划算的程度。对于做UDS协议栈和刷写工具的工程师来说懂这些安全机制不是为了天天写攻击代码而是在设计协议栈时留好扩展点比如后续要加MAC校验报文结构里就要预留空间不能等车型量产后才发现安全机制加不进去。我自己在带刷写项目时最深刻的一个体会是36服务的代码量不大但它承上启下把34协商的参数、27的安全校验、37的收尾串成一条线。很多时候刷写出问题问题不在36本身而在上下游没有对齐。排查问题的时候别只盯着36的报文把34协商的块大小、27的会话状态、37的结束标志一起拉出来看往往一眼就能定位。最后再分享一个小技巧刷写完成后不要急着复位ECU先读一下34服务指定的地址区域或者执行一次31服务的CRC校验确认数据真的写对了再走11服务复位这个习惯能帮你省掉大量“刷写成功但起不来”的烂摊子。
网站建设高端定制企业官网