bash shell不警告您算术溢出等原因是什么?


9

bashShell 的算术评估功能设置了限制。该手册简要介绍了Shell算术的这一方面,但指出

评估以固定宽度的整数完成,不检查溢出,尽管陷阱会被除以0并标记为错误。运算符及其优先级,关联性和值与C语言相同。

它所指的是哪个固定宽度整数,实际上是关于所使用的数据类型(以及为何超出此范围的详细信息),但是极限值是以/usr/include/limits.h这种方式表示的:

#  if __WORDSIZE == 64
#   define ULONG_MAX     18446744073709551615UL
#  ifdef __USE_ISOC99
#  define LLONG_MAX       9223372036854775807LL
#  define ULLONG_MAX    18446744073709551615ULL

一旦知道了,就可以像下面这样确认事实:

# getconf -a | grep 'long'
LONG_BIT                           64
ULONG_MAX                          18446744073709551615

这是一个64位整数,在算术评估的上下文中直接在shell中转换:

# echo $(((2**63)-1)); echo $((2**63)); echo $(((2**63)+1)); echo $((2**64))
9223372036854775807        //the practical usable limit for your everyday use
-9223372036854775808       //you're that much "away" from 2^64
-9223372036854775807     
0
# echo $((9223372036854775808+9223372036854775807))
-1

因此,在2 63和2 64 -1之间,您将得到负整数,显示您与ULONG_MAX的距离为1。当评估达到该限制并溢出时,无论按什么顺序,您都不会收到警告,并且该评估的一部分会重置为0,这可能会产生一些异常行为,例如,右联想幂运算:

echo $((6**6**6))                      0   // 6^46656 overflows to 0
echo $((6**6**6**6))                   1   // 6^(6^46656) = 6^0 = 1
echo $((6**6**6**6**6))                6   // 6^(6(6^46656)) = 6^(6^0) = 6^1
echo $((6**6**6**6**6**6))         46656   // 6^(6^(6^(6^46656))) = 6^6
echo $((6**6**6**6**6**6**6))          0   // = 6^6^6^1 = 0
...

使用sh -c 'command'不会改变任何东西,因此我必须假设这是正常且合规的输出。现在,我认为我对算术范围和极限以及它在表达式评估外壳中的含义有了基本但具体的理解,我认为我可以快速了解Linux中其他软件使用的数据类型。我使用了一些bash必须补充此命令输入的资源:

{ shopt -s globstar; for i in /path/to/source_bash-4.2/include/**/*.h /usr/include/**/*.h; do grep -HE '\b(([UL])|(UL)|())LONG|\bFLOAT|\bDOUBLE|\bINT' $i; done; } | grep -iE 'bash.*max'

bash-4.2/include/typemax.h:#    define LLONG_MAX   TYPE_MAXIMUM(long long int)
bash-4.2/include/typemax.h:#    define ULLONG_MAX  TYPE_MAXIMUM(unsigned long long int)
bash-4.2/include/typemax.h:#    define INT_MAX     TYPE_MAXIMUM(int)

有与多个输出if语句,我可以像搜索命令awk太等我通知我用正则表达式没有关于任意精度的工具,我有诸如捕捉到任何bcdc


问题

  1. awk当算术评估溢出时,不警告您(例如评估2 ^ 1024时)的原因是什么?为什么最终用户评估某些东西时2 63和2 64 -1 之间的负整数会暴露给最终用户?
  2. 我读过某个地方的某些UNIX风格可以交互更改ULONG_MAX?有谁听说过吗?
  3. 如果有人随意更改in中无符号整数最大值的值limits.h,然后重新编译bash,我们会发生什么?

注意

我想更清楚地说明我所看到的,因为这是非常简单的经验性资料。我注意到的是:

  • (a)任何给出<2 ^ 63-1的评估都是正确的
  • (b)任何给定=> 2 ^ 63至2 ^ 64的求值都将给出负整数:
    • 该整数的范围是x到y。x = -9223372036854775808和y = 0。

考虑到这一点,类似于(b)的评估可以表示为2 ^ 63-1加上x..y内的值。例如,如果我们确实被要求评估(2 ^ 63-1)+100 002(但可以是小于(a)中的任何数字),则得到-9223372036854675807。我只是在说显而易见的话,但这也意味着以下两个表达式:

  • (2 ^ 63-1)+ 100002 AND;
  • (2 ^ 63-1)+(LLONG_MAX-{外壳为((2 ^ 63-1)+ 100002)提供了什么,即-9223372036854675807}),请使用正值;
    • (2 ^ 63-1)+(9223372036854775807-9223372036854675807 = 100 000)
    • = 9223372036854775807 + 100000

确实非常接近。第二个表达式是(2 ^ 63-1)+ 100 002之外的“ 2”,即我们要评估的内容。这就是我得到的负整数,表示您与2 ^ 64的距离有多远。我的意思是说,有了这些负整数并知道极限,那么您无法在bash shell的x..y范围内完成评估,但是您可以在其他地方进行评估-在这种情况下,数据最多可以使用2 ^ 64(我可以添加将其放在纸上或在bc中使用)。除此之外,当达到极限时,行为类似于6 ^ 6 ^ 6的行为,如下文Q中所述。


