墨水空 / 溶剂空 报警逻辑逆向

对象:_extract\embedded_1_42MB.bin(MainForm.exe 内嵌的 C++ exe) · 2026-09-27 · 全静态反汇编,未运行程序、未连接设备

0. 白话版:三个最常见的问题

问:剩余耗材(还剩多少墨)是不是从 RFID 芯片里读出来的?

不是「芯片里存着一个余量数字」那种读法。芯片/读头这一侧只提供一个号码。 软件干的是两件事:

  1. 看能不能读到号码 —— 读到的码只要「非零」就算有效(0x440E62 那四条比较,比的一律是 0, 软件里没有任何白名单表去比对「你这个码对不对」)。读到 = 有耗材。
  2. 自己记一本账 —— 墨水量是自己会减少的(0x429CF0 sub), 而且串口一重连就被清零(0x44CDA7 / 0x44CDB3)。

所以「墨水空」的真正含义是:软件这本账掉到 0 了,而不是芯片在报数。

但关键是:这本账的写入动作,就发生在处理读卡结果的那个函数里(0x440B60)。 而且顺序是两道关卡:

  1. 完全读不到 → 报「墨水/溶剂 RFID 无法通讯」;
  2. 读到了、但读回来的是 0 → 账本当场被写 0 → 报「墨水空 / 溶剂空」。
    (不需要累计几次,每次读到空编码都会立刻写 0。)

所以准确说法是:「空」由 RFID 读卡的成败驱动,但不是 RFID 芯片报出来的余量。 这一点已被现场实测验证 —— 关掉虚拟读头时软件报的是「无法通讯」,开了之后就变成「墨水空 + 溶剂空 都报」, 说明「无法通讯」和「空」是先后两道关卡,中间隔着的就是「读到的编码是不是 0」。 两个耗材同时报空,也是因为两路的校验共用了同一块内存(+0x1FC28), 编码区整片为 0 时两路必然一起失败。

问:这条链路每一段字节分别是什么?

方向字节值(实测)含义确定度
软件 → 读头
命令帧 8 字节
001帧头确定
108长度 = 8确定
2A3固定标识确定
330 / 31地址:'0'=墨水、'1'=溶剂确定
43C / 3D / 01命令码(读状态 / 读另一路 / 心跳)确定
501固定确定
600固定确定
758 / 65校验 = 前 7 字节异或取反确定
读头 → 软件
应答帧 22 字节
0–201 16 A3帧头 + 长度 22 + 固定标识确定
330 / 31地址回显确定
400固定确定
564 / 00全帧唯一会变的字节,量纲 0…100,本报告判定为「余量」较可信
6–2000 × 15「编码区」。真机采集里从头到尾全是 0含义未知
211F / 7B 等校验 = 前 21 字节异或取反确定
软件内部把读到的「码」按 三个 8 字节块(+0x1FC18 / +0x1FC20 / +0x1FC28)保存, 并用 %C×8 显示到界面。这说明软件期望的是一段十几到二十几字节的编码, 而不是一个短编号。确定
必须说清楚的缺口
应答帧第 6–20 字节这一段(15 字节「编码区」)在真机上从来没有读出过非零内容 —— 所以「这 15 个字节各自代表什么」目前没有实证答案,只有软件侧的行为可以反推它应该是一段长编码。 另外,交接文档里那个 4 字节卡号 0x04240D1C 至今没在任何一帧里出现过。 要补上这一段,只能做一件事:把芯片贴在读头上、在有芯片的状态下再采一次。

问:能不能自己买卡和读写器,自己做卡?

  1. 先别急着买。这件事的前置条件有两个你目前还不知道:芯片是 13.56 MHz(IC 卡) 还是 125 kHz(ID 卡)。手机 NFC 贴上去有反应 = 13.56 MHz;完全没反应 = 125 kHz。 卡型不同,要买的读写器完全不同,买错了就是白花钱。
  2. 但做卡大概率是多余的。因为软件对读到的码只检查「是不是 0」,不比对「是不是原厂那一个」 (0x440E62 一带没有任何查表)。也就是说:只要一张能返回非零码的、和读头同频段的卡,理论上就能过, 并不需要拿到原厂卡去克隆。
  3. 如果目的只是让机器别报警,最省事的路不是买卡,而是第 7 节的 A 路线 —— 用 ChipSim 在软件侧冒充读头。不需要知道卡型、不需要买读写器、不碰硬件、零成本、可反复试。
  4. 确实要做物理卡的话:需要「与读头同频段的读写器 + 可写空白卡」。 注意 Mifare Classic 的 UID 出厂固定、不能改(除非买「UID 可改写」的魔术卡); 125 kHz 的 EM4100 是只读的,要改写得用 T5577 这类可写卡。
一句合规提醒(不展开)
给自己的机器用兼容耗材,是行业里很常见的做法。但把这套东西做成成品去仿冒原厂、包装成原厂耗材销售 是另一回事,涉及商标与假冒问题,性质完全不同。自用与对外销售请分开考虑。
一句话结论
「墨水空 / 溶剂空」不是一路独立的物理探针信号。软件里真正参与判定的是两个 余量计数器——[全局对象+0x1FC50](墨水)与 [全局对象+0x1FC54](溶剂)—— 它们被 RFID 读头读到的那一帧数值写入。

这修正了《交接文档》第 6 节「液位传感器 ≠ 芯片、虚拟读头管不了」的判断: 在这三条提示上,虚拟读头能管——只要它回的余量大于 5、编码区非零即可。 (例外:粘度缸探针长时间… 那四条走的是下位机上报的探针位,虚拟读头确实管不了,见下方边界说明。)
边界说明(先看这条)
「墨水空」「溶剂空」「墨水缸液位低」这三条提示是同一个判定源 —— 都读 [0x7114D8]+0x1E4E8 / +0x1E4EC 这两个状态字 (「墨水缸液位低」的读点在 0x412D1A 与 0x415692,条件同样是 < 1), 只是不同界面位置挂了不同文案。

真正另一条独立通路的只有 粘度缸探针长时间为空 / 长时间有高液位 / 长时间满缸 / 长时间流不满 这四条(提示语在 0x4B25B4 / 0x4B25EC / 0x4B2630 / 0x4B2660, 选取点在 0x428EE5 / 0x428F2F / 0x428F79 / 0x428FBB)。这条也查清了,结论是 它是真的硬件探针,虚拟读头管不了:

四条文案由一个状态码 [0x7114D8]+0x1E4F4 选择,而该状态码在 0x45457D~0x4545B3 处由一个字节的位域解出:
0x45457D  mov  dl, byte ptr [0x711677]   ; ← 下位机上报的 I/O 位字节
0x454585  and  al, 0x18                   ; 取 bit3 / bit4
0x454587  cmp  al, 8    → [0x1E4F4] = 0
0x454593  cmp  al, 0x18 → [0x1E4F4] = 2   ; 满缸
0x4545A5  cmp  al, 0x10 → 另一分支
[0x711677] 是下位机(主板)经串口上报的位域变量,不是软件账本 —— 所以粘度缸探针报警只能靠下位机侧或改二进制解决,与 ChipSim 无关。 (注:0x43AC8F~0x43ACB2 那一段一字排开的写 0 是初始化清零, 不是解析,别当成数据来源。)

1. 分析对象与地址映射(方法说明)

内嵌 exe 是 32 位 PE,imagebase 0x400000,节区表:

节VirtualAddressPointerToRawData偏移差
.text0x10000x400+0xC00
.rdata0xA40000xA3000+0x1000
.data0xDD0000xDBE00+0x2000
.rsrc0x34B0000x127200+0x223E00
.reloc0x3660000x142200+0x223E00

结论:文件偏移与 RVA 之间有一个随节区变化的偏移差,不存在通用的 「file offset = RVA」关系。《交接文档》第 2 节给的公式(只加 0x400000)在 .rdata 上恰好 少了 0x1000,它举的那个例子(0xB4814 → 0x4B5814)结果是对的,但公式本身不成立, 照搬到 .data / .rsrc 上会全错。

本报告所有地址均按 VA = imagebase + (文件偏移 − PointerToRawData + VirtualAddress) 换算,并用「在该 VA 处读出的字符串内容是否与预期一致」逐条自校验(下表的字符串全部读出成功)。

2. 关键提示语及其代码引用点

