翻译自:Encoding of immediate values on AArch64
AArch64 是一个具有 32 位固定指令宽度的指令集架构(ISA),这意味着在单个指令中没有足够的空间来存储一个 64 位的立即数。与作者之前熟悉的 x86 架构相比,在 x86-64 上处理立即数要简单一些,因为 x86 指令可以有可变宽度。例如,在 x86-64 上,一个 64 位的立即数只是字节序列:
mov rax, 0x1122334455667788 # 编码为:0x48, 0xB8, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11
像 ARM 这样的固定宽度指令集必须以不同的方式处理立即数。将相同的值赋给寄存器 x0
需要四个指令:
movz x0, 0x7788
movk x0, 0x5566, lsl 16
movk x0, 0x3344, lsl 32
movk x0, 0x1122, lsl 48
移动宽立即数
移动指令(movz
、movn
和 movk
)有空间存储一个 16 位的无符号立即数,该数可以左移 0、16、32 或 48 位(移位由 2 位表示)。
movz
将给定的 16 位值赋给由移位操作数指定的位置,并将所有其他位清零。
movk
执行相同的操作,但是 k
保留其他位的值而不是将它们清零。所以在最坏的情况下,一个 64 位的立即数需要 4 条指令。但是,许多常见的立即数可以用更少的指令编码:
# x0 = 0x10000
movz x0, 0x1, lsl 16
# x0 = 0x10001
movz x0, 0x1
movk x0, 0x1, lsl 16
只有当 64 位寄存器中非零的 16 位部分才需要被初始化。
现在我们已经看到了这个,我们如何编码 -1 呢?在这种情况下,所有位都是 1,所以即使只使用 movz
和 movk
,我们也必须再次使用 4 条指令。
对于这样的数字,AArch64 引入了 movn
指令,它将表达式 ~(imm16 << shift)
赋给寄存器。因此,-1 可以用单一指令编码:movn x0, 0
。
movn
也可以与 movk
结合使用,用它来设置数字中不是全 1 的部分。
例如,v8 真的决定了通过 movn
或 movz
编码一个立即数是否更有益(这意味着使用更少的指令)。
加/减立即数
除了移动指令中的立即数,一些指令如 add
或 sub
也接受立即数作为操作数。
这允许直接在指令中编码一些数字,而不是使用临时寄存器。
所有加/减立即指令类允许一个 12 位的无符号立即数,可以可选地左移 12 位(移位由 1 位表示)。
如果你想使用这些指令与无法以这种格式编码的立即数,你别无选择,只能使用临时寄存器,可能还需要多条指令来初始化这个寄存器。
尽管负数,例如 -1(全 1)不能使用 add
指令编码,但可以使用 sub
指令减去 1:sub x0, x0, 1
。
逻辑立即数
还有另一个指令类允许立即数作为操作数:逻辑立即数。这个指令类用于 and
(按位与)、orr
(按位或)、eor
(按位异或)和 ands
(按位与并设置标志)。
这个指令类是最复杂和直观的(至少对我来说),也是我开始写这篇博客文章的原因。
让我们看看 ARM 参考手册中的定义:
逻辑立即指令接受掩码立即数 bimm32 或 bimm64。 这样的立即数由一个至少有一个非零位和一个零位的连续序列组成,在一个 2、4、8、16、32 或 64 位的元素内; 然后该元素被复制到寄存器宽度,或者是这样值的按位取反。 所有零和全一的立即值都不能被编码为掩码立即数,因此汇编器必须为具有这样立即数的逻辑指令生成一个错误, 或者一个用户友好的汇编器可以将它转换为实现预期结果的其他指令。
这段话包含了很多信息。
我将尝试用我自己的话来描述这种格式:
逻辑立即指令有 13 位用于编码立即数,它由三个字段 N
(1 位)、immr
(6 位)和 imms
(6 位)组成。
这种格式不允许将 0 或 ~0(全一)编码为立即数。
尽管这听起来一开始可能有问题,但实际上这并不是一个限制:这种格式只用于像按位 and
和 orr
这样的指令,这些常数在这里并不是很有用(例如 x0 | 0
可以优化为 x0
而 x0 | ~0
可以优化为 ~0
)。
立即数的位模式由相同子模式组成,长度为 2、4、8、16、32 或 64 位。
子模式的大小和值存储在 N
和 imms
字段中。
位模式需要是一个至少有一个零位的连续序列,后面是至少有一个一位的连续序列(该模式的正则表达式将是 0+1+
)。
要生成位模式,格式实际上只存储元素中的连续一的数目和元素的大小。
特别指定的元素值可以通过右旋转最多元素大小减 1 位来移动一的序列的开始到元素的任何其他点。
旋转的次数存储在 immr
中,它有 6 位,所以在元素大小为 64 位的情况下允许最多 63 次旋转。
元素大小为 2 只允许 0 或 1 次旋转,在这种情况下,只有最低有效位被考虑,immr
的上 5 位被简单地忽略。
元素被复制直到达到 32 或 64 位。
13 位可以存储 8192 个不同的值,但由于例如旋转并不总是被充分利用到其全部潜力的较小元素大小,它实际上允许较少的不同值,但可能是一组更有用位模式。
由于 immr
实际上相当无聊(只存储旋转次数),让我们看看 N
和 imms
如何同时存储元素大小和连续一的数目:
N | imms | 元素大小 |
---|---|---|
0 | 1 | 1 |
0 | 1 | 1 |
0 | 1 | 1 |
0 | 1 | 0 |
0 | 1 | 1 |
0 | 1 | 0 |
0 | 0 | x |
0 | 0 | x |
1 | x | x |
上面的位指定元素大小,而用 x
标记的低位用于存储连续一的序列。
0 表示位模式中有 1 个 1,1 表示有两个 1,依此类推。
同时,不允许将所有 x
设置为 1,因为这将允许创建全 1 的位模式(记住:格式不允许 0 或全一被编码)。
让我们看一些例子:
0 | 111100
表示元素01
(2 位元素大小,一个 1)0 | 110101
表示元素00111111
(8 位元素大小,六个 1)
Stack Overflow 上有一个有趣的答案,列举了所有 5334 个可能的 64 位立即数,使用这种编码。
我将这个代码移植到 Ruby 并转储了所有值的字段 n
、immr
和 imms
。
在这里查看脚本的全部输出。
通过比较所有值与 AArch64 汇编器的输出,我验证了输出。
滚动查看所有值、元素大小、旋转等应该可以快速了解哪些数字可以被编码成这种表示。
对于一些
源代码示例,见 LLVM,它也处理逻辑立即数的编码和解码。
其他立即数
还有更多接受立即数作为操作数的指令类(甚至有一个接受浮点数的)。
但我认为它们不像逻辑立即数类那样复杂。