
不开任何玩笑你把一台电脑彻底拆开从CPU到内存到硬盘翻遍每一颗芯片都找不到数字“2”。你在屏幕上看到的2是无数个“开”和“关”的状态拼出来的幻觉。芯片里只有两种电压高低对应两种状态——这正是二进制的物理根基。这个认知我想了很久才真正通透二进制不是数学家拍脑袋发明的进制规则而是电子元器件在物理约束下逼出来的最优解。既然硬件只能用两种状态存东西那所有计算——加、减、乘、除、小数、负数、图像、声音、神经网络——全都要在“0和1”这个贫瘠的基础上重建。搞清楚“二进制的计算”到底怎么运转等于拿到了理解计算机一切行为的总钥匙。这篇文章不打算从教科书定义讲起。我按自己踩坑和复盘时真正用到的路径来写先扯清楚加减法的物理逻辑再把负数、小数的坑一个个踩平最后落到开发运维里的真实场景——nginx二进制包部署、memtester的ARM版本适配、VSCode打开二进制文件、strstr查二进制内存的翻车案例全部用二进制计算的原理解释透。1. 二进制的加法从电子开关到进位链条1.1 为什么计算机偏偏用二进制而不是十进制先解决一个很多人问过我的问题人类算数用十进制理所当然为什么计算机不直接用电平组合表示0-9答案是可靠性和复杂度。一个电路要想区分10种不同的电压等级对制造工艺、噪声容限、功耗的要求是指数级上升的。而只区分“高”和“低”两种状态设计制造都非常成熟。你可以把CPU里的晶体管想象成无数个同时开合的水龙头要么全开要么全关没有“开一半”的中间态。中间态在信号传输里就是灾难电压稍微波动一下十进制的5可能就变成了6。这是物理层面的约束不是偏好问题。既然只有两种状态那每一位就只能表示0或1进位规则从十进制“逢十进一”变成“逢二进一”。这就是二进制的全部起点。1.2 加法真值表与半加器1110是怎么来的二进制加法的规则极其简单四行真值表就能写全0 0 00 1 11 0 11 1 0进位1写成10最后一条是理解二进制加法的关键。在十进制里112在二进制里没有“2”这个数字所以只能向高位进1本位归0。所以二进制里1110读作“壹零”不是“十”。这个10的值和十进制的2相等但它是二进制的写法。硬件实现上这个规则对应的是半加器两个输入A和B两个输出S本位和和C进位。你去看半加器的电路S A异或BC A与B。异或运算在逻辑层面完美表达了“相同为0不同为1”的加法本位规则。这就是为什么数字电路课永远从异或门讲起——它不是某个抽象的逻辑游戏而是二进制加法在新硬件上的直接投影。1.3 从半加器到全加器多位二进制加法的完整链条单个半加器只能计算一位加法。如果要算8位二进制数比如11011001 00100111就必须把每一位的计算串起来低位产生的进位要送到高一位去参与计算这就是全加器存在的意义。全加器比半加器多一个输入来自低位的进位Cin。它的逻辑是S A异或B异或CinCout (A与B) 或 (Cin与(A异或B))这串表达式你不用硬记抓住本质就是一个8位加法器由8个全加器级联组成第i位算完产生的进位作为第i1位的Cin输入。高位能不能算对完全依赖低位同步算完喂过来的进位信号这就是所谓的“串行进位链”。现代CPU里的加法器优化了很多种进位方式比如超前进位加法器核心目的就一个缩短这条进位链的延迟。性能优化的本质往往就是在缩短物理上必须等待的依赖链条。1.4 实操用手算一遍217转二进制先看217转换成二进制。最通用的方法是短除法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。但短除法只是套路真正有信息量的是第二种做法——权值展开。217最直接的拆法是128 64 16 8 1这些数恰好是2的幂2^7 2^6 2^4 2^3 2^0。二进制每一位的1就代表一个2的幂所以第7位、第6位、第4位、第3位、第0位是1其他位是0连起来同样是11011001。权值展开的思维优势在于它让你看到二进制数字本质上是一个“2的幂的线性组合”。后面讲二进制扩展法、快速幂、甚至浮点数尾数时这个视角都极其好用。2. 负数与补码计算机真正计算减法的秘密2.1 如果按直觉用“符号位”表示负数会发生什么灾难人类最容易想到的负数方案是拿最高位当符号位1表示负、0表示正剩下的位表示绝对值。这样5写成00000101-5写成10000101。听起来特别直观但这个方案有个致命缺陷1 (-1) 会算出什么00000001 10000001 10000010结果是 -2。1加上-1等于-2这在数学上是彻底崩坏的。为了让计算成立硬件不得不在加法器之外再单独搞一套减法器设计复杂度直接翻倍而且还会出现正0和负0两个零。这个方案在真实工程里完全走不通。2.2 时钟模型模运算视角下的补码真正被全行业采用的方案是补码。理解补码绕开教科书的枯燥定义我用一个时钟模型来讲。想象一个8位寄存器它最多能表示256个状态从0到255满了之后回到0。这就像一个只有256刻度的钟走完一圈重新开始。在这个钟上“-1”这个动作和“255”完全等价——因为走完一圈又回到原点-1和255落在同一个刻度上。数学上这叫做模256下的同余-1 ≡ 255 (mod 256)。那怎么得到255255 256 - 1。对于一个8位数n怎么算出256 - n观察一个规律n的按位取反等于255 - n。比如5是00000101取反是11111010换成十进制是250正好等于255 - 5。所以255 - n 取反 1的关系就出来了256 - n (255 - n) 1 按位取反 1这就是“取反加一”的来历。它不是一个玄学口诀而是从时钟模型里自然推导出的数学结论。所以-5的8位补码就是5取反得11111010再加1得到11111011。现在再算1 (-1)00000001 11111111 1 00000000结果溢出了第8位把溢出的进位丢掉剩下00000000。答案恰好是0完美。2.3 补码方案的三大优势补码能成为所有现代计算机的统一方案是有硬道理的唯一的零00000000表示0没有正0负0之分。符号位方案里10000000这个状态就浪费了补码把它用来表示-128。减法即加法a - b 可以直接算成 a (~b 1)CPU不需要单独的减法器大幅降低硬件复杂度。溢出判定统一最高位进位和符号位进位不一致时判定溢出规则简单且对所有位宽一致。所以以后面试官问“负数的二进制是什么”正确答案不是“最高位变1”而是“该负数的补码”。以-128为例8位补码是10000000这也暴露了一个边界8位补码能表示的负数范围是-128到-1正数范围是0到127总范围-128到127共256个状态。因为10000000这个状态要用来表示-128所以正数比负数少一个。2.4 负数右移的坑算术右移为什么补1很多人写代码时被负数右移坑过。C/C里对有符号整数做右移绝大多数编译器采用的是算术右移高位补的是符号位不是0。-5的8位补码是11111011右移一位11111101十进制是-3注意-5整除2应该是-2.5向负无穷方向取整得到-3。算术右移保证了“负数右移k位”等价于“除以2^k后向下取整”这个语义在实现二分查找取中间值、计算平均分时非常有用。而如果是无符号整数右移高位补0那叫逻辑右移。如果你用Java或Python要格外留意语言规范Java的是算术右移是逻辑右移Python没有无符号整数概念负数右移结果和C一致。真要去处理底层协议比如解析网络包头里面的有符号字段这个细节必须刻进脑子里。3. 小数的二进制0.1 0.2 不等于 0.3 的真相3.1 十进制小数怎么转二进制乘2取整整数转二进制是除2取余小数转二进制正好反过来——乘2取整。以0.625为例0.625 × 2 1.25取整数部分10.25 × 2 0.5取整数部分00.5 × 2 1.0取整数部分1小数部分归零结束从前往后写0.625的二进制是0.101精确对应2^-1 2^-3 0.5 0.125。但关键问题来了0.625是个“好数”因为它能表示为有限个2的幂之和。那0.1呢动手算一下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 → 00.4 × 2 0.8 → 00.8 × 2 1.6 → 10.6 × 2 1.2 → 1看到没从0.2开始进入无限循环。0.1的二进制展开是0.00011001100110011...是一个无限循环小数。这就是“十进制小数转换为二进制”的精度问题的根源十进制下看着干净的小数二进制下可能根本写不完。3.2 精度限制与舍入float和double是怎么妥协的计算机内存有限不可能存一个无限循环小数。于是以IEEE 754为代表的浮点数标准登场用科学计数法思想把小数拆成符号位、指数位、尾数位三段。float1位符号 8位指数 23位尾数共32位double1位符号 11位指数 52位尾数共64位尾数只有这么多位存不下的低位只能舍掉。IEEE 754默认采用“就近舍入”策略看被舍去的部分最接近0还是最接近1如果恰好一半则舍入到偶数的尾数位。这个策略其实就是“四舍五入”在二进制下的精确版。因此0.1在double里存的是一个近似值。0.1的实际存储值经计算是0.1000000000000000055511151231257827021181583404541015625。不信的话用Python打印from decimal import Decimal print(Decimal(0.1)) # 0.10000000000000000555111512312578270211815834045410156250.1加0.2的结果自然也不是0.3而是一个接近0.3但不完全相等的数。这就是JS里经典的0.1 0.2 0.3返回false的根源。它本质上是一个二进制表示误差问题查多少次资料都不如自己手算一遍0.1的二进制展开来得通透。3.3 实际处理策略金融精度场景绝不直接上浮点既然浮点数有精度陷阱怎么规避分场景看一般业务数据展示用toFixed(2)或Math.round(x * 100) / 100这类方案做展示层处理前提是你清楚这是“展示上的修正”。金额、费率、库存这类要求精确计算的场景不要用二进制浮点数直接换使用十进制表示的定点方案。Java用BigDecimalPython用decimal.Decimal字符串构造精确到分或厘。TS/JS的超级大数直接在库层面采用十进制字符串或整数分存储。一个重要原则如果确定要做“十进制小数的精确算术”就别让二进制浮点参与中间过程。把小数放大成整数计算最后再缩小或者用专门的十进制运算库都能绕开精度问题。实际业务里因为这个精度问题造成的资损事故和线上bug我经历过不止一次。3.4 带约束的舍入二分法里的二进制扩展法前面热词里有个“十进制小数转换为二进制有精度限制时需要考虑舍入吗”解答很明确只要转换后的位数超过了存储位宽就必须舍入而且舍入策略要提前定好。IEEE 754默认的就近舍入在绝大多数场景可用但有些协议、文件格式、网络通信里可能要求截断或向零舍入如果两边不统一同一个数据在不同机器上解析出来差1个ulp最小精度单位调起错来非常折磨人。二进制扩展法是另一种经常被忽略的工具。它的思路和权值展开一致任何一个整数或定点小数都能拆成若干2的幂的线性组合。这个思路在二分查找多项式拟合、切比雪夫逼近、快速傅里叶变换的蝶形运算里都能看到影子。理解了“二进制扩展法”本质上是把一个数按照2的幂阶拆分你能顺带理解为什么快速幂算法能把指数计算复杂度从O(n)压到O(log n)——因为指数直接按二进制位拆开每个1位对应一次乘法。4. 二进制除法、移位与扩展法CPU算术逻辑单元里的运算拼图4.1 乘法的硬件秘密移位-相加二进制乘法有个非常巧妙的性质竖式乘法里每一步只需要做“抄一遍或者写0”因为二进制乘法表实在太小——只有0和1两个选择。以110113乘以1015为例1101 × 101 ------- 1101 (1101 × 1) 0000 (1101 × 0左移一位) 1101 (1101 × 1左移两位) ------- 1000001 (65正确)看到关键了吗每一位的部分积要么是0要么是被乘数本身左移若干位。所以CPU里做乘法不需要乘法表只需要两件事移位器 加法器。这就是“移位-相加”乘法器。硬件上乘以一个常数2^k直接用左移指令完成编译器也会把乘以2的幂自动优化成左移。4.2 除法移位-减法的长除法二进制除法比乘法复杂一点但本质和十进制长除法一模一样。以11010153除以1015为例101 进 110商1减去101得1 拉下一位1得11不够除110不够商0 再拉下一位0得110够除商1减去101得1 再拉下一位1得11结束商是1015余数是1。整个过程的核心就是反复比较“当前够不够减”够就商1并减去除数不够就商0。CPU里的除法器如果按最朴素的方式实现就是逐位执行这个步骤所以除法比加减法慢很多——这也是为什么写程序时能用位运算精简化就尽量别用除法的底层原因。4.3 边界坑除零、负数除法与取整方向无符号除法逻辑清晰但一旦进入有符号除法有两个坑非常容易翻车除零异常整型直接崩溃或触发硬件异常。在协议解析、用户输入校验场景先判断除数为0是基本功。除法取整方向C/C标准里整数除法是向零截断的——9 / 2 4-9 / 2 -4。注意这和算术右移向下取整不是一个方向。举典型例子(-9) / 2在C里是-4而在语义上如果用-9 1算术右移结果是-5。两者差了1这是非常隐蔽的bug源头。一个小经验当你需要“向下取整除法”时可以借助除数符号做修正或者直接用算术右移仅限除数是2的幂。当需要“向零取整除法”时直接用/。写代码前想清楚语义比事后Debug省太多时间。4.4 快速幂二进制扩展法的高光时刻快速幂是二进制扩展法最经典的应用。计算 a^1313的二进制是1101也就是13 8 4 1。所以a^13 a^(841) a^8 × a^4 × a^1算法只需要不断让底数自乘a、a^2、a^4、a^8……然后看指数对应二进制位是否为1是就乘进结果。复杂度从O(n)降低到O(log n)。模板def fast_pow(a, b, mod): result 1 base a % mod while b 0: if b 1: result result * base % mod base base * base % mod b 1 return result这里的b 1判断最低位是否为1b 1逐位消费指数。整个循环跑的轮数就是b的二进制位数而不是b本身的大小。这就是大整数快速幂、RSA加密里大量用到二进制的直接原因。5. 真刀真枪处理二进制开发与运维里的经典实战场景5.1 strstr() 为什么不能用来查找二进制内存热词里有条“c语言strstr()能否用于查找二进制内存”这是一个我见过无数人翻车的问题。strstr的设计目标是在“以空字符结尾的字符串”里做子串查找。它内部按字节扫描遇到\0也就是0x00就认为字符串结束了。但二进制数据里充斥着0x00——比如一个4字节整数1在小端序下是01 00 00 00里面有一堆空字节。用strstr去搜这种数据一旦遇到第一个0x00直接返回失败完全没法工作。而且即使你搜的内容不含0x00strstr本身也没有长度参数无法限定搜索范围。正确做法是用memmem它显式接收haystack和needle的长度参数遇到0不停止纯粹按内存字节比较#define _GNU_SOURCE #include string.h void *pos memmem(haystack, haystack_len, needle, needle_len); if (pos ! NULL) { // 命中 }如果平台不支持memmem比如Windows的MSVC只能手写一个循环 memcmp代码不复杂但要注意搜索的终止条件不能用strlen必须用传入的长度。而且这还没完——二进制搜索可能命中一个“伪模式”某个数据里恰好出现了你要找的字节序列但语义上并不是你想匹配的目标。真正的协议解析还要处理对齐、字段边界、大小端序不是一个字符串查找就能解决的问题。5.2 VSCode怎么打开二进制文件十六进制视图的正确姿势用VSCode默认打开一个二进制文件大概率会得到一堆乱码、一大堆替换字符和编辑器卡顿。因为编辑器默认按UTF-8文本解码二进制字节序列很多是不合法字符。最简单、最稳妥的方案是装微软官方HexEditor扩展。装完之后右键文件选择“Open With”选Hex Editor即可。你看到的将是按16字节一行的十六进制偏移、字节值、ASCII对照比任何在线工具都方便做格式分析。如果不想装插件命令行选手也有自己的方案xxd file.bin | head -20或者用hexdump排版更密集hexdump -C file.bin | head -20-C参数同时输出十六进制和ASCII是排查文件头的标配。看到ELF开头的7f 45 4c 46、PNG图片开头的89 50 4e 47都是靠这类工具识别出来的。5.3 CentOS部署Nginx二进制包为什么要手工编译安装“centos 部署nginx二进制包”这个词背后的典型场景是默认yum源里的nginx版本太老或需要自定义模块或处于离线内网环境yum装不了。这时候就必须用官方编译好的二进制包或源码自行编译。用源码编译的基本路径# 1. 安装编译依赖 yum install -y gcc pcre-devel zlib-devel openssl-devel # 2. 解压源码配置编译参数prefix指定安装路径enable http_ssl_module开启https支持 wget https://nginx.org/download/nginx-1.26.2.tar.gz tar -xzf nginx-1.26.2.tar.gz cd nginx-1.26.2 ./configure --prefix/usr/local/nginx --with-http_ssl_module --with-http_v2_module # 3. 编译并安装 make -j$(nproc) make install # 4. 校验配置并启动 /usr/local/nginx/sbin/nginx -t /usr/local/nginx/sbin/nginx为什么要单独提到二进制相关因为你会发现configure阶段有一堆--with-xx_module参数这本质上是在二进制的“可执行文件层面”决定要不要把某个模块代码链接进去。编译生成的nginx就是一个大的二进制可执行文件里面静态链接了pcre正则库、openssl加密库等。这里我强烈建议不要跳过sha256校验这一步。sha256sum nginx-1.26.2.tar.gz下载的压缩包如果哈希值和官网不一致说明文件被篡改或下载损坏后面编译出来的二进制行为无法预料。二进制文件的验证、签名校验在安全要求高的环境里是底线不能省。用yum安装和手工编译安装最大的区别在于yum装的是发行版维护者编译的“通用二进制”模块相对保守手工编译相当于按你的CPU架构和需求定制了一套“专用二进制”。代价是升级、卸载都得自己管没有包管理器帮你清理。5.4 下载适配平板CPU架构的memtester二进制包arm/arm64memtester是一个经典内存压力测试工具。它做的事情是在用户态分配一大块内存反复写入并校验不同位模式通过二进制数据的写入、读取、比较来检测内存单元是否有位翻转问题。理解它为什么有效正好用上二进制计算的思路它用到的校验模式如0x5501010101和0xAA10101010就是专门检测相邻位之间短路问题的——0x55和0xAA的每一位都和邻居相反相邻位如果物理上粘连校验立刻失败。热词里的场景是给平板ARM平板找一个能跑的memtester。这考验的正是“二进制是CPU架构专属”这个认知ARMv7平板的CPU是32位ARM架构要下arm版本ARMv8及以上平板是64位要下arm64也叫aarch64版本。下错架构会直接报“cannot execute binary file: Exec format error”。先确认CPU架构uname -m # aarch64 表示64位ARMarmv7l 表示32位ARM然后下载对应版本wget https://pyropus.ca/software/memtester/current/memtester-4.6.0.tar.gz tar -xzf memtester-4.6.0.tar.gz cd memtester-4.6.0 make下载二进制包后检查文件属性file memtester # 输出里会有 ELF 64-bit LSB executable, ARM aarch64确认架构匹配后执行测试一般分配1GB跑5轮./memtester 1G 5看到PASS才说明这一轮通过。如果只跑了几秒就退出八成就是架构不对或权限不足。另一个常见坑是平板自带内核的内存回收机制比如低内存杀掉进程测试内存在板子上可能被系统回收导致测试结果异常这种情况最好加上sudo或者降低分配量。这类工具能感知到的位翻转和磁盘坏块不一样它只在内存介质本身出问题时才会报错所以做硬件评估、二手设备验收时特别有用。5.5 Docker默认socket文件位置与二进制安全“docker默认sock文件位置”这条热词看起来和二进制计算无关但它恰恰是“本地二进制端点”的典型。Docker守护进程默认监听在Unix socket/var/run/docker.sock注意很多系统/var/run是指向/run的软链所以/run/docker.sock是同一个东西。这个socket文件的权限默认是ls -l /var/run/docker.sock # srw-rw---- 1 root docker ...它的厉害之处在于谁有权限访问这个socket文件谁就有Docker守护进程的完整控制权。因为控制Docker不是走TCP鉴权而是直接向这个本地二进制端点发命令。所以把宿主的docker.sock挂载进容器等于把宿主机root权限间接交给了容器里的进程docker run -it -v /var/run/docker.sock:/var/run/docker.sock:ro ubuntu不做只读挂载的话容器里的进程可以直接创建新容器、挂载宿主目录权限瞬间提权到宿主根。实际生产里因为这条被攻破的案例非常多。这非常能说明一个通用安全原则二进制层面的数据格式、文件权限、网络端点状态决定了真实的安全边界。你以为只是“挂了一个文件”实际上挂的是“一段不可被拦截的命令通道”。6. 二进制计算在AI与高性能场景里的延伸6.1 二进制人工智能二值化神经网络与1-bit模型热词里有“二进制人工智能”连起来说其实是二值化神经网络Binary Neural Network。思路是这样的深度学习模型的参数如果用32位浮点存储动辄几百MB甚至几个GB移动端、嵌入式设备根本跑不动。但既然权重在训练后被证明有一定的冗余度那能不能把权重和激活值都约束成只有两个值1和-1好处极其巨大原本的浮点乘法在二值网络里退化成XNOR异或非位运算然后再用位计数指令popcnt统计1的个数得到卷积结果。一个乘法器变成几条位指令延迟和功耗都降了几个数量级。近年来甚至出现了1-bit大语言模型BitNet核心思想跟二值网络一脉相承都是在“只有两种状态”的约束下重新设计计算流程。这个方向还带火了另一个有意思的技术Morton码Z-order curve。它的原理是把多维坐标的二进制位交错拼成一个整数。比如二维点(x, y)把x和y的每一位交错排列后生成一个新整数这个整数保留了一定的空间邻近性被大量用于空间索引、GPU材质缓存、稀疏矩阵重排。看两个点点A坐标(1, 1)二进制(01, 01)Morton码0101点B坐标(2, 2)二进制(10, 10)Morton码1000虽然它们在空间上很近但因为二进制展开方式的差异Morton码相差不小——这恰好说明二进制编码方案不唯一不同的交错、不同的位序会产生不同的性质设计中要结合实际场景选择。6.2 位运算在当代工程里的地位很多从后端业务开发入行的朋友觉得位运算不常用但一旦进入协议解析、权限系统、高性能计算、网络包处理这些领域位运算就是生存技能。举几个真实场景权限系统用32位整数每一位代表一个操作权限检查用(perm FLAG) ! 0授予用perm | FLAG回收用perm ~FLAG。所有操作都是几个CPU周期的事。哈希散列把一个大对象按照二进制分布均匀打散到多个桶本质上依赖对二进制位分布的分析能力。掩码操作IP地址里子网掩码的计算、CPU Cache里Tag和Index的划分全是位运算的直接应用。我自己写高性能日志采集器的时候大量时间花在读字节流、拼接字段、按位解析协议标志上。每次遇到需要把两个字节拼成一个16位整数、或者从32位整数里提取出某几个bit时靠的都是二进制概念里的移位、掩码、大小端序。这些基础真正扎实了看很多框架源码时进度会快很多。最后分享一点个人体会这几年带过不少新人发现一个规律能在技术上走远的人往往不是背了多少API而是能把底层机制理解透。二进制的计算就是这样一个底层机制——它不像框架那样有版本迭代几十年前是什么规则现在还是什么规则学会之后复利效应极其明显。我个人实际调试经验里最难忘的一次是排查一个跨平台文件解析问题同样的文件在x86服务器上解析正常换到ARM开发板上就多了几个字节的差异。最后定位到是结构体里没有加与字段对齐相关的处理导致的字段在两种架构下端点和对齐方式不同而这个问题的根源正是二进制数据在内存中的排列规则没有被充分考虑。所以如果你正卡在“为什么0.10.2不等于0.3”或者“为什么下载的二进制包跑不了”这类问题上不要烦躁。这说明你已经在往底层深挖了把二进制的计算弄明白后面学什么都会顺畅很多。这套工具思维不只是应付面试它会反复出现在你未来每一次排查线上问题、做性能优化、理解协议规范的现场。先把整数二进制加减法、负数补码、十进制小数转二进制这几块基础真正练到脑子里后面的路会越走越省力。