ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

一条 MOV 指令报错引出 ARM32/AArch64 立即数编码与合法性判据

一条 MOV 指令报错引出 ARM32/AArch64 立即数编码与合法性判据 1. 一条 MOV 指令报错引出的连锁疑问前段时间帮人看一段启动代码他写了这么一行MOV R0, #0x102汇编器直接甩回来一句invalid constant。他盯着这数看了半天没想明白——0x102 换算成十进制才 258连三位十六进制都没占满怎么就不能当立即数用了更让他困惑的是把 0x102 改成 0x100 就过了改成 0x101 又不过。三个相邻的数两个非法一个合法规律在哪儿这个问题我在带新人的时候遇到过太多次也见过不少做了很多年底层的人在被追问时答不完整。原因不复杂**立即数的合法性跟数值大小几乎没关系跟这个数在二进制层面长什么样关系极大。**你写的十进制或者十六进制写法只是给人看的真正决定命运的是那串 32 位或者 64 位的 bit 能不能塞进指令编码里留出来的那十几个位。本文要拆的就是这件事。我打算从 ARM32 的数据处理立即数开始因为它是最经典、判据最容易被讲错的一个然后走到 AArch64 的逻辑立即数那里编码方式完全换了一套思路再横向对比 RISC-V、MIPS、x86 的边界最后给一套遇到invalid immediate时可以直接照着走的排查链路。适合正在写汇编、调启动代码、做编译器后端或者反汇编分析的同学也适合只想知道为什么我写的常量编不过的嵌入式开发者。读完之后你应该能拿一张纸和一支笔手算出一个数合不合法而不用反复试错。2. 立即数合法性到底在约束什么位数预算与编码方式2.1 指令字长是那道搬不动的墙先建立一个最基础的认知指令本身是有固定长度的。ARM32 是 32 位定长AArch64 也是 32 位定长RISC-V 的 32 位指令同样定长压缩指令是 16 位。一条指令要同时装下操作码、目标寄存器、源寄存器、条件码、标志位设置位等等剩下的才轮到立即数。这就像一张 A4 纸的表格行高列宽都定死了你想往备注那一栏里塞一句话格子就那么大。举个具体的ARM32 数据处理指令MOV、ADD、AND、CMP 这些的 32 位里条件码占 4 位操作类型和 S 位占 4 位Rn 占 4 位Rd 占 4 位剩下 12 位留给第二个操作数。12 位放一个 32 位数显然放不下。所以架构设计者必须做取舍——要么把立即数范围压缩要么把范围保留但换一种编码方式表达更多信息。2.2 立即数位宽和可用数值范围不是一回事这里是最容易混淆的地方**立即数字段有 N 位不代表你能用的数值范围就是 2 的 N 次方。**如果是直接的二进制补码编码那确实 12 位能表示 -2048 到 2047。但 ARM32 偏偏不这么干它把这 12 位拆成了 4 位旋转量加 8 位数据用数据 循环右移的方式把可用集合撑大到了一定程度代价是集合变得不连续——中间有大量空洞。这两种设计路线的差别值得说清楚。RISC-V、MIPS、x86 走的是老实补码路线字段多少位就是多少位的连续数值范围代价是能装的数小。ARM32 走的是编码扩展路线12 位字段能表达大约 4000 多个不同的 32 位常量覆盖了不少常见掩码和常量但分布是散的判断起来需要算。你以后看到任何架构的立即数限制第一件事就是问它用的是哪种路线这决定了你后面要用看数值范围还是看位模式的方法去判断。2.3 为什么架构设计者要选这种别扭的编码有人会问既然不连续这么麻烦为什么不干脆都做成 16 位有符号补码简单明了答案是指令位宽被别的东西吃掉了。ARM 的 12 位立即数字段本来是要和移位后的寄存器操作数共用同一个编码位置的。在一条ADD R0, R1, R2, LSL #3里那 12 位装的是R2、移位类型和移位量在ADD R0, R1, #0x3F里同样的 12 位装的是立即数。为了不额外增加指令位宽两条路径共享一个字段就必须让立即数的 12 位也带上旋转语义。这是一个非常典型的在约束下做权衡的设计。这就是为什么在 ARM32 上0x3F、0xFF000000、0xF000000F这些看起来毫无规律的数反而是合法的而0x101、0x102、0xFFFF这些人畜无害的数反而非法。理解了这个设计动机后面的判据就不会觉得是死记硬背了。3. ARM32 数据处理立即数8 位数据加 4 位循环右移的完整判据3.1 12 位立即数字段的拆解方式先把编码讲清楚。ARM32 数据处理指令里12 位的立即数字段是这样分配的位区间名称含义bit[11:8]rotate循环右移量单位是 2 位bit[7:0]imm88 位无符号数据实际参与运算的值value ROR(imm8, 2 * rotate)其中ROR(x, n)是 32 位环上的循环右移。注意rotate只有 4 位取值 0 到 15所以实际移位量是 0、2、4……30共 16 种全是偶数。为什么是偶数因为如果允许奇数移位很多位模式会绕不过一圈编码空间会有浪费。举个实际例子。MOV R0, #0x100里0x100的位模式是0000 0000 0000 0000 0000 0001 0000 0000。这个值等于ROR(0x01, 24)所以imm8 0x012 * rotate 24即rotate 12。编码字段是1100 00000001。再看MOV R0, #0xFF等于ROR(0xFF, 0)rotate 0imm8 0xFF编码字段是0000 11111111。3.2 判据的朴素版本环状视角下找 24 个连续的 0把上面的编码公式反过来推就能得到判据。因为imm8只有 8 位扩展到 32 位参与循环移位时它的高 24 位全是 0。循环移位只是把这 24 个 0 搬到环上的另一个位置但它们的连续性是保持的。所以任何一个合法值在 32 位环形视角下必然存在至少 24 个连续的 0。反过来也成立如果你在一个 32 位数里找到了连续 24 个 0那剩下的 8 位就是imm8把整个环旋回去就能得到编码而且因为剩下的 8 位是连续的旋转量天然是 8 的倍数也就一定是偶数。于是判据可以写成一句话把数值写成 32 位二进制首尾相接成一个环如果环上存在连续 24 个或更多的 0这个数就能用 MOV 类指令直接编码。回到开头那个问题。0x100的二进制是...0001 0000 0000从 bit31 到 bit9 一共 23 个 0再接上 bit7 到 bit0 又是 8 个 0但因为 bit8 是 1 夹在中间最长连续 0 串是 23 加 8 里各算各的——实际最长是 23不够 24等等这里得仔细算。我重新数一遍0x100bit31 ... bit9 23 个 0 bit8 1 bit7 ... bit0 8 个 0环上的最长连续 0 串是 23 8 31 个吗不是因为 bit8 的 1 把它们隔断了环上从 bit9 逆时针走到 bit7 必须穿过 bit8。所以是两段一段 23 个一段 8 个。最长 23。但那 8 个 0 是接在环的头尾的——bit7 到 bit0 之后紧接 bit31、bit30……它们本来就是连着的。所以环上从 bit7 往低位方向走bit7、bit6……bit0然后跳到 bit31、bit30……一直到 bit9共 8 23 31 个连续 0。是 31够了。我之前数错了方向这里正好说明一个实操要点**算最长连续 0 串的时候必须把首尾接起来看不能只看线性序列。**很多人在纸上数着数着就忘了首尾相接导致误判。那0x101呢bit0 和 bit8 都是 1。环上的 0 段被切成两截bit31 到 bit9 是 23 个bit7 到 bit1 是 7 个。最长 23不够 24非法。0x102同理bit1 和 bit8 是 1最长 0 串 23非法。3.3 一张表看清哪些数合法、哪些不合法把常见常量过一遍感受会更直观数值32 位模式特征最长连续 0 串MOV 可编码说明0x00000000全 032是imm80, rot00x000000FF低 8 位全 124是imm80xFF, rot00x0000FF00次低 8 位全 124是imm80xFF, rot120x00000100单个 bit831是imm80x01, rot120x00000101bit8bit023否被两个 1 夹断0x00000102bit8bit123否同上0x0000FFFF低 16 位全 116否需要 MOVW0x3F000000bit29..bit24 全 124是imm80x3F, rot40xF000000F首尾各 4 位全 124是imm80xFF, rot20xFF00FF00交替 8 位块8否最长 0 串不够0x55555555交替单 bit1否完全散开0xF000000F这一行很多人第一眼会判成非法因为看起来两头都有 1中间一大片 0很别扭。但它恰恰是合法的imm8 0xFF循环右移 4 位正好得到这个模式。这也说明眼睛看形状不可靠一定要走判据或者写脚本。3.4 用 Python 把判据写成可执行代码纸面判断容易出错我一般直接写个小函数放在工具库里随时算def arm32_mov_imm(value): 判断 value 能否作为 ARM32 数据处理指令的立即数MOV 形态。 返回 (rotate, imm8)不合法返回 None。 value 0xFFFFFFFF for rotate in range(16): shift 2 * rotate # ROR(x, n) 等价于 (x n) | (x (32 - n)) if shift 0: rotated value else: rotated ((value shift) | (value (32 - shift))) 0xFFFFFFFF # 旋转之后如果高 24 位全为 0说明低 8 位就是我们需要的 imm8 if rotated 0xFF: return rotate, rotated return None for v in [0x100, 0x101, 0x102, 0xFFFF, 0x3F000000, 0xF000000F, 0xFF00FF00]: print(hex(v), arm32_mov_imm(v))跑出来结果是0x100 (12, 1) 0x101 None 0x102 None 0xffff None 0x3f000000 (4, 63) 0xf000000f (2, 255) 0xff00ff00 None注意这里的循环方向我做的事情是把待判定的值循环右移回去看能不能落到 0 到 0xFF 范围内。这和前面找 24 个连续 0是等价的但代码实现更不容易数错。提示这个函数只回答能不能用 MOV 形态编码。实际写汇编时还要考虑是否可以用 MVN 形态下一节专门讲。4. MOV 与 MVN 的分工为什么 0xFFFFFF00 也算能被编码4.1 取反之后再看一遍0xFFFFFF00这个数最长连续 0 串只有 8 位低 8 位最长连续 1 串是 24 位高 24 位。按上一节的判据MOV 形态是编不出来的。但如果你在汇编里写MOV R0, #0xFFFFFF00某些工具链未必报错——因为它会换成MVN R0, #0xFF。MVN 是取反后传送它对立即数的约束和 MOV 完全一样只是内部多了一次按位取反。所以MVN Rd, #imm 等价于 Rd ~ROR(imm8, 2*rotate)由此得到扩展判据如果value的最长连续 0 串不足 24就把~value拿出来再算一次。若~value满足条件则该数可以用 MVN 形态编码。用0xFFFFFF00验证~0xFFFFFF00 0x000000FF最长连续 0 串 24 位imm8 0xFFrotate 0。所以写成MVN R0, #0xFF。再看0xFFFFFFFF~0xFFFFFFFF 0x00000000合法MVN R0, #0就是全 1。而MOV R0, #0xFFFFFFFF在任何 ARM32 汇编器上都会被拒绝这不是工具的问题是你把指令选错了。把判据扩展开就得出了一个更常用的工程写法**判断时同时看24 个连续 0和24 个连续 1前者走 MOV后者走 MVN。**很多资料上说的最长连续相同位串达到 24 就合法说的就是这个合并版本但省略了 MOV/MVN 的区分。如果你只知道合并版判据就会出现函数说合法但汇编器还是报错的尴尬——因为你把该用 MVN 的数写成 MOV 了。4.2 汇编器与编译器对这个问题的处理差异这里有个实操细节值得说清楚。不同工具链对自动换 MVN这件事的态度不一样。ARM 官方的 armasm 在大部分情况下不会自动把MOV Rd, #const换成MVN它会直接报错逼你显式写 MVN。GNU as 在某些版本上会尝试替换某些版本不会。LLVM 的集成汇编器则倾向于严格按指令语义报错。所以你写代码时最稳的做法是自己判断自己选指令不要指望汇编器帮你兜底。编译器后端就宽容得多。你写 C 代码a 0xFFFFFF00;编译器会自己决定是生成 MOV 移位、MVN还是从常量池加载。这也解释了一个现象同样的常量在 C 里从来不报错一挪到内联汇编就编不过——因为 C 编译器有完整的常量构造能力而汇编指令本身没有。顺便说一句常量池加载ARM32 上的LDR Rd, const是终极兜底方案。它把常量放在代码段附近的字面量池里用 PC 相对寻址加载。代价是一次访存好处是任意 32 位常量都能用。我的经验是如果某个常量在一个循环里只出现一次用常量池没什么损失如果在热循环里反复出现就要花点心思用 MOV/MVN/ORR 组合把它构造出来。5. AArch64 的逻辑立即数N:immr:imms 三段式编码5.1 元素重复模式才是合法性的本质从 ARM32 换到 AArch64很多人第一反应是64 位了立即数应该更宽松吧。实际情况是算术立即数确实变成了 12 位可选左移 12 位但逻辑立即数AND、ORR、EOR、ANDS 用的换成了一套完全不同的编码判据比 ARM32 还要绕。AArch64 逻辑立即数用三个字段联合描述N1 位、immr6 位、imms6 位。它的核心思想不是一个值循环移位而是一个位模式在寄存器内重复填充。理解这一点之后很多现象就顺了。下面这些值都是合法的0x00000000000000FF 元素 64 位低 8 位全 1 0x0101010101010101 元素 8 位每字节都是 0x01 0x0F0F0F0F0F0F0F0F 元素 8 位每字节都是 0x0F 0x0000FFFF0000FFFF 元素 32 位每 32 位是 0x0000FFFF 0xFF00FF00FF00FF00 元素 16 位每 16 位是 0xFF00 0x5555555555555555 元素 2 位重复 0b01看着毫无规律的一堆常量共同点是它们在某个元素粒度上是完全一致的重复。而下面这些就不合法0x0000000000000101 0x01 和 0x00 在字节级不一致 0x123456789ABCDEF0 任何粒度都不重复 0xFFFFFFFFFFFFFFFF 全 1编码里被保留 0x0000000000000000 全 0编码里被保留最后两条值得单独讲见 5.3。5.2 解码算法逐步推演ARM 官方手册给出的解码流程大致是这样len 最高有效 1 的位置 (基于 (N 6) | (~imms 0x3F)) 如果 len 1则该编码未分配 size 1 len 如果 size 数据宽度32 或 64则该编码未分配 levels size - 1 S imms levels R immr levels 如果 S levels则该编码未分配 esize 1 size welem (全 1 的 esize 位数) (esize - 1 - S) # 低 S1 位为 1其余为 0 wmask welem 按 esize 复制铺满整个数据宽度再在元素内循环右移 R 位反过来做判断就更直观**给定一个值尝试所有元素大小2、4、8、16、32、64看是否存在某个元素大小使得整个值由同一个元素模式重复填充构成且该元素模式本身是循环移位意义下的连续 1 串。**两个条件都满足就合法。拿0xFF00FF00FF00FF00走一遍。先试元素 16 位每个 16 位块都是0xFF00一致。再看0xFF00本身是不是连续 1 串的循环移位——0xFF00是0000 0000 1111 1111反过来看0xFF00循环右移 8 位得到0x00FF而0x00FF的低 8 位全 1是连续 1 串。所以R 8S 7。合法。再拿0x0000FFFF0000FFFF走一遍。试元素 32 位两个 32 位块都是0x0000FFFF一致。0x0000FFFF的低 16 位全 1本身就是连续 1 串R 0S 15。合法。再看0x0000000000000101。元素 64 位时它不是连续 1 串bit0 和 bit8 分开了。试元素 2 位低位字节是0x01按 2 位切是01 00 00 00不一致。元素 8 位0x01和0x00不一致。元素 16 位0x0101和0x0000不一致。全试完非法。5.3 全 0 和全 1 为什么反而进不去这是 AArch64 上一个非常经典的坑。AND x0, x1, #0编不过ORR x0, x1, #0xFFFFFFFFFFFFFFFF也编不过。原因在解码算法里元素模式的 1 的个数由S决定是S 1个。而当S levels时编码被标记为未分配。levels size - 1元素大小是1 size位。也就是说元素内最多只能有size个 1而size远小于元素位数。元素 64 位时size 6最多 6 个 1全 1 根本表达不了。全 0 的问题更直接welem至少要有一个 1因为S 1至少是 1S不能是负数所以元素模式不能全 0。实际操作里遇到这两种情况怎么办用零寄存器或者 MOV 别名AND x0, x1, xzr ; 结果为 0 ORR x0, x1, xzr ; 相当于 MOV MOV x0, #0 ; 汇编器展开为 ORR x0, xzr, xzr MOV x0, #-1 ; 汇编器展开为 MOVN x0, #0注意MOV x0, #-1这个伪指令在 AArch64 汇编里是允许的它被展开成MOVN x0, #0走的是移动立即数MOVN/MOVZ/MOVK那条路径和逻辑立即数不是一套编码。5.4 ADD/SUB 的 12 位立即数与左移 12 位算术立即数比逻辑立即数好理解得多。ADD、SUB、ADDS、SUBS用的格式是 12 位立即数加一个可选的左移 12 位标志value imm12 (12 if shift else 0)所以合法范围是形态取值范围步长不移位0x000 ~ 0xFFF1左移 12 位0x1000 ~ 0xFFF0000x1000这意味着ADD x0, x1, #0x1000是合法的imm12 1shift 1而ADD x0, x1, #0x1001是非法的不是 0x1000 的倍数ADD x0, x1, #0x100000也非法超出 0xFFF000。我遇到过有人写ADD sp, sp, #0x2000编不过然后怀疑栈指针指令有什么特殊限制——其实不是只是 0x2000 虽然超过了 0xFFF但它刚好是 0x1000 的倍数imm12 2加左移就合法了。真正编不过的是ADD sp, sp, #0x1800这类不是 4096 倍数的值。构造任意大常量时AArch64 的套路是 MOVZ/MOVK 组合MOVZ x0, #0x5678 ; x0 0x0000000000005678 MOVK x0, #0x1234, LSL #16 ; x0 0x0000000012345678 MOVK x0, #0x9ABC, LSL #32 ; x0 0x00009ABC12345678 MOVK x0, #0xDEF0, LSL #48 ; x0 0xDEF09ABC12345678四个 16 位片段拼出任意 64 位常量最多四条指令。汇编器通常会自动帮你做这件事写MOV x0, #0xDEF09ABC12345678就够了但你要知道它背后展开成什么才能估算代码大小和流水开销。6. 换个架构看同一件事RISC-V、MIPS、x86 的立即数边界6.1 RISC-V 的 12 位有符号立即数RISC-V 的 I 型指令ADDI、ANDI、ORI、XORI、SLTI 等给立即数是 12 位而且是有符号补码。所以范围是-2048到2047。addi a0, a0, 2047 # 合法 addi a0, a0, 2048 # 非法汇编器报错 addi a0, a0, -2048 # 合法注意逻辑运算 ANDI、ORI、XORI 虽然也是 12 位字段但它们在语义上先做符号扩展再参与运算这一点和直觉不太一样。写andi a0, a0, 0xFFF的时候实际参与运算的是0xFFFFFFFFFFFFF...一堆 1 再补 0xFFF 的模式不是简单的高 20 位清零。这个细节在做位掩码的时候容易翻车。超过 12 位的常量怎么办用li伪指令汇编器会展开成luiaddi或者更长的序列。li a0, 0x12345678在 RV32 上会展开成两条lui a0, 0x12345和addi a0, a0, 0x678。如果低 12 位是负数bit11 为 1还需要调整高 20 位的值汇编器会自动处理这个进位修正。6.2 MIPS 的 lui ori 常量构造MIPS 的思路和 RISC-V 类似但更原始。算术立即数是 16 位有符号逻辑立即数ori、andi、xori是 16 位无符号。构造 32 位常量走两条lui a0, 0x1234 # a0 0x12340000 ori a0, a0, 0x5678 # a0 0x12345678这里有个细节值得注意addi和ori的低 16 位处理方式不同。addi做符号扩展ori做零扩展。所以构造一个低 16 位最高位是 1 的常量时用ori更省事。如果用luiaddiaddi会把0x8000当成负数结果要额外加 1 补偿。这类细节在写 MIPS 汇编的时候不注意就会得到一个差了 0x10000 的地址调试起来相当烦人——我就见过因为这个原因某个外设基址偏了一整段查了两小时才发现是符号扩展。6.3 x86 的宽松与代价x86 在这方面是另一个极端32 位立即数可以直接编码进指令mov eax, 0x12345678就是一条指令加 4 字节立即数没有任何合法性约束只有指令长度约束最长 15 字节。mov eax, 0x12345678 ; B8 78 56 34 12 mov eax, 0xFFFFFFFF ; B8 FF FF FF FF cmp dword ptr [rdi], 0xFF ; 83 3F FF用 imm8 符号扩展形式宽松的代价是指令变长。mov eax, 0x12345678是 5 个字节而mov eax, 1用B8 01 00 00 00也是 5 字节浪费了 3 个字节。所以编译器在优化尺寸时会尽量用xor eax, eax代替mov eax, 0用push 1 / pop rax之类的小技巧。另外cmp系列有 imm8 符号扩展的形式83开头值在-128到127之间时能省 3 个字节这在热路径的代码尺寸优化里挺有用。64 位模式下要注意64 位立即数不能直接作为大多数指令的操作数只有movabs支持 64 位立即数其他指令的立即数仍然是 32 位并做符号扩展。这是个很隐蔽的坑add rax, 0xFFFFFFFF会变成加-1而不是加 42 亿。6.4 横向对照表把四种架构放在一起看架构典型立即数字段编码方式可用集合特征超范围处理ARM3212 位数据处理8 位数据 4 位循环右移不连续约 4000 余个常量MVN、常量池、ORR 组合ARM64 逻辑N:immr:imms元素重复 元素内循环 1 串高度结构化重复模式拆成移位加运算ARM64 算术12 位 可选左移 12连续带步长0~0xFFF 或 0x1000 倍数MOVZ/MOVK 组合RISC-V12 位有符号补码连续li 伪指令、luiaddiMIPS16 位补码或零扩展连续lui orix86-3232 位补码基本全覆盖无需处理x86-6432 位符号扩展补码连续movabs、分步构造看完这张表你会发现一个规律指令字长固定、寄存器数量多的架构倾向于压缩立即数位宽反之则宽松。这不是巧合是编码空间分配的结果。7. 遇到 invalid immediate 时的排查链路7.1 第一步确认架构与指令形态别急着改代码。先在脑子里过三个问题这是哪个架构的哪一代ARM32 和 AArch64 的立即数规则完全不同别混。这条指令属于哪一类数据处理、逻辑、算术、访存、移动立即数规则各不一样。汇编器报的错具体是什么invalid constant、invalid immediate、immediate out of range、cannot encode指向的问题不完全相同。我见过有人拿着 AArch64 的ANDS x0, x1, #0xFFFFFFFF报错去翻 ARM32 的立即数资料越看越糊涂。分类错了后面全白费。7.2 第二步把值打成二进制看模式十六进制对判断帮助不大二进制才看得出结构。我一般在脚本里加一行def show32(v): b format(v 0xFFFFFFFF, 032b) print( .join(b[i:i4] for i in range(0, 32, 4))) for v in [0x100, 0x101, 0x102, 0xFFFF, 0xF000000F]: print(hex(v)) show32(v) print()输出按 4 位分组一眼就能看出 1 和 0 的分布。如果最长连续 0 串卡在 23 位就差那一位你会立刻明白问题在哪。这一步做完大部分情况下答案已经出来了。提示判断前先确认是 32 位还是 64 位运算。0xFFFFFF00在 ARM32 上和 AArch64 上的行为完全不同AArch64 的 64 位逻辑立即数看的是完整 64 位模式。7.3 第三步用汇编器做二分验证如果拿不准用汇编器做二分实验是最可靠的。写一个小文件把待验证的值放进去编译一次看报不报错.text .global _start _start: mov r0, #0x100 试这个值 mov r1, #0x101用 binutils 交叉工具链arm-none-eabi-as -o test.o test.s报错就换值从两端逼近期望结果。这个方法笨但绝对准确——它用的是真实工具链的编码器不会骗你。更聪明的做法是用反汇编反推。找一条已知能编过的指令编译后用objdump反汇编看机器码里立即数字段的实际值arm-none-eabi-objdump -d test.o机器码里MOV R0, #0x100的指令字是E3A00C01低 12 位C01就是1100 00000001rotate 12imm8 0x01和我们的推算完全吻合。这种编码-反汇编-对照的练习做上十几遍你对立即数的直觉就会建立起来。7.4 常见误判清单把我在实际排查中遇到过的误判整理成清单误判实际情况数值小就一定合法0x101 是反例数值大就一定非法0xFF000000 是合法反例连续 0 串可以线性数必须首尾接环最长连续 1 串达 24 就能用 MOV应该用 MVN16 位常数在 ARM32 上总能用 MOVWMOVW 是 ARMv6T2 之后才有的AArch64 逻辑立即数全 0 或全 1 可以两者都是保留编码内联汇编报错说明编译器有问题先确认指令形态本身是否成立最后一条我特别想强调。内联汇编里写asm(mov %0, #0x12345678)编不过大概率不是编译器 bug而是你写的指令在这个架构上根本不成立。先自己算一遍再怀疑工具。8. 几个实战里反复踩到的坑8.1 负数的立即数写法陷阱MOV R0, #-1在很多 ARM32 汇编器上是能被接受的因为它等价于#0xFFFFFFFF走 MVN 路径。但MOV R0, #-256就不一定了——-256是0xFFFFFF00需要MVN R0, #0xFF。有些汇编器会替你换有些不会。我个人的习惯是永远写十六进制不写负数。写#0xFFFFFFFF和写#-1在人眼里是同一个数在汇编器眼里可能是两条路径。写十六进制让编码形态一目了然也方便自己用判据检查。8.2 链接器重定位阶段的立即数问题这是另一个很多人没意识到的坑。立即数合法性不只是能不能编进指令还包括这个值在链接期是否确定。MOV R0, #my_symbol 非法即使 my_symbol 恰好合法 LDR R0, my_symbol 合法走常量池第一条指令为什么非法因为MOV的立即数必须编进指令字里而符号地址要等到链接才知道汇编器没法把一个未知值编进指令编码。这时候必须用LDR伪指令它会生成一个常量池条目在链接期填入真实地址。在 AArch64 上类似的写法是ADR、ADRP配合ADD的低 12 位偏移。理解这一点就能明白为什么有些常量在汇编阶段编不过改成加载形式就好了。8.3 位带操作与掩码常量的构造习惯写外设寄存器操作的时候经常要构造掩码。比如要把某个 32 位寄存器的 bit[15:8] 清零掩码是0xFFFF00FF。算一下最长连续 0 串是 8 位bit15 到 bit8最长连续 1 串是 8 位bit31 到 bit24加 8 位bit7 到 bit0但它们是分开的最长 8 位。所以 MOV 不行MVN 也不行。怎么办三种写法各有适用场景 写法一常量池 LDR R1, 0xFFFF00FF AND R0, R0, R1 写法二两步构造 MOV R1, #0xFF BIC R0, R0, R1, LSL #8 BIC 是位清除寄存器源支持移位 写法三先取反再与 MVN R1, #0xFF, 8 R1 ~(0xFF 8) 0xFFFF00FF AND R0, R0, R1写法三用到了立即数带移位的一点余地——MVN R1, #0xFF, 8里的,8是源操作数的移位后缀它和立即数编码里的 rotate 字段是两回事很多人会混淆。,8是给寄存器源用的桶形移位器语法虽然它也能作用于立即数源在编码允许时但语义上是额外一层。实际工程中我倾向于写法二。它的优点是掩码的意图非常清楚——清掉 bit15 到 bit8读代码的人一眼就知道在干什么而一个0xFFFF00FF的魔数需要停下来算。代码可读性在底层开发里比省一条指令重要得多除非你在写中断里执行的关键路径。8.4 写常量时的几条个人习惯最后分享几条我自己写底层代码时积累的习惯都是从踩坑里攒出来的。**第一任何超过 0xFF 的常量先在脑子里过一遍判据。**不是要真的算出编码而是快速判断这个数大概能不能用。形成直觉后写代码时就会自然地避开那些麻烦的值。**第二热路径上用构造冷路径上用常量池。**中断服务程序、循环体内部尽量用 MOV/MVN/ORR 组合把常量构造出来避免访存。初始化代码、错误处理分支直接用LDR Rd, const就好可读性优先。第三写掩码时用有名字的宏或者位域表达式。(1 8)这种写法比0x100更容易维护改位号的时候不用重新算十六进制。当然前提是编译器或者汇编器能把(1 8)折叠成合法立即数——如果是编译期常量一般没问题。**第四遇到报错先怀疑自己的数不要先怀疑工具链。**我见过有人因为立即数问题去给 GCC 提 bug最后发现是自己算错了连续 0 串。工具链在立即数编码这块的实现普遍是可靠的出错概率远低于人眼判断。**第五写一个自己的校验脚本集成到构建流程里。**如果是手写汇编的项目可以在构建脚本里加一步扫描.s文件里所有#0x开头的立即数用上面那个函数批量验证一遍。这样能在编译前就发现问题比等汇编器报错要快。这个脚本我自己用了好几年改成 AArch64 版本也就是把判据函数换掉的事。
返回列表