Lý do cho vỏ bash không cảnh báo bạn về tràn số học, vv là gì?


9

Có các giới hạn được đặt cho các khả năng đánh giá số học của hệ bashvỏ. Hướng dẫn ngắn gọn về khía cạnh này của số học shell nhưng nêu rõ :

Đánh giá được thực hiện trong các số nguyên có chiều rộng cố định mà không kiểm tra tràn, mặc dù phép chia cho 0 bị kẹt và bị gắn cờ là lỗi. Các toán tử và mức độ ưu tiên, kết hợp và giá trị của chúng giống như trong ngôn ngữ C.

Số nguyên có chiều rộng cố định này thực sự đề cập đến loại dữ liệu nào được sử dụng (và chi tiết cụ thể tại sao giá trị này vượt quá mức này) nhưng giá trị giới hạn được thể hiện /usr/include/limits.htheo kiểu này:

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

Và một khi bạn biết điều đó, bạn có thể xác nhận trạng thái thực tế này như vậy:

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

Đây là số nguyên 64 bit và phần này dịch trực tiếp trong hệ vỏ trong bối cảnh đánh giá số học:

# 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

Vì vậy, giữa 2 63 và 2 64 -1, bạn sẽ có được số nguyên âm hiển thị cho bạn cách xa từ ULONG_MAX bạn là 1 . Khi đánh giá đạt đến giới hạn đó và tràn ra, theo bất kỳ thứ tự nào, bạn sẽ không nhận được cảnh báo nào và phần đánh giá đó được đặt lại về 0, điều này có thể mang lại một số hành vi bất thường với ví dụ như lũy thừa liên kết phải :

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
...

Việc sử dụng sh -c 'command'không thay đổi bất cứ điều gì vì vậy tôi phải cho rằng đây là đầu ra bình thường và tuân thủ. Bây giờ tôi nghĩ rằng tôi đã có một sự hiểu biết cơ bản nhưng cụ thể về phạm vi và giới hạn số học và ý nghĩa của nó trong vỏ để đánh giá biểu thức, tôi nghĩ rằng tôi có thể nhanh chóng xem qua loại dữ liệu nào mà phần mềm khác trong Linux sử dụng. Tôi đã sử dụng một số bashnguồn mà tôi phải bổ sung cho đầu vào của lệnh này:

{ 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)

Có nhiều đầu ra hơn với các ifcâu lệnh và tôi cũng có thể tìm kiếm một lệnh như awkvậy, v.v ... Tôi nhận thấy biểu thức thông thường mà tôi đã sử dụng không nắm bắt được gì về các công cụ chính xác tùy ý mà tôi có như bcdc.


Câu hỏi

  1. Lý do nào để không cảnh báo bạn (giống như awkkhi đánh giá 2 ^ 1024) khi đánh giá số học của bạn tràn ra? Tại sao các số nguyên âm giữa 2 63 và 2 64 -1 tiếp xúc với người dùng cuối khi anh ta đánh giá thứ gì đó?
  2. Tôi đã đọc ở đâu đó rằng một số hương vị của UNIX có thể thay đổi tương tác ULONG_MAX? có ai từng nghe cái này chưa?
  3. Nếu ai đó tự ý thay đổi giá trị của số nguyên tối đa không dấu trong limits.hđó, sau đó biên dịch lại bash, chúng ta có thể mong đợi điều gì sẽ xảy ra?

Ghi chú

1. Tôi muốn minh họa rõ hơn những gì tôi thấy, vì nó là công cụ thực nghiệm rất đơn giản. Điều tôi nhận thấy là:

  • (a) Mọi đánh giá đưa ra <2 ^ 63-1 đều đúng
  • (b) Mọi đánh giá đưa ra => 2 ^ 63 lên đến 2 ^ 64 đều cho số nguyên âm:
    • Phạm vi của số nguyên đó là x đến y. x = -9223372036854775808 và y = 0.

Xem xét điều này, một đánh giá giống như (b) có thể được biểu thị bằng 2 ^ 63-1 cộng với một cái gì đó trong x..y. Chẳng hạn, nếu chúng tôi được yêu cầu đánh giá theo nghĩa đen (2 ^ 63-1) +100 002 (nhưng có thể là bất kỳ số nào nhỏ hơn trong (a)), chúng tôi nhận được -9223372036854675807. Tôi chỉ nói rõ ràng tôi đoán nhưng điều này cũng có nghĩa là hai biểu thức sau:

  • (2 ^ 63-1) + 100 002 VÀ;
  • (2 ^ 63-1) + (LLONG_MAX - {những gì vỏ mang lại cho chúng tôi ((2 ^ 63-1) + 100 002), đó là -9223372036854675807}), sử dụng các giá trị dương mà chúng tôi có;
    • (2 ^ 63-1) + (9223372036854775807 - 9223372036854675807 = 100 000)
    • = 9223372036854775807 + 100 000