5
我的猜测是,基本原理可以归结为“外壳不是正确的数学工具”。它不是为它设计的,并且不会尝试像显示的那样优雅地对其进行处理。地狱,大多数贝壳甚至都不处理浮子!
terdon

@terdon尽管在这种情况下shell处理数字的方式与我所听说过的每种高级语言完全相同。整数类型是固定大小,可能会溢出。
goldilocks 2014年

@terdon的确,自从我研究了6 ^ 6 ^ 6时序QI之后,我就意识到了这一点。我还猜到我找不到太多内容的原因是因为这与C甚至C99有关。因为我既不是开发人员也不是IT人员,所以我必须接受所有与这些假设相关的知识。当然,需要任意精度的人知道数据类型,但显然我不是那个人:)(但是我确实注意到awk的行为@ 2 ^ 53 + 1,即float double;精度和内部与打印等无关,这超出了我的范围) !)。

1
如果要在shell中处理大量数字,请使用bc,例如:$num=$(echo 6^6^6 | bc)。不幸的是,bc插入了换行符,因此您必须num=$(echo $num | sed 's/\\\s//g')事后才这样做。如果在管道中执行此操作,则可以使用实际的换行符,尽管使用sed还是很尴尬的num=$(echo 6^6^3 | bc | perl -pne 's/\\\s//g')。无论哪种情况,您现在都可以使用一个整数,例如num2=$(echo "$num * 2" | bc)
goldilocks 2014年

1
......有人在这里指出,你可以禁用这个换行功能bc的设置BC_LINE_LENGTH=0
goldilocks 2014年

Answers:


11

因此,在2 ^ 63和2 ^ 64-1之间,您会得到负整数,显示与ULONG_MAX的距离有多远。

你怎么知道的? 以您自己的示例,最大值为:

> max=$((2**63 - 1)); echo $max
9223372036854775807

如果“溢出”的意思是“您得到的负整数表示您与ULONG_MAX的距离有多远”,那么如果我们向其加一个,我们是否不应该得到-1?但反而:

> echo $(($max + 1))
-9223372036854775808

也许您的意思是,您可以添加一个数字$max以获得负差,因为:

> echo $(($max + 1 + $max))
-1

但这实际上并没有继续成立:

> echo $(($max + 2 + $max))
0

这是因为系统使用二进制补码来实现有符号整数。1 溢出产生的值并非试图为您提供差值,负差值等。 它实际上是将值截断为有限位数,然后将其解释为二进制补码整数的结果。 。例如,之所以$(($max + 1 + $max))成为-1,是因为2的补码中的最高值是最高位(表示负数)以外的所有其他位。将它们加在一起基本上意味着将所有位都保留在左侧,所以最终得到的结果是(如果大小为16位而不是64位):

11111111 11111110

现在设置高(符号)位,因为它会在加法中继续。如果再添加一个(00000000 00000001),则所有位均已设置,在二进制补码中为-1。

我认为这部分回答了您的第一个问题的后半部分-“为什么负整数...暴露给最终用户?”。首先,因为根据64位二进制补码的规则,这是正确的值。这是大多数(其他)通用高级编程语言的常规做法(我想不到没有这样做的一种),因此bash遵循惯例。这也是第一个问题的第一部分的答案-“原理是什么?”:这是编程语言规范中的规范。

关于WRT的第二个问题,我还没有听说过交互更改ULONG_MAX的系统。

如果有人随意更改limits.h中无符号整数最大值的值,然后重新编译bash,那么我们会发生什么?

算术运算的结果不会有任何区别,因为这不是用于配置系统的任意值,而是一个方便的值,用于存储反映硬件的不可变常数。以此类推,您可以将c重新定义为55 mph,但是光速仍将是每秒186,000英里。c不是用于配置Universe的数字-它是对Universe本质的推论。

ULONG_MAX完全相同。它是根据N位数字的性质推论/计算的。 如果在limits.h假定该常数代表系统实际的某个地方使用该常数,则将其更改将是一个非常糟糕的主意

而且,您无法更改硬件所强加的现实。


1.我不认为此(整数表示的方法)实际上是由保证的bash,因为它取决于基础C库,而标准C不保证这一点。但是,这是大多数普通现代计算机上使用的。


我非常感谢!和房间里的大象达成共识并思考。是的,第一部分主要是关于单词。我更新了我的Q,以表明我的意思。我将研究为什么二进制补码描述了我所看到的一些内容,而您的答案对于理解这一点非常宝贵!就UNIX Q而言,我在这里一定对ARG_MAX和AIX有误读。干杯!

1
实际上,如果您确定所描述的值在> 2 *范围内,可以使用二进制补码确定值$max。我的观点是:1)不是目的,2)确保您了解是否要这样做,3)由于适用性非常有限,因此不是很有用,4)根据脚注,实际上并不能保证系统可以用二进制补码。简而言之,尝试在程序代码中利用它是很差的做法。有“大量”库/模块(用于POSIX下的shell bc)-如果需要,请使用它们。
goldilocks 2014年

直到最近,我才看到一种利用两者的补码来实现带有4位二进制加法器和快速进位IC的ALU的功能。甚至与一个人的补语进行了比较(看情况如何)。您的解释对我来说是很重要的,因为我能够将我在这里看到的内容与这些视频中讨论的内容进行命名和关联,从而增加了我真正掌握所有含义的机会,再次感谢!干杯!
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.