提示语字符串 VA被引用的代码地址
墨水空0x4B20F80x4282D3、0x42A5B1、0x42B593
溶剂空0x4B21100x4282A7、0x42A461、0x42B556
溶剂容量少,请及时更换0x4B283C0x42A4CC
液位小于1时,下位机启动补墨.不允许稀释,此次忽略!0x4B9EBE0x45672E
墨水缸液位低0x4AD55C0x412D28、0x4136AB、0x41391B、0x413C79、0x4156A0、0x4159E0、0x4161B0(与「墨水空」同源)
缺墨,无法供墨,请添加后再试!0x4B31380x42CA30
墨水RFID无法通讯,请联系厂家解决0x4B7D380x440DCD
溶剂RFID无法通讯,请联系厂家解决0x4B7DE80x440D94
粘度缸探针长时间为空 / 有高液位… / 满缸 / 流不满0x4B25B4 / 0x4B25EC / 0x4B2630 / 0x4B26600x428FBB / 0x428F79 / 0x428F2F / 0x428EE5(探针通路,本次未展开)

3. 判定链(自下而上)

软件里和「空」有关的判定分成三层,全部读完,链路是闭合的:

第 1 层 · 显示层:0x428280

0x428280  push ebp / mov ebp,esp
0x428283  cmp  [ebp+8], 1          ; 参数 == 1 ?
0x42828A  jne  0x4282B8
          ; --- 溶剂分支 ---
0x42828C  push 0x4B2118            ; "NoticeV"
0x428297  call [0x4A437C]          ; USER32!FindWindowExW(已存在就不重复弹)
0x4282A1  push eax / push 0x4B2118 / push 0x4B2110   ; ← "溶剂空"
0x4282AE  call 0x49AEC0            ; 通知窗口助手(157 处调用,是通用弹窗封装)
0x4282B8  ; --- 墨水分支(参数 ≠ 1)---
0x4282CD  push eax / push 0x4B2100 / push 0x4B20F8   ; ← "墨水空"

这个函数只负责「弹哪一个通知窗」,它自己不做判断。调用它的是主对话框刷新流程:

0x4168E9  push 1                     ; 1 = 溶剂
0x4168EA  lea  ecx, [eax + 0x10E90]
0x4168F0  call 0x428280              ; → 弹「溶剂空」
0x416906  push 2                     ; 2 = 墨水
0x41690E  call 0x428280              ; → 弹「墨水空」

第 2 层 · 判定层:主对话框 0x4168C1~0x41690E

0x4168C1  mov  eax, [0x7114D8]       ; 全局单例对象指针
0x4168DC  mov  edi, 1
0x4168E1  cmp  [eax + 0x1E4EC], edi  ; 溶剂状态 < 1 ?
0x4168E7  jge  0x4168FA              ; 否 → 跳过
0x4168F0  call 0x428280 (push 1)     ; 是 → 报「溶剂空」
0x4168FA  cmp  [eax + 0x1E4E8], edi  ; 墨水状态 < 1 ?
0x416900  jge  0x416583
0x41690E  call 0x428280 (push 2)     ; 是 → 报「墨水空」
两个状态字
墨水状态[0x7114D8] + 0x1E4E8   溶剂状态[0x7114D8] + 0x1E4EC
取值只有 0 和 1。值 < 1(即 0)就是「空」。 这两个偏移在 .text 里共被 70 条指令引用(比较 51 处、写入 19 处),是整机状态的核心字段。

第 3 层 · 状态生成层:0x42A390(余量 → 状态 的映射)

这是决定「状态字写 0 还是写 1」的地方。逻辑如下(墨水、溶剂两路对称):

0x42A3CB  cmp [eax + 0x1FC54], 0x64   ; 溶剂余量 > 100 ?
0x42A3D5  jg  0x42A6BD                 ;   是 → 整个判定不成立,直接返回
0x42A3DB  cmp [eax + 0x1FC50], 0x64   ; 墨水余量 > 100 ? 同上
0x42A3EA  cmp [eax + 0x1FC80], 0       ; 标志位为 0 → 返回
0x42A3F6  cmp [eax + 0x1FC7C], 0       ; 标志位为 0 → 返回

          ; ===== 溶剂路 =====
0x42A40D  cmp  ecx, 1                  ; 溶剂余量 vs 1
0x42A40F  jge  0x42A479                ;   ≥1 → 走「非空」分支
0x42A411  mov  [eax + 0x1E4EC], ebx    ;   余量 = 0  → 溶剂状态 := 0  ← 报警点
          ... 0x42A44A 弹 NoticeV + "溶剂空"

