bash外壳的算术评估能力设置了限制。该手册简明扼要地介绍了 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
Run Code Online (Sandbox Code Playgroud)
一旦你知道了这一点,你就可以像这样确认这个事实状态:
# getconf -a | grep 'long'
LONG_BIT 64
ULONG_MAX 18446744073709551615
Run Code Online (Sandbox Code Playgroud)
这是一个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
Run Code Online (Sandbox Code Playgroud)
因此,在 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
...
Run Code Online (Sandbox Code Playgroud)
使用sh -c 'command'不会改变任何东西,所以我必须假设这是正常且合规的输出。现在我认为我对算术范围和限制以及它在 shell 中用于表达式评估的含义有了基本但具体的了解,我想我可以快速了解 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)
Run Code Online (Sandbox Code Playgroud)
if语句有更多输出,我可以搜索类似awktoo 等的命令。我注意到我使用的正则表达式没有捕获任何关于我拥有的任意精度工具的内容,例如bc和dc。
问题
awk当您的算术评估溢出时不警告您(就像评估 2^1024 时那样)的理由是什么?为什么 2 63和 2 64 -1之间的负整数在最终用户评估某些东西时会暴露给他?limits.h,然后重新编译bash,我们能期待会发生什么?笔记
1. 我想更清楚地说明我所看到的,因为它是非常简单的经验性东西。我注意到的是:
考虑到这一点,类似于 (b) 的评估可以表示为 2^63-1 加上 x..y 内的某个值。例如,如果我们从字面上要求评估 (2^63-1)+100 002(但可以是任何小于 (a) 中的数字),我们会得到 -9223372036854675807。我只是在说明我猜的明显问题,但这也意味着以下两个表达式:
确实非常接近。除了 (2^63-1) + 100 002 即我们正在评估的内容之外,第二个表达式是“2”。这就是我的意思,你得到负整数,显示你离 2^64 有多远。我的意思是使用那些负整数和极限知识,你无法在 bash shell 中的 x..y 范围内完成评估,但你可以在其他地方 - 从这个意义上说,数据最多可以使用 2^64(我可以添加它写在纸上或在公元前使用它)。然而,除此之外,行为类似于 6^6^6 的行为,因为达到了以下 Q...
gol*_*cks 12
所以在 2^63 和 2^64-1 之间,你会得到负整数,显示你离 ULONG_MAX 有多远。
号你怎么知道的? 通过您自己的示例,最大值为:
> max=$((2**63 - 1)); echo $max
9223372036854775807
Run Code Online (Sandbox Code Playgroud)
如果“溢出”意味着“你得到的负整数表明你离 ULONG_MAX 有多远”,那么如果我们加一个,我们不应该得到 -1 吗?但反而:
> echo $(($max + 1))
-9223372036854775808
Run Code Online (Sandbox Code Playgroud)
也许您的意思是这是一个您可以添加的数字$max以获得负差异,因为:
> echo $(($max + 1 + $max))
-1
Run Code Online (Sandbox Code Playgroud)
但这实际上并没有继续成立:
> echo $(($max + 2 + $max))
0
Run Code Online (Sandbox Code Playgroud)
这是因为系统使用二进制补码来实现有符号整数。1 溢出产生的值并不是试图为您提供差异、负差异等。 它实际上是将值截断为有限位数,然后将其解释为二进制补码有符号整数的结果. 例如,之所以$(($max + 1 + $max))出现-1,是因为二进制补码中的最高值是除最高位(表示为负)之外的所有位设置;将这些加在一起基本上意味着将所有位都向左移动,所以你最终得到(如果大小是 16 位,而不是 64):
11111111 11111110
Run Code Online (Sandbox Code Playgroud)
高(符号)位现在已设置,因为它在加法中继续存在。如果再添加一个 (00000000 00000001),则所有位都已设置,在二进制补码中为 -1。
我认为这部分回答了你第一个问题的后半部分——“为什么负整数......暴露给最终用户?”。首先,因为根据 64 位二进制补码规则,这是正确的值。这是大多数(其他)通用高级编程语言的常规做法(我想不出一个不这样做的),因此bash遵守惯例也是如此。这也是第一个问题的第一部分的答案——“基本原理是什么?”:这是编程语言规范中的规范。
WRT 第二个问题,我还没有听说过交互更改 ULONG_MAX 的系统。
如果有人随意改变了limits.h中无符号整数最大值的值,然后重新编译bash,我们能期待会发生什么?
它不会对算术的结果产生任何影响,因为这不是用于配置系统的任意值——它是一个方便的值,用于存储反映硬件的不可变常量。以此类推,您可以将c重新定义为 55 英里/小时,但光速仍将是每秒 186,000 英里。c不是用于配置宇宙的数字——它是对宇宙本质的推论。
ULONG_MAX 完全一样。它是根据 N 位数字的性质推导出/计算出来的。 如果假设它应该代表系统的现实,在某处使用该常量,则更改它将limits.h是一个非常糟糕的主意。
而且您无法改变硬件强加的现实。
1. 我不认为这(整数表示的方式)实际上是由 来保证的bash,因为它取决于底层的 C 库,而标准 C 不保证这一点。但是,这是大多数普通现代计算机上使用的。