马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
本帖最后由 pbai 于 2026-11-6 05:56 编辑
PBIDEA:用 uo_asn1 做 ASN.1 DER 编解码全解(8 种字段的字节布局、解码类型回读与 crypto 崩溃实录,PB10 基准 · PB12.5 实测)
阅读说明
1. 适用版本:一律以顶部【版本声明】为准;本文写作与实测基准为 PB10,PB 12.5 双轨读数逐字一致(快照见第九节)
2. 支持数据库:本文不涉及数据库,全部为内存与本地文件操作
3. 操作系统与环境要求:Windows 7+,需安装 PBIDEA 运行库(PbIdea.dll,与 uo_asn1 同目录的 websuite.pbl)
4. 难度系数:★★★☆☆(三星,需要先懂一点点 TLV 的读法,但不需要懂密码学)
5. 其它阅读说明:文中出现的十六进制读数全部来自 PB10 与 PB12.5 双轨实跑,代码块可直接抄;纯 PowerScript 的 blob 基础可先看《PowerScript Blob 全解》那一篇
PB 做接口的同行大概都遇到过同一个坎:对方给的接口文档里写着「报文体采用 ASN.1 DER 编码」,然后附一张字段表。PB 本身没有 ASN.1 支持,SQL 里没有、PowerScript 里也没有,长期以来大家只能在 PB 外面挂一个 DLL、或者干脆让中间件去转。PBIDEA 的 uo_asn1 就是补这个洞的:把「往报文里塞字段」和「从报文里取字段」这两件事,做成了一组 Add 与 Decode。
这一篇不讲密码学、不讲证书链,只讲清楚一件事:这个对象到底把什么字节写进了报文,又把什么字节读回来了。因为踩过的人都知道,ASN.1 出问题从来不是「编不出来」,而是编出来的字节和对方期望的差一个 tag、或者多补了一个 0x00。
一、uo_asn1 的对象全貌
先把 API 摊平。uo_asn1 从 nonvisualobject 继承,不是 autoinstantiate,用之前必须 create,用完 destroy。
| 方法 | 形态 | 用途 | 实测注意 | | AddLong | boolean AddLong(readonly long) | 加一个 long | 返回值可用 | | AddLongLong | boolean AddLongLong(readonly LongLong) | 加一个 8 字节整数 | 返回值可用 | | AddInteger | boolean AddInteger(readonly uo_biginteger) | 加大整数 | 返回值恒为 false,值照样进包 | | AddString | boolean AddString(readonly string) | 加一个字符串 | 内部自动转 UTF-8 | | AddBoolean | boolean AddBoolean(readonly boolean) | 加一个布尔 | true 编成 FF,false 编成 00 | | AddDouble | boolean AddDouble(readonly double) | 加一个浮点 | 编成 ASN.1 REAL,读回来见第五节 | | AddBlob | boolean AddBlob(readonly blob) | 加一段二进制 | 空 blob 返回 false 且不入包 | | AddFile | ulong AddFile(readonly string) | 把一个文件的内容当一段二进制加进去 | 成功返 1,文件不存在返 0 | | Encode | blob Encode(boolean crypto) | 打包,返回 DER | crypto=false 为明文 DER | | Encode | blob Encode(boolean crypto, readonly string saveToFileName) | 打包并存盘 | 返回值与落盘内容完全一致 | | Decode | int Decode(boolean cryptoed, readonly blob, ref any[]) | 解包,只取值 | 返回字段个数 | | Decode | int Decode(boolean cryptoed, readonly blob, ref any[], ref string[]) | 解包 + PB 类型名 | 第二个数组是 typeName | | Decode | int Decode(boolean cryptoed, readonly blob, ref any[], ref string[], ref string[]) | 解包 + PB 类型名 + ASN.1 类型名 | 第三个数组是 asn1type | | Decode | 同上三种,第一参换成 readonly string fileName | 直接从文件解 | 少一次读文件 | | Decode | int Decode(boolean cryptoed, readonly blob 或 fileName, ref uo_xml) | 解成 XML 树 | 需要 uo_xml 参与 | | Clear | subroutine Clear() | 清空待编码字段 | 只清待编码项,见第四节 | | SetKey | subroutine SetKey(readonly string pri, readonly string pub) | 设置密钥对 | 与 crypto=true 配合,见第六节 |
看到这里就能明白它的设计取舍:编码走「一路 Add,最后一刀 Encode」,解码走「一刀 Decode,吐出三个平行数组」。没有中间态、没有游标、没有 seek,用起来非常省心,代价是它只能处理「顺序排列的扁平字段」——嵌套结构要自己想办法。
二、编码侧:8 种字段的字节布局
先立一条最容易搞错的规矩:uo_asn1 永远在最外面套一层 SEQUENCE。哪怕你只塞了一个 long,出来的也不是 3 个字节,而是 5 个字节——外面那层 30 03 是它自己加的。
单值 long=1 的实测:3003020101。拆开看是 30(SEQUENCE)03(长度 3 字节)02(INTEGER)01(长度 1)01(值)。
八种 Add 方法各自用什么 tag,双轨实跑逐字节对过了:
| Add 方法 | 实测入参 | 出来的 ASN.1 类型 | tag | 实测 hex(含外层 30) | | AddLong | 1 | INTEGER | 02 | 3003020101 | | AddLong | 0 | INTEGER | 02 | 3003020100 | | AddLong | -1 | INTEGER | 02 | 30030201ff | | AddLongLong | 4294967296 | INTEGER | 02 | 300702050100000000 | | AddInteger | 大整数 255 | INTEGER | 02 | 3004020200ff | | AddString | 'abc' | UTF8STRING | 0C | 30050c03616263 | | AddString | 空串 | NULL | 05 | 30020500 | | AddBoolean | true | BOOLEAN | 01 | 30030101ff | | AddBoolean | false | BOOLEAN | 01 | 3003010100 | | AddDouble | 1.5 | REAL | 09 | 3005090380ff03 | | AddBlob | blob('AB') | OCTET STRING | 04 | 3006040441004200 |
2.1 整数:正数高位是 1 就必须补 00
ASN.1 的 INTEGER 是带符号的二补码,最高位是符号位。所以 128 不能写成 02 01 80(那会被读成 -128),必须补一个前导零变成 02 02 00 80。实测边界值一路扫下来,规则完全符合 DER:
| 值 | 实测编码 | 说明 | | 0 | 020100 | 最短形式,一个 00 | | 127 | 02017f | 最高位 0,不补 | | 128 | 02020080 | 最高位 1,补前导 00 | | 255 | 020200ff | 同上 | | 256 | 02020100 | 两个字节刚好 | | 32767 | 02027fff | 边界 | | 32768 | 0203008000 | 补 00 变三字节 | | -1 | 0201ff | 二补码,全 1 | | 2147483647 | 02047fffffff | long 上限 |
longlong 同理,4294967296 编出来是 02 05 01 00 00 00 00,-4294967296 编出来是 02 05 ff 00 00 00 00。没有「无符号整数」的选项——如果你的协议字段是无符号 64 位,超过 2^63 之后它编不出来,这是它的天花板。
2.2 字符串:写进去的是 UTF-8,不是 GBK
中文两个字的实测是 3008 0c06 e4b8ade69687——e4b8ad 是「中」的 UTF-8,e69687 是「文」,一个汉字三字节。所以一条含 4 个汉字的字段,字符串值占 12 字节,不是 8 字节,也不是 4 字节。
这一点很重要:PB 内部字符串是 UTF-16,落盘到 GBK 库是 GBK,但 ASN.1 里永远是 UTF-8。如果对方是国密体系里的老系统,期待 PrintableString 或者 GBK 字节串,那 uo_asn1 给的 UTF8STRING 它可能不认——实测把 PrintableString(tag 13)喂回去,它也确实不认识(见 5.2)。
长度字段在 128 字节处换挡:200 个 A 的实测是 3081cb 0c81c8 4141...,长度用了 0x81 开头的一字节长式(0xcb=203,0xc8=200)。这是标准 DER 行为,不需要自己拼。
2.3 「空字符串」变成了 NULL —— 全篇最坑的一条
这条必须单独拎出来:AddString('') 编出来的不是「长度为 0 的 UTF8STRING」(那应该是 0c00),而是 NULL(05 00)。实测:
- AddString('') → 30020500,整个报文 4 字节,里面是 05 00
- 解回来时 typeName 是 any、asn1type 是 NULL,取到的 any 值是 IsNull 为 true
意味着两件事:
- 对方如果有「字段必须存在、可以为空串」的校验逻辑,NULL 和空串在 ASN.1 语义上是两种不同的东西,很可能被判为「字段缺失」;
- 你自己解回来往数据库写的时候,NULL 会一路传染——把它拼进字符串,整条结果变 NULL(我在探针里就吃了一次:'cnt=' + String(3) + '~r~n' + 拼了 NULL,结果整个返回值是空字符串,文件里只剩一个 PASS)。
如果协议要求「空串就是空串」,只能自己写 0c00,或者约定用占位符(比如一个空格)绕过去。
2.4 blob 与文件
AddBlob(blob('AB')) 的实测结果值得单独说一句:出来的 OCTET STRING 长度是 4,内容是 41004200。也就是说 blob('AB') 在这台机器的 PB 环境里给的是 UTF-16 的 4 个字节,不是 2 个字节。所以往 ASN.1 里塞二进制之前,先确认你手上的 blob 到底几个字节,否则对方解出来是一串带 00 的垃圾。
AddBlob(空 blob) 返回 false,而且这一段根本不会进包——不是加了个空 OCTET STRING,是彻底没有。这个「静默丢弃」很危险:如果你靠字段个数跟对方对账,少一个字段就会全部错位。
AddFile 是把文件内容当 OCTET STRING 加进去,文件名不会出现在报文里。实测:把一个 10 字节的 .der 文件加进去,出来 300c 040a <10 字节原样>。文件不存在时返回 0,同样静默跳过,不报错。
2.5 double 用的是 REAL,不是八个字节的 IEEE754
AddDouble(1.5) 出来的是 09 03 80 ff 03:tag 是 09(REAL),值只有 3 个字节,是 ASN.1 特有的二进制浮点写法,不是我们熟悉的 8 字节 double。如果你的对接方按 IEEE754 的八字节双精度来解析,那它对不上——这是协议层要先谈好的事,不是代码能修的。
而且注意,这三个字节的 REAL 在回读时是坏的,详见 5.4。
2.6 大整数:AddInteger 的返回值是假的
AddInteger 收的是 uo_biginteger。实测很有意思:
- 大整数先 FromHex('FF')(返回 true,说明值设进去了),然后 AddInteger 返回 false;
- 但我把结果编出来一看,是 3004020200ff——INTEGER 255,完全正确。
也就是说 AddInteger 的布尔返回值不可信,跟同门 uo_biginteger.FromStr 那种「永远返 false」的毛病一个路子。判断成功与否只能看编出来的字节,不能看返回值。
三、状态语义:一次 Encode 消费一次
uo_asn1 内部只有一份「待编码字段」清单,生命周期全靠 Encode 和 Clear 管。四条实测结论:
| 操作序列 | 实测结果 | 结论 | | Add(1) → Encode | 3003020101 | 正常 | | Add(1) → Encode → 再 Encode | 第二次得到 3000 | Encode 已经把待编码项消费干净,空了就出一个空 SEQUENCE | | Add(1) → Clear → Add(2) → Encode | 3003020102 | Clear 确实清掉了待编码项 | | Add(1) → Encode → Add('abc') → Encode | 第二次只有 abc(30050c03616263) | 不会重复输出前面的字段 | | 实例 A 编完 destroy,新建实例 B 编 | 只有 B 自己的字段 | 实例之间不串数据,可以放心复用变量名 |
两条实用推论:
- 每次编一条报文,就 create 一个新的 uo_asn1,编完 destroy,不要图省事复用一个实例——省下来的那点开销,抵不上一次「多塞了一个字段」的排查成本;
- 空的实例直接 Encode 出来是 3000(合法的空 SEQUENCE),这可以作为「字段没加进去」的探针:如果你期待 20 个字节,拿到 2 个字节,那就是所有 Add 都失败了。
顺便记一条测量坑,我自己踩的:想打印编码结果的十六进制,用 uo_blob 的时候 set(blob) 是追加语义,同一个 uo_blob 连续 set 两次,hex() 会把两段接在一起,看起来就像「编码器把上次的数据也带出来了」。我一开始就是这么误判的,直到换成每次新建 uo_blob 才发现每次 Encode 都是干净的。
四、解码侧:一刀切出三个数组
Decode 的设计是「你把容器给我,我把内容摊平给你」:
- 第一个 ref 参数 any datas[]:字段值,下标从 1 开始,个数等于返回值;
- 第二个 ref 参数 string typeName[]:这个值在 PB 侧被还原成什么类型(long / string / boolean / double / blob / any);
- 第三个 ref 参数 string asn1type[]:这个值在 ASN.1 侧是什么类型(INTEGER / UTF8STRING / BOOLEAN / REAL / OCTET STRING / NULL)。
三个数组的上界都等于返回值,一一对应。实测一条「long 20261106 + 中文 + true」的报文,解出来是:
- cnt=3
- #1 long/INTEGER = 20261106
- #2 string/UTF8STRING = 中文测试
- #3 boolean/BOOLEAN = true
复制代码
中文完整回来,没有乱码——这也是它「UTF-8 进出」的另一个侧证。
4.1 类型映射表(实测)
| 原字段 | typeName | asn1type | 取回来的值 | | AddLong(258) | long | INTEGER | String() 得 '258' | | AddString('abc') | string | UTF8STRING | 'abc' | | AddBoolean(true) | boolean | BOOLEAN | String() 得 'true' | | AddDouble(2.5) | double | REAL | 0(见 4.4) | | AddBlob | blob | OCTET STRING | String() 得 '?' | | AddString('') | any | NULL | IsNull 为 true | | 手工喂 PrintableString | blob | PRINTABLESTRING | String() 得 '?' |
最后一行说明它的解码是白名单式的:认识 INTEGER / UTF8STRING / BOOLEAN / REAL / OCTET STRING / NULL 这几种,其余 tag 一律当一段 blob 原样交出来,连 asn1type 名字都给了(PRINTABLESTRING),但值不给你转成字符串。所以真碰上 PrintableString、IA5String、BIT STRING,得自己按 blob 二次处理。
4.2 它对方位的假设很硬:必须有外层 SEQUENCE
四条容错实测(都是手工构造字节喂进去的):
| 喂进去的字节 | 返回值 | 说明 | | 020101(裸 INTEGER,没有外层 SEQUENCE) | 0 | 它只认 SEQUENCE 开头 | | 3003020101 3003020102(两条报文首尾相接) | 1 | 只解第一条,后面那条静默忽略 | | 30030201(被截断的) | 0 | 严格校验长度 | | 整个 blob 是空 | 0 | 不崩,返 0 | | 随便几个字节(比如 'ABC') | 0 | 不崩,返 0 |
所以解码失败的唯一信号就是返回值 0,没有任何错误码、没有异常、messagebox 也不会弹。写代码时把 0 当「失败」处理是必须的,千万别拿「不报错」当成功。
第二条尤其值得记:如果你的 socket 一次收到两帧拼在一起(粘包),Decode 会老老实实只解第一帧,第二帧的字节就这么丢了。粘包还得自己在字节层切开。
4.3 一个 any 数组里的坑
datas 的元素类型是 any,所以直接用 String 取数组元素时,不同类型的行为不一样:long、string、boolean 都能得到可读文本,blob 只能得到 '?'。想读 blob 字段,得先把它赋给 blob 变量:
- blob lb_field
- lb_field = lany[i]
复制代码
而 String(any) 这种「让 PB 自己决定」的写法,在 double 上会骗人(下一节)。
4.4 double 回读恒为 0 —— 写进去能出,读出来是废的
这是本篇最需要提前知道的一条。实测三个不同的值:
- AddDouble(2.5) → 编出来 09 03 ...(REAL 正常)→ 解回来 typeName 是 double、asn1type 是 REAL,但把第一个元素赋给 double 变量再 String(ld, '0.0000') 格式化,得到 0.0000;
- AddDouble(1.5)、AddDouble(0.5) 同样得 0;
- 更阴的是直接用 String 取数组元素,得到的是 '0',看起来像「值真的是 0」,而不是「没解出来」。
结论:uo_asn1 的 REAL 编码是能用的,REAL 解码是坏的。协议里有浮点字段时,要么让对端把金额之类的改成「整数分」或者字符串,要么自己按 09 tag 去解析那三个字节——不要指望它原样还给你。
五、crypto=true 与 SetKey
Encode 的第一个参数是 boolean crypto,false 就是明文 DER,true 则会把整条报文加密再包一层 OCTET STRING。实测形态:
| 项目 | 实测结果 | | 外层 tag | 04(OCTET STRING),不是 30 | | 明文 12 字节 → 密文长度 | 107 字节(多出约 95 字节固定开销) | | 明文 5 字节 → 密文长度 | 102 字节 | | 明文 103 字节 → 密文长度 | 201 字节 | | 同样的明文编两次 | 两次结果不同(有随机 IV 或随机填充) | | Decode(true) 能否还原 | 能,字段个数与值都对(long 7、string abc 都回来了) | | 密文用 Decode(false) 解 | 返回 0,不崩 | | 明文用 Decode(true) 解 | 整个进程崩溃(exit code 3765269347,即 0xE06D7363 C++ 异常),双版本各复现一次 | | SetKey 塞一对垃圾字符串 | 不报错、不返回任何东西,照样加密出 104 字节 |
最后两行是这轮实测里最刺眼的两条:
- cryptoed 标志写反会崩进程,不是返回 0。这意味着「用错参数」在这个 API 上是致命错误,线上必须靠配置项严格锁死,绝不能让调用方自由传。
- SetKey 对垃圾密钥完全沉默。它既不校验、也不报错、也不影响 Encode 的成功与否——你根本不知道这对密钥有没有生效。要验证,只能拿一段已知明文加密后,用对方的私钥去解,看能不能解出来。
另外注意长度:明文 12 字节加密后 107 字节,如果你的数据库字段按「报文长度 × 1.2」设计的,会直接截断。
六、三个可以直接抄的例子
下面三段是正文示例的完整形态,双轨(PB10 / PB12.5)实跑通过,输出即注释里写的值。
6.1 组装一条报文并打印 DER
演示说明:先 create 编码器,按顺序 Add 三个字段,Encode(false) 拿到明文 DER,再用 uo_blob 转成十六进制看字节。注意每次转十六进制都要新建 uo_blob(set 是追加语义)。
- long ll_order_id // 输入:订单号,long 范围内
- string ls_cust // 输入:客户名,内部自动转 UTF-8
- boolean lb_vip // 输入:是否 VIP
- uo_asn1 la
- uo_blob lhb
- blob lb_der
- string ls_hex
- ll_order_id = 20261106
- ls_cust = '中文测试'
- lb_vip = true
- la = create uo_asn1
- la.AddLong(ll_order_id)
- la.AddString(ls_cust)
- la.AddBoolean(lb_vip)
- lb_der = la.Encode(false)
- destroy la
- lhb = create uo_blob
- lhb.set(lb_der)
- ls_hex = lhb.hex()
- destroy lhb
- MessageBox('DER 长度 ' + String(Len(lb_der)), ls_hex)
复制代码
实跑输出:长度 25,hex = 30170204013528f20c0ce4b8ade69687e6b58be8af950101ff。手工核一遍对得上:30 17(SEQUENCE 23 字节)、02 04 013528f2(INTEGER 20261106)、0c 0c(UTF8STRING 12 字节)后面是 4 个汉字的 UTF-8(e4b8ad e69687 e6b58b e8af95)、01 01 ff(BOOLEAN true)。字节级和 Python 侧手算的 DER 完全一致,这条是我专门用来对拍的基准。
6.2 把报文解回三个字段
演示说明:把上一步的十六进制转成 blob(辅助函数见附录),Decode 取出三个平行数组,按 typeName 逐项读。
- string ls_in // 输入:上一步产出的报文字节序
- blob lb_der
- uo_asn1 la
- any lany[] // 输出:字段值
- string lsn[] // 输出:PB 侧类型名
- string lsa[] // 输出:ASN.1 类型名
- long ll_cnt
- long i
- string ls_report
- ls_in = '30170204013528f20c0ce4b8ade69687e6b58be8af950101ff'
- lb_der = of_hex2blob(ls_in)
- la = create uo_asn1
- ll_cnt = la.Decode(false, lb_der, lany, lsn, lsa)
- la.Clear()
- destroy la
- ls_report = ''
- for i = 1 to ll_cnt
- ls_report += '#' + String(i) + ' ' + lsn[i] + '/' + lsa[i] + ' = [' + String(lany[i]) + ']~r~n'
- next
- MessageBox('共解出 ' + String(ll_cnt) + ' 个字段', ls_report)
复制代码
实跑输出:共 3 个字段;#1 是 long/INTEGER 值 20261106;#2 是 string/UTF8STRING 值「中文测试」;#3 是 boolean/BOOLEAN 值 true。
6.3 直接落盘、直接从文件解
演示说明:Encode 的第二个重载可以直接写文件,Decode 也接受文件名,省一次 blob 拷贝。适合「报文留档 + 事后复算」的场景。
- string ls_file // 输入:报文留档路径
- uo_asn1 la
- blob lb_der
- any lany[]
- long ll_cnt
- string ls_out
- ls_file = 'd:\temp\order.der'
- la = create uo_asn1
- la.AddLong(1001)
- la.AddString('PAY')
- la.AddBoolean(true)
- lb_der = la.Encode(false, ls_file)
- la.Clear()
- ll_cnt = la.Decode(false, ls_file, lany)
- destroy la
- ls_out = '文件字节 ' + String(FileLength(ls_file))
- ls_out += ' / 内存字节 ' + String(Len(lb_der))
- ls_out += ' / 字段数 ' + String(ll_cnt)
- ls_out += ' / #1=' + String(lany[1]) + ' #2=' + String(lany[2])
- MessageBox('落盘与回读', ls_out)
复制代码
实跑输出:文件 14 字节 = 内存 14 字节,字段数 3,前两个字段分别是 1001 和 PAY。落盘内容与返回值逐字节一致,不会多一个换行、也不会少一个 BOM。
七、坑清单(16 条,按踩到的概率排序)
| 序号 | 坑 | 后果 | 对策 | | 1 | 以为编码结果没有外层 SEQUENCE | 手工拼报文时少 2 字节 | 永远记得最外层是 30 | | 2 | AddString('') 编成 NULL 而不是空 UTF8STRING | 对端判「字段缺失」 | 空串要么自己写 0c00,要么用占位符 | | 3 | AddBlob(空 blob) 返回 false 且静默不入包 | 字段整体错位 | 加之前先判 Len(blob) | | 4 | AddInteger 返回值恒 false | 误判为失败、重复处理 | 只认编出来的字节 | | 5 | 用 Decode(true) 解明文 | 进程崩溃(0xE06D7363) | cryptoed 标志走配置项、严格锁死 | | 6 | 以为 Decode 会报错 | 失败时只返回 0 | 把 0 当失败处理 | | 7 | 粘包两条报文一起喂 | 只解第一条,其余静默丢弃 | 字节层先切帧 | | 8 | 拿 double 字段当金额 | 解回来恒 0 | 金额用整数分或字符串 | | 9 | 以为中文是 GBK | 多 8 个字节、对端乱码 | 一律 UTF-8,一个汉字 3 字节 | | 10 | 正数 128 直接写 02 01 80 | 被读成 -128 | DER 规则,高位为 1 要补 00 | | 11 | 期待无符号 64 位整数 | 超过 2^63 编不出来 | 改用 blob 或字符串 | | 12 | 反复用同一个 uo_blob 打 hex | hex 越看越长,误判「编码器累积」 | 每次新建 uo_blob | | 13 | 复用一个 uo_asn1 编多条报文 | 字段串联 | 一条报文一个实例 | | 14 | 期待嵌套结构 | 内层 DER 只被当成 OCTET STRING | 嵌套自己拆,或整段当 blob 传 | | 15 | 以为 SetKey 会校验密钥 | 垃圾密钥也照样加密 | 用已知明文 + 对方私钥实测 | | 16 | 按明文长度设计数据库字段 | crypto 模式约多 95 字节被截断 | 字段按密文长度留 |
八、和相近方案比一比
| 方案 | 适合什么 | 不适合什么 | | uo_asn1 | 国密/CA/医保那类「TLV 顺序字段」报文 | 嵌套结构、需要控制 tag 的场景 | | uo_json | 前后端 HTTP 接口、结构灵活 | 对方只认 ASN.1 的老系统 | | uo_bits | 定长位域报文(通信协议) | 变长字段、字符串 | | uo_crypto / uo_rsa | 签名验签(PKCS#7、SM2 with SM3) | 只负责编码不负责签名 | | 手写 DER 拼接 | 需要精确控制 tag 和长度的场合 | 字段多了极难维护 |
一句话选型:只要对方文档写的是 ASN.1 DER,先用 uo_asn1 把能跑通的部分跑通,剩下的特殊 tag(PrintableString、BIT STRING、嵌套 SEQUENCE)再用 blob 拼接补齐——实测它把不认识的类型原样以 blob 交出来,这个特性正好可以当逃生门用。
九、PB12.5 差异与版本快照
9.1 版本快照(2026-11-06 核验)
| 项目 | 实测值 | | 版本源目录 | 维护方 PBIDEA 最新构建(10 = PB10 轨,125 = PB12.5 轨) | | 运行库字节数与 md5 | 18,291,200 字节 / 740f9f474d841e3fa4d06d532ecd8994 | | PE 导出函数数 | 2608(解析 PE 导出表得出,不看目录名、不看体积) | | 源码导出目录 | export_20261010(GBK、无 BOM、575 文件) | | 核验日期 | 2026-11-06 |
PBIDEA 的版本会持续更新,运行库字节数与 md5 每次补丁都可能变,而接口行为以当时的构建为准。本文结论对应上面这个快照;如果你手上的 PbIdea.dll 不是这个字节数,建议先按第十节第 1 条做一次最小自检再上生产。
9.2 两版行为对照
与 PB10 行为完全一致。 47 组探针(含编码字节、解码返回值、类型名,以及 2 组故意触发的崩溃用例)在 PB10 与 PB12.5 上逐条比对,除了 crypto 模式的随机密文以外,读数逐字节相同;两个版本的崩溃用例退出码也一致。
编译层面两侧同样是 0 错误:PB10 轨(--pb-version 100)全量 rebuild 0 错、对象齐全;PB12.5 轨(--pb-version 125)同样 0 错,pbtest 全 PASS。
十、怎么自己复验
- 取一段最小报文:la.AddLong(1) 之后 Encode(false),打印出 3003020101,能对上就说明链路通了(实测版打包时是把中文改用 Char() 码点构造的——直接把中文写进 .sru 在一些工具链上会被静默转错编码,读数全错却不报错,这一点对老 IDE 同样成立);
- 把 30020500(NULL)和 0c00(空 UTF8STRING)分别喂给 Decode,看 typeName 的差异——这是本文第 2.3 节那条坑的最小复现;
- 用明文 DER 调一次 Decode(true, ...),在单独的测试进程里做(会崩,别在生产进程里试),确认退出码,你就知道为什么这个标志必须锁死;
- 拿一条真实报文,把 Decode 的三个数组打出来,和对方文档的字段顺序逐项对,顺序、类型、字节数三样都对上,才算真通。
附录:十六进制转 blob 的辅助函数
- // 演示:把十六进制字符串还原成 blob,调试与对拍常用
- // 输入:as_hex,形如 '3003020101',不区分大小写
- public function blob of_hex2blob (string as_hex);
- uo_blob lhb
- blob lb_out
- lhb = create uo_blob
- lhb.FromHex(as_hex)
- lhb.get(1, lb_out, lhb.size())
- destroy lhb
- return lb_out
- end function
复制代码
这个函数双轨实跑过:喂 020101 得到 3 字节 blob,喂 30 04 13 02 41 42 得到 6 字节 blob,都能原样参与解码。但它对非法输入没有任何校验:喂 ZZ 实测得到 1 个 0xFF 字节、连长度都不可信;奇数位同样是静默处理。所以进这个函数之前,先用正则确认长度是偶数、字符只含 0-9A-Fa-f。
|