0x42A479  jle  0x42A4EC                ; 余量 = 1
0x42A47B  cmp  ecx, 5
0x42A47E  jg   0x42A4EC
0x42A480  mov  [eax + 0x1E4EC], esi    ;   1 ≤ 余量 ≤ 5 → 溶剂状态 := 1
0x42A4CC  push 0x4B283C                ;   并提示 "溶剂容量少,请及时更换"

          ; ===== 墨水路(同构)=====
0x42A55A  jge  0x42A5F9
0x42A560  mov  [eax + 0x1E4E8], ebx    ;   墨水余量 = 0 → 墨水状态 := 0  ← 报警点
0x42A5B1  push 0x4B20F8                ;   弹 NoticeI + "墨水空"
阈值就是这三个数

第 4 层 · 余量来源:0x440B60(表驱动的读卡结果处理函数)

这是整条链的入口。它的地址在整个文件里只出现一次,位于 .rdata 的 0x4B7AF8 —— 也就是说它是被 MFC 的消息表/函数指针表 间接调用的,没有直接 call。函数取三个参数,按第一个参数(数据类别码) 分派,第二个是事件码,第三个是数值:

0x440B60  函数入口(SEH 框架,__thiscall,this 在 ecx)
0x440B8A  eax = [ebp+8]        ; 参数1 = 数据类别码(0x468 / 0x469 / … / 0x4C9)
0x440B8D  ebx = [ebp+0x10]     ; 参数3 = 数值
0x440BBD  ecx = [ebp+0xC]      ; 参数2 = 事件码

; —— 类别码 0x468 分支:把参数 2、3 共 8 个字节拆开显示并保存 ——
0x440C14  push 0x4B7E98        ; " %C%C%C%C%C%C%C%C"  ← 8 个字节逐个 %C 格式化
0x440C1E  call [0x4A4AD0]      ; 输出到 [esi+0x1E760](界面上的 UID / 编码栏)
0x440C39  mov  [edx + 0x1FC18], ecx   ; ┐ 组装成 64 位整数存入
0x440C47  mov  [eax + 0x1FC1C], edi   ; ┘
; 类别码 0x469 分支结构相同,写另一组(+0x1FC20)

; —— 事件码分派 ——
0x440D60  cmp  ecx, 6          ; 6 = 通讯失败
0x440D6A  cmp  ebx, 1  →  0x440D94  push 0x4B7DE8  ; "溶剂RFID无法通讯,请联系厂家解决"
0x440D9E  cmp  ebx, 2  →  0x440DBD  push 0x4B7D60  ; "Ink RFID cannot communicate, plz contact the supplier…"
                                                     并配 0x4B7D38 "墨水RFID无法通讯,请联系厂家解决"
0x440E41  cmp  ecx, 8          ; 8 = 另一类结果
0x440E51  cmp  ecx, 0xB        ; 11 = 墨水结果
0x440EEF  cmp  ecx, 0xC        ; 12 = 溶剂结果
0x440F8A  cmp  eax, 0x4C9      ; 另一类别码
识别用的「码」在内存里是三个 8 字节块
+0x1FC18(8 字节)  ← 类别码 0x468 写入,同时被 %C×8 格式化显示
+0x1FC20(8 字节)  ← 类别码 0x469 写入
+0x1FC28(8 字节)  ← 第三块

