在汇编编程中,想要从寄存器的低位计算某些内容是相当常见的,但不能保证其他位清零。在 C 等高级语言中,您只需将输入转换为小尺寸,然后让编译器决定是否需要分别将每个输入的高位清零,或者是否可以在事实。
This is especially common for x86-64 (aka AMD64), for various reasons1, some of which are present in other ISAs.
I'll use 64bit x86 for examples, but the intent is to ask about/discuss 2's complement https://en.wikipedia.org/wiki/Two%27s_complement and unsigned binary arithmetic in general, since all modern CPUs use it https://stackoverflow.com/questions/2931630/how-are-negative-numbers-represented-in-32-bit-signed-integer. (Note that C and C++ don't guarantee two's complement4, and that signed overflow is undefined behaviour.)
As an example, consider a simple function that can compile to an LEA
instruction2. (In the x86-64 SysV(Linux) ABI http://www.x86-64.org/documentation.html3, the first two function args are in rdi
and rsi
, with the return in rax
. int
is a 32bit type.)
; int intfunc(int a, int b) { return a + b*4 + 3; }
intfunc:
lea eax, [edi + esi*4 + 3] ; the obvious choice, but gcc can do better
ret
gcc 知道加法,即使是负符号整数,也只能从右向左进位,因此输入的高位不会影响进入的内容eax
。因此,它保存一个指令字节并使用 http://goo.gl/h3UKGC lea eax, [rdi + rsi*4 + 3]
还有哪些其他操作具有结果低位的这种属性,而不依赖于输入的高位?
为什么它有效?
脚注
1 Why this comes up frequently for x86-64 https://stackoverflow.com/tags/x86/info:
x86-64 has variable-length instructions, where an extra prefix byte changes the operand size (from 32 to 64 or 16), so saving a byte is often possible in instructions that are otherwise executed at the same speed. It also has false-dependencies (AMD/P4/Silvermont) when writing the low 8b or 16b of a register (or a stall when later reading the full register (Intel pre-IvB)): For historical reasons, only writes to 32b sub-registers zero the rest of the 64b register https://stackoverflow.com/questions/11177137/why-do-most-x64-instructions-zero-the-upper-part-of-a-32-bit-register. Almost all arithmetic and logic can be used on on the low 8, 16, or 32bits, as well as the full 64bits, of general-purpose registers. Integer vector instructions are also rather non-orthogonal, with some operations not available for some element sizes.
此外,与 x86-32 不同,ABI 在寄存器中传递函数参数,并且对于窄类型,高位不需要为零。
2 LEA: Like other instructions, the default operand size of LEA https://stackoverflow.com/a/7071164/224132 is 32bit, but the default address size is 64bit. An operand-size prefix byte (0x66
or REX.W
) can make the output operand size 16 or 64bit. An address-size prefix byte (0x67
) can reduce the address size to 32bit (in 64bit mode) or 16bit (in 32bit mode). So in 64bit mode, lea eax, [edx+esi]
takes one byte more than lea eax, [rdx+rsi]
.
这是可以做的lea rax, [edx+esi]
,但地址仍然只用 32 位计算(进位不会设置的位 32)rax
)。你会得到相同的结果lea eax, [rdx+rsi]
,短了两个字节。因此,地址大小前缀对于以下情况没有用处:LEA
,正如 Agner Fog 优秀的 objconv 反汇编器的反汇编输出中的注释所警告的那样。
3 x86 ABI:
The caller doesn't have to zero (or sign-extend) the upper part of 64bit registers used to pass or return smaller types by value. A caller that wanted to use the return value as an array index would have to sign-extend it (with movzx rax, eax
, or the special-case-for-eax instruction cdqe
. (not to be confused with cdq
, which sign-extends eax
into edx:eax
e.g. to set up for idiv
.))
这意味着函数返回unsigned int
可以在 64 位临时中计算其返回值rax
,并且不需要mov eax, eax
将高位清零 https://stackoverflow.com/questions/11177137/why-do-most-x64-instructions-zero-the-upper-part-of-a-32-bit-register of rax
。这种设计决策在大多数情况下效果很好:调用者通常不需要任何额外的指令来忽略上半部分中未定义的位。rax
.
4 C and C++
C 和 C++ 专门做not需要二进制补码二进制有符号整数(除了C++ std::atomic types http://en.cppreference.com/w/cpp/atomic/atomic/operator_arith2). 还允许使用补码和符号/量值 https://stackoverflow.com/a/2931680/224132, 因此对于fully可移植 C,这些技巧仅适用于unsigned
类型。显然,对于有符号运算,符号/数值表示中设置的符号位意味着其他位被减去,而不是相加。我还没有研究过补语的逻辑
However, bit-hacks http://www.hackersdelight.org/ that only work with two's complement https://graphics.stanford.edu/~seander/bithacks.html#IntegerAbs are widespread http://aggregate.org/MAGIC/#Average%20of%20Integers, because in practice nobody cares about anything else. Many things that work with two's complement should also work with one's complement, since the sign bit still doesn't change the interpretation of the other bits: it just has a value of -(2N-1) (instead of 2N). Sign/magnitude representation does not have this property: the place value of every bit is positive or negative depending on the sign bit.
另请注意,C 编译器可以假设有符号溢出永远不会发生,因为这是未定义的行为。所以例如编译器可以并且确实假设(x+1) < x总是假的 http://goo.gl/h3UKGC。这使得在 C 中检测有符号溢出相当不方便。请注意无符号环绕(进位)和有符号溢出之间的区别 http://teaching.idallen.com/dat2343/10f/notes/040_overflow.txt.