Proxmark3(Iceman Fork)NG 变长帧格式:主机-设备通信协议的设计、API 与实现剖析
【免费下载链接】proxmark3Iceman Fork - Proxmark3项目地址: https://gitcode.com/GitHub_Trending/pr/proxmark3
本文围绕 doc/new_frame_format.md 展开,系统讲解 Proxmark3 在客户端与设备之间引入的 NG(新一代)变长帧格式:帧头/帧尾的逐字段定义、新旧格式的双向兼容策略、客户端与固件两侧的收发 API、Bootrom 的兼容边界,以及 USART 重写与超时调优背后的实测依据,并对照当前仓库源码给出每一处结论的文件级证据。读完本文,你可以完整理解 Proxmark3 通信协议中"一帧数据在线路上长什么样",并具备新增命令或排查通信问题的协议层基础。
需要说明的是:原文档标注其"主要面向开发者",因此本文也定位为协议实现级内容,面向需要编写、转换或调试命令处理的开发者。
旧格式:固定 544 字节的问题
在 NG 格式出现之前,主机与 Proxmark3 之间双向的帧结构是:
uint64_t cmd; uint64_t arg[3]; union { uint8_t asBytes[PM3_CMD_DATA_SIZE]; uint32_t asDwords[PM3_CMD_DATA_SIZE / 4]; } d;当时PM3_CMD_DATA_SIZE = 512,且没有任何 API 抽象——每个模块各自手工拼装/解析这类帧。由此带来两个问题:
- 帧大小固定为 544 字节,哪怕只是回一个简单 ACK,也要在线路上发送完整的 512 字节 payload(
8 + 24 + 512 = 544); - 链路分片放大:抓 USB 传输可以观察到,主机一次发出 544B 的 Bulk 帧,而设备侧受内部缓冲区限制,只能按 128B、128B、128B、128B、32B 共 5 个包发回,一条"空 ACK"被拆成 5 次发送。
对于 USB 这个开销尚可忍受,但对 FPC/USART/蓝牙这类低速链路(9600~460800 波特率)而言,固定 512B 的尾随浪费是致命的——这正是引入变长帧的主要动机。
对应的新结构体如今定义在 include/pm3_cmd.h:
typedef struct { uint64_t cmd; uint64_t arg[3]; union { uint8_t asBytes[PM3_CMD_DATA_SIZE_OLD]; uint32_t asDwords[PM3_CMD_DATA_SIZE_OLD / 4]; } d; } PACKED PacketCommandOLD;注意关键点:旧格式结构体改用PM3_CMD_DATA_SIZE_OLD定死在512,与PM3_CMD_DATA_SIZE解耦。头文件注释(include/pm3_cmd.h#L34-L36)说明了原因:bootloader 只说 OLD 格式,若把旧结构体跟着新值变大,会对所有已部署的 bootrom 破坏刷写兼容性。
而PM3_CMD_DATA_SIZE本身在仓库中已按平台分化(include/pm3_cmd.h#L28-L41):
#if defined(ON_DEVICE) && !defined(PM5) #define PM3_CMD_DATA_SIZE 624 #else #define PM3_CMD_DATA_SIZE 4064 // PM5 firmware and the client #endif // 旧帧钉死在 512 #define PM3_CMD_DATA_SIZE_OLD 512 // FPC/BWM 前向缓冲较小,设备经 FPC 回复时限制 payload #define PM3_FPC_MAX_DATA 2048即文档写作时"暂时 512"的上限,在当前仓库中 PM3 设备固件为 624、PM5 固件与客户端为 4064,客户端侧SendCommandNG会以g_conn.max_cmd_data_size做上限检查(见下文)。
NG 帧的逐字段定义
NG 格式从零设计:即使只把旧格式的 payload 变成变长,单帧最少也要 32 字节且字段任意宽,收益有限,因此直接重新设计。
命令帧(客户端 → Proxmark3)
uint32_t magic; uint16_t length : 15; bool ng : 1; uint16_t cmd; uint8_t data[length]; uint16_t crc;| 字段 | 含义 |
|---|---|
magic | 任意魔数PM3a(0x61334d50),用于出流后重新同步 |
length | 变长 payload 长度,0 表示无;上限为PM3_CMD_DATA_SIZE |
ng | 标志位:数据按 NG 格式还是 OLD 格式编码(过渡期用) |
cmd | 16 位命令码,"够用了" |
data | 变长 payload |
crc | 真实 CRC(CRC-14443A)或占位魔数a3 |
应答帧(Proxmark3 → 客户端)
uint32_t magic; uint16_t length : 15; bool ng : 1; int8_t status; int8_t reason; uint16_t cmd; uint8_t data[length]; uint16_t crc;字段差异:magic为PM3b(0x62334d50),占位魔数为b3;新增status(命令执行状态)与reason(对指定命令的状态细化说明)两个 8 位字段。
CRC 的规则(文档原文):CRC 可选,接收侧接受a3/b3作为占位值,若不是占位值则按真实 CRC 校验;USART 链路默认启用 CRC,USB 双向默认禁用 CRC。这一策略在当前代码中仍然成立——client/src/comms.c#L208-L214 中SendCommandNG依据send_via_fpc_usart/send_with_crc_on_fpc/send_with_crc_on_usb决定是否调用compute_crc(CRC_14443_A, ...),否则写入COMMANDNG_POSTAMBLE_MAGIC(a3)。
由此得到帧大小结论(见文末"参考帧"):NG 命令帧 10~522 字节、NG 应答帧 12~524 字节(按上限 512 计),OLD 帧恒 544 字节。以最小的hw ping为例,NG 命令帧仅10 字节,OLD 格式则固定544 字节——这就是 FPC/USART/BT 链路的核心收益。
内部结构体与 API 抽象
文档区分了两类结构体:
- 传输用打包结构体(
PACKED,只描述线上字节):PacketCommandNGPreamble、PacketCommandNGPostamble、PacketCommandNGRaw、PacketResponseNGPreamble、PacketResponseNGPostamble、PacketResponseNGRaw; - API 用结构体(
PacketCommandNG/PacketResponseNG):向开发者隐藏帧细节,同时保留oldarg[3]与ng标志以兼容尚未转换的调用。
两者都在 include/pm3_cmd.h。传输侧:
typedef struct { uint32_t magic; uint16_t length : 15; // 变长部分长度,0 表示无 bool ng : 1; uint16_t cmd; } PACKED PacketCommandNGPreamble; #define COMMANDNG_PREAMBLE_MAGIC 0x61334d50 // PM3a #define COMMANDNG_POSTAMBLE_MAGIC 0x3361 // a3API 侧(include/pm3_cmd.h#L67-L78):
typedef struct { uint16_t cmd; uint16_t length; uint32_t magic; // NG uint16_t crc; // NG uint64_t oldarg[3]; // OLD union { uint8_t asBytes[PM3_CMD_DATA_SIZE]; uint32_t asDwords[PM3_CMD_DATA_SIZE / 4]; } data; bool ng; // 存的是 NG 数据还是 OLD 数据? } PacketCommandNG;PacketResponseNG同形,多出status/reason。文档明确:布尔字段ng表示该结构体保存的是 NEW 还是 OLD 格式数据;OLD 来源既可能是旧的 544B 帧,也可能是"混合帧"(变长但仍携带 oldargs);过渡完成后可能会移除oldarg与ng字段——事实上当前仓库中 MIX 形态已删除(见下文),oldarg的读取方仅剩 client/src/flash.c 与 client/src/proxmark3.c 两处,且都是与 bootrom 经SendCommandBL通信。
客户端发送侧 API(client/src/comms.c)
void SendCommandNG(uint16_t cmd, uint8_t *data, size_t len); void SendCommandBL(uint64_t cmd, uint64_t arg0, uint64_t arg1, uint64_t arg2, void *data, size_t len); void SendCommandOLD(uint64_t cmd, uint64_t arg0, uint64_t arg1, uint64_t arg2, void *data, size_t len);原型见 client/src/comms.h#L95-L97,实现见 client/src/comms.c#L111-L235:
SendCommandNG:所有命令的统一入口。参数打包进临时PACKED结构体放进data字段即可。实现中它会:① 用g_conn.max_cmd_data_size检查 payload 上限(PM5 场景下 4064 比设备缓冲大,必须钳制);② 填 preamble(magic/ng=1/length/cmd);③ 按链路策略计算或写入 CRC 占位;④ 计算总长sizeof(PacketCommandNGPreamble) + len + sizeof(PacketCommandNGPostamble);⑤ 交给通信线程,uart_wakeup()打断接收循环让发送尽快上线。SendCommandBL:SendCommandOLD的"改名别名",专门标记"必须保持 OLD 格式"的 bootrom 帧,源码注释(client/src/comms.c#L108-L113)写明了两类用途:进入 bootloader 模式的命令(可能与旧固件对话)、直接发给只支持 OLD 帧的 bootloader。SendCommandOLD:不再有SendCommandBL之外的其他调用者。SendCommandMIX与PM3_CMD_DATA_SIZE_MIX已删除:MIX 是历史折中——NG 帧封装但前 24 字节仍放三个 64 位 oldarg。现在客户端已无法构造 MIX 帧。
内部流程:这些函数准备帧后经uart_communication调uart_send上线。
设备接收与发送侧(armsrc/)
接收
设备侧主循环AppMain调receive_ng(armsrc/cmd.c),receive_ng再调usb_read_ng/usart_read_ng取出PacketCommandNG,交给PacketReceived做命令分发(broker)。无论来的是旧帧还是新帧,检查PacketCommandNG.ng字段即可判断oldarg中是否有效数据,旧 handler 仍可在oldarg里找到原参数。
receive_ng_internal的实现(armsrc/cmd.c#L181-L199)体现了变长接收的要点:先读 8 字节 preamble,按其中length决定 payload 大小,rx->data只在确有包进入时整体清零一次(文档注释提到:对主循环每次空闲轮询都清零的开销"比主循环其余部分还大")。
发送
回复 API 定义在 armsrc/cmd.h#L32 附近,实现在 armsrc/cmd.c#L43-L179:
int reply_old(uint64_t cmd, uint64_t arg0, uint64_t arg1, uint64_t arg2, const void *data, size_t len); int reply_ng(uint16_t cmd, int8_t status, const uint8_t *data, size_t len); int reply_reason(uint16_t cmd, int8_t status, int8_t reason, const uint8_t *data, size_t len);reply_mix已删除;reply_old只保留给 bootrom 同样服务的 handler——注意它无论len是多少都发送sizeof(PacketResponseOLD)即 544 字节(armsrc/cmd.c#L70-L72 中usb_write(..., sizeof(PacketResponseOLD))),而reply_ng只发送你给它的精确长度;- 双通道(USB + FPC/BWM)同时可用时,两个通道的发送结果取故障优先、USB 优先的裁决逻辑(armsrc/cmd.c#L159-L168)。
转换完成后的典型 handler 写法——校验长度后在自己的操作码上应答(文档示例,可直接作为模板):
case CMD_FOOBAR: { if (packet->length != sizeof(foobar_t)) { reply_ng(CMD_FOOBAR, PM3_EINVARG, NULL, 0); break; } foobar_t *payload = (foobar_t *)packet->data.asBytes; ... reply_ng(CMD_FOOBAR, PM3_SUCCESS, resp, resplen); break; }过渡期部分 handler 曾有if (packet->ng) { ... } else { ... }双模分支,以便新旧调用方共存;目前这类分支已全部移除——不带ng位的帧现在会收到PM3_EINVARG应答。
另一个值得注意的实现细节:NG 应答 preamble 为 10 字节,payload 会落在非字对齐偏移上,而 ARM7TDMI 上非对齐 32 位读会静默位旋转而不是触发异常。reply_ng_internal用 2 字节前垫把整个帧偏移 2 字节,使data[]落回字边界(线上字节不变),见 armsrc/cmd.c#L94-L102 的注释。
客户端接收侧(client/src/comms.c)
流程:uart_communication调uart_receive构造出PacketResponseNG,交给PacketResponseReceived(client/src/comms.c#L297)。它要么立即处理(如调试打印),要么存入环形缓冲rxBuffer(CMD_BUFFER_SIZE个PacketResponseNG,头/尾指针 + 互斥锁,见 client/src/comms.c#L62-L73)。命令层通过WaitForResponseTimeout/WaitForResponseTimeoutW(或下载场景的dl_it)调getReply取包。
与"超时"直接相关的一处关键机制在PacketResponseReceived入口(client/src/comms.c#L299-L303):每收到一个包就把WaitForResponseTimeout/dl_it的超时计时重置。因此实际超时是"距最近一个包的时间",与已收到的包数量无关——这对hw status、lf read这类先连发大量帧再收尾的命令在 9600 波特率下的存活至关重要。
迁移清单与工程实践笔记
把一个命令从 OLD 迁到 NG,四个方向各改一处:
| 方向 | 改动 |
|---|---|
| 客户端发送 | SendCommandOLD→SendCommandNG(一切参数装进临时 PACKED 结构体放data) |
| 设备接收 | 解析PacketCommandNG,从oldarg改为只读data |
| 设备发送 | reply_old→reply_ng(同样参数进 PACKED 结构体) |
| 客户端接收 | 解析PacketResponseNG,从oldarg改为只读data |
文档"从整树转换中得到的实践笔记"值得逐条保留,它们都是踩坑的产物:
- 用自己命令的操作码应答,永远别用
CMD_ACK。匿名 ACK 迫使过去做 hack——例如 iCLASS 把自己的CMD_HF_ICLASS_SIMULATE塞进回复的arg0里让客户端区分来源。 - 成功标志放 NG 的
status字段,而不是塞进数据字节;需要第二层区分时用reply_reason()。 - 两边都要在解引用前校验
packet->length。有多个已转换命令此前会读越短帧的尾部。 PACKED不是免费的:它把结构体对齐设为 1,对任何宽于字节的成员取地址会触发-Werror=address-of-packed-member;且 ARM7TDMI 上非对齐 32 位加载会静默位旋转。只在"去掉填充"时才用 PACKED:若sizeof打不打包都一样,就别打。文档给了对照:epa_replay_t(打包,uint16_t落在奇偏移)对比epa_result_t、t55xx_setconfig_t(未打包,成员天然无填充且会被取地址)。PM3_CMD_DATA_SIZE_MIX是 MIX 专用常量。任何改为 NG 的分块传输必须用PM3_CMD_DATA_SIZE - sizeof(payload_t)来定块大小。reply_old恒发 544 字节,reply_ng只发给定长度。- 一条命令可以应答多次:例如
CMD_HF_ISO14443A_READER带ISO14A_CONNECT | ISO14A_RAW时会应答两次(select 一次、raw 交换一次),客户端必须把两次都消费掉。
大块下载
CMD_DOWNLOAD_BIGBUF、CMD_DOWNLOAD_EML_BIGBUF、CMD_SPIFFS_DOWNLOAD、CMD_FLASHMEM_DOWNLOAD使用download_req_t/download_chunk_t/download_done_t(见 include/pm3_cmd.h)。块头只带 offset——帧长本身就给出了块大小——传输以download_done_t结束,且应答在原CMD_DOWNLOAD_*操作码上而非匿名 ACK。client/src/comms.c中的dl_it()仍理解 OLD 块格式,因为CMD_READ_MEM_DOWNLOAD由 bootrom 服务。
设备侧遗留 OLD 应答(截至文档时的状态)
C 客户端与 ARM 固件均已完成转换。客户端无SendCommandOLD/SendCommandMIX调用点;设备上仅剩三处刻意保留的 legacy 应答:
| 位置 | 保留原因 |
|---|---|
armsrc/appmain.c—CMD_READ_MEM_DOWNLOADED及其CMD_ACK终止符 | bootrom.c同样服务它,而 bootrom 只说 OLD |
armsrc/appmain.c—CMD_DEVICE_INFO | 同上 |
SendCommandBL标记了必须保持 OLD 的帧,不要"转换"它们。这些位置按PM3_CMD_DATA_SIZE_OLD而非PM3_CMD_DATA_SIZE分块:reply_old会把 payload 钳制到 OLD 尺寸,若发送方按 NG 尺寸分块会构造出超大块,被线路截断后oldarg[1]里仍声明全长。
另有一处从遗留清单中"移出"的案例:armsrc/hfsnoop.c的CMD_FPGAMEM_DOWNLOADED曾因 FPGA 追踪 DMA 双缓冲"看起来塞不下 NG 头"而保留 OLD,现已转换——DMA 直接写入download_chunk_t的chunk->data,填满即完整 NG payload。文档特别提醒:循环中每次传输只武装"还差多少字节",因为FPGA_TRACE_SIZE不是DOWNLOAD_CHUNK_MAX的整数倍,若对最后一个短块仍武装整块,会永远等待 FPGA 永远不会发送的字节。
Bootrom:刻意保留旧格式
Bootrom 继续用 OLD 帧,理由有二(文档原文):
- 绝大多数 bootrom 帧恰好携带 512B payload,新旧开销差异可忽略;
- 经 USART 走 flash 既危险又慢(115200 波特 vs 7M 波特),不值得。
同时它需要保持与仍支持旧格式的其他仓库的兼容。设备侧路径(bootrom/bootrom.c):
- 接收:
usb_read(common/usb_cdc.c)⇒UsbPacketReceived(bootrom/bootrom.c#L106)⇒CMD_DEVICE_INFO/CMD_START_FLASH/CMD_FINISH_WRITE/CMD_HARDWARE_RESET;另有usb_enable/usb_disable。 - 发送:
reply_old(bootrom.c)⇒usb_write(common/usb_cdc.c)。
客户端侧的 flasher(client/src/flash.c 等)因此必须继续用 OLD 帧,它与现行客户端代码共用少数原语:OpenProxmark、CloseProxmark、SendCommandOLD(⇒ 上述四条 bootrom 命令),接收侧仍走WaitForResponseTimeout ⇒ PacketResponseNG(旧帧被同一套传输层支持)。
USART RX FIFO 重写:应对未知大小的包
变长帧要求 USART 侧彻底重写以应对未知尺寸的数据包。方案要点:
- USART 全双工,RX 与 TX 均为双 DMA 缓冲;
- RX 增加内部 FIFO。
各函数职责(文档"New usart RX FIFO"一节):
usart_init:USART 在usart_init()后全程保持激活,RX/TX 例程无需再碰:pUS1->US_PTCR = AT91C_PDC_RXTEN | AT91C_PDC_TXTEN。usart_writebuffer_sync:仍用 DMA,但接受任意包大小;删掉了不必要的 memcpy;返回前等待 DMA 缓冲处理完毕,故称 "sync"(可以做成异步版,但调用方必须保证 DMA 缓冲在此期间不被回收;既然同步就不需要第二个 DMA 缓冲)。usart_read_ng:调用方告知期望包长;依赖usart_rxdata_available判断 FIFO 中是否有数据;从 FIFO 取数;重试次数是动态的,随 FPC 链路速度变化。usart_rxdata_available:轮询usart_fill_rxfifo,返回 FIFO 中可用字节数。usart_fill_rxfifo:若下一 DMA 缓冲已移入当前缓冲(US_RNCR == 0),说明一个 DMA 缓冲已满——把当前 DMA 缓冲数据转入 FIFO、换到另一个 DMA 缓冲、把腾空的 DMA 缓冲挂为下一缓冲;若当前 DMA 缓冲部分填充,则转入可用数据并记录已复制量。
接收侧的重试上限也在调优之列(common/usart.c,文档给出片段,当前仓库中USART_SLOW_LINK宏在 include/pm3_cmd.h#L26 定义并注释"用于 BT 等慢速链路"):
uint32_t usart_read_ng(uint8_t *data, size_t len) { // 实测经验值:3000000 / USART_BAUD_RATE // 取 10 倍 uint32_t tryconstant = 0; #ifdef USART_SLOW_LINK // BT 链路在 460800 下实测过 13200 次 tryconstant = 50000; #endif uint32_t maxtry = 10 * (3000000 / USART_BAUD_RATE) + tryconstant; ...超时与实测时延
链路吞吐参考(新格式引入前)
Linux USB:#db#实测 PM3 ⇒ Client 约 545109 Bytes/s;Windows 虚拟机上 ProxSpace USB 约 233998 Bytes/s。
USART(波特率定义于common/usart.h的USART_BAUD_RATE):
| 链路 | 实测 PM3 ⇒ Client |
|---|---|
| 9600 | 934 Bytes/s |
| 115200 | 11137 Bytes/s |
| 460800 | 43119 Bytes/s |
| Linux USB | 666624 Bytes/s(等效约 7M 波特) |
接收超时
经 USART 接收比 USB 慢得多:USB 下 30ms 内完成的接收,USART 上会报"部分包"错误。经验值:
FTDI 9600 hw status ⇒ 需 20ms FTDI 115200 hw status ⇒ 需 50ms FTDI 460800 hw status ⇒ 需 30ms BT 115200 hf mf fchk --1k -f file.dic ⇒ 需 140ms对应常量定义在 include/pm3_cmd.h#L1449 一带:
#define UART_FPC_CLIENT_RX_TIMEOUT_MS 200 #define UART_USB_CLIENT_RX_TIMEOUT_MS 20 #define UART_NET_CLIENT_RX_TIMEOUT_MS 500 #define UART_TCP_LOCAL_CLIENT_RX_TIMEOUT_MS 40 #define UART_UDP_LOCAL_CLIENT_RX_TIMEOUT_MS 20这些值最终进入uart_posix.c的timeval结构与uart_win32.c的serial_port_windows结构。初始用UART_FPC_CLIENT_RX_TIMEOUT_MS,一旦检测到走 USB 就降为UART_USB_CLIENT_RX_TIMEOUT_MS(切换逻辑见 client/src/comms.c#L982)。超时可用hw timeout命令在线调整,且下限即 USB 超时——client/src/cmdhw.c#L1565-L1567 会对小于UART_USB_CLIENT_RX_TIMEOUT_MS的取值给出告警。
通信延时补偿
WaitForResponseTimeout与dl_it的超时里会自动加入一段"通信延时",仅在走 FPC 时启用:timeout = 2 × 实测 FTDI 线缆经验延时:
// client/src/comms.c static size_t communication_delay(void) { if (conn.send_via_fpc_usart) return 2 * (12000000 / uart_speed); return 100; }(hw ping -l 512实测:USB 6–32ms,460800 下 40–70ms,9600 下 1100–1150ms。)
新旧应答路径的实测对比
hw status(DbpStringEx)耗时对比(文档实测值):
| 路径 | USB (/dev/ttyACM0) | 460800 | 115200 | 9600 |
|---|---|---|---|---|
reply_old | 2.52s | 3.03s | 4.88s | 26.5s |
reply_mix(9600) | — | — | — | 7.08s |
reply_ng | 2.10s | 2.22s | 2.43s | 5.75s |
lf read(9600 vs 115200):50.38s vs 6.28s;mem dump(USB vs 115200 FPC):1.48s vs 25.34s。可见reply_ng在慢链路上把单命令耗时压低了一个量级,USB 上则与旧路径持平略优。
快速连发(fast push mode)
连续发送多条命令仍可能变慢:客户端会周期性地等待入站 RX 帧,而超时设置因 BT 而偏保守(uart_posix.c的struct timeval现为 200ms)。若确定下一条命令无需等待应答,可以借用 flasher 的技巧——
// fast push mode conn.block_after_ACK = true; some loop { if (sending_last_command) // 关闭快速模式 conn.block_after_ACK = false; SendCommandOLD / SendCommandMix if (WaitForResponseTimeout(CMD_ACK, &resp, some_timeout) == false) { ... conn.block_after_ACK = false; return PM3_ETIMEOUT; } } return PM3_SUCCESS;若难以判定何时是最后一条命令:
// fast push mode conn.block_after_ACK = true; some loop { SendCommandNG if (WaitForResponseTimeout(CMD_FOOBAR, &resp, some_timeout) == false) { ... conn.block_after_ACK = false; return PM3_ETIMEOUT; } } // 关闭快速模式,并发送一条哑命令使其生效 conn.block_after_ACK = false; SendCommandNG(CMD_PING, NULL, 0); WaitForResponseTimeout(CMD_ACK, NULL, 1000); return PM3_SUCCESS;参考帧(调试用)
帧大小的硬约束(按上限 512 计):
- OLD 命令与应答帧恒为544 字节;
- NG & MIX 命令帧10~522 字节;
- NG & MIX 应答帧12~524 字节。
链路分片行为:
- Linux USB:发送包可为 544B;接收包最大 128B,故 544 = 128+128+128+128+32;
- Linux UART(FTDI):发送包最大 256B,544 = 256+256+32;接收包最大 512B,544 = 512+32。
hw ping三个时代的线上字节对照(十六进制小端):
// 旧版(OLD 请求 + MIX 应答) TestProxmark: SendCommandOLD(CMD_PING, 0, 0, 0, NULL, 0); ->544=0901000000000000000000000000000000000000000000000000000000000000 -> OLD CMD_PING: reply_mix(CMD_ACK, reply_via_fpc, 0, 0, 0, 0); <-36=504d336218000000ff0000000000000000000000000000000000000000000000 <- MIX // 中间版(双向 MIX) CmdPing SendCommandMIX(CMD_PING, 0, 0, 0, NULL, 0); ->34=504d33611800090100000000000000000000000000000000000000000000000000 -> MIX CMD_PING reply_mix(CMD_ACK, reply_via_fpc, 0, 0, 0, 0); <-36=504d336218000000ff00000000000000000000000000000000000000000000000 <- MIX // 现行 NG 版 CmdPing SendCommandNG(CMD_PING, data, len); ->10=504d3361008009016133 -> NG CMD_PING reply_ng(CMD_PING, PM3_SUCCESS, packet->data.asBytes, packet->length); <-12=504d33620080000009016233 <- NG解析一下现行 NG 版:50 4d 33 61即小端的PM3a;00 80是 length/ng 联合体(length=0、ng=1);09 01是CMD_PING(0x0109);61 33是占位魔数a3。应答50 4d 33 62(PM3b)、00 80、00 00(status=0 即PM3_SUCCESS、reason=0)、09 01、62 33(占位b3)——12 字节收尾,而旧版同样语义要 544 字节请求 + 36 字节应答。
hw ping -l 512(NG)的完整收发则演示了大 payload 在 128B 接收窗口下的自然分片:
->522=504d336100820901000102030405060708090a0b0c0d0e0f1011121314151617 -> NG <-128=504d3362008200000901000102030405060708090a0b0c0d0e0f101112131415 <- NG <-128=767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495 <-128=f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415 <-128=767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495 <-12=f6f7f8f9fafbfcfdfeff6233注意请求length字段为00 82(0x0200=512,高位 ng=1),且整帧 522 字节 = 10 头 + 512 payload + 2 CRC 尾,正好落在"NG 命令帧 10~522"的上界;应答 524 字节 = 12 头 + 512 payload + 2 尾,落在应答帧上界,随后按 128B 窗口分 5 段到达。
小结:NG 格式的设计取舍
把 doc/new_frame_format.md 与当前仓库源码放在一起看,这套协议演进遵循几条清晰的原则:
- 变长解决慢链路:10 字节的空命令帧 vs 544 字节的旧帧,在 9600 波特链路上就是 26.5s 与 5.75s 的差距;
- 可辨识与可重同步:
PM3a/PM3b魔数 + 可选 CRC,慢链路(USART/BT)默认开 CRC,USB 用占位魔数省一次计算; - 语义回到应答本身:
status/reason字段 + "用自己的操作码应答",消灭了匿名CMD_ACK带来的 arg 借位 hack; - 兼容边界精确锁定:只有 bootrom 相关帧(
SendCommandBL/reply_old)保持 OLD,OLD 结构体尺寸钉死在 512,其余路径完成转换后旧字段(oldarg、ng、MIX 常量)按计划删除。
开发者定位协议问题时的入口文件:字段与魔数在 include/pm3_cmd.h,客户端收发在 client/src/comms.c,设备侧回复与接收在 armsrc/cmd.c 与 armsrc/appmain.c,bootrom 在 bootrom/bootrom.c,刷写旧帧路径在 client/src/flash.c。
【免费下载链接】proxmark3Iceman Fork - Proxmark3项目地址: https://gitcode.com/GitHub_Trending/pr/proxmark3
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考