资讯详情

资讯详情

进制转换与内存存储:从二/八/十/十六进制到补码字节序

1. 四种进制不是四个知识点而是同一件事的四种写法刚接触编程那阵子我最怕看到的就是一行全是0x开头的地址、一串0101的位掩码再加上文档里随口一句偏移 16 个字节。三个数字混在一份文档里我得来回换算算错一次就要重新调半天。后来才想明白进制转换这件事的难点根本不在会不会算而在于大多数人把它当成了四个孤立的知识点去背而没有意识到它们只是同一堆数据换了四套衣服。二进制、八进制、十进制、十六进制讲的是同一件事用有限的符号配合逢几进一的规则把一个数写出来。十进制用十个符号0-9逢十进一二进制只有两个符号0、1逢二进一八进制八个符号十六进制十六个符号0-9 加 A-F。同一个人穿上不同的衣服体重不会变同一个数写成不同的进制值也不会变。这篇文章要解决的就是三件事第一四种进制之间两两互转怎么算、怎么验第二为什么计算机圈子偏偏爱用二、八、十六这三种别扭的进制第三算出来的数字到了内存里到底是以什么形态躺着的。前两件事偏手工活第三件事偏概念但三者是连在一起的——你不理解进制的分组规律就看不懂内存里字节的排列你不理解内存的存储方式前面算的那些转换就只是纸上谈兵。适合谁看如果你刚学编程看到0xFF、0b1010这种写法会愣一下如果你写过嵌入式、网络协议、文件格式解析但每次都要打开计算器如果你能算对转换却说不清为什么负数在内存里长得那么奇怪——那这篇基本就是给你写的。下面所有内容都按能直接照着算、照着验的标准来组织不用你死记硬背。1.1 进制的本质底数和位权撑起了整个体系任何一个进制系统只有两个核心参数底数基数和位权。底数就是逢几进一里的那个几十进制底数是 10二进制底数是 2。位权则决定了每一位上的数字值多少钱规则是底数的位序号次方位序号从右往左、从 0 开始数。拿十进制数352举例。最右边的2在第 0 位位权是 10⁰ 1所以它贡献 2 × 1 2中间的5在第 1 位位权是 10¹ 10贡献 5 × 10 50最左边的3在第 2 位位权是 10² 100贡献 3 × 100 300。三者相加 300 50 2 352这就是所谓的按权展开。同样的规则套到二进制上1101的值就是1×2³ 1×2² 0×2¹ 1×2⁰ 8 4 0 1 13。你会发现二进制看起来很长但它每一位的含金量是固定的从右往左依次是 1、2、4、8、16、32、64、128、256…… 这串数字我建议直接背下来它是后面所有心算的基础。提示位权不是从 1 开始往上乘而是从 1 开始每往左一位乘一次底数。二进制最右边的位权永远是 1不是 2。这个起点搞错后面全错。理解了位权很多看似奇怪的现象就顺了。比如为什么十六进制的一位正好能装下二进制的四位因为 2⁴ 16。八进制一位装三位因为 2³ 8。这不是巧合而是设计出来的——八进制和十六进制之所以存在本质原因就是它们是二进制的压缩写法。这一点在第三节还会展开。1.2 为什么计算机圈偏爱 2、8、16 这三种进制计算机内部用的是二进制这个大家都听过但真正的原因值得说清楚硬件层面能稳定区分的状态只有两种——有电/没电、高电平/低电平、磁化方向正/反。想做出能稳定分辨十种电压等级的电路成本高、抗干扰差、出错率高。所以从物理层开始一切就都被压到了两个状态上。二进制的1 和 0不是数学上的优雅选择而是物理上的无奈选择。但二进制写起来太要命了。一个 32 位的整数写成二进制是 32 个字符眼睛根本扫不过来抄错一位就全错。于是就有了八进制和十六进制这两种人类友好版——它们不改变数据只改变书写长度。十六进制把 4 位二进制压成 1 位32 位整数就变成了 8 个字符长度缩短到四分之一而且每一位之间的对应关系是固定的一一映射肉眼就能双向翻译。八进制在早期计算机上用得更多有些老架构的字长是 3 的倍数现在主要出现在两个地方一是 Linux 文件权限比如chmod 755里的每一位就是三位二进制7 1115 101二是某些语言里八进制字面量还保留着但用得少了。十六进制则彻底成了通用语。内存地址、颜色值、哈希值、协议字段、字符编码、机器码几乎全是十六进制。原因很简单它是二进制最紧凑、最不容易出错的可读形式而且一个字节恰好对应两位十六进制字符数和字节数的换算不需要动脑。1.3 前缀标记法是防止自己看错的第一道防线写代码时最容易犯的低级错误是把一个十六进制数当十进制用。所以各语言都保留了一套前缀约定这套约定值得记住前缀进制示例对应的十进制值0b二进制0b1101130o或0八进制0o17/01715无前缀十进制13130x十六进制0x1F31要注意的是0b和0o是相对现代的写法C 语言早期并不支持0b现在 C23 才开始标准化但一直支持用前导0表示八进制。这就埋了一个经典的坑int x 010;在 C 里不是 10而是 8。我见过不止一次有人把编号010当十进制写进代码结果数组下标越界。所以在任何语言里除非你明确要写八进制否则绝不要用前导零。注意Python 3 里010是语法错误这是好事直接拦住但0o10是合法的 8。JavaScript 在严格模式下010也报错非严格模式下行为还跟八进制有关历史包袱很重。跨语言迁移代码时这类字面量必须逐个核对。还有个细节十六进制里的字母A-F大小写都合法0xFF和0xff完全等价。但风格要统一。我在团队里推的规则是代码里的数值常量用大写0xFF日志和调试输出用小写0xff这样一眼就能区分这是我写的还是这是打印出来的。看着是小节排查问题时节省的时间很实在。2. 十进制这条主干道和任意进制互转的通用套路如果你只想记住一套方法那就记十进制——它是最好的中转站。任意进制 A 转任意进制 B最稳的路径是 A → 十进制 → B。这条路永远有效不需要记特殊规则。等你熟练了再学第三节的分组捷径那是加速不是替代。这里有个容易混淆的认知很多人以为十进制转二进制和二进制转十进制是两个不同的知识点。其实它们是一对互逆运算就像乘法和除法。理解了其中一个另一个是它的反向验证手段。所以我在实操中的习惯是算完立刻反向验一次成本很低但能拦住 90% 的手误。2.1 十进制转其他进制除基取余的完整推演方法叫除基取余逆序排列。以十进制 200 转二进制为例底数是 2就一直除以 2记下每次的余数直到商为 0然后把余数从下往上读步骤被除数÷ 2 的商余数12001000210050035025042512151260663073118101逆序读余数11001000。验算一下128 64 8 200对上了。为什么是逆序因为第一次除出来的余数是数值的最低位——它反映的是这个数对 2 取模的结果而最末位恰好就是模 2 的值。后面每除一次相当于把整体右移一位得到的余数依次对应更高位。所以最后得到的自然是最低位在前、最高位在后读的时候必须反过来。转八进制就把除数换成 8转十六进制换成 16。200 转十六进制200 ÷ 16 12 余 812 ÷ 16 0 余 12也就是 C逆序读得C8即0xC8。转八进制200 ÷ 8 25 余 025 ÷ 8 3 余 13 ÷ 8 0 余 3逆序读得310即0o310。提示除基取余时最后一步的商必须是 0 才能停。如果商还剩 1说明你少算了一位。这是手算最常见的漏步尤其是十进制转十六进制因为除得快容易提前收工。对于小数字还有个更快的心算办法从最大的位权往下减。比如 200 转二进制找不超过 200 的最大 2 的幂是 1282⁷减掉剩 72下一个是 642⁶减掉剩 8跳过 32、1682³刚好减完。所以第 7、6、3 位是 1其余是 0得到11001000。这个方法数字越大越好用熟练之后比除法快得多。2.2 其他进制转十进制按权展开与心算技巧这个方向更简单就是前面说的按权展开求和。十六进制0xC8转十进制12 × 16¹ 8 × 16⁰ 192 8 200。八进制3103 × 8² 1 × 8¹ 0 × 8⁰ 192 8 0 200。心算上有两个提速技巧值得一提。技巧一从高位往低位滚雪球。不要一位一位算幂次而是前一位的结果乘底数再加上当前位。0xC8就是先取 1212 × 16 192加 8 得 200。0x1A2B就是1 → 1×161026 → 26×162418 → 418×16116699。这样全程只做乘加不用碰幂运算心算轻松很多而且不容易把位权指数算错。技巧二十六进制转十进制用分块拆。0x12345678这种大数拆成0x1234和0x5678分别算出十进制再乘以 65536 加和。虽然还是要算但每一步都是四位数以内心算可控。注意转换方向搞反是新手最大的坑。十六进制转十进制和十进制转十六进制只有一字之差但方法完全不同。我的习惯是看到转十进制先问自己终点是不是十进制——是那就按权展开不是那就除基取余。把方向在心里念一遍再动笔。2.3 小数部分的转换乘基取整与它的精度陷阱整数部分讲完了小数部分是完全另一套规则很多人在这里翻车。方法叫乘基取整顺序排列把小数部分乘以底数取出整数部分作为当前位剩下的小数部分继续乘直到小数部分为 0 或达到精度要求这一次是正序读。十进制 0.625 转二进制0.625 × 2 1.25取整 1剩 0.250.25 × 2 0.5取整 0剩 0.50.5 × 2 1.0取整 1剩 0结束。正序读得.101。验算1/2 0/4 1/8 0.5 0.125 0.625对上了。真正的坑在这里不是所有十进制小数都能用有限位二进制表示。十进制 0.1 转二进制会得到0.00011001100110011...无限循环。因为二进制只能表示分母为 2 的幂的分数而 0.1 1/10分母含质因子 5永远不会收敛。这就是0.1 0.2 ! 0.3的根源。十进制小数二进制表示能否有限位精确表示0.50.1能0.250.01能0.6250.101能0.10.000110011...不能无限循环0.20.001100110...不能无限循环注意这条性质直接决定了一条工程铁律——涉及金额、精确计数、ID 这类不能有误差的场景绝不要用浮点数。金额用整数存最小单位分或者用定点数、十进制库。我在实际项目中见过因为浮点累加导致对账差几分钱的案例排查了三四个小时最后定位到就是这一行小数表示误差。顺带说一句十六进制的小数在工程里几乎不用八进制更不用说。如果真要表示带小数的值用定点数把数值整体乘以 10 的幂然后按整数存比用十六进制小数靠谱得多。3. 二、八、十六进制之间的抄近道分组对齐法十进制是主干道但如果你每次都在十进制上绕一圈那效率就太低了。二进制、八进制、十六进制三者之间互转完全可以跳过十进制因为它们的底数都是 2 的幂8 2³16 2⁴。这意味着它们在结构上是嵌套的转换就是纯粹的分组重排不涉及任何计算。这个性质是这套体系里最实用的一点也是理解内存存储的关键。我先说规则再说为什么。3.1 三位一组与四位一组的分组规则二进制 → 八进制从右往左三位一组每组独立转成 0-7。11001000从右切000、001、11左边不足三位补 0 变成011得到011 001 000即3 1 0写作310。二进制 → 十六进制从右往左四位一组每组独立转成 0-F。11001000从右切1000、1100得到C、8写作0xC8。反向转换正好相反一位八进制拆成三位二进制一位十六进制拆成四位二进制。0xC8→C拆成11008拆成1000合起来11001000。这里有个硬性要求拆出来的每组必须补齐到固定位数前面用 0 填充。8拆成1000而不是1000……等一下8拆四位就是1000没问题但2拆四位必须是0010不能写成10。这个补零动作是刚性的丢掉前导零值就变了。常用的对应表建议直接做成肌肉记忆十六进制二进制十六进制二进制0000081000100019100120010A101030011B101140100C110050101D110160110E111070111F1111八进制的那张表就是这张表的前半部分0到7对应三位二进制不需要单独记。3.2 补零方向搞反是最常见的翻车点分组法本身不难难点全在方向。我把最容易错的两个地方列出来第一个分组永远从右往左补零永远在左边。因为最低位才是尾它在转换中位置不变。如果你从左往右分组就会把整数的位权对错。比如1010011十进制 83转十六进制从右切是0101 0011得0x53如果从左切就成了1010 011→ 补成1010 0110得0xA6 166完全错了。第二个多字节数据里每个字节单独处理不要跨字节分组。这点在处理内存数据时极其重要。比如一个 32 位整数0x12345678拆成字节是12 34 56 78每个字节对应两位十六进制。如果你把整个数写成二进制再四位一组结果是完全一样的但一旦涉及字节序问题就必须以字节为单位思考否则大小端一混就彻底乱套。提示手算时养成一个动作——先写出完整二进制串用铅笔从最右边开始每四位画一道竖线。别凭眼力数尤其是一长串 0 和 1 中间夹杂的时候眼睛极容易错位。我在纸上算的时候宁可多画两道线也不愿意回头重算。3.3 十六进制为什么成了内存世界的通用语理解了分组规律就能明白为什么内存地址、指针值几乎清一色用十六进制显示。一个字节 8 位 两位十六进制这个换算关系是精确的、无余数的。也就是说你在调试器里看到的每一个十六进制字节都精确对应内存里的一格不多不少。如果换成十进制呢255 这个数用十进制是三位字符用十六进制是两位看起来只是省一个字符。但关键在于对齐一屏内存数据如果是0x1A 0x2B 0x3C每一列都是两位纵向扫一眼就能看出规律换成十进制26 43 60位数一变就错位了。再加上二进制和十六进制之间的转换是纯查表、零计算而二进制和十进制之间必须做算术这在快速排查时差别极大。还有一个原因十六进制的每一位恰好对应二进制的四位而四位的组合数是 16正好能用 0-9 加 A-F 这 16 个单字符表示。如果当年选了别的进制比如用二十进制符号就不够用了。这是个设计上的刚刚好。实际工作中我几乎不用二进制直接看数据。除了两种场景一是看位掩码/标志位哪一位被置 1二是看硬件寄存器的位定义。其余全是十六进制。你可以把十六进制当作二进制的可读形式二进制当作十六进制的展开形式两者是一体两面。4. 数字进入内存之后一个整数在内存条里的真实形态前面全是在纸面上算现在换个视角——这些数字落到内存里是以什么形态存在的。很多人学完进制转换脑子里还是数字就是数字一旦接触到指针、内存越界、序列化协议立刻懵掉。原因就是没把数值和存储表示这两件事分开。这一节的核心认知只有一句内存里没有数只有字节数是你和编译器约定好的一种解释方式。同样的 4 个字节按有符号整数解释是一个值按无符号解释是另一个值按浮点解释又是第三个值按四个字符解释还能是一串文本。改变的不是数据是解读规则。4.1 位、字节、字长三层概念不要混着用这三个词经常被随口混用但精度完全不同概念含义常见换算位bit最小单位只能是 0 或 1—字节byte8 个位内存编址的基本单位1 B 8 bit字长wordCPU 一次能处理的位数32 位机 4 B64 位机 8 B关键点在于内存的每一个字节都有一个独立地址。不是每一位也不是每一个变量而是每一个字节。所以一个 4 字节的int实际占用的是 4 个连续的地址。这一点直接决定了后面所有关于字节序的讨论。顺带把常见的存储单位换算也理一遍因为这里的1000 还是 1024经常引起歧义操作系统和内存厂商口径不完全一致。内存条标称的 GB 通常按 1024³ 进位1 GiB硬盘标称容量常用 1000³ 进位1 GB。所以买回来的硬盘容量缩水不是被坑了是两套口径。这个和进制本身无关但同属数字表示的认知范围值得一起弄清楚。再说一个尺寸上的直观感受32 位系统的地址是 32 位能表示 2³² 个不同的地址也就是 4 GiB 的寻址空间。这就是为什么老系统装 8G 内存只能认到 3 点几 G——不是系统不认是地址位宽不够编不出那么多门牌号。64 位系统地址空间是 2⁶⁴这个数字大到目前完全用不完所以实际实现里通常只用 48 位左右。4.2 用代码把一个整数拆开看理论说再多不如亲手拆一次。下面这段 C 代码把一个 32 位整数按字节打印出来#include stdio.h int main(void) { int v 0x12345678; unsigned char *p (unsigned char *)v; for (int i 0; i sizeof(v); i) { printf(byte[%d] 0x%02x\n, i, p[i]); } return 0; }%02x里的02是宽度补零意思是至少两位不够左边补 0。这个细节很重要如果不写02值为0x05的字节会打印成5看起来就像只有一位对齐全乱。我在日志里排查字节数据时格式串一律写%02x。在常见的 x86 平台上这段代码的输出是byte[0] 0x78 byte[1] 0x56 byte[2] 0x34 byte[3] 0x12注意顺序是反的。我们写的0x12345678最高位字节0x12反而排在最后一个地址上。这不是 bug这是小端序第 5 节会细讲。Python 里做同样的事更省事用struct模块import struct v 0x12345678 print(struct.pack(I, v).hex()) # 78563412小端 print(struct.pack(I, v).hex()) # 12345678大端 print(struct.pack(I, v)[0]) # 120也就是 0x78表示小端表示大端I表示无符号 32 位整数。这套格式串在解析二进制协议、文件头、网络包时几乎是必备技能比手写位运算靠谱得多。Python 内置的进制转换函数也顺手记一下都是返回带前缀的字符串print(bin(200)) # 0b11001000 print(oct(200)) # 0o310 print(hex(200)) # 0xc8 print(int(11001000, 2)) # 200 print(int(310, 8)) # 200 print(int(c8, 16)) # 200 print(int(0xc8, 0)) # 200base0 时自动识别前缀最后一个base0的用法很实用解析配置文件里的数值时用户可能写0xFF也可能写255用int(s, 0)一次搞定不用自己判断前缀。4.3 内存地址与每字节一个门牌号把内存想成一排邮箱每个邮箱容量固定是 1 字节每个邮箱有自己的编号——这个编号就是内存地址。地址本身也是个数字只是它通常用十六进制显示因为和字节的对应关系最直观。这里有个容易绕晕的点地址的单位是字节不是位。所以指针算术p 1的语义要看p的类型char*加 1 前进 1 字节int*加 1 前进 4 字节在 32 位int的平台上。很多越界问题就出在这里——以为加 1 是加一个元素实际加的是一个元素的大小。int arr[4] {1, 2, 3, 4}; int *p arr; printf(%p\n, (void*)p); // 比如 0x7ffd1234a000 printf(%p\n, (void*)(p1)); // 0x7ffd1234a004前进了 4 字节再补一个概念栈区、堆区、静态区这些内存分区本质上就是不同的地址范围。栈通常在高地址往下长堆在低地址往上长全局变量在中间一段固定的区域。这些分区本身不影响数值的表示方式但影响生命周期和访问速度。真正需要关注一个字节长什么样的场景是序列化、协议解析、跨语言传输、内存调试这几类。提示打印指针时一定要转成(void*)再传给%p否则在某些编译器上会报警告甚至打印出错误的值。这算是 C 语言里一个历史遗留的小坑但踩上去排查起来很浪费时间。5. 补码、溢出与字节序真正会咬人的三个细节前面四节基本都是能算对就行这一节是真正会出问题的地方。补码、溢出、字节序这三个概念共同点是它们不会报错只会给你一个看起来正常但错误的数。这类问题最难查因为程序跑得好好的只是结果不对。我个人的经验是这三个点必须一次性吃透而不是遇到再查。因为一旦线上出问题现场压力大靠临时翻资料基本来不及。5.1 补码不是玄学是让减法消失的设计先说结论正整数在内存里就是它的二进制原样负整数用的是补码规则是取反加一。以 8 位为例-5 的表示先写 5 的二进制0000 0101按位取反1111 1010加 11111 1011即0xFB验证一下0xFB当作无符号数是 251而 251 5 256 2⁸正好溢出归零。这就是补码的精髓——负数 -n 被表示为 2⁸ - n加上 n 之后自然变成 0模 2⁸ 意义下。为什么不用更直观的符号位 绝对值有两个致命问题。第一会出现两个零0000 0000和1000 0000都表示 0硬件要额外判断浪费一种编码。第二减法没法直接用加法电路实现得单独做一套减法逻辑。而补码把减法变成了加一个负数加减法共用一套电路还能让零的表示唯一。这是个用一点理解成本换巨大硬件简化的经典设计。补码带来的一个重要结果n 位有符号整数的范围是 -2^(n-1) 到 2^(n-1) - 1不对称。8 位是 -128 到 12716 位是 -32768 到 3276732 位是 -2147483648 到 2147483647。负数比正数多一个因为1000 0000这个编码没有被负零占用被分配给了 -128。类型位数有符号范围无符号范围int88-128 ~ 1270 ~ 255int1616-32768 ~ 327670 ~ 65535int3232-2147483648 ~ 21474836470 ~ 4294967295int6464-2⁶³ ~ 2⁶³-10 ~ 2⁶⁴-1有一类坑特别隐蔽有符号和无符号混用比较。在 C 里如果拿一个有符号的-1和一个无符号的1比较编译器会把-1转成无符号变成 4294967295于是-1 1成立。这类问题在循环边界判断里出现得非常多而且不报警告。5.2 溢出边界的实测算术溢出不是数字变得特别大而是绕回去了。8 位有符号数 127 加 1结果是 -128不是 128。因为0111 1111加 1 变成1000 0000而补码里1000 0000就是 -128。#include stdio.h #include limits.h int main(void) { int a INT_MAX; printf(%d\n, a); // 2147483647 printf(%d\n, a 1); // -2147483648绕回去了 return 0; }无符号数的溢出是取模8 位无符号 255 加 1 变成 0因为 256 mod 256 0。这个行为在 C 标准里是明确定义的而有符号溢出在标准里属于未定义行为编译器可以做任意优化。这意味着同一段溢出代码开-O2和不开-O2可能给出不同结果。我在实际项目中遇到过因为溢出导致的死循环在调试版下跑得好好的发布版直接卡死查了很久才发现是编译器把溢出后的判断优化掉了。注意不要依赖有符号整数溢出的行为。真要处理边界就在运算前判断或者用更大的类型承接。写解析逻辑时对来自外部的长度、索引、计数值一律先做上界校验再运算。还有个容易忽略的位运算和进制的关系。x 1等价于乘 2x 1等价于除 2对非负数。这个等价关系只对无符号数或非负有符号数成立。负数右移的行为在不同语言里不一样Python 是算术右移保持符号有些语言是逻辑右移高位补 0。跨语言移植时这是个暗坑。5.3 大端与小端同一个数在两种机器上的两种排列回到 4.2 节那个例子0x12345678在内存里排成78 56 34 12。这就是小端序little-endian低位字节放在低地址。反过来大端序big-endian高位字节放在低地址排成12 34 56 78。字节序低地址 → 高地址典型场景大端12 34 56 78网络字节序、部分 RISC 架构、Java 序列化小端78 56 34 12x86、ARM多数默认、Windows/Linux 本地内存为什么会存在两种纯粹是历史选择没有技术上的优劣。大端的好处是人读起来顺和书写顺序一致小端的好处是类型转换方便——把 32 位当 16 位读的时候低 16 位正好在低地址不用偏移。两者都活得很好于是就有了网络字节序这个标准网络传输统一用大端本地存储则随平台。这带来的实际麻烦是同样的字节序列在不同平台上解读出来的数不一样。做协议解析、文件格式读写、跨平台数据交换时必须显式指定字节序。import struct # 写入时指定小端 data struct.pack(I, 0x12345678) print(data.hex()) # 78563412 # 读取时也指定小端才能还原 val struct.unpack(I, data)[0] print(hex(val)) # 0x12345678提示判断本机字节序有个简单办法把一个 16 位整数0x0001存进去看第一个字节是1还是0。是 1 就是小端是 0 就是大端。这个方法我在写跨平台代码时经常用来自检比查文档快。实际踩过的坑解析某设备的二进制报文字段定义里写的是4 字节长度字段。我按本机小端读出来是 0x02000000三千万多实际应该是 0x000000022。整整差了千万倍程序直接分配了几十 G 内存被系统杀掉。后来在协议文档角落找到一句all multi-byte fields are big-endian改一个符号就通了。从那以后我看到任何二进制协议的第一件事就是找字节序声明。6. 我在实际使用中踩过的坑和一套自查流程前面五节讲的是应该怎么算这一节讲实际会怎么错。这两者的差距比想象中大得多——纸面上算错你一眼能看出来但代码里的进制错误往往表现为数据不对偶尔出错在某个机器上才对排查成本高一个量级。我把自己和团队踩过的坑整理成了清单再加上一套手算校验流程基本能覆盖日常遇到的绝大多数情况。6.1 几个高频错误清单错误一前导零被当成八进制。前面提过这里再强调一次。int id 010;在 C 里是 8。JSON 不支持前导零但配置文件里的数字如果被某些解析器读进来也可能出问题。规则很简单数值常量不写前导零。错误二hex()返回的字符串带前缀直接参与拼接会出错。Python 里hex(255)得到0xff如果你要拼一个定长字段前缀会占位置。要用格式化字符串f{255:02x}得到ff。错误三二进制手抄错位。长串 0 和 1 里错一位值就全变。对策是分组写、分组读每四位空一格1100 1000比11001000好核对得多。错误四把字节数当位数。这个字段 8 位和这个字段 8 字节差了 8 倍。协议文档里 bit 和 byte 混写的情况非常多看到就必须确认。错误五负数转十六进制忘了补码。想把 -1 写成十六进制直接写0x-1是不合法的。8 位下应该是0xFF32 位下应该是0xFFFFFFFF。这个宽度是跟着类型走的不是固定的。错误六有符号/无符号类型混用。前面说过比较和运算都会被静默转换。对策是在跨类型比较前显式转换或者干脆全程用无符号处理字节数据。错误七浮点数参与进制转换。int(0.1 * 100)在某些语言里会得到 9 而不是 10。涉及小数转整数一律先明确舍入规则。6.2 手算校验的三步自检法我自己算完转换后会走一遍这三步全程不超过二十秒但能拦住绝大多数错误第一步看范围。算出来的数对不对先看它在不在合理区间。一个 8 位无符号数不可能超过 255如果算出 300那肯定错了。一位十六进制字符不可能表示超过 15 的值。第二步反向验算。把结果转回去看能不能得到原数。这是最有效的一步成本极低。0xC8算出来是 200那就用 200 除 16 再验一次。第三步看位数和分组。二进制串的长度、十六进制串的长度是否和类型宽度匹配。一个uint32用十六进制写不足 8 位就该警觉——可能是漏了高位也可能是高位本来就该补零。这套流程听着啰嗦但真正养成习惯之后是几秒钟的事。我在写序列化代码时每构造一个字段都会顺手想一下这个长度对不对比事后调试省太多时间。6.3 工具与代码速查手算能力要有但日常干活不能全靠手算。下面这几个是长期在用的# 带前缀适合调试输出 print(bin(200), oct(200), hex(200)) # 定宽无前缀适合拼接报文 print(f{200:08b}, f{200:o}, f{200:02x}) # 11001000 310 c8 # 任意进制字符串转整数自动识别前缀 print(int(0b11001000, 0), int(0o310, 0), int(0xc8, 0)) # 按字节拆解 import struct v 200 print(struct.pack(I, v).hex()) # c8000000 print(list(struct.pack(H, v))) # [0, 200]几个格式化符号的记法b二进制o八进制x十六进制X大写十六进制前面的数字是总宽度0表示左补零。f{200:08b}就是8 位宽、左补零的二进制得到11001000正好一个字节的长度看着就舒服。如果要处理大块的十六进制文本Python 的bytes.fromhex()和.hex()是最省事的两个方法.hex()把字节串转成无前缀小写十六进制字符串bytes.fromhex()反过来而且会自动忽略空格。我在分析抓包数据时经常把十六进制串直接粘进去空格换行都不用清理。再提一个习惯凡是涉及二进制数据的地方日志里一定打印十六进制不要打印十进制。十进制打印出来的字节值你既没法直接和文档对照也没法一眼看出哪一位是标志位。这个习惯坚持下来后续排查问题会省下大量时间。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →