进制这个东西,很多人第一次接触是在计算机课上,老师讲了一堆"逢二进一""逢八进一",然后考试考完就还给老师了。但只要你真正开始碰底层的东西——看二进制文件、调寄存器、分析协议报文、甚至只是想在调试器里改一个字节——进制就会像影子一样跟着你。我见过太多人卡在"0x45到底是多大"这种问题上,也见过有人把十六进制文件整体改反了方向还浑然不觉。这篇内容不打算写成教科书,而是把我这些年跟进制打交道时踩过的坑、总结的规律、以及真正用得上的换算技巧,一次性讲清楚。不管你是刚学编程的新手,还是已经工作几年但进制换算全靠计算器的老手,都能从里面找到点有用的东西。
1. 进制到底在解决什么问题
1.1 从"数数"这件事说起
我们先抛开所有技术术语,回到最原始的场景:数数。你有一堆石子,怎么表示它们的数量?最直接的办法是画竖线,一根代表一个。但石子多了以后,画满一屏也数不清。于是人类发明了"进位"——每数到一定数量就归为一组,用一个新符号表示。这个"一定数量"就是基数,也就是我们说的"几进制"。
十进制之所以成为日常主流,纯粹是因为人有十根手指。这不是数学上的必然,只是生理上的巧合。如果人类有八根手指,我们大概率会用八进制;如果有十六根,那十六进制就是日常。所以当你觉得二进制"反人类"的时候,要明白:它反的只是"十进制习惯",而不是"逻辑"。二进制在逻辑上反而是最简洁的——只有两个状态,对应电路的"通"和"断",对应逻辑的"真"和"假"。
理解这一点很关键:进制不是数字本身,而是数字的表示方式。同一个数量,用不同进制写出来长得不一样,但它代表的"多少"是固定的。十进制里的 255,二进制写成 11111111,十六进制写成 FF,它们说的是同一件事。
1.2 为什么计算机偏偏选了二进制
这个问题我被问过无数次。答案其实不复杂:物理器件的稳定性。早期计算机尝试过十进制、三进制,甚至模拟计算。但电子器件最容易实现的稳定状态就是两种——有电压和无电压(或者高电平和低电平)。要在一个电路里稳定区分十种不同的电压等级,对噪声、温度、器件一致性的要求高得离谱,成本也下不来。而两种状态之间的容错空间大得多,一个 0.3V 的波动不会把"高"误判成"低"。
三进制在理论上其实有优势,信息密度比二进制高,苏联当年也做过三进制计算机。但工程上,三态器件的制造和可靠性控制比二态难,最终没能成为主流。所以二进制胜出,不是因为它数学上最优,而是因为它工程上最省心。这个逻辑在技术选型里反复出现:理论最优往往输给工程可行。
1.3 八进制和十六进制是"偷懒"的产物
既然计算机用二进制,为什么还要搞出八进制和十六进制?答案就一个字:短。一个 32 位的二进制数写出来是 32 个 0 和 1,人眼根本没法快速阅读和比对。而 8 和 16 都是 2 的幂(2³ 和 2⁴),这意味着二进制和它们之间的转换可以按位分组、直接映射,不需要做除法。
具体来说,3 位二进制正好对应 1 位八进制,4 位二进制正好对应 1 位十六进制。这种"整除"关系让转换变得极其机械,看一眼就能换,不用算。这就是八进制和十六进制存在的全部理由——它们是二进制的"压缩写法",方便人读写,仅此而已。八进制现在用得少了,主要出现在 Unix 文件权限(比如 chmod 755)和一些老系统里;十六进制则是绝对主流,内存地址、文件内容、颜色值、哈希值,到处都是它。
2. 四种进制的换算逻辑与手算方法
2.1 十进制转其他进制:除基取余
这是最基础的换算,核心就四个字:除基取余。把十进制数不断除以目标进制的基数,记录每次的余数,直到商为 0,然后把余数从下往上读出来。
举个例子,把 217 转成二进制:
217 ÷ 2 = 108 ... 余 1 108 ÷ 2 = 54 ... 余 0 54 ÷ 2 = 27 ... 余 0 27 ÷ 2 = 13 ... 余 1 13 ÷ 2 = 6 ... 余 1 6 ÷ 2 = 3 ... 余 0 3 ÷ 2 = 1 ... 余 1 1 ÷ 2 = 0 ... 余 1余数从下往上读:11011001。验证一下:128+64+16+8+1 = 217,对的。
转十六进制同理,只是除数是 16。217 ÷ 16 = 13 余 9,13 ÷ 16 = 0 余 13(也就是 D),所以结果是 D9。这里要注意,余数超过 9 的时候要用字母 A-F 表示,10 是 A,11 是 B,一直到 15 是 F。这个映射必须记牢,否则换算时容易卡壳。
提示:手算的时候,除基取余是最不容易出错的方法,因为它每一步都是机械的除法。心算快的人可以用"凑幂次"的方法,但对大多数人来说,老老实实列竖式更稳。
2.2 其他进制转十进制:按权展开
反过来,把任意进制转成十进制,用的是按权展开求和。每一位数字乘以基数的"位权次方",然后全部加起来。位权从右往左数,最右边是 0 次方。
比如二进制 11011001 转十进制:
1×2⁷ + 1×2⁶ + 0×2⁵ + 1×2⁴ + 1×2³ + 0×2² + 0×2¹ + 1×2⁰ = 128 + 64 + 0 + 16 + 8 + 0 + 0 + 1 = 217十六进制 D9 转十进制:
D×16¹ + 9×16⁰ = 13×16 + 9 = 208 + 9 = 217这个方法对任何进制都通用。八进制 331 转十进制就是 3×64 + 3×8 + 1 = 192 + 24 + 1 = 217。你会发现它们都指向同一个数,这就是前面说的"表示方式不同,数量相同"。
2.3 二进制与十六进制的"四位一组"速换法
这是实战中最常用的技巧,必须掌握。因为 4 位二进制正好等于 1 位十六进制,所以转换时从右往左每 4 位分一组,每组直接查表换成十六进制字符。
对照表如下,建议背下来:
| 二进制 | 十六进制 | 二进制 | 十六进制 |
|---|---|---|---|
| 0000 | 0 | 1000 | 8 |
| 0001 | 1 | 1001 | 9 |
| 0010 | 2 | 1010 | A |
| 0011 | 3 | 1011 | B |
| 0100 | 4 | 1100 | C |
| 0101 | 5 | 1101 | D |
| 0110 | 6 | 1110 | E |
| 0111 | 7 | 1111 | F |
拿 11011001 举例,从右往左分组:1101 和 1001。查表,1101 是 D,1001 是 9,合起来就是 D9。整个过程不需要任何除法,纯查表,几秒钟搞定。
反过来,十六进制转二进制就是把每一位拆成 4 位。比如 0x45,4 拆成 0100,5 拆成 0101,合起来 01000101。注意高位补零,不能写成 1000101,那样位数就错了。
注意:分组时如果最左边一组不足 4 位,要在前面补 0 凑够 4 位。比如二进制 101 转十六进制,要看成 0101,结果是 5,而不是直接当成 101 去查。
2.4 八进制的"三位一组"与它的现实用途
八进制的逻辑和十六进制一样,只是分组变成 3 位一组。二进制 11011001 转八进制:从右往左分 011、011、001(最左边补 0),查表得 3、3、1,结果是 331。
八进制现在最实际的用途就是Unix/Linux 文件权限。chmod 755 里的 7、5、5 分别对应"读+写+执行""读+执行""读+执行"。每一位八进制数展开成 3 位二进制,正好对应 rwx 三个权限位。7 = 111(rwx),5 = 101(r-x),0 = 000(---)。理解了这层映射,权限设置就不再是死记硬背了。
3. 十六进制在真实工作场景里的样子
3.1 内存地址与调试器里的十六进制
只要你用过调试器,就一定见过十六进制地址。为什么地址用十六进制而不是十进制?因为内存地址本质上是二进制,而十六进制和二进制是 4 位对 1 位的直接映射。一个 32 位地址写成十六进制是 8 个字符,写成十进制是 10 个字符,而且十六进制能让你一眼看出哪些位是高位、哪些是低位。
比如地址 0x7FFF5FBFF8C0,你能直接看出它的结构;换成十进制 140734799806144,你根本看不出任何规律。在调试器里看内存 dump,左边是地址,中间是十六进制字节,右边是 ASCII 字符,这个布局是几十年来的标准,因为十六进制让"字节"这个概念变得直观——两个十六进制字符就是一个字节。
3.2 文件头识别:常见格式的十六进制特征
分析文件格式时,文件头(magic number)是最先要看的东西。这些头都是固定的十六进制字节序列,认出来就能判断文件类型。下面这张表是我平时查得最多的:
| 文件类型 | 十六进制文件头 | ASCII 表现 |
|---|---|---|
| PNG 图片 | 89 50 4E 47 0D 0A 1A 0A | .PNG.... |
| JPEG 图片 | FF D8 FF | ... |
| PDF 文档 | 25 50 44 46 | |
| ZIP 压缩包 | 50 4B 03 04 | PK.. |
| ELF 可执行文件 | 7F 45 4C 46 | .ELF |
| GIF 图片 | 47 49 46 38 | GIF8 |
用十六进制编辑器打开一个文件,看开头几个字节,基本就能确定它是什么。这个技能在处理"文件后缀名被改错"或者"下载的二进制程序包找不到类型"这类问题时特别有用。我遇到过好几次下载下来的文件没有后缀,用十六进制一看文件头是 7F 45 4C 46,就知道是 ELF 可执行文件,直接 chmod +x 就能跑。
3.3 十六进制编辑器:改字节的必备工具
十六进制编辑器(hex editor)是处理二进制文件的瑞士军刀。它的界面通常分三栏:偏移地址、十六进制字节、ASCII 预览。你可以直接定位到某个偏移,修改某个字节的值。
用十六进制编辑器时有几个坑必须注意。第一,修改前一定要备份,因为二进制文件没有"撤销"的通用保障,改错了可能整个文件就废了。第二,注意字节序,多字节数值在文件里可能是小端序(低位在前)也可能是大端序(高位在前),改的时候方向搞反了,值就完全错了。第三,注意文件长度字段,很多格式在头部记录了文件长度或校验和,你改了内容但没更新这些字段,文件就会被认为是损坏的。
提示:如果你发现"十六进制文件全反了",大概率是字节序理解错了,或者用了错误的编码方式打开。先确认文件本身的字节序约定,再动手改。
3.4 在十六进制模式下搜索字符串
有时候你需要在一个二进制文件里找某个特定的字符串,比如配置项moz_require_signing=true。普通文本搜索工具可能因为编码问题找不到,这时候就要用十六进制模式搜索。
做法是把目标字符串转成十六进制字节序列,然后在编辑器里搜这个序列。ASCII 字符的十六进制很好转:每个字符对应一个字节,比如m是 6D,o是 6F,z是 7A。所以moz就是 6D 6F 7A。搜索时输入这串十六进制,就能精确定位。
这个技巧在逆向分析、协议调试、配置文件定位时非常实用。因为二进制文件里可能夹杂着各种不可见字符,纯文本搜索容易漏掉或被干扰,而十六进制搜索是精确匹配字节,不会出错。
4. 二进制运算与那些容易翻车的细节
4.1 二进制加减法:其实和十进制一个道理
二进制加法规则很简单:0+0=0,0+1=1,1+0=1,1+1=10(进位)。本质上和十进制加法一样,只是"逢十进一"变成了"逢二进一"。
1011 + 0110 ------ 10001从右往左加:1+0=1,1+1=10(写0进1),0+1+1=10(写0进1),1+0+1=10(写0进1),最后进位得1,结果是 10001。
减法稍微麻烦一点,因为涉及借位。二进制借位是"借一当二",而不是"借一当十"。这个区别在手动计算时特别容易搞混。我的建议是:减法统一转成加法来做,用补码。这样就不用单独处理借位逻辑,而且补码本身就是计算机做减法的实际方式。
4.2 二进制除法与"二进制扩展法"
二进制除法就是反复的"移位+减法",和十进制长除法结构一样。比如 1100 ÷ 10(也就是 12 ÷ 2):看被除数前两位 11,比 10 大,商 1,余 1;拉下一位 0 得 10,正好等于除数,商 1,余 0;拉下一位 0 得 0,比除数小,商 0。结果是 110(6)。
所谓"二进制扩展法",我理解是指把二进制数按位权展开来做运算或者转换的方法,本质还是按权展开求和那一套。这个方法在处理"二进制表示十进制小数"时特别重要。比如 0.6 的二进制表示是无限循环的 0.100110011001...,因为 0.6 不能表示成 2 的负幂次之和的有限项。这就是为什么浮点数会有精度问题——不是计算机算错了,而是有些十进制小数在二进制里根本表示不精确。
4.3 位运算:与、或、异或、移位
位运算是在二进制层面直接操作,效率极高,在底层编程、加密、压缩、图形处理里到处都是。
- 与(&):两位都是 1 才得 1,常用来"掩码"提取特定位。
- 或(|):有一位是 1 就得 1,常用来"置位"。
- 异或(^):两位不同得 1,相同得 0,常用来"翻转"或做简单加密。
- 左移(<<):整体左移,右边补 0,相当于乘 2。
- 右移(>>):整体右移,相当于除以 2(对无符号数)。
举个例子,判断一个数的第 3 位是不是 1:用n & 0b1000,结果非零就是 1。把第 3 位置 1:用n | 0b1000。翻转第 3 位:用n ^ 0b1000。这些操作在写驱动、处理寄存器、优化性能时是家常便饭。
4.4 校验码里的二进制:以 LRC 为例
LRC(纵向冗余校验)是一种简单的校验方式,原理就是把所有字节做异或或者求和,得到一个校验字节。计算过程完全在二进制层面进行。
比如一串字节 0x01、0x02、0x03,做异或校验:01 ^ 02 = 03,03 ^ 03 = 00,所以 LRC 是 0x00。接收方收到数据后重新算一遍,如果结果对不上,就说明传输过程中有字节出错了。
这里有个常见疑问:"需要换算成二进制相加吗?"答案是:异或运算本身就是逐位进行的,不需要你先手动转成二进制再算。工具和代码会自动处理位级操作。你只需要理解它的原理是"逐位比较",具体计算交给程序就行。但如果你要手算验证,那就得把每个字节展开成 8 位二进制,逐位做异或,再合回去。
5. 学习进制的实用路径与常见误区
5.1 不要死记硬背,要建立"位权直觉"
很多人学进制的方式是背转换公式,结果一到实际用就懵。我的建议是:先建立位权直觉。什么是位权直觉?就是看到 2 的幂次能条件反射地反应出数值:2⁰=1,2¹=2,2²=4,2³=8,2⁴=16,2⁵=32,2⁶=64,2⁷=128,2⁸=256,2⁹=512,2¹⁰=1024。
这组数字要熟到像乘法口诀一样。因为二进制转十进制本质上就是"把为 1 的位对应的权值加起来"。你看到 1010,立刻反应出是 8+2=10,而不是去列竖式。这个直觉建立起来之后,换算速度会快一个数量级。
同理,十六进制的位权也要熟:16⁰=1,16¹=16,16²=256,16³=4096。看到 0x100 就知道是 256,看到 0x1000 就知道是 4096。
5.2 用工具验证,但别依赖工具
计算器和在线转换工具当然要用,但前提是你自己能手算验证。我见过有人用计算器算完,结果输错了一位,自己完全没察觉,因为他不具备"结果大概是多少"的判断力。
正确的做法是:先手算或者心算估个范围,再用工具精确计算,两者对不上就说明有问题。比如你要把 0x45 转十进制,心算 4×16+5=69,然后用工具验证,如果工具给出别的结果,那肯定是输入错了。这种"双向验证"的习惯能帮你避免大量低级错误。
5.3 常见误区:进制、编码、字节序是三件事
这是最容易混淆的地方,必须掰清楚:
- 进制:数字的表示方式,是"怎么写"的问题。
- 编码:字符和字节的映射关系,比如 ASCII、UTF-8,是"字符怎么变成字节"的问题。
- 字节序:多字节数值的存储顺序,大端还是小端,是"字节怎么排列"的问题。
这三者互相独立。一个十六进制数,它可以是任何编码的字节,也可以按任何字节序存储。搞混了这三者,就会出现"文件全反了""字符串搜不到""数值对不上"这类问题。排查的时候要一个一个排除,先确认进制对不对,再确认编码,最后确认字节序。
5.4 从"看得懂"到"用得上"的练习建议
光看理论没用,得动手。我的练习路径是这样的:
- 每天手算 5 个十进制转二进制,坚持两周,位权直觉基本就建立了。
- 用十六进制编辑器打开一个图片文件,找到文件头,对照表格确认格式。
- 写一段代码做进制转换,不用库函数,自己实现除基取余和按权展开。
- 找一个二进制文件,搜索一个已知字符串,练习十六进制模式搜索。
- 手动计算一次 LRC 校验,理解逐位运算的过程。
这几步走下来,进制就不再是考试题,而是你手里的工具了。
6. 几个真实场景的排查记录
6.1 "下载的二进制程序包找不到"怎么定位
有次同事下载了一个工具,文件名没有后缀,双击没反应,以为是下载坏了。我让他用十六进制编辑器打开看头几个字节,结果是 7F 45 4C 46,标准的 ELF 可执行文件。问题不是文件坏了,而是没有执行权限。chmod +x 之后就能跑了。
这个案例的教训是:文件类型看内容,不看后缀。后缀可以随便改,但文件头是格式规范的一部分,改不了。遇到"打不开""找不到类型"的文件,先看文件头。
6.2 "十六进制文件全反了"的排查链路
这个问题的排查要按顺序来:
- 确认是不是字节序问题。如果文件里多字节数值的字节顺序和你预期相反,那就是大小端搞反了。
- 确认是不是位序问题。有些场景(比如某些硬件寄存器)位序是从左往右数的,和常规相反。
- 确认是不是编码问题。用错误的编码打开文件,字符会显示成乱码,看起来像"反了"。
- 确认是不是工具设置问题。有些编辑器有"反转字节"的显示选项,可能被误开了。
按这个顺序排查,基本能定位到原因。最怕的是一上来就乱改,把原本正确的文件改坏了。
6.3 在二进制内存里搜索字符串的注意事项
前面提到用十六进制模式搜索moz_require_signing=true。这里补充几个细节:
- 注意大小写。十六进制搜索是精确匹配,大小写不同字节就不同。
- 注意字符串结尾。C 风格字符串以 0x00 结尾,搜索时可能要把结尾的 00 也算进去。
- 注意对齐。有些数据结构有对齐要求,字符串可能不在你以为的偏移上。
- 注意编码。如果字符串是 UTF-16 编码的,每个字符占两个字节,十六进制序列会完全不同。
这些细节决定了搜索能不能成功。搜不到的时候,不要急着怀疑工具,先检查这几个点。
6.4 关于"e进制"和"三进制"的闲聊
偶尔会看到有人讨论"e进制"(自然常数 e 约等于 2.718)是不是最优进制。从信息论角度,e 进制确实是"单位成本信息量最大"的进制,但工程上没法实现,因为进制数必须是整数。最接近 e 的整数是 3,所以三进制在理论上比二进制更高效。
但前面说过,三态器件的工程难度大,所以二进制胜出。至于"三进制"相关的硬件项目,那是另一个层面的探索,和日常开发关系不大。了解这个背景知识有助于理解"理论最优不等于工程最优"这个道理,但不必在这上面花太多精力。
7. 把进制变成肌肉记忆
进制这东西,说到底是个熟练度问题。理论就那么几条,谁都能看懂,但真正用起来顺不顺手,取决于你练了多少。我的经验是:不要等到需要用的时候才去学,平时就保持手感。
具体怎么做?我自己的习惯是,看到任何数字都下意识地想一下它的二进制和十六进制长什么样。看到 255 想到 FF,看到 1024 想到 400,看到 0x7F 想到 127。这种条件反射一旦建立,处理底层问题时就不会被进制拖后腿。
还有一个习惯是读文档时留意进制标注。技术文档里经常混用十进制、十六进制、二进制,比如"寄存器地址 0x20""掩码 0b0011""超时 500ms"。读的时候主动区分,时间长了自然就敏感了。
最后说一个我踩过的坑:不要假设所有工具都用同一种进制显示。有的调试器默认十六进制,有的默认十进制;有的颜色值用 #RRGGBB,有的用 0xAARRGGBB。用之前先确认显示格式,否则很容易把 0x10(16)当成 10 来理解,差出 6 去。这种错误在调参数、算偏移的时候特别致命,而且因为数值"看起来合理",往往要排查很久才能发现。养成"先确认进制再读数"的习惯,能省下大量调试时间。