如何在机器代码级别处理数学相等运算符

How are Mathematical Equality Operators Handled at the Machine-Code Level

所以我今天想问一个相当存在的问题,我觉得这个问题好像大多数程序员都跳过了,只是接受了一些有用的东西,而没有真正问 "how" 它有用的问题。问题很简单:>= 运算符是如何编译成机器码的,机器码是什么样的?在最底层,它必须是大于测试,混合了 "is equal" 测试。但这实际上是如何实施的呢?仔细想想似乎很矛盾,因为在最底层不可能有 > 或 == 测试。需要有别的东西。我想知道这是什么。

计算机如何在基本级别测试是否相等和大于?

这是一个简单的 C 函数:

bool lt_or_eq(int a, int b)
{
    return (a <= b);
}

在 x86-64 上,GCC 将其编译为:

    .file   "lt_or_eq.c"
    .text
    .globl  lt_or_eq
    .type   lt_or_eq, @function
lt_or_eq:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    movl    -4(%rbp), %eax
    cmpl    -8(%rbp), %eax
    setle   %al
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   lt_or_eq, .-lt_or_eq

重要的部分是 cmpl -8(%rbp), %eax; setle %al; 序列。基本上,它使用 cmp instruction to compare the two arguments numerically, and set the state of the zero flag and the carry flag based on that comparison. It then uses setle 来决定是否将 %al 寄存器设置为 01,具体取决于这些标志的状态.调用者从 %al 寄存器中获取 return 值。

首先计算机需要弄清楚数据的类型。在像 C 这样的语言中,这将在编译时进行,python 将在 运行 时分派到不同类型的特定测试。假设我们来自编译语言,并且我们知道我们正在比较的值是整数,编译器将确保值在寄存器中然后发出:

SUBS  r1, r2 
BGE   @target

减去寄存器,然后检查 zero/undflow。这些指令是在CPU上运行的。 (我在这里假设是 ARM-like 有很多变体)。

确实没有 >== 测试本身。相反,汇编器中的最低级别比较由 binary subtraction 工作。 在 x86 上,整数比较的操作码是 CMP。这真的是 一条指令 来统治他们。例如在 80386 Programmer's reference manual:

中描述了它是如何工作的

CMP subtracts the second operand from the first but, unlike the SUB instruction, does not store the result; only the flags are changed.

CMP is typically used in conjunction with conditional jumps and the SETcc instruction. (Refer to Appendix D for the list of signed and unsigned flag tests provided.) If an operand greater than one byte is compared to an immediate byte, the byte value is first sign-extended.

基本上,CMP A, B(在Intel operand ordering) calculates A - B, and then discards the result. However, in an x86 ALU中,算术运算根据运算结果在CPU的标志寄存器中设置条件标志。与算术运算相关的标志是

Bit  Name   Function

 0   CF     Carry Flag -- Set on high-order bit carry or borrow; cleared
            otherwise.
 6   ZF     Zero Flag -- Set if result is zero; cleared otherwise.
 7   SF     Sign Flag -- Set equal to high-order bit of result (0 is
            positive, 1 if negative).
11   OF     Overflow Flag -- Set if result is too large a positive number
            or too small a negative number (excluding sign-bit) to fit in
            destination operand; cleared otherwise.

例如,如果计算结果为零,则设置Zero Flag ZFCMP A, B 执行 A - B 并丢弃结果。当且仅当A == B,减法的结果为0。因此 ZF 仅当操作数相等时才会被设置,否则被清除。

进位标志 CF 将被设置当且仅当 无符号减法 将导致 borrow,即 A - B 将是 < 0 如果 AB 被认为是 unsigned 数字和 A < B.

只要设置结果的 MSB 位,就会设置符号标志。这意味着作为带符号数的结果在 2 的补码中被视为负数。但是,如果考虑8位减法01111111(127)-10000000(-128),结果是11111111,解释为8位有符号2的补数是-1,尽管 127 - (-128) 应该是 255。发生了有符号整数溢出单独的符号标志并不能单独说明哪个有符号数量更大 - OF 溢出标志说明在先前的算术运算中是否发生了有符号溢出。

现在,根据使用的地方,Byte Set on Condition SETcc or a Jump if Condition is Met Jcc 指令用于解码标志并对其进行操作。如果布尔值用于设置变量,那么聪明的编译器会使用 SETcc; Jcc 更适合 if...else.

现在,>= 有 2 个选择:我们想要有符号比较或无符号比较。

int a, b;
bool r1, r2;
unsigned int c, d;
r1 = a >= b; // signed
r2 = c >= d; // unsigned

在英特尔汇编中,无符号不等式的条件名称使用abovebelow;有符号相等的条件使用词 greaterless。因此,对于 r2,编译器可以决定使用 Set on Above 或 Equal,即 SETAE,如果 (CF=0),则将目标字节设置为 1 .对于 r1 结果将由 SETGE 解码 - 将字节设置为大于或等于,这意味着 (SF=OF) - 即减法结果解释为 2 的补码是正数而没有溢出,或者是负数发生溢出。

最后一个例子:

#include <stdbool.h>
bool gte_unsigned(unsigned int a, unsigned int b) {
    return a >= b;
}

在 x86-64 Linux 上生成的优化代码是:

 cmp     edi, esi
 setae   al
 ret

符号比较也是如此

bool gte_signed(int a, int b) {
    return a >= b;
}

生成的程序集是

 cmp     edi, esi
 setge   al
 ret