如何在机器代码级别处理数学相等运算符
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 寄存器设置为 0
或 1
,具体取决于这些标志的状态.调用者从 %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 ZF
。 CMP A, B
执行 A - B
并丢弃结果。当且仅当A == B
,减法的结果为0。因此 ZF
仅当操作数相等时才会被设置,否则被清除。
进位标志 CF
将被设置当且仅当 无符号减法 将导致 borrow,即 A - B
将是 < 0
如果 A
和 B
被认为是 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
在英特尔汇编中,无符号不等式的条件名称使用above和below;有符号相等的条件使用词 greater 和 less。因此,对于 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
所以我今天想问一个相当存在的问题,我觉得这个问题好像大多数程序员都跳过了,只是接受了一些有用的东西,而没有真正问 "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 寄存器设置为 0
或 1
,具体取决于这些标志的状态.调用者从 %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 theSUB
instruction, does not store the result; only the flags are changed.
CMP
is typically used in conjunction with conditional jumps and theSETcc
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 ZF
。 CMP A, B
执行 A - B
并丢弃结果。当且仅当A == B
,减法的结果为0。因此 ZF
仅当操作数相等时才会被设置,否则被清除。
进位标志 CF
将被设置当且仅当 无符号减法 将导致 borrow,即 A - B
将是 < 0
如果 A
和 B
被认为是 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
在英特尔汇编中,无符号不等式的条件名称使用above和below;有符号相等的条件使用词 greater 和 less。因此,对于 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