thực sự rất gần Biểu thức thứ hai là "2" ngoài (2 ^ 63-1) + 100 002 tức là những gì chúng ta đang đánh giá. Ý tôi là bạn nhận được số nguyên âm cho bạn thấy bạn cách xa 2 ^ 64. Ý tôi là với những số nguyên âm và kiến ​​thức về các giới hạn, bạn cũng không thể hoàn thành việc đánh giá trong phạm vi x..y trong bash shell nhưng bạn có thể ở nơi khác - dữ liệu có thể sử dụng tới 2 ^ 64 theo nghĩa đó (tôi có thể thêm nó lên giấy hoặc sử dụng nó trong bc). Ngoài ra, hành vi tương tự như hành vi của 6 ^ 6 ^ 6 khi đạt đến giới hạn như được mô tả dưới đây trong Q ...


5
Tôi đoán là lý do sôi nổi đến "cái vỏ không phải là công cụ phù hợp cho toán học". Nó không được thiết kế cho nó và không cố gắng xử lý nó một cách duyên dáng như bạn thể hiện. Địa ngục, hầu hết các vỏ thậm chí không đối phó với phao!
terdon

@terdon Mặc dù cách hệ vỏ xử lý các con số trong trường hợp này hoàn toàn giống với mọi ngôn ngữ cấp cao mà tôi từng nghe. Các loại số nguyên là một kích thước cố định và có thể tràn.
goldilocks

@terdon Thật vậy, khi tôi nghiên cứu điều này kể từ 6 giờ 6 phút, QI đã nhận ra điều đó. Tôi cũng đoán lý do tại sao tôi không thể tìm thấy nhiều nội dung là vì điều này phải làm với C, hoặc thậm chí là C99. Vì tôi không phải là nhà phát triển cũng không phải là dân IT, tôi phải đồng ý với tất cả kiến ​​thức làm nền tảng cho những giả định này. Chắc chắn ai đó yêu cầu độ chính xác tùy ý biết về loại dữ liệu nhưng rõ ràng tôi không phải là người đó :) (nhưng tôi đã nhận thấy hành vi của awk @ 2 ^ 53 + 1 tức là nổi gấp đôi; chỉ là độ chính xác và nội bộ so với in, v.v. !).

1
Nếu bạn muốn làm việc với số lượng lớn trong trình bao, hãy sử dụng bc, ví dụ : $num=$(echo 6^6^6 | bc). Thật không may, bcđặt trong các ngắt dòng, vì vậy bạn phải num=$(echo $num | sed 's/\\\s//g')sau đó; nếu bạn làm điều đó trong một đường ống, có những nhân vật dòng mới thực sự, rất khó xử với sed, mặc dù num=$(echo 6^6^3 | bc | perl -pne 's/\\\s//g')hoạt động. Trong cả hai trường hợp, bây giờ bạn có một số nguyên có thể được sử dụng, ví dụ : num2=$(echo "$num * 2" | bc).
goldilocks

1
... Ai đó ở đây chỉ ra rằng bạn có thể tắt tính năng ngắt dòng này bcbằng cách cài đặt BC_LINE_LENGTH=0.
goldilocks

Câu trả lời:


11

Vì vậy, trong khoảng từ 2 ^ 63 đến 2 ^ 64-1, bạn sẽ nhận được các số nguyên âm cho bạn thấy bạn cách ULONG_MAX bao xa.

Không. Làm thế nào để bạn tìm ra điều đó? Theo ví dụ của bạn, tối đa là:

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

Nếu "tràn" có nghĩa là "bạn nhận được các số nguyên âm cho bạn thấy bạn cách ULONG_MAX bao xa", thì nếu chúng ta thêm một số vào đó, chúng ta có nên lấy -1 không? Nhưng thay vì:

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

Có lẽ bạn muốn nói đây là một số bạn có thể thêm vào $maxđể có sự khác biệt tiêu cực, vì:

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

Nhưng điều này thực tế không tiếp tục đúng:

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

Điều này là do hệ thống sử dụng phần bù hai để thực hiện các số nguyên đã ký. 1 Giá trị do tràn tràn KHÔNG phải là một nỗ lực cung cấp cho bạn một sự khác biệt, một sự khác biệt tiêu cực, v.v ... Đó thực sự là kết quả của việc cắt một giá trị thành một số bit giới hạn, sau đó được hiểu là một số nguyên có chữ ký bổ sung hai . Ví dụ, lý do $(($max + 1 + $max))được đưa ra là -1 là vì giá trị cao nhất trong phần bù của hai là tất cả các bit được đặt ngoại trừ bit cao nhất (biểu thị âm); kết hợp những thứ này lại với nhau về cơ bản có nghĩa là mang tất cả các bit sang trái để bạn kết thúc (nếu kích thước là 16 bit chứ không phải 64):

