Bash 中的按位运算未按预期工作

Pou*_*rko 12 bash arithmetic

我遇到了一个奇怪的问题。为了演示,让我们取我机器上最大的无符号数(printf "%X \n" -1给我FFFFFFFFFFFFFFFF),并尝试移动一些位。?首先,向左移动:

printf "%X \n" $(( 0xFFFFFFFFFFFFFFFF<<4 ))
FFFFFFFFFFFFFFF0
printf "%X \n" $(( 0xFFFFFFFFFFFFFFFF<<8 ))
FFFFFFFFFFFFFF00
printf "%X \n" $(( 0xFFFFFFFFFFFFFFFF<<16 ))
FFFFFFFFFFFF0000
Run Code Online (Sandbox Code Playgroud)

到现在为止还挺好。正如预期的那样。现在让我们尝试右移:

printf "%X \n" $(( 0xFFFFFFFFFFFFFFFF>>4 ))
FFFFFFFFFFFFFFFF
printf "%X \n" $(( 0xFFFFFFFFFFFFFFFF>>8 ))
FFFFFFFFFFFFFFFF
printf "%X \n" $(( 0xFFFFFFFFFFFFFFFF>>16 ))
FFFFFFFFFFFFFFFF
Run Code Online (Sandbox Code Playgroud)

等等,什么??为什么这不起作用?这是一个错误吗?


编辑:

我害怕有人会建议与提出的符号位有某种联系。但我们不是在谈论算术,所以符号的概念在这里没有立足之地。其他工具如*/用于算术。拥有一个可以操作位的工具的全部意义在于能够操作位——无论我以后如何选择显示这些位,有符号还是无符号。对?喜欢:

printf "%u \n" -1
18446744073709551615
Run Code Online (Sandbox Code Playgroud)

有人有什么想法吗?

编辑

由于这里的答案直接涉及乘法或除法,让我尝试更清楚地解释我的担忧。乘法/除法和位移位是两种不同的东西,尽管我可以在长期程序员的脑海中看到它们之间的联系。做算术的时候,必须要有符号的概念;对于位移,你没有。Bash 为我们提供了两种截然不同的工具集来处理这两种不同的事情。当我想将一个数字乘以 2 时,我会使用该*工具。Bash 在底层可以使用位移进行算术这一事实超出了重点。

引用其中一个答案...

如果没有复制符号位,结果将变成无符号数。例如,将 8 位值1111 0000向右移动一次将给出0111 1000

1111 0000变成0111 1000正是我想要的。如果我想做一个除法,那么我会改用算术运算符。

无论如何,是否至少有某种方法可以明确指定在移位时应该填充什么样的位?

ilk*_*chu 13

常用的右移两种不同的方法

“逻辑右移”在左边插入零位,因此右移一位的结果对应于将无符号二进制数除以二。echo $(( 16 >> 1 ))8.

并且,“算术右移”在左边插入了符号位的副本,所以右移一位的结果对应于有符号二进制数除以二。echo $(( 16 >> 1 ))8,并echo $(( -16 >> 1 ))-8。除了在二进制补码上,它与实际除法的舍入不匹配:-15 >> 1给出-8;而-15 / 2-7.

如果符号位没有被复制,而是被清零,结果将是一个正数。例如,将 8 位值1111 0000(0xf0, -16) 向右移动一次将得到0111 1000(0x78, +120)。


现在,使用其中的哪一个是更棘手的问题。

在实践中,许多实现会对有符号数使用算术移位,而 shell 算术主要是在有符号 long 上完成的。

但这并不能完全保证。shell 算术的 POSIX 定义是指大多数行为的 C 标准,例如运算符表没有说明>>应该是哪种类型的移位。(请参阅:Shell 命令语言、2.6.4 算术扩展Shell 与实用程序、1.1.2 源自 ISO C 标准的概念:算术精度和运算

整数变量和常量,包括操作数和选项参数的值,[...] 应实现为等效于 ISO C 标准有符号长数据类型 [...]

算术运算符和控制流关键字的实现应与引用的 ISO C 标准部分 [...]
<<中的相同>>:第 6.5.7 节,按位移位运算符

cppreference.com C运算符的说

对于 negative a, 的值a >> b是实现定义的(在大多数实现中,这执行算术右移,因此结果保持为负)。

(这可能是一个并非所有事物都是二进制补码的世界的残余。一个补码或符号幅度数的右侧移位与二进制补码数的右侧移位不同。但结果是一样的:它是实现定义的。)

其他一些编程语言,如 Javascript,有不同的算术右移运算符>>和逻辑右移运算符>>>。但是 C 没有,我试过的任何 shell 也没有。

顺便说一句,如果您要以大于字宽的偏移量进行移位,您还会看到奇怪的事情发生。在 x86 上,1 << 64只是1,因为处理器只查看移位值的最低 6 位,所以它与1 << 0. (1 << 32) << 320,但是,其结果可能是在另一个处理器不同。


你说,

但是符号的概念在这里没有立足之地。我的意思是,一个数字就是一个数字,不管你以后会选择把它显示为有符号还是无符号,对吧?

对于二进制补码机上的加法、减法和乘法的低部分(例如 32x32 -> 32)来说也是如此。

但对于乘法或除法的高部分,通常情况并非如此。8 位值0xff可以表示无符号数 255 或有符号数 -1。例如,8x8 -> 16 的乘法0xff * 0xff0x00010xfe01,具体取决于它是有符号 (-1 * -1) 还是无符号 (255 * 255)。还例如0xff / 3是任一00x55,如果它的签名取决于(-1 / 3 == 0),或无符号(255/3 = = 85)。

  • @Pourko,是的,它们是不同的东西。但事实仍然是,通常存在三种类型的移位:左移、在右侧插入零;“逻辑”右移,在左边插入零;和“算术”右移,复制左边的符号位。您可能只想始终使用“逻辑”变体,我不怪您。但是由于存在“算术”变体,我想有人已经找到了它的用途,无论如何,无论喜欢与否,这就是您可能会在外壳中获得的东西。 (2认同)

Qua*_*odo 10

您期望逻辑移位,但 Bash 执行算术移位。这确实与签名有关。

手册

评估以固定宽度的整数完成。运算符及其优先级、结合性和值与 C 语言中的相同。

整数常量遵循 C 语言定义,没有后缀或字符常量。带有前导 0 的常量被解释为八进制数。前导“0x”或“0X”表示十六进制。

并引用计算机系统,Randal Bryant 和 David O'Hallaron 的话

C 标准没有精确定义应该使用哪种类型的右移。对于无符号数据,右移必须符合逻辑。对于有符号数据(默认),可以使用算术或逻辑移位。(...) 然而,在实践中,几乎所有编译器/机器组合都对带符号的数据使用算术右移,许多程序员认为情况就是这样。

Stack Overflow 中也有人问过这个问题:C 中的移位运算符 (<<, >>) 是算术还是逻辑?