4.2 算术、逻辑与移位指令的语义


4.2 算术、逻辑与移位指令的语义

本节摘要:本节逐类清算运算指令的语义承诺:结果放哪、溢出去哪、移位空位填什么、比较结果怎么表达。x86 与 RISC-V 两套风格对照着讲——标志位派与寄存器比较派。读完你应当能预判任意一条运算指令的边界行为,这正是排查数值缺陷时最值钱的预判力。

运算指令的语义清单

"加法就是加法"是最危险的假设。指令的语义承诺至少要回答四个问题:结果写到哪;溢出时发生什么;参与比较时答案放哪;除法的余数与舍入方向是什么。四个问题的答案组合起来,才是完整的"加法"或"除法"。本节按算术、逻辑、移位、比较的顺序清点,每类都用两家的真实指令对照。

**算术指令:溢出去哪是分水岭。**x86 的 add 把结果照算不误,同时把"无符号进位"记入 CF 标志、"有符号溢出"记入 OF 标志——溢出是副产品登记,不影响结果本身。RISC-V 走另一路:add 就是无脑截断,溢出不留痕迹;需要知道溢出时用组合拳——add 后接 sltu(无符号比较)比对结果大小,或干脆用带内建溢出判定的扩展指令。SIMD 领域还有第三种答案:饱和运算,溢出不回绕而是钳到最大最小值(音频处理最爱,削顶不失真)。三种语义各有辖区,写跨平台数值代码时先确认你要的是哪一种。

**乘除法:高半段与余数的去向。**两个 32 位数相乘,积可能要 64 位才装得下,于是乘法指令家族分裂成两个半场:mul 只给低半段(高位直接丢弃),mulh 只给高半段——要完整积就得两条连用。x86 更干脆:mul/imul 把 64 位积整体拆进两个固定寄存器(edx:eax 联手)。除法的余数同样有专属寄存器(x86 的 edx 收余数;RISC-V 用 rem 与 div 两条指令分开取商与余)。还有一处高频陷阱:有符号整数除法的舍入方向是向零取整——-7 / 2 = -3 而不是 -4;对应地,余数的符号跟被除数走。以为除法向下取整的代码,在负数上必然翻车。

**逻辑指令:位级的无溢出世界。**与、或、异或、取反四件套语义干净:按位独立、无进位无溢出。值得记住的是几个高频技巧对应的指令形态:清零用自身异或(xor eax, eax,比 mov 短且不碰内存),取低位掩码用与(andi a0, a0, 0xff),交换两寄存器用三连异或(教学趣味为主,实战少用)。逻辑指令是位包装规则(4.1 节)的直系消费者——你对补码包装的理解有多深,读逻辑代码就有多顺。

**移位指令:空位填什么是本质。**左移低位补零,没有争议;右移的空位是分水岭——逻辑右移高位补零(适合无符号),算术右移高位补原符号位(适合有符号,保持负数仍为负)。C 代码里的 >> 对有符号与无符号变量生成的就是这两种不同指令。还有两条边界纪律:移位量超过位宽时结果归零或未定义(C 语言里移位量大于等于 32 是未定义行为,编译器可以生成任何东西);x86 的可变移位量只认 cl 寄存器,RISC-V 则随便用哪个源寄存器。编译器把"乘除 2 的幂"换成移位是有条件的:无符号除法 x >> 3 直接等价,有符号除法则要额外加修正项来对齐向零取整——反汇编里看到"加常量再右移"的组合,就是它在补这个修正。

两家风格对照表

语义点 x86(标志位派) RISC-V(寄存器比较派)
溢出记录 CF(无符号)与 OF(有符号)双标志 不记录,需 slt 系比较指令自查
比较结果 减法 + 标志,跳转消费 slt/sltu 直接把 0 或 1 写进寄存器
乘法高半段 固定拆进 edx:eax mulh 系列指令按需取高半段
余数 除法指令顺带写入 edx rem/remu 独立成指令
可变移位 移位量必须在 cl 任意源寄存器

💡 两派没有优劣,只有服务对象不同:标志位派为"算完立刻条件跳转"的密集控制流省指令;寄存器比较派为乱序执行扫清障碍——标志位是全核共享的单点状态,乱序机器要额外硬件去"猜名字"(5.4 节展开),干脆不用它。

一个完整的语义现场

把这些语义放进同一段代码。设有符号 32 位运算 int r = (a * b) / (c + 1);,编译器在 x86-64 上的典型展开:

mov eax, [a] ; 取 a imul eax, [b] ; 有符号乘:只保留低 32 位,溢出标志 OF 若置位也不停机 add ecx, 1 ; c+1,若 c 是 -1 则除数为零 cdq ; 把 eax 符号扩展进 edx:eax(除法前的标准动作) idiv ecx ; 有符号除:商进 eax,余数进 edx,向零取整 ; 若 ecx 为零 → 触发除零异常;若商装不下 → 同样异常

每一行都对应本节的一个语义点:imul 的溢出静默、cdq 的符号扩展铺垫、idiv 的双寄存器结果与向零取整、以及两个真实的异常入口(除零与商溢出,注意有符号最小值除以 -1 会商溢出——INT_MIN / -1 是著名的崩溃配方)。读懂这段汇编,你就读懂了半本整数语义的说明书。

容易踩的坑

第一坑:混淆"未定义行为"与"有定义的静默"。C 语言的移位越界是有符号溢出都是未定义行为,编译器有权为所欲为;而机器指令层面的截断与标志位是精确定义的。两层语义之间的落差(语言层未定义、机器层有定义)是优化器"删掉你的检查代码"的根源——比如判断 x + 1 < x 来检测溢出,优化器认为有符号溢出不可能而整段删除。要可靠检测溢出,用编译器内建函数(如按 4.2 的语义包装好的 checked 算术内建),别用依赖溢出的比较。第二坑:把无符号移位当成"更安全的除法"。x >> 1x / 2 在负数上不等价:前者向下取整(算术右移),后者向零取整。第三坑:忽视 SIMD 饱和语义与标量语义的差异,把饱和结果当成回绕结果继续算——音视频代码里这是精度事故的常见源头。

语义自查三连问

给本节的清单配上一个使用入口:读到任何一段运算代码,按三连问过一遍。第一问,操作数的宽度与符号——char 提升成 int 再运算、无符号与有符号混比,都在这一步决定走哪条语义分支;第二问,结果的消费者是谁——直接比较(关注标志或比较指令)、继续参与运算(关注截断与舍入)、还是存回窄类型(关注再次截断);第三问,边界输入是什么——零、最大值、最小值、负一,四个边界值过一遍,溢出与除零的坑基本现形。拿 int r = a * b; if (r < 0) 走一遍:宽度 32 位有符号、结果用于比较、边界 a 与 b 同负异号——INT_MIN * -1 的商溢出、乘法的静默截断两个隐患当场现形。三连问不能替代测试,但能在代码评审与排错时把直觉升级为 checklist。

本节要点回顾

  • 运算语义四问:结果放哪、溢出去哪、比较怎么答、除法怎么舍——每条指令的完整承诺都由这四问构成。
  • 溢出三种处理:x86 登记标志、RISC-V 静默截断、SIMD 饱和钳位——辖区不同,别混用。
  • 乘法有高半段,除法有余数寄存器;有符号除法向零取整,负数除法与移位不等价。
  • 右移分逻辑与算术两种,空位填零或填符号位是它们的全部分别。
  • 标志位派与寄存器比较派的分野,本质是控制流密度与乱序执行友好度的取舍。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U