11111111 11111110

Bit cao (dấu) hiện được đặt vì nó được chuyển sang bổ sung. Nếu bạn thêm một (00000000 00000001) vào đó, thì bạn đã thiết lập tất cả các bit , trong đó bổ sung của hai là -1.

Tôi nghĩ rằng một phần nào đó trả lời nửa sau câu hỏi đầu tiên của bạn - "Tại sao các số nguyên âm ... tiếp xúc với người dùng cuối?". Đầu tiên, vì đó là giá trị chính xác theo quy tắc của các số bù hai bit 64 bit. Đây là cách thực hành thông thường của hầu hết các ngôn ngữ lập trình cấp cao có mục đích chung (khác) (tôi không thể nghĩ ra một ngôn ngữ không làm điều này), vì vậy, bashtuân thủ quy ước. Đó cũng là câu trả lời cho phần đầu tiên của câu hỏi đầu tiên - "Cơ sở lý luận là gì?": Đây là tiêu chuẩn trong đặc tả của ngôn ngữ lập trình.

WRT câu hỏi thứ 2, tôi chưa nghe nói về các hệ thống thay đổi tương tác ULONG_MAX.

Nếu ai đó tùy ý thay đổi giá trị của số nguyên tối đa không dấu trong giới hạn.h, sau đó biên dịch lại bash, chúng ta có thể mong đợi điều gì sẽ xảy ra?

Nó sẽ không tạo ra bất kỳ sự khác biệt nào đối với cách số học xuất hiện, bởi vì đây không phải là một giá trị tùy ý được sử dụng để định cấu hình hệ thống - đó là một giá trị tiện lợi lưu trữ hằng số bất biến phản ánh phần cứng. Bằng cách tương tự, bạn có thể xác định lại c là 55 mph, nhưng tốc độ của ánh sáng vẫn sẽ là 186.000 dặm mỗi giây. c không phải là một số được sử dụng để cấu hình vũ trụ - đó là một suy luận về bản chất của vũ trụ.

ULONG_MAX hoàn toàn giống nhau. Nó được suy ra / tính toán dựa trên bản chất của số N-bit. Thay đổi nó limits.hsẽ là một ý tưởng rất tồi nếu hằng số đó được sử dụng ở đâu đó giả sử nó được cho là đại diện cho thực tế của hệ thống .

Và bạn không thể thay đổi thực tế áp đặt bởi phần cứng của bạn.


1. Tôi không nghĩ rằng điều này (phương tiện biểu diễn số nguyên) thực sự được đảm bảo bởi bashvì nó phụ thuộc vào thư viện C cơ bản và tiêu chuẩn C không đảm bảo điều đó. Tuy nhiên, đây là những gì được sử dụng trên hầu hết các máy tính hiện đại bình thường.


Tôi rất biết ơn! Đến với các con voi trong phòng và suy nghĩ. Vâng, trong phần đầu tiên, chủ yếu là về lời nói. Tôi đã cập nhật Q của tôi để hiển thị những gì tôi có ý nghĩa. Tôi sẽ nghiên cứu lý do tại sao phần bổ sung của hai mô tả một số điều tôi đã thấy và câu trả lời của bạn là vô giá trong việc hiểu điều đó! Theo như UNIX Q có liên quan, tôi phải đọc sai một vài thứ về ARG_MAX với AIX ở đây . Chúc mừng!

1
Trong thực tế, bạn có thể sử dụng phần bù hai để xác định giá trị nếu bạn chắc chắn mình ở trong phạm vi> 2 * $max, như bạn mô tả. Quan điểm của tôi là 1) đó không phải là mục đích, 2) đảm bảo bạn hiểu nếu bạn muốn làm điều đó, 3) nó không hữu ích lắm vì khả năng áp dụng rất hạn chế, 4) theo chú thích thực sự không đảm bảo rằng hệ thống thực hiện sử dụng hai phần bù. Nói tóm lại, cố gắng khai thác rằng trong mã chương trình sẽ được coi là một thực tiễn rất kém. Có các thư viện / mô-đun "số lượng lớn" (đối với hệ vỏ trong POSIX, bc) - sử dụng các thư viện nếu bạn cần.
goldilocks

Chỉ gần đây tôi mới xem một cái gì đó tận dụng sự bổ sung của cả hai để thực hiện ALU với bộ cộng nhị phân 4 bit với IC mang nhanh; thậm chí còn có một so sánh với phần bổ sung của một người (để xem nó như thế nào). Lời giải thích của bạn là công cụ giúp tôi có thể đặt tên và kết nối những gì tôi thấy ở đây với những gì được thảo luận trong các video đó , làm tăng cơ hội tôi có thể thực sự nắm bắt được tất cả các hàm ý khi dòng này chìm xuống. Cảm ơn một lần nữa cho điều đó! Chúc mừng!
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.