
手边正好在调一个蓝牙协议日志里一串十六进制尾巴对着文档翻来翻去最后还是得老老实实转成二进制一位位看。这种事我干过太多次了——二进制这东西真到用的时候比什么花哨框架都实在。它能做进制换算、能解释负数为什么在计算机里长得那么怪、能算小数精度误差工程里从部署一个二进制包到定位内存问题都绕不开它。这篇就围绕“二进制的计算”展开适合刚接触底层开发、或者被补码和浮点数坑过的朋友也适合需要在嵌入式、服务端、协议分析里跟二进制较劲的人。1. 先从手算讲起进制换算到底在算什么很多人一看到二进制就条件反射去背口诀什么“除2取余、乘2取整”背是会背但一问为什么就卡住。我自己的经验是先把“进制”当成一种计数习惯理解再上手算就顺了。1.1 217转二进制两种思路都可以先拿“217转化为二进制”这个热搜词开刀。十进制217意思是2个100、1个10、7个1。换成二进制它表达的是2的幂次堆出来的和。所以第一步不是急着除而是想217离哪个2的幂最近128、256217在128和256之间那最高位就是第7位从0开始数就是bit7代表128。剩下217减128等于89。89里包含一个64剩25。25里包含一个16剩9。9包含一个8剩1。1就是2的0次方。把命中的位写成1没命中的写0Bit7到Bit0分别是1、1、0、1、1、0、0、1也就是11011001。这个“凑位法”是我平时手算速度最快的方式尤其是在协议分析时一眼要把一个字节拆成功能位比从下往上倒着写余数直观得多。但如果你习惯标准方法除2取余也完全没问题217除以2商108余1108除以2商54余054除以2商27余027除以2商13余113除以2商6余16除以2商3余03除以2商1余11除以2商0余1把余数从下往上排同样得到11011001。1.2 二进制扩展法知道它是干嘛的才不会被名字吓到“二进制扩展法”听起来像个高级概念实际上就是把一个数拆成若干个2的幂相加的过程也就是我在凑位法里做的事。比如217等于128641681每一项对应一个二进制的“1”位。这个方法在协议解析里尤其好用因为一个字节的各个bit常常代表不同的开关或标志位拆开看比直接看十进制数字要清晰太多。我建议新手别死记“扩展法”这个术语而是记住三个关键词幂次、权重、命中。一个bit位上的值就是它所在位置的权重只有0和1两种状态乘上权重再求和就是十进制。反过来给一个十进制数往2的幂次上拆就是二进制。1.3 手算最容易错的两个地方我踩过的坑一是顺序搞反。除2取余的结果必须从下往上读我见过不少人从上往下读最后算出来的数明显不对劲还不知道哪里错。二是拆权值时漏位。比如217拆完后中间隔了两个0写结果时容易把位对错建议在草稿纸上先画出8个空格对应bit7到bit0再往空格里填1或0能规避大半错误。2. 负数的二进制补码不是规定是被逼出来的搜“负数的二进制”和“二进制补码”的人多半是被两个问题折磨负数到底怎么表示以及为什么非要绕一圈用补码。我用自己的理解讲清楚这件事。2.1 为什么不用原码和反码最直观的思路是拿一个bit当符号位0正1负这就是原码。但它有两个硬伤第一0会有正0和负0两种表示等于浪费了一个编码第二做加减法时符号位不能参与运算计算机得单独判断电路复杂度立刻上去了。反码只是把原码按位取反依然没有解决0的二义性问题而且如果直接用反码做加法还会出现进位循环修正这种麻烦事。真正好用的是补码。补码的核心价值一句话就能说清把减法变成加法让符号位参与运算而且0只有一种表示。2.2 我自己推导补码的办法我记补码从来不靠背公式而是靠“借位”这个小学概念。以8位二进制为例要表示-5我就想现在是一个计算器显示范围是00000000到11111111如果我做一个减法0减5会得到什么0减去5不够减必须先向高位借位。在8位二进制里借来的1相当于256256减5等于251。251转成二进制是11111011。这就是-5的补码。你可以验证一下5的二进制是00000101用11111011加00000101结果是1 00000000丢掉最高位进位留下来的是0。我的直觉是补码本质上就是“模运算”。8位二进制能表达256个数在这个环里加5和加251是同一个效果所以-5就是加251。2.3 补码实战溢出判定和边界值搞懂补码后很多奇怪现象就顺理成章了。比如8位有符号整数最大值是01111111对应127再加1就变成10000000按补码规则这是-128。这个“溢出回绕”在很多系统底层bug里出现过常见于计数器、定时器、颜色值计算。排查这类问题我有个习惯先把变量按有符号和无符号两种情况各算一遍再对着调用代码确认它到底是什么类型。曾经一个颜色渐变动画总是闪一下查到最后是某个分量加过头从255变成0又变回255就是典型的溢出回绕。3. 小数点后面的二进制为什么0.1加0.2不等于0.3这是程序员界的经典梗也是“二进制表示十进制小数”和“十进制小数转换为二进制”这两个热搜背后的痛点。整数部分靠除2取余小数部分靠乘2取整思路完全是对称的。3.1 乘2取整法一步步拆解拿0.625举例。0.625乘以2等于1.25取整数部分1剩下的0.25继续0.25乘以2等于0.5取整数部分0剩下的0.5继续0.5乘以2等于1.0取整数部分1结束。从前往后把取出的整数拼起来得到0.101。这就是0.625的二进制表示而且是精确的因为4乘2的负一次方加0乘2的负二次方加1乘2的负三次方正好等于0.625。但很多十进制小数没那么幸运。0.1乘以2等于0.2取00.2乘以2等于0.4取00.4乘以2等于0.8取00.8乘以2等于1.6取10.6乘以2等于1.2取10.2乘以2等于0.4取0……后面就开始循环。0.1的二进制是0.00011001100110011...无限循环。3.2 有精度限制时舍入到底该不该考虑搜索词里有人问“十进制小数转换为二进制有精度限制时需要考虑舍入吗”我的回答是必须考虑而且得在协议设计阶段就想清楚。比如嵌入式设备里用16位或32位定点数表示温度每一个bit都代表固定的最小刻度。此时0.1°C这种值转换完有循环小数如果直接截断多次累加后误差会越来越大。常见的做法是“舍入到最接近的可表示值”而不是“直接截断”。比如0.1用8位不够精确就取离它最近的那个8位二进制小数多种几次运算误差会小很多。C语言里lroundf比roundf多了“远离零”的语义而fesetround可以切换舍入模式这些细节在涉及金额、传感器数据、帧同步时都至关重要。3.3 工程里的精度策略我常用的三种第一种是纯整数运算把小数放大到整数域再算比如用千分之一度为单位存温度。第二种是固定位数的定点数适合没有浮点单元的MCU。第三种才是浮点但必须记录误差并且在高精度场景用双精度甚至高精度库。我自己做数据采集时喜欢用定点数因为日志打印、范围校验、边界判断都更可控。浮点数的坑往往不是“算错”而是“看起来没错累加多了就偏了”尤其在使用滑动平均或PID这类带反馈的算法时一个微小偏差会被持续放大。4. 二进制的四则运算加减乘除在计算机里都是移位游戏如果你只看教科书会觉得二进制加减乘除就是列竖式繁琐但直接。但工程里真正关心的不是竖式而是计算机到底怎么“算得快”以及为什么移位操作随处可见。4.1 二进制加减法本质是位运算二进制加法在硬件里就是异或加进位的组合。不考虑进位时1加1在当前位是0同时向高位进1这个和“异或”一致进位则靠与运算和左移这种组合实现。正是这个原因CPU加法器能做得非常快而减法又通过补码变成加法所以CPU根本不需要专门的减法器。对普通程序员来说明白这一层对调试很有用。比如判断一个整数是不是2的幂用n 0 (n (n - 1)) 0想快速让负数变为正数用-x或者~x 1想交换两个数可以用异或。这些技巧全是二进制位运算的直接应用。4.2 二进制乘法左移就是乘2很多刚入门的人不理解为什么C语言里x 1能替代x * 2因为二进制数左移一位相当于每一位的权重都乘2整体数值自然翻了倍。同理右移一位是整除2。乘法器在硬件里通常通过移位和加法组合实现比如某数乘10可以拆成(x 3) (x 1)因为10等于8加2。我排查过一些老代码里“乘法开销大”的瓶颈改成移位和加法组合后性能明显提升。当然现代编译器优化很成熟写代码时不必刻意手写移位乘法但理解它有助于读懂汇编、排查极致的性能问题。4.3 二进制除法实例101101除以11除法和十进制长除法长得非常像只是每一步只判断“够不够减”。拿101101除以11来说我从最高位开始取前两位10比除数11小不能商1再取前三位101够减11商上1余数101减11等于10把下一位1拉下来变成101够减11商上1余10把下一位0拉下来变成100够减11商上1余1把下一位1拉下来变成11够减11商上1余0。商是1111余数0。整个过程用位运算实现就是“循环左移比较减法”对应CPU里除法器的核心逻辑。除法是四则运算里开销最大的所以写驱动或算法时能避免除法就尽量避免。比如数组索引计算如果长度是2的幂可以用index (length - 1)代替index % length这种优化我实测在某些平台上能省几十个时钟周期临界在高频循环里依然有意义。5. 二进制的名字出现在工程里往往不是计算本身二进制除了是“数字的一种表示”还是“文件的一种形态”。这个区别很重要很多新手搜“二进制包”“二进制内存”时会懵因为话题从数学跳到了计算机系统。5.1 部署nginx二进制包其实是在部署编译产物拿“centos 部署nginx二进制包”来说这里的“二进制”指编译好的可执行文件及其依赖的库不是源码。选二进制包而不是源码编译图的是快省去装编译器、下载依赖、编译等待的时间。但代价是你得信任构建方并确保运行环境的glibc版本、CPU指令集和构建方一致。我自己的习惯是生产环境能预编译就预编译但装完一定要跑一下ldd /usr/local/nginx/sbin/nginx检查动态库依赖尤其留意libc.so.6的版本。曾经有过同一份二进制从CentOS 7拷到CentOS 6直接报GLIBC_2.14 not found的惨痛经历。如果追求可移植性最好是找官方针对目标系统的二进制包而不是自己在别处随便编译一个拷贝过去。另外“下载适配你平板cpu架构的memtester二进制包arm/arm64版本”也是一个典型场景。memtester这种内存压力测试工具如果不是针对arm架构编译的二进制直接跑会报“无法执行二进制文件”。这类工具通常用交叉编译链编一次然后部署到目标设备上验证内存稳定性比如./memtester 512M 2就是申请512MB内存测两轮。做嵌入式测试时我几乎离不开这类小工具它们清一色都是二进制可执行文件。5.2 用strstr查找二进制内存是个经典陷阱热搜里有“c语言strstr()能否用于查找二进制内存”这是个特别好的问题。答案是不能用至少不能直接用。strstr把内存当字符串处理按\0判断结束而二进制数据里随时可能出现0x00。一旦遇到0x00它就会提前停止再往后全部忽略。要找二进制模式正确思路是写一个限定长度的匹配函数比如用memmemGNU扩展或者自己实现循环比较。我处理协议抓包时经常需要在一段缓冲区里找特征字串比如帧头0xAA55就会自己写一个按长度比较的查找函数绝不让strstr接管。5.3 vscode里用二进制方式打开文件“vsc怎么指定二进制打开文件”这个问题很多人是想查看二进制文件的内容又不想通过命令行hexdump。我的做法是安装Hex Editor扩展装完后在命令面板里执行“Hex Editor: Open Active File in Hex Editor”文件就会以十六进制加ASCII对照的形式打开。这个场景我常用于分析日志文件里混入的不可见字符或者查看打包后的二进制资源文件。相比命令行走查一行行翻可视化工具的跳转和搜索方便太多。不过要提醒一句用十六进制编辑器改文件时务必先备份因为改错一个字节就能让文件彻底不可用而且几乎无法撤销。5.4 二进制遇上人工智能是权重的存储和计算“二进制人工智能”这个词有点标题党但它指向的其实是AI模型底层的数据表示模型权重、激活值、梯度本质上都是一堆二进制的浮点数或整数。做量化时比如把FP32的权重变成INT8模型体积能缩小到四分之一推理速度也更快代价是有精度损失。这种量化后的模型存储时就是一个庞大的二进制文件。我在实际部署中做过一次模型量化把100多MB的模型压到30MB左右在边缘设备上推理速度快了几乎一倍。关键是要选好校准数据集否则量化误差会让准确率明显掉点。单纯看二进制文件里的0和1很难还原出“这是个猫还是狗”但了解底层是二进制张量运算能帮你理解为什么量化能提速CPU做INT8乘法的速度远快于FP32。5.5 Docker的sock文件为什么也是“二进制”的一种存在“二进制docker 默认sock文件位置”里提到的sock全称是Unix domain socket它同样不是可执行文件而是一个特殊文件用于进程间通信。Docker客户端的请求就是通过/var/run/docker.sock这个文件发给Docker守护进程的。如果你写的脚本需要通过socket调用Docker API就必须知道这个文件的位置和权限。不少安全加固建议里都会提到不要让普通用户对docker.sock有写权限因为这等同于授予宿主机root级别的能力。我在排查“明明安装了docker但客户端连不上”时第一反应就是看/var/run/docker.sock是否存在、当前用户是否在docker组里。6. 常见问题与排查技巧速查表把上面提到的典型问题整理成一张表方便你遇到时快速定位。问题现象常见原因解决办法手算217转二进制写出来是乱码除2取余后顺序读反余数从下往上读或用凑位法先画8个位负数用原码表示后加减法不对没有采用补码统一使用补码符号位参与运算0.1加0.2不等于0.3二进制无法精确表示0.1用定点数比较时允许误差金额场景用整数左移后数值远超预期有符号整数溢出回绕确认位宽使用无符号类型或做边界检查执行二进制包报“Exec format error”架构不匹配查看目标CPU架构下载对应arm/arm64/x86版本中心服务器上nginx二进制包运行报GLIBC版本错误编译环境与运行环境不一致在目标环境重新编译或使用官方对应发行版的包strstr找不到二进制内存中的模式strstr遇\0提前结束改为memmem或自定义长度限定匹配函数vscode打开二进制文件乱码未使用十六进制编辑器安装Hex Editor扩展用十六进制视图打开Docker容器连不上/var/run/docker.sock权限不对检查socket文件权限把用户加入docker组排查这类问题我的通用手段是先把“数字计算”和“文件形态”两条线分开思考。如果问题发生在数值层面回到补码、精度、位宽如果问题发生在文件层面去看架构、依赖、权限。千万别一上来就用od或xxd大段刷屏先明确你到底想看什么。最后分享一个我自己的习惯不管处理什么二进制问题第一件事永远是“把宽度和目标格式写清楚”。是8位还是32位是有符号还是无符号是整数还是小数是CPU小端还是网络大端。这四个问题答清楚了至少能避开我踩过的一半坑。二进制这东西越到后面越不靠背规则靠的是把它当成“数”和“文件”两种身份去理解再带着目标去纸上算一算、到系统里查一查基本就能找到答案。