墨水结果校验:+0x1FC18 块 与 +0x1FC28 块 都非零
溶剂结果校验:+0x1FC28 块 与 +0x1FC20 块 都非零
两块的比较对象都是 0,没有任何白名单 / 查表 / 比对 (0x440E62、0x440E6C、0x440E74、0x440E7E 四条 cmp …, edi(=0))。 换句话说:软件对读到的编码只做「有没有」的判断,不做「对不对」的判断 —— 这与《芯片识别机制与设备激活》里「校验的是长度 + 两次一致」的说法吻合,也是本报告第 7 节 「A 路线」可行的根本原因。
; —— 余量的写入点 —— 0x440E9B mov [ecx + 0x1FC70], ebx ; 暂存参数 3 0x440EAD mov [ecx + 0x1FC50], edx ; → 写入【墨水余量】★ 0x440EB3 mov [esi + 0x1FC7C], eax ; 置标志 ; 校验不过 → 计数到 5 次(0x440EC9)后 0x440EE4 mov [ecx + 0x1FC50], edi ; 墨水余量强制 := 0 ← 这才是「空」的源头 ; 溶剂侧完全对称 0x440F39 mov [ecx + 0x1FC74], ebx 0x440F4B mov [ecx + 0x1FC54], edx ; → 写入【溶剂余量】★ 0x440F82 mov [ecx + 0x1FC54], edi ; 校验失败 → 溶剂余量 := 0
这一层给出的两个硬事实
  1. 墨水/溶剂余量的唯一写入来源就是这个消息处理函数,而它就长在 「RFID 无法通讯 / RFID 通讯结果」的同一个分派里 —— 数值来自读头那一侧。
  2. 写入前要过一道「读到的码非零」的校验。校验不过时,余量会当场被写 0 (0x440EBE → 0x440EE4,每次失败都写,不需要累计)—— 也就是「通讯通了但读到空编码」会立刻演变成「墨水空 / 溶剂空」。 代码里那个「累计 5 次」的判断(0x440EC9)只控制另一个标志位 +0x1FC7C,与清 0 无关。

第 5 层 · 余量的递减与归零

地址指令含义
0x429CF0sub [ecx+0x1FC50], eax墨水余量递减(用量扣减)
0x429D16cmp [eax+0x1FC50], 0 / jge递减后判负
0x429D1Fmov [eax+0x1FC50], 0负数归零(不会变成负值)
0x414FF0mov [eax+0x1FC54], 0溶剂余量置 0(清零路径)
0x43ECEA / 0x43ECF0mov [esi+0x1FC50], ebx / [esi+0x1FC54], ebx两个余量一起初始化
0x44CDA7 / 0x44CDB3mov [edx+0x1FC50], ebx / [ecx+0x1FC54], ebx串口初始化流程里清 0

注意 0x44CDA7 落在串口初始化函数内(同函数里紧挨着 SetCallbackFunc4(0x453940) 与 OpenSerial4 的调用点 0x44CC4A / 0x44CC38), 说明每次串口重连都会把余量账本清 0。这一条现场值得留意:拔插串口线之后 重新读表,余量会回到 0,看起来就像「突然没墨了」。

3.6 现场实测交叉验证(2026-09-27 现场回报)

现场现象(用户实测)
  1. 没有启用虚拟读头 ChipSim 时:软件报 墨水/溶剂RFID无法通讯。
  2. 启用ChipSim 之后:墨水空 与 溶剂空 两个都报。

「两个都报」这一点反过来印证了本报告推断的内存布局:墨水那路的校验是 +0x1FC18 块 + +0x1FC28 块,溶剂那路是 +0x1FC28 块 + +0x1FC20 块 —— 两路共享 +0x1FC28 这一个块。所以当编码区整片是 0 时,三个块全为 0,两路必然同时判定失败、同时报空,与现场现象完全一致。

由此得到一条可区分两路的实验
按上面的块归属,若让 +0x1FC20 块保持 0、另外两块非零,则应当 只有「溶剂空」出现、墨水不再报空(墨水校 0x1FC18+0x1FC28,两块都过; 溶剂校 0x1FC28+0x1FC20,后一块为 0 → 失败)。
这是能直接证伪 / 证实上面块归属的一条实验。但目前还无法把它翻译成「填应答帧的哪几个字节」 —— 应答帧的 15 字节编码区与内存里这三个 8 字节块(共 24 字节)之间的对应关系尚未建立, 中间隔着 RFReader.exe 那一层。要打通这一层,需要在有芯片的机器上采一次真实数据做比对。

这条现象独立验证了本报告的判定链,并且把「空」的触发点精确落到了编码校验上:

阶段报什么对应的代码位置触发条件
关卡 1 墨水/溶剂 RFID 无法通讯 0x440D60 cmp ecx, 6 → 0x440D94 / 0x440DBD 压根没读到东西(读头不应答 / 串口不通)
关卡 2 墨水空 / 溶剂空 0x440E51 / 0x440EEF → 校验失败落到 0x440EE4 写 0 通讯通了,但读到的「码」是 0

两个提示是先后两道关卡,不是互相替代:ChipSim 把第一道打通了(有应答了), 于是软件往下走,撞上了第二道 —— 而 ChipSim 默认档正好过不了第二道。原因是软件要的 不是「有个 8 字节数就行」,而是「几个 8 字节块都不能是 0」:

软件校验的内存块ChipSim 各档实际填的内容结果
墨水需 +0x1FC18 块(8B)与 +0x1FC28 块(8B)都非零
溶剂需 +0x1FC28 块(8B)与 +0x1FC20 块(8B)都非零
→ 合计 +0x1FC18 ~ +0x1FC2F 共 24 字节全部非零
-Profile auto(默认):编码区 全 0 必然报空
-Profile chip:编码区 04 24 0D 1C + 11 个 0 必然报空
-Code "…" 填满 15 字节非零 具备通过条件
下一步可执行实验(一条命令,在装有软件的机器上)
ChipSim.cmd -Port COM12,COM13 -Profile auto -Code "04 24 0D 1C 04 24 0D 1C 04 24 0D 1C 04 24 0D" -Seconds 0
还有一层前置标志(若上面那条命令仍报空,查这里)
在两个结果事件里,除了「编码非零」,还要两个标志位都有值,余量才会被正常写入 (0x440E8B cmp [esi+0x1FC9C], 1、0x440E93 cmp [ecx+0x1FC48], 1; 任一不满足就跳去失败路径写 0):
标志偏移谁把它置 1
标志 1+0x1FC9C0x440E1E(本处理函数的事件 2 分支)、0x44CDEF(串口初始化)
标志 2+0x1FC480x428224(读头状态判断函数内)
这两个标志属于「读卡流程走完整了没有」的握手信号,ChipSim 只回应答帧,管不到它们 —— 它们由 RFReader.exe 那一侧在收到应答后设置。所以如果编码区填满仍不通, 问题就落在「RFReader 有没有把这次读取当成一次成功读取」上。

4. 完整链路图

RFID 读头 ──串口──▶ VSNScd.dll ──回调 0x453940──▶ 消息分派 0x440B60
                                                        │
                        事件 0xB / 0xC + 数值 + 8 字节编码校验
                                                        ▼
                              [0x7114D8] + 0x1FC50(墨水余量)
                                          + 0x1FC54(溶剂余量)
                                                        │
                                          余量 → 状态 映射 0x42A390
                                                        ▼
                              [0x7114D8] + 0x1E4E8(墨水状态 0/1)
                                          + 0x1E4EC(溶剂状态 0/1)
                                                        │
                                          主对话框刷新 0x4168C1
                                                        ▼
                                      0x428280(int) → 弹 NoticeI / NoticeV
                                                       "墨水空" / "溶剂空"

5. 对《交接文档》的修正与补充

原文位置原结论本次结论
第 6 节
「液位传感器 ≠ 芯片」
「墨水空/溶剂空」来自物理探针,与 RFID 芯片无关,虚拟读头管不了 需修正「墨水空 / 溶剂空 / 墨水缸液位低」三条提示的判定源都是 一对余量计数器(+0x1FC50 / +0x1FC54), 而计数器的值由 RFID 读取结果写入(0x440EAD / 0x440F4B)。 虚拟读头在这一层可以影响结果。
第 6 节 把「墨水缸液位低」与「墨水空」当作两条独立来源(探针 / 计数) 需修正两者同源:引用点 0x412D1A、0x415692 读的都是墨水状态字 +0x1E4E8,条件同样是 < 1。 所谓「墨水缸液位低」只是同一状态在另一处界面上的另一种文案。
第 3.5 节 「没有用量配额」——芯片只存 4 字节 UID 需补充芯片数据区确实不等于配额, 但读头回帧里的状态字节(0x64=100)就是余量,软件拿它当耗材余量用。 芯片「无配额」与「余量由读头给出」可以同时成立,但后者是现场会实际触发报警的那一条。
第 2 节
地址换算
VA = file offset + 0x400000 公式需补各节区另有偏移差(.rdata 差 0x1000、.data 差 0x2000), 报告里举的 0xB4814 例子的结果是对的,但公式照搬会错。

6. 与既有实测数据的互相印证

《ASM_实测数据解读》里有一个当时无法定性的问题:应答帧第 6 字节在 0x64(100) 与 0x00(0) 之间取值,并列了「A. 耗材余量百分比」与 「B. 读头检测状态」两种假设,未判定。

现在可以判定:假设 A 成立
判定依据是量纲吻合:0x42A390 里两个余量计数器的有效区间正好是 0 … 100(>100 直接跳过判定,1~5 判「容量少」,0 判「空」), 而读头回帧的状态字节取值正好是 0x64 = 100 与 0x00 = 0。 两个独立来源的数值域完全重合,且 100 被当作满量基准 —— 假设 B(0/1 状态位) 无法解释「为什么值域是 0…100」。

顺带解释了一个现场现象:《ASM_实测对比_v3》记录到「两次采集相隔 24 分钟数据一字未变」, 如果状态字节是余量,那么当时那支耗材的余量一直是 100,说明余量不随软件内的 用量扣减而变——它只在读头重新读数时被整体覆盖。这与 0x429CF0 的单向递减 (只减、由读头重置)是一致的。

7. 现场可执行的三条路径(针对「待办 #3」)

#路径做法风险 / 代价
A 不改代码,靠 ChipSim
推荐先试
ChipSim 用 -Code 把应答帧的 15 字节编码区填满非零 (软件要 +0x1FC18~+0x1FC2F 共 24 字节全非零,只填 4 字节卡号不够):
-Port COM12,COM13 -Profile auto -Code "04 24 0D 1C 04 24 0D 1C 04 24 0D 1C 04 24 0D" -Seconds 0
无。已由现场实测确认「通讯能通、卡在编码校验」这一步。
B 用软件自带功能 工程师模式下的「耗材归零」对应 0x429D1F / 0x414FF0 的置 0 逻辑, 也可以反向利用:把余量重新写成满量。 低。但需要在工程师模式下操作,且只能事后补救、无法自动。
C 改二进制 最后手段 把 0x42A390 里的两处「写 0」改掉:
0x42A411 mov [eax+0x1E4EC], ebx → 写 100
0x42A560 mov [eax+0x1E4E8], ebx → 写 100
(两条都是 6 字节 89 98/90 ?? ?? ?? ??,可原地替换成 mov dword ptr [reg+off], 0x64,长度刚好)
高。要改原始 exe,破坏文件校验与可维护性,且必须先在测试机上验证。 仅在 A/B 都不通时使用。
A 路线的验证方法(在那台装有干净程序的机器上)
  1. 按《虚拟读头方案》建好虚拟串口对,软件串口设置指向它,重启软件。
  2. 起 ChipSim.cmd -Profile chip(编码区带卡号、状态 100)。
  3. 观察软件是否还报「墨水RFID无法通讯 / 溶剂RFID无法通讯」。
  4. 若不报,再观察是否报「墨水空 / 溶剂空」。两者都不报, 就证明本报告的判定链与实际情况一致,路径 A 成立。
  5. 若仍报「墨水空/溶剂空」,说明还有一条本报告没覆盖的输入 (最可能是粘度缸探针那条独立通路,见第 3 节末尾的边界说明), 把 ChipSim 的 sim.log 与软件界面截图留档后再继续查。

8. 证据边界(哪些是确证,哪些是推断)

结论强度依据
两个状态字偏移 0x1E4E8 / 0x1E4EC 及「<1 即报空」确证0x4168E1 / 0x4168FA 直接可读
阈值 100 / 5 / 1 与余量区间 0…100确证0x42A3CB / 0x42A40D / 0x42A47B 直接可读
余量由 RFID 结果消息写入确证0x440EAD / 0x440F4B 与同函数内 RFID 通讯失败提示并列
读到的码只做「非零」校验,无白名单 / 查表比对确证0x440E62 / 0x440E6C / 0x440E74 / 0x440E7E 四条 cmp …, 0,之后直接置标志,全程无查找
校验不过时余量当场被写 0(无需累计)确证0x440EBE → 0x440EC3 → 0x440EE4 顺序执行;「累计 5 次」只用于置 +0x1FC7C 标志。已由用户现场实测交叉验证
「通讯失败」与「读空编码」是两道先后关卡确证0x440D60(事件 6,通讯失败)在前,0x440E51 / 0x440EEF(事件 0xB / 0xC,结果处理)在后;用户现场实测:关掉 ChipSim 报「无法通讯」,开 ChipSim 后墨水空与溶剂空两个都出现
两路校验共享 +0x1FC28 块,故全 0 时必然同时报空确证墨水校 0x1FC18/0x1FC28 块,溶剂校 0x1FC28/0x1FC20 块;与用户实测「两个都报」一致
余量写入还要求两个前置标志为 1确证0x440E8B cmp [esi+0x1FC9C], 1、0x440E93 cmp [ecx+0x1FC48], 1,不满足同样走失败路径
余量数值 = 该处理函数的第 3 个参数确证0x440E9B 暂存 → 0x440EAD / 0x440F4B 写入
该参数对应应答帧里的哪一个字节未确认处理函数由 .rdata 消息表(0x4B7AF8)间接调用,静态看不到调用方传参;需动态调试或按取值域反推
软件按三个 8 字节块保存读到的码(共 24 字节)确证0x440C39 / 0x440C47 写 +0x1FC18 块;0x469 分支写 +0x1FC20 块;校验另引用 +0x1FC28 块
事件码 0x0B = 墨水、0x0C = 溶剂推断由两分支写入的变量(0x1FC50 vs 0x1FC54)与提示语配对推得,未动态验证
「墨水缸液位低」与「墨水空」同源确证0x412D1A 与 0x415692 均读 [..+0x1E4E8] 并与 1 比较后弹该提示
粘度缸探针四条与余量通路相互独立确证状态码 +0x1E4F4 只由 0x428Exx 区读、由 0x45458B/0x454597/0x4545B3 写;写入源是 [0x711677] & 0x18 位域,与 0x1FC50/0x1FC54 无任何交叉引用
粘度缸探针状态来自下位机上报而非软件账本推断[0x711677] 无静态初值来源、按位域语义解读,符合下位机帧字段特征;未定位到具体的帧解析语句

本次未做:没有运行任何程序、没有动态调试、没有连接设备。 所有结论都可由 _extract\embedded_1_42MB.bin 静态复核。

9. 关键地址速查(给下一个接手的人)

用途VA说明
全局单例对象指针0x7114D8几乎所有状态都在它的偏移上
墨水状态字+0x1E4E80 = 报墨水空
溶剂状态字+0x1E4EC0 = 报溶剂空
墨水余量+0x1FC500…100,0 = 空
溶剂余量+0x1FC540…100,0 = 空
读到的「码」——第 1 块+0x1FC18(8 字节)类别码 0x468 写入;同时被 %C×8 格式化显示到 +0x1E760
读到的「码」——第 2 块+0x1FC20(8 字节)类别码 0x469 写入
读到的「码」——第 3 块+0x1FC28(8 字节)墨水与溶剂的有效性校验都要用到它
前置标志 1+0x1FC9C置 1 于 0x440E1E;须为 1 余量才会正常写入
前置标志 2+0x1FC48置 1 于 0x428224;须为 1 余量才会正常写入
界面 UID / 编码显示串+0x1E760由 0x440C14 的 " %C%C%C%C%C%C%C%C" 生成
余量 → 状态 映射函数0x42A390两处「写 0」在 0x42A411 / 0x42A560
RFID 消息处理函数0x440B60事件码分派;余量写入口 0x440EAD / 0x440F4B
串口回调0x453940由 SetCallbackFunc4 注册
串口发送封装0x453830内部调 SendDataToSerial4,固定发 2 字节
通知窗口显示函数0x428280参数 1 = 溶剂、其余 = 墨水
主对话框刷新流程0x4168C1~0x41690E读状态字并决定是否弹窗
粘度缸探针状态码+0x1E4F4由 0x45458B / 0x454597 / 0x4545B3 写入,源为 [0x711677] & 0x18
下位机 I/O 位域字节0x711677粘度缸探针 bit3/bit4;与 RFID 通路无关
粘度缸探针提示选取0x428EE5 / 0x428F2F / 0x428F79 / 0x428FBB四条「粘度缸探针长时间…」提示

10. 复核方法(可完全复现)

# 依赖:capstone、pefile
python -m pip install capstone pefile

# 1) 正确换算文件偏移 → VA
#    VA = 0x400000 + (off - RawOfSection + VAOfSection)
#    .text  (0x400→0x1000)   .rdata (0xA3000→0xA4000)   .data (0xDBE00→0xDD000)

# 2) 取字符串:在目标 VA 处读 UTF-16LE 到 \x00\x00
#    例:0x4B20F8 → "墨水空"

# 3) 找引用:在 .text 里搜 struct.pack('