1. 为什么每个学汇编的人最后都要回头补位运算这一课刚接触汇编语言那会儿我犯过一个特别典型的错误把寄存器里的值当成数字来理解。比如看到MOV AX, 0FFH脑子里想的是哦AX 等于 255然后继续往下读。直到某天写一个端口控制的小程序需要单独把某个寄存器的第 3 位清零、第 5 位置一我盯着手册上那一串AND、OR、XOR、NOT指令愣了半天——明明每个字都认识组合起来就是不知道该怎么下手。后来才想明白问题根本不在指令本身而在于我对数在机器里到底长什么样这件事没有建立起正确的直觉。汇编语言和高级语言最大的区别就在这里高级语言里你操作的是值汇编里你操作的是位。一个字节就是八个开关一个寄存器就是十六个或者三十二个开关排成一排。你写的每一条指令本质上都是在拨这些开关。所以这篇内容我想聊的不是位运算的语法而是怎么把位这个视角真正装进脑子里。它适合两类人一类是刚开始学汇编、被各种进制和位操作绕晕的新手另一类是用惯了高级语言、想往下探一层、搞清楚底层到底在干什么的开发者。我会从进制这个最基础的地方讲起然后过渡到位的表示、位运算的实际用途最后落到汇编里那些和位打交道的指令该怎么用、什么时候用、用的时候容易踩什么坑。有一点要先说清楚进制和位运算不是学汇编的前置知识而是学汇编的核心内容本身。你越早把这块打通后面看寻址方式、看标志位、看内存对齐、看指令编码都会顺畅得多。反过来如果这块一直是模糊的那你写出来的汇编代码大概率是能跑但不知道为什么能跑一旦出问题就完全无从下手。2. 进制不是数学题是怎么数数的约定2.1 十进制的位权到底意味着什么我们从小用十进制用得太熟反而从来没想过它为什么长这样。十进制之所以叫十进制是因为它用十个符号0 到 9来表示数而且每个位置上的符号代表的分量不一样。拿 352 这个数来说它其实是352 3 × 100 5 × 10 2 × 1 3 × 10² 5 × 10¹ 2 × 10⁰这里的 10²、10¹、10⁰ 就是位权。从右往左每一位的权重是 10 的幂次指数从 0 开始递增。这个规则看起来简单但它是理解所有进制的钥匙——任何进制都是用 N 个符号位权是 N 的幂次。十进制用 10 个符号位权是 10 的幂二进制用 2 个符号0 和 1位权是 2 的幂十六进制用 16 个符号0-9 加上 A-F位权是 16 的幂。就这么点区别。2.2 二进制机器为什么偏偏选了两个符号很多人第一次学二进制都会问为什么计算机不用十进制明明十进制更符合人的习惯。答案藏在硬件里。计算机的底层是电路电路最擅长表达的状态就是通和断两种。要让它稳定地区分十种状态对应十进制的十个符号对电路的精度的要求会高得离谱稍微有点干扰就分不清 7 和 8 了。而只区分通和断两种状态容错空间极大成本也低。所以机器选了二进制不是因为它先进而是因为它省事且可靠。二进制的位权是 2 的幂次。比如二进制10111011 1 × 2³ 0 × 2² 1 × 2¹ 1 × 2⁰ 8 0 2 1 11从右往左权重分别是 1、2、4、8、16、32、64、128……这个序列建议直接背下来因为后面看寄存器、看内存地址、看标志位全是这些数在反复出现。2.3 十六进制给二进制瘦身的临时工二进制有个致命问题太长。一个 32 位的寄存器值写成二进制是 32 个 0 和 1眼睛都要看花。所以实际工作中我们几乎不用二进制直接书写而是用十六进制。十六进制和二进制之间有一个非常优雅的对应关系一位十六进制正好对应四位二进制。因为 16 2⁴。这意味着二进制和十六进制之间的转换不需要任何计算只需要按四位一组直接替换。十六进制二进制十进制000000100011200102300113401004501015601106701117810008910019A101010B101111C110012D110113E111014F111115这张表我建议你花十分钟默写几遍直到看到0xC脑子里立刻浮现1100看到1010立刻反应出A。这不是死记硬背而是后面所有位操作的基本功。举个例子二进制1101 0110按四位一组拆开1101是 D0110是 6所以它就是0xD6。反过来0x3F就是0011 1111。整个过程不需要做除法不需要算位权纯粹是查表替换。2.4 进制转换的通用方法除基取余与按权展开虽然十六进制和二进制之间可以秒转但十进制和它们之间的转换还是需要方法的。这里有两个方向方法不一样。十进制转其他进制除基取余倒序排列。比如把十进制的 156 转成二进制156 ÷ 2 78 ... 余 0 78 ÷ 2 39 ... 余 0 39 ÷ 2 19 ... 余 1 19 ÷ 2 9 ... 余 1 9 ÷ 2 4 ... 余 1 4 ÷ 2 2 ... 余 0 2 ÷ 2 1 ... 余 0 1 ÷ 2 0 ... 余 1把余数从下往上读10011100。验证一下128 16 8 4 156对的。其他进制转十进制按权展开求和。比如0x9C转十进制9 × 16¹ C × 16⁰ 9 × 16 12 × 1 144 12 156有意思的是156 的二进制是10011100十六进制是0x9C它们表示的是同一个数。这就是进制的本质——同一个量的不同书写方式就像一百五十六和156和CLVI说的是一回事。提示实际写汇编的时候不同汇编器对进制前缀的约定不完全一样。常见的是0x或H后缀表示十六进制0b或B后缀表示二进制0前缀表示八进制。写代码前先确认你用的汇编器认哪种写法否则会莫名其妙报错。3. 位、字节、字机器眼里的数据分层3.1 一个字节为什么是八位位bit是机器里最小的数据单位它只有两个状态通常记作 0 和 1。单独一个位能表达的信息极其有限所以机器把八个位捆在一起组成一个字节byte。为什么是八位而不是六位、十位这有一段历史演变简单说就是早期各家厂商各搞各的后来八位逐渐成为事实标准一直沿用至今。对现在的我们来说记住这个结论就够了一个字节 八位能表示 0 到 255无符号或者 -128 到 127有符号。再往上字word这个词在不同架构里含义不一样。在 16 位架构里一个字是 16 位两个字节在 32 位架构里一个字是 32 位64 位架构里就是 64 位。这个字长其实就是 CPU 一次能处理的数据宽度也是寄存器的大小。单位位数无符号范围有符号范围位 (bit)10 ~ 1-1 ~ 0字节 (byte)80 ~ 255-128 ~ 127字 (word, 16位)160 ~ 65535-32768 ~ 32767双字 (dword, 32位)320 ~ 4294967295-2147483648 ~ 21474836473.2 无符号和有符号同一串位两种解读这里有一个特别容易让人困惑的点机器里存的永远只是一串位它本身没有正负的概念。所谓有符号和无符号是我们解读这串位的方式不同。拿1111 1111来说如果按无符号解读它是 255如果按有符号解读最高位是 1表示负数它的值是 -1。同一串位两种答案取决于你怎么看它。有符号数的表示方法叫补码。补码的规则是正数的补码就是它本身负数的补码是取反加一。比如要表示 -11 的二进制0000 0001 取反 1111 1110 加一 1111 1111所以 -1 就是1111 1111。这也解释了为什么有符号的 8 位范围是 -128 到 127——1000 0000被规定为 -128而0000 0000是 0。补码这套设计不是随便定的它有个巨大的好处加法和减法可以用同一套电路实现。A - B在补码体系里等价于A (-B)机器不需要专门做减法直接做加法就行。这是硬件设计上的一个经典取舍。3.3 位权视角下的最高位和最低位在位的世界里有两个位置特别重要最高位MSBMost Significant Bit和最低位LSBLeast Significant Bit。最高位是一个数据里权重最大的那一位。对 8 位数据来说就是最左边那位权重是 2⁷ 128。它还有个特殊身份在有符号解读下它就是符号位0 表示正1 表示负。最低位是最右边那位权重是 2⁰ 1。它的特殊之处在于它决定了这个数是奇数还是偶数——最低位是 1 就是奇数是 0 就是偶数。这个性质在做奇偶判断、做位掩码的时候经常用到。理解这两个位置是理解后面所有位操作的前提。因为位操作很多时候就是在针对某一位做文章而最高位和最低位是最常被针对的两个。4. 四种基本位运算把开关拨来拨去4.1 AND清零和提取的利器AND 运算的规则很简单两个位都是 1 结果才是 1否则是 0。1 AND 1 1 1 AND 0 0 0 AND 1 0 0 AND 0 0AND 在汇编里有两个最典型的用途。第一个用途是清零某些位。任何位和 0 做 AND 都会变成 0和 1 做 AND 则保持不变。所以如果你想保留某些位、清掉另一些位就用一个掩码去 AND。比如想把1011 0110的高四位清零、低四位保留1011 0110 AND 0000 1111 ------------- 0000 0110掩码0000 1111就是0x0F。这个操作在提取一个字节的低四位时特别常用。第二个用途是检测某一位是否为 1。把目标位和 1 做 AND如果结果是 1说明那一位是 1如果是 0说明那一位是 0。比如检测1011 0110的第 2 位从 0 开始数1011 0110 AND 0000 0100 ------------- 0000 0100结果非零说明第 2 位是 1。这种用 AND 做位检测的写法在判断标志位、判断硬件状态寄存器的时候到处都是。4.2 OR置位和合并的主力OR 的规则是两个位只要有一个是 1结果就是 1。1 OR 1 1 1 OR 0 1 0 OR 1 1 0 OR 0 0OR 最典型的用途是置位。任何位和 1 做 OR 都会变成 1和 0 做 OR 则保持不变。所以想把某些位置 1、其他位不动就用一个掩码去 OR。比如想把1011 0110的第 0 位置 11011 0110 OR 0000 0001 ------------- 1011 0111OR 的另一个用途是合并两个数据的不同部分。比如你有两个半字节想把它们拼成一个字节高四位来自 A低四位来自 B那就把 A 左移四位后和 B 做 OR。4.3 XOR翻转和清零的双面手XOR异或的规则是两个位不同结果为 1相同结果为 0。1 XOR 1 0 1 XOR 0 1 0 XOR 1 1 0 XOR 0 0XOR 有两个非常有意思的性质。性质一和 1 做 XOR 会翻转该位和 0 做 XOR 保持不变。所以 XOR 可以用来翻转指定的位。比如想把1011 0110的第 3 位翻转1011 0110 XOR 0000 1000 ------------- 1011 1110性质二一个数和自己做 XOR 结果是 0。这个性质常被用来清零寄存器比如XOR AX, AX就是把 AX 清零。它比MOV AX, 0有个好处指令更短执行更快而且不依赖立即数。这是汇编里一个很经典的小技巧。4.4 NOT全盘取反NOT 是一元运算规则最简单0 变 11 变 0。NOT 1011 0110 0100 1001NOT 单独用的场景不算多但它经常和 AND、OR 配合用来构造反向掩码。比如你想清零某些位但掩码写起来是要保留的位那就先 NOT 一下再 AND。4.5 移位运算位运算里的乘除法除了上面四种逻辑运算还有移位运算包括左移SHL、逻辑右移SHR、算术右移SAR和循环移位ROL、ROR。左移一位相当于乘以 2右移一位相当于除以 2对无符号数而言。这个性质让移位成为一种极快的乘除法替代方案。比如SHL AX, 1就是把 AX 乘以 2比用乘法指令快得多。逻辑右移和算术右移的区别在于最高位补什么。逻辑右移补 0算术右移补符号位。所以算术右移用于有符号数的除法能保持符号不变。运算符号典型用途与AND清零、提取、检测位或OR置位、合并异或XOR翻转、清零寄存器非NOT取反、构造反向掩码左移SHL乘以 2 的幂逻辑右移SHR无符号除以 2 的幂算术右移SAR有符号除以 2 的幂5. 位运算在汇编里的真实使用场景5.1 用掩码操作硬件寄存器这是位运算在汇编里最硬核的用途。硬件设备的控制寄存器通常是一个字节或者一个字里面每一位或者几位代表一个功能开关。你要控制某个功能就得精确地操作对应的位不能影响其他位。假设某个控制寄存器的定义是这样的位功能0使能位1中断允许2-3工作模式00/01/10/114保留5复位6-7保留现在你想使能设备、允许中断、设置工作模式为 10其他位不动。做法是; 假设寄存器地址在 DX 指向的端口 IN AL, DX ; 先读当前值 OR AL, 0000 0011B ; 置位第 0、1 位 AND AL, 1111 0011B ; 清零第 2、3 位 OR AL, 0000 1000B ; 设置第 3 位为 1即模式 10 OUT DX, AL ; 写回注意这里的顺序先读、再改、最后写回。这个读-改-写的模式是操作硬件寄存器的标准流程因为很多寄存器不支持单独写某一位你只能整个字节写回去所以必须先把当前值读出来改完再写回去否则会把其他位冲掉。注意读-改-写这个流程在多任务或者中断环境下要特别小心。如果读和写之间被打断另一个流程也改了同一个寄存器你的写回就会覆盖掉对方的修改。这种问题叫竞态排查起来非常痛苦。如果环境允许尽量用硬件提供的原子位操作指令或者用关中断保护这段代码。5.2 用位运算做乘除法优化编译器在优化代码的时候经常把乘以常数替换成移位和加减的组合。比如x * 10会被优化成(x 3) (x 1)因为 10 8 2。这样比直接用乘法指令快。在汇编里手写这种优化需要对二进制分解很熟。比如乘以 9x * 9 x * (8 1) (x 3) x乘以 15x * 15 x * (16 - 1) (x 4) - x这种技巧在性能敏感的代码里很常见。不过要注意现代 CPU 的乘法指令已经很快了这种优化不一定总是有收益具体要看目标平台。我的经验是先写清楚的版本测出瓶颈再优化不要一上来就手动移位。5.3 用位运算压缩和打包数据当内存或者存储空间紧张的时候我们经常把多个小数据塞进一个字节或者一个字里。比如一个字节存两个 4 位的值一个字存四个 4 位的值。打包; 把高四位 A 和低四位 B 打包成一个字节 MOV AL, A SHL AL, 4 ; A 移到高四位 OR AL, B ; 低四位放 B解包; 从 AL 里取出高四位和低四位 MOV AH, AL AND AH, 0F0H ; 取高四位 SHR AH, 4 ; 移到低位 AND AL, 0FH ; 取低四位这种打包解包在图形处理、通信协议、文件格式解析里到处都是。它的核心就是移位 掩码这两个动作。5.4 用位运算实现标志位的批量管理程序里经常需要管理一组布尔状态比如是否登录是否有权限是否已读等等。如果每个状态用一个变量存浪费空间用一个整数的不同位来存就非常紧凑。; 定义标志位 FLAG_LOGIN EQU 0000 0001B FLAG_ADMIN EQU 0000 0010B FLAG_VERIFIED EQU 0000 0100B ; 设置标志 OR FLAGS, FLAG_LOGIN ; 清除标志 AND FLAGS, NOT FLAG_LOGIN ; 检测标志 TEST FLAGS, FLAG_ADMIN JNZ IS_ADMINTEST指令和AND类似但它不保存结果只影响标志位。所以检测某一位的时候用TEST比AND更合适因为它不会破坏原值。6. 新手最容易栽的几个坑6.1 混淆位和字节的编号这是最经典的坑。说第 3 位的时候到底是从 0 开始数还是从 1 开始数是从左往右还是从右往左行业惯例是从 0 开始从右往左数。最低位是第 0 位往左依次是第 1、2、3 位。所以一个字节的第 7 位是最高位。但问题是有些文档不遵守这个惯例有的从 1 开始数有的从左往右数。我踩过的坑就是看一份硬件手册它说设置第 1 位使能我按从 0 开始的理解去操作结果怎么都不对后来才发现它说的第 1 位其实是最低位也就是我理解的第 0 位。我的建议是看任何文档先确认它的位编号约定最好找一张位定义图对照。如果文档没写清楚就找例子反推。6.2 有符号和无符号混用前面说过同一串位按有符号和无符号解读结果不同。如果你在应该用无符号的地方用了有符号指令或者反过来结果就会出错。最典型的是右移。逻辑右移SHR补 0算术右移SAR补符号位。对一个负数做逻辑右移结果会变成一个很大的正数完全不是你想要的效果。MOV AX, -8 ; AX 1111 1111 1111 1000 SAR AX, 1 ; AX 1111 1111 1111 1100 -4正确 SHR AX, 1 ; AX 0111 1111 1111 1100 32764错误所以做除法的时候一定要先想清楚数据是有符号还是无符号再选对应的移位指令。6.3 掩码写错一位位运算的掩码是二进制写的一位写错整个逻辑就错了。而且这种错误特别隐蔽因为程序可能看起来能跑只是某个功能偶尔不对。我的习惯是掩码不要直接写二进制而是用移位表达式来写。比如要第 3 位的掩码写1 3而不是0000 1000B。这样不容易数错位而且意图更清楚。如果汇编器支持常量表达式尽量用这种方式。6.4 忘记位运算的优先级在汇编里位运算指令是独立的不存在优先级问题。但在高级语言里嵌入汇编或者写宏的时候位运算的优先级经常出问题。AND的优先级比OR高XOR在两者之间移位又比它们都高。记不清就加括号这是最省事的做法。6.5 忽视标志位的影响很多位运算指令会影响标志位比如AND、OR、XOR、TEST都会影响 ZF零标志、SF符号标志、PF奇偶标志并且会把 CF进位标志和 OF溢出标志清零。如果你在一条位运算指令后面依赖之前设置的 CF 或 OF就会出错。CMP AX, BX ; 设置标志位 AND CX, 0FH ; 这条指令把 CF 和 OF 清零了 JA LABEL ; 这里的跳转条件已经被破坏这种错误在写条件跳转的时候特别容易犯。解决办法是位运算和条件跳转之间不要插入其他指令或者把标志位先保存起来。7. 把位运算练成肌肉记忆的几个方法7.1 手算二进制和十六进制的互转这是最基础的练习。随便写一个十六进制数比如0xA7然后立刻在纸上写出它的二进制1010 0111再写出它的十进制 167。反过来也一样。每天练十个一周之后基本就是条件反射了。这个练习看起来枯燥但它是后面所有位操作的地基。地基不牢后面看掩码、看寄存器定义、看指令编码全是懵的。7.2 用位运算解决实际问题光看规则记不住得用。我建议找几个小场景来练手判断一个数是不是 2 的幂n AND (n - 1)等于 0 就是 2 的幂交换两个数不用临时变量a XOR b三次统计一个字节里 1 的个数循环移位加计数反转一个字节的位序逐位取出再拼回去这些练习不需要多高深但能让你对位运算的手感建立起来。7.3 读真实的寄存器定义文档找一份硬件手册看它的寄存器定义表然后尝试用位运算写出设置某个功能的代码。这个过程会让你真正理解为什么位运算在底层编程里这么重要。我个人的经验是看十遍规则不如动手写一次读-改-写的代码。当你亲手把一个寄存器的某一位从 0 改成 1并且看到硬件真的响应了那种理解是看书看不来的。7.4 用调试器观察位的变化写一段位运算的代码用调试器单步执行每一步都观察寄存器的值怎么变。特别是看二进制视图能直观地看到哪一位被改了、哪一位没动。这个习惯我强烈推荐。很多人学汇编只看书不调试结果对位在寄存器里怎么排列始终没有直观印象。调试器能把这个过程可视化比任何文字描述都有效。8. 进制和位运算的延伸从字节到更大的世界把位和进制这块打通之后你会发现很多之前觉得神秘的东西突然变得清晰了。比如内存地址。为什么地址经常写成0x7FFF_FFFF这种形式因为十六进制和二进制对应看十六进制就能直接知道哪些位是 1。0x7FFF_FFFF的最高位是 0说明这是一个正数接近 32 位有符号数的上限。比如字符编码。ASCII 码为什么 A 是 65、a 是 97因为 65 是0100 000197 是0110 0001它们只差第 5 位。所以大小写转换可以用XOR 0x20来实现这是一个非常经典的位运算技巧。比如网络协议里的标志位。TCP 头部的标志字段有 6 个标志位每个位代表一个控制信号。解析这些标志的时候就是标准的位检测操作。比如图形处理里的颜色。一个 32 位颜色值通常是 ARGB 或者 RGBA每个通道 8 位。要提取红色通道就是右移 16 位再 AND 0xFF。这些东西表面上五花八门底层全是同一套东西进制是书写方式位是存储方式位运算是操作方式。把这三件事想通了剩下的都是应用。我在实际写汇编的过程中最大的体会是位运算不是需要记住的一堆规则而是看数据的一种视角。当你习惯性地把每个字节看成八个开关把每个寄存器看成一排开关很多指令的用途就变得不言自明了。AND就是保留某些开关、关掉其他开关OR就是打开某些开关XOR就是翻转某些开关。用这个视角去看汇编代码就不再是天书而是一份清晰的开关操作说明书。最后分享一个小技巧如果你在写位运算代码的时候不确定掩码对不对就把它写成二进制然后一位一位地对照需求数。数的时候用手指着屏幕从左往右或者从右往左都行关键是保持方向一致。这个笨办法帮我避免过无数次差一位的低级错误。
阅读完成 · 觉得有帮助?