Phát hiện tràn đã ký trong C / C ++


81

Thoạt nhìn, câu hỏi này có vẻ giống như một bản sao của Làm thế nào để phát hiện tràn số nguyên? , tuy nhiên nó thực sự khác biệt đáng kể.

Tôi đã tìm thấy rằng trong khi phát hiện một lỗi tràn số nguyên unsigned là khá tầm thường, phát hiện một tràn trong C / C ++ thực sự là khó khăn hơn so với hầu hết mọi người nghĩ.

Cách rõ ràng nhất, nhưng ngây thơ, để làm điều đó sẽ là một cái gì đó như:

int add(int lhs, int rhs)
{
 int sum = lhs + rhs;
 if ((lhs >= 0 && sum < rhs) || (lhs < 0 && sum > rhs)) {
  /* an overflow has occurred */
  abort();
 }
 return sum; 
}

Vấn đề với điều này là theo tiêu chuẩn C, tràn số nguyên có dấu là hành vi không xác định. Nói cách khác, theo tiêu chuẩn, ngay khi bạn gây ra tràn đã ký, chương trình của bạn cũng không hợp lệ như thể bạn đã tham chiếu đến một con trỏ null. Vì vậy, bạn không thể gây ra hành vi không xác định và sau đó cố gắng phát hiện tràn sau thực tế, như trong ví dụ kiểm tra sau điều kiện ở trên.

Mặc dù kiểm tra ở trên có thể hoạt động trên nhiều trình biên dịch, nhưng bạn không thể tin tưởng vào nó. Trên thực tế, vì tiêu chuẩn C cho biết tràn số nguyên có dấu là không xác định, một số trình biên dịch (như GCC) sẽ tối ưu hóa việc kiểm tra ở trên khi các cờ tối ưu hóa được thiết lập, vì trình biên dịch giả định việc tràn số có ký là không thể. Điều này hoàn toàn phá vỡ nỗ lực kiểm tra tràn.

Vì vậy, một cách khả thi khác để kiểm tra tràn sẽ là:

int add(int lhs, int rhs)
{
 if (lhs >= 0 && rhs >= 0) {
  if (INT_MAX - lhs <= rhs) {
   /* overflow has occurred */
   abort();
  }
 }
 else if (lhs < 0 && rhs < 0) {
  if (lhs <= INT_MIN - rhs) {
   /* overflow has occurred */
   abort();
  }
 }

 return lhs + rhs;
}

Điều này có vẻ hứa hẹn hơn, vì chúng tôi không thực sự cộng hai số nguyên với nhau cho đến khi chúng tôi đảm bảo trước rằng thực hiện một phép cộng như vậy sẽ không dẫn đến tràn. Do đó, chúng tôi không gây ra bất kỳ hành vi không xác định nào.

Tuy nhiên, giải pháp này không may là kém hiệu quả hơn nhiều so với giải pháp ban đầu, vì bạn phải thực hiện một phép tính trừ chỉ để kiểm tra xem phép toán cộng của bạn có hoạt động hay không. Và ngay cả khi bạn không quan tâm đến cú đánh hiệu suất (nhỏ) này, tôi vẫn không hoàn toàn tin rằng giải pháp này là phù hợp. Biểu thức lhs <= INT_MIN - rhscó vẻ giống hệt như loại biểu thức mà trình biên dịch có thể tối ưu hóa loại bỏ, nghĩ rằng việc tràn có dấu là không thể.

Vì vậy, có một giải pháp tốt hơn ở đây? Một cái gì đó được đảm bảo 1) không gây ra hành vi không xác định và 2) không cung cấp cho trình biên dịch cơ hội để tối ưu hóa việc kiểm tra tràn? Tôi đã nghĩ rằng có thể có một số cách để làm điều đó bằng cách chuyển cả hai toán hạng thành không dấu và thực hiện kiểm tra bằng cách lăn số học phần bù của riêng bạn, nhưng tôi không thực sự chắc chắn về cách làm điều đó.


1
Thay vì cố gắng phát hiện, không phải là theo đuổi tốt hơn để viết mã không có khả năng bị tràn?
Arun

9
@ArunSaha: Thực sự rất khó để tính toán và đảm bảo rằng chúng sẽ không bị tràn, và không thể chứng minh được trong trường hợp chung. Thực hành thông thường là sử dụng kiểu số nguyên càng rộng càng tốt và hy vọng.
David Thornley

6
@Amardeep: Tham chiếu đến một con trỏ null cũng không được xác định giống như tràn có dấu. Hành vi không xác định có nghĩa là, theo tiêu chuẩn, bất cứ điều gì có thể xảy ra. Người ta không thể cho rằng hệ thống sẽ không ở trạng thái không hợp lệ và không ổn định sau khi tràn đã ký. OP đã chỉ ra một hệ quả của điều này: việc loại bỏ mã phát hiện tràn đã ký khi nó xảy ra là hoàn toàn hợp pháp.
David Thornley

16
@Amardeep: Tôi đã đề cập đến việc triển khai như vậy. GCC sẽ xóa mã kiểm tra tràn khi các cờ tối ưu hóa được đặt. Vì vậy, về cơ bản nó sẽ phá vỡ chương trình của bạn. Điều này được cho là tồi tệ hơn so với bỏ tham chiếu con trỏ rỗng, vì nó có thể dẫn đến các lỗi bảo mật tinh vi, trong khi tham chiếu đến null có thể sẽ chỉ làm tắc nghẽn chương trình của bạn bằng một segfault.
Kênh 72

2
@Amardeep: Tôi chắc chắn có vẻ như triển khai trong đó, tùy thuộc vào cài đặt trình biên dịch, tràn sẽ gây ra một cái bẫy. Sẽ rất tuyệt nếu các ngôn ngữ cho phép người ta chỉ định liệu các biến hoặc đại lượng không dấu cụ thể có nên (1) bọc sạch, (2) lỗi, hoặc (3) làm bất cứ điều gì thuận tiện. Lưu ý rằng nếu một biến nhỏ hơn kích thước thanh ghi của máy, việc yêu cầu các số lượng chưa được đánh dấu phải được bọc sạch sẽ có thể ngăn cản việc tạo mã tối ưu.
supercat

Câu trả lời:


26

Cách tiếp cận của bạn với phép trừ là chính xác và được xác định rõ ràng. Một trình biên dịch không thể tối ưu hóa nó đi.

Một cách tiếp cận đúng khác, nếu bạn có sẵn kiểu số nguyên lớn hơn, là thực hiện phép tính ở kiểu lớn hơn và sau đó kiểm tra xem kết quả có phù hợp với kiểu nhỏ hơn không khi chuyển đổi lại.

int sum(int a, int b)
{
    long long c;
    assert(LLONG_MAX>INT_MAX);
    c = (long long)a + b;
    if (c < INT_MIN || c > INT_MAX) abort();
    return c;
}

Một trình biên dịch tốt phải chuyển đổi toàn bộ phép cộng và ifcâu lệnh thành một phép cộng có intkích thước và một bước nhảy có điều kiện duy nhất và không bao giờ thực sự thực hiện phép cộng lớn hơn.

Chỉnh sửa: Như Stephen đã chỉ ra, tôi đang gặp sự cố khi tải một trình biên dịch (không tốt lắm), gcc, để tạo asm lành mạnh. Mã mà nó tạo ra không quá chậm, nhưng chắc chắn là không tối ưu. Nếu ai đó biết các biến thể trên mã này sẽ giúp gcc làm đúng, tôi rất muốn xem chúng.


1
Đối với bất kỳ ai muốn sử dụng cái này, hãy chắc chắn rằng bạn đang xem phiên bản đã chỉnh sửa của tôi. Trong bản gốc, tôi đã bỏ qua một cách ngu ngốc dàn diễn viên long longtrước khi bổ sung.
R .. GitHub DỪNG TRỢ GIÚP ICE

2
Vì tò mò, bạn đã thành công trong việc sử dụng trình biên dịch để thực hiện việc tối ưu hóa này chưa? Một cuộc kiểm tra nhanh với một số trình biên dịch đã không cho thấy bất kỳ trình biên dịch nào có thể làm được điều đó.
Stephen Canon

2
Trên x86_64, không có gì kém hiệu quả khi sử dụng số nguyên 32 bit. Hiệu suất giống với 64-bit. Một động lực để sử dụng các loại kích thước từ nhỏ hơn gốc là nó cực kỳ hiệu quả để xử lý các điều kiện tràn hoặc mang (đối với số học có độ chính xác tùy ý) vì tràn / mang xảy ra ở một vị trí có thể truy cập trực tiếp.
R .. GitHub NGỪNG TRỢ GIÚP ICE

2
@R., @Steven: không có mã trừ mà OP đưa ra là không đúng, vui lòng xem câu trả lời của tôi. Tôi cũng đưa ra một mã ở đó chỉ thực hiện nó với nhiều nhất hai phép so sánh. Có lẽ các trình biên dịch sẽ làm tốt hơn với điều đó.
Jens Gustedt

3
Cách tiếp cận này không hoạt động trong nền tảng không phổ biến sizeof(long long) == sizeof(int). C chỉ xác định điều đó sizeof(long long) >= sizeof(int).
chux - Phục hồi Monica

36

Không, mã thứ 2 của bạn không đúng, nhưng bạn đã đóng: nếu bạn đặt

int half = INT_MAX/2;
int half1 = half + 1;

kết quả của một phép cộng là INT_MAX. ( INT_MAXluôn là số lẻ). Vì vậy, đây là đầu vào hợp lệ. Nhưng trong thói quen của bạn, bạn sẽ có INT_MAX - half == half1và bạn sẽ phá thai. Một dương tính giả.

Lỗi này có thể được sửa chữa bằng cách đặt <thay vì <=trong cả hai lần kiểm tra.

Nhưng sau đó mã của bạn cũng không tối ưu. Điều sau sẽ làm:

int add(int lhs, int rhs)
{
 if (lhs >= 0) {
  if (INT_MAX - lhs < rhs) {
   /* would overflow */
   abort();
  }
 }
 else {
  if (rhs < INT_MIN - lhs) {
   /* would overflow */
   abort();
  }
 }
 return lhs + rhs;
}

Để thấy rằng điều này là hợp lệ, bạn phải cộng một cách tượng trưng lhsvào cả hai vế của bất đẳng thức và điều này cung cấp cho bạn chính xác các điều kiện số học mà kết quả của bạn nằm ngoài giới hạn.


+1 cho câu trả lời hay nhất. Nhỏ: đề nghị /* overflow will occurred */nhấn mạnh rằng toàn bộ điểm là để phát hiện rằng tràn sẽ xảy ra nếu mã đã làm lhs + rhsmà không thực sự làm tổng.
Chux - Khôi phục Monica

16

IMHO, cách dễ nhất để đối phó với mã C ++ không nhạy cảm tràn là sử dụng SafeInt<T>. Đây là một mẫu C ++ đa nền tảng được lưu trữ trên mã plex cung cấp các đảm bảo an toàn mà bạn mong muốn ở đây.

Tôi thấy nó rất trực quan để sử dụng vì nó cung cấp nhiều kiểu sử dụng giống như các hoạt động số bình thường và thể hiện các luồng qua và dưới thông qua các ngoại lệ.


14

Đối với trường hợp gcc, từ ghi chú phát hành gcc 5.0, chúng ta có thể thấy nó hiện cung cấp thêm tính năng __builtin_add_overflowkiểm tra lỗi tràn:

Một bộ hàm tích hợp mới dành cho số học với tính năng kiểm tra tràn đã được thêm vào: __builtin_add_overflow, __builtin_sub_overflow và __builtin_mul_overflow và để tương thích với clang cũng như các biến thể khác. Các nội trang này có hai đối số tích phân (không cần phải có cùng một kiểu), các đối số được mở rộng thành kiểu có dấu chính xác vô hạn, +, - hoặc * được thực hiện trên chúng và kết quả được lưu trữ trong một biến số nguyên được trỏ tới bởi đối số cuối cùng. Nếu giá trị được lưu trữ bằng kết quả chính xác vô hạn, các hàm tích hợp sẵn trả về false, ngược lại là true. Kiểu của biến số nguyên sẽ giữ kết quả có thể khác với kiểu của hai đối số đầu tiên.

Ví dụ:

__builtin_add_overflow( rhs, lhs, &result )

Chúng ta có thể thấy từ tài liệu gcc Chức năng Tích hợp để Thực hiện Số học với Kiểm tra Tràn rằng:

[...] các hàm tích hợp này có hành vi được xác định đầy đủ cho tất cả các giá trị đối số.

clang cũng cung cấp một tập hợp các nội trang số học đã kiểm tra :

Clang cung cấp một tập hợp các nội dung triển khai số học đã kiểm tra cho các ứng dụng quan trọng về bảo mật theo cách nhanh chóng và dễ dàng diễn đạt trong C.

trong trường hợp này, nội trang sẽ là:

__builtin_sadd_overflow( rhs, lhs, &result )

Chức năng này dường như là rất hữu ích ngoại trừ một điều: int result; __builtin_add_overflow(INT_MAX, 1, &result);không rõ ràng nói gì được lưu trữ trong resultngày tràn và không may là yên tĩnh trên xác định hành vi undefined nào không xảy ra. Chắc chắn đó là mục đích - không có UB. Tốt hơn nếu nó chỉ định rằng.
chux - Phục hồi Monica

1
@chux điểm tốt, nó nói ở đây kết quả luôn được xác định, tôi đã cập nhật câu trả lời của mình. Thật là trớ trêu nếu không phải như vậy.
Shafik Yaghmour,

Thú vị tham khảo mới của bạn không có (unsigned) long long *resultcho __builtin_(s/u)addll_overflow. Chắc chắn đây là lỗi. Khiến người ta ngạc nhiên về tính xác thực của các khía cạnh khác. IAC, rất vui khi thấy những điều này __builtin_add/sub/mull_overflow(). Hy vọng họ sẽ đạt được thông số C vào một ngày nào đó.
chux - Phục hồi Monica

1
+1 điều này tạo ra lắp ráp tốt hơn nhiều so với bất kỳ thứ gì bạn có thể nhận được trong tiêu chuẩn C, ít nhất là không dựa vào trình tối ưu hóa của trình biên dịch của bạn để tìm ra những gì bạn đang làm. Người ta nên phát hiện khi nào có sẵn các nội trang như vậy và chỉ sử dụng giải pháp tiêu chuẩn khi trình biên dịch không cung cấp giải pháp đó.
Alex Reinking

11

Nếu bạn sử dụng trình hợp dịch nội tuyến, bạn có thể kiểm tra cờ tràn . Một khả năng khác là bạn có thể sử dụng kiểu dữ liệu safeint . Tôi khuyên bạn nên đọc bài báo này về Bảo mật số nguyên .


6
+1 Đây là một cách nói khác "Nếu C không định nghĩa nó, thì bạn bị buộc phải thực hiện hành vi dành riêng cho nền tảng". Vì vậy, nhiều thứ dễ dàng được thực hiện trong quá trình lắp ráp không được xác định trong C, tạo ra những ngọn núi từ những nốt ruồi nhân danh tính di động.
Mike DeSimone

5
Tôi đã phản đối cho một câu trả lời asm cho một câu hỏi C. Như tôi đã nói, có những cách chính xác, di động để viết séc bằng C, cách này sẽ tạo ra chính xác asm mà bạn sẽ viết bằng tay. Đương nhiên nếu bạn sử dụng những thứ này, tác động hiệu suất sẽ giống nhau và nó sẽ ít ảnh hưởng hơn nhiều so với những thứ mà bạn cũng đã đề xuất.
R .. GitHub DỪNG TRỢ GIÚP ICE

1
@Matthieu: Nếu bạn đang viết mã chỉ được sử dụng trên một lần triển khai và việc triển khai đó đảm bảo rằng một thứ gì đó sẽ hoạt động và bạn cần hiệu suất số nguyên tốt, bạn chắc chắn có thể sử dụng các thủ thuật dành riêng cho việc triển khai. Tuy nhiên, đó không phải là những gì OP yêu cầu.
David Thornley

3
C phân biệt hành vi do triển khai xác định và hành vi không xác định vì những lý do chính đáng và ngay cả khi thứ gì đó với UB "hoạt động" trong phiên bản triển khai hiện tại của bạn, điều đó không có nghĩa là nó sẽ tiếp tục hoạt động trong các phiên bản sau. Cân nhắc gcc và hành vi tràn ký ...
R .. GitHub DỪNG GIÚP ICE

2
Vì tôi đã dựa trên -1 của mình trên một tuyên bố rằng chúng ta có thể lấy mã C để tạo ra mã asm giống hệt nhau, nên tôi nghĩ chỉ công bằng khi rút lại nó khi tất cả các trình biên dịch lớn hóa ra là rác về mặt này ..
R .. GitHub DỪNG LẠI TRỢ GIÚP ICE

6

Cách nhanh nhất có thể là sử dụng nội trang GCC:

int add(int lhs, int rhs) {
    int sum;
    if (__builtin_add_overflow(lhs, rhs, &sum))
        abort();
    return sum;
}

Trên x86, GCC biên dịch điều này thành:

    mov %edi, %eax
    add %esi, %eax
    jo call_abort 
    ret
call_abort:
    call abort

sử dụng tính năng phát hiện tràn tích hợp của bộ xử lý.

Nếu bạn không hài lòng với việc sử dụng nội trang GCC, cách nhanh nhất tiếp theo là sử dụng các phép toán bit trên các bit dấu. Ngoài ra, tràn đã ký xảy ra khi:

  • hai toán hạng có cùng dấu, và
  • kết quả có một dấu khác với các toán hạng.

Bit dấu của ~(lhs ^ rhs)là trên iff các toán hạng có cùng dấu và bit dấu của lhs ^ sumlà trên iff kết quả có dấu khác với các toán hạng. Vì vậy, bạn có thể thực hiện việc bổ sung ở dạng không dấu để tránh hành vi không xác định, sau đó sử dụng bit dấu của ~(lhs ^ rhs) & (lhs ^ sum):

int add(int lhs, int rhs) {
    unsigned sum = (unsigned) lhs + (unsigned) rhs;
    if ((~(lhs ^ rhs) & (lhs ^ sum)) & 0x80000000)
        abort();
    return (int) sum;
}

Điều này tổng hợp thành:

    lea (%rsi,%rdi), %eax
    xor %edi, %esi
    not %esi
    xor %eax, %edi
    test %edi, %esi
    js call_abort
    ret
call_abort:
    call abort

nhanh hơn rất nhiều so với truyền sang loại 64 bit trên máy 32 bit (với gcc):

    push %ebx
    mov 12(%esp), %ecx
    mov 8(%esp), %eax
    mov %ecx, %ebx
    sar $31, %ebx
    clt
    add %ecx, %eax
    adc %ebx, %edx
    mov %eax, %ecx
    add $-2147483648, %ecx
    mov %edx, %ebx
    adc $0, %ebx
    cmp $0, %ebx
    ja call_abort
    pop %ebx
    ret
call_abort:
    call abort

1

Bạn có thể gặp may mắn hơn khi chuyển đổi sang số nguyên 64 bit và thử nghiệm các điều kiện tương tự như vậy. Ví dụ:

#include <stdint.h>

...

int64_t sum = (int64_t)lhs + (int64_t)rhs;
if (sum < INT_MIN || sum > INT_MAX) {
    // Overflow occurred!
}
else {
    return sum;
}

Bạn có thể muốn xem xét kỹ hơn cách hoạt động của tiện ích mở rộng biển báo ở đây, nhưng tôi nghĩ nó đúng.


Loại bỏ bitwise-và và ép kiểu khỏi câu lệnh return. Chúng không đúng như đã viết. Việc chuyển đổi từ kiểu số nguyên có dấu lớn hơn sang kiểu nhỏ hơn hoàn toàn được xác định rõ ràng miễn là giá trị phù hợp với kiểu nhỏ hơn và nó không cần ép kiểu rõ ràng. Bất kỳ trình biên dịch nào đưa ra cảnh báo và gợi ý rằng bạn thêm một kiểu truyền khi bạn vừa kiểm tra rằng giá trị không bị tràn là trình biên dịch bị hỏng.
R .. GitHub DỪNG TRỢ GIÚP ICE

@R Bạn nói đúng, tôi chỉ thích nói rõ về dàn diễn viên của mình. Tuy nhiên, tôi sẽ thay đổi nó cho đúng. Đối với những người đọc trong tương lai, dòng trả về sẽ đọc return (int32_t)(sum & 0xffffffff);.
Jonathan

2
Lưu ý rằng nếu bạn viết sum & 0xffffffff, sumđược chuyển đổi hoàn toàn thành kiểu unsigned int(giả sử là 32-bit int) vì 0xffffffffcó kiểu unsigned int. Sau đó, kết quả của bitwise và là một unsigned int, và nếu sumlà âm, nó sẽ nằm ngoài phạm vi giá trị được hỗ trợ bởiint32_t . Việc chuyển đổi thành int32_tsau đó có hành vi do triển khai xác định.
R .. GitHub DỪNG TRỢ GIÚP ICE

Lưu ý rằng điều này sẽ không hoạt động trong môi trường ILP64 nơi ints là 64-bit.
rtx13

1

Làm thế nào về:

int sum(int n1, int n2)
{
  int result;
  if (n1 >= 0)
  {
    result = (n1 - INT_MAX)+n2; /* Can't overflow */
    if (result > 0) return INT_MAX; else return (result + INT_MAX);
  }
  else
  {
    result = (n1 - INT_MIN)+n2; /* Can't overflow */
    if (0 > result) return INT_MIN; else return (result + INT_MIN);
  }
}

Tôi nghĩ rằng điều đó nên làm việc cho bất kỳ hợp pháp INT_MININT_MAX(đối xứng hoặc không); chức năng như clip hiển thị, nhưng nó phải rõ ràng làm thế nào để có được các hành vi khác).


+1 cho cách tiếp cận thay thế tốt có lẽ trực quan hơn.
R .. GitHub NGỪNG TRỢ GIÚP ICE

1
Tôi nghĩ điều này - result = (n1 - INT_MAX)+n2;- có thể tràn, nếu n1 nhỏ (giả sử 0) và n2 là số âm.
davmac

@davmac: Hmm ... có lẽ cần phải chia ra ba trường hợp: bắt đầu với một cho (n1 ^ n2) < 0, điều này trên máy bổ sung của hai sẽ ngụ ý rằng các giá trị có dấu đối nghịch và có thể được thêm trực tiếp. Nếu các giá trị có cùng dấu, thì cách tiếp cận đưa ra ở trên sẽ an toàn. Mặt khác, tôi tò mò nếu các tác giả của Tiêu chuẩn mong đợi rằng các triển khai cho phần cứng chống tràn im lặng bổ sung của hai phần cứng sẽ nhảy ra khỏi đường ray trong trường hợp tràn theo cách không buộc phải chấm dứt chương trình bất thường ngay lập tức, nhưng gây ra sự gián đoạn không thể đoán trước của các tính toán khác.
supercat

0

Giải pháp rõ ràng là chuyển đổi thành unsigned, để có được hành vi tràn unsigned được xác định rõ:

int add(int lhs, int rhs) 
{ 
   int sum = (unsigned)lhs + (unsigned)rhs; 
   if ((lhs >= 0 && sum < rhs) || (lhs < 0 && sum > rhs)) { 
      /* an overflow has occurred */ 
      abort(); 
   } 
   return sum;  
} 

Điều này thay thế hành vi tràn đã ký không xác định bằng chuyển đổi giá trị nằm ngoài phạm vi được triển khai xác định giữa có ký và chưa ký, vì vậy bạn cần kiểm tra tài liệu trình biên dịch của mình để biết chính xác điều gì sẽ xảy ra, nhưng ít nhất nó phải được xác định rõ và nên làm điều đúng đắn trên bất kỳ máy bổ sung twos nào không làm tăng tín hiệu về chuyển đổi, đó là điều mà hầu hết mọi máy và trình biên dịch C được xây dựng trong 20 năm qua.


Bạn vẫn đang lưu trữ kết quả sum, đó là một int. Điều đó dẫn đến kết quả do triển khai xác định hoặc tín hiệu do triển khai xác định được nâng lên nếu giá trị của (unsigned)lhs + (unsigned)rhslớn hơn INT_MAX.
R .. GitHub NGỪNG TRỢ GIÚP ICE

2
@R: đó là toàn bộ điểm - hành vi được triển khai được xác định, thay vì không được xác định, vì vậy việc triển khai phải ghi lại những gì nó làm và thực hiện nó một cách nhất quán. Một tín hiệu chỉ có thể được nâng lên nếu việc triển khai ghi lại nó, trong trường hợp đó, nó phải luôn được nâng lên và bạn có thể sử dụng hành vi đó.
Chris Dodd

0

Trong trường hợp thêm hai longgiá trị, mã di động có thể chia longgiá trị thành các phần thấp và cao int(hoặc thành shortcác phần trong trường hợp longcó cùng kích thước int):

static_assert(sizeof(long) == 2*sizeof(int), "");
long a, b;
int ai[2] = {int(a), int(a >> (8*sizeof(int)))};
int bi[2] = {int(b), int(b >> (8*sizeof(int))});
... use the 'long' type to add the elements of 'ai' and 'bi'

Sử dụng lắp ráp nội tuyến là cách nhanh nhất nếu nhắm mục tiêu một CPU cụ thể:

long a, b;
bool overflow;
#ifdef __amd64__
    asm (
        "addq %2, %0; seto %1"
        : "+r" (a), "=ro" (overflow)
        : "ro" (b)
    );
#else
    #error "unsupported CPU"
#endif
if(overflow) ...
// The result is stored in variable 'a'

-1

Tôi nghĩ rằng điều này hoạt động:

int add(int lhs, int rhs) {
   volatile int sum = lhs + rhs;
   if (lhs != (sum - rhs) ) {
       /* overflow */
       //errno = ERANGE;
       abort();
   }
   return sum;
}

Việc sử dụng dễ bay hơi ngăn trình biên dịch tối ưu hóa thử nghiệm vì nó cho rằng sum có thể đã thay đổi giữa phép cộng và phép trừ.

Sử dụng gcc 4.4.3 cho x86_64, hợp ngữ cho mã này thực hiện phép cộng, phép trừ và kiểm tra, mặc dù nó lưu trữ mọi thứ trên ngăn xếp và các hoạt động ngăn xếp không cần thiết. Tôi thậm chí đã thửregister volatile int sum = nhưng việc lắp ráp vẫn như cũ.

Đối với phiên bản chỉ có int sum =(không có biến động hoặc thanh ghi), hàm không thực hiện kiểm tra và thực hiện phép cộng chỉ bằng một lealệnh ( lealà Địa chỉ hiệu quả tải và thường được sử dụng để thực hiện phép cộng mà không cần chạm vào thanh ghi cờ).

Phiên bản của bạn có mã lớn hơn và có nhiều bước nhảy hơn, nhưng tôi không biết cái nào sẽ tốt hơn .


4
-1 vì lạm dụng volatileđể che dấu hành vi không xác định. Nếu nó "hoạt động", bạn vẫn chỉ đang "gặp may".
R .. GitHub DỪNG TRỢ GIÚP ICE

@R: Nếu nó không hoạt động, trình biên dịch không triển khai volatilechính xác. Tất cả những gì tôi đang cố gắng là một giải pháp đơn giản hơn cho một vấn đề rất phổ biến về một câu hỏi đã được trả lời.
nategoose

Tuy nhiên, nơi nó có thể không thành công sẽ là một hệ thống có biểu diễn số bao bọc các giá trị thấp hơn khi tràn cho số nguyên.
nategoose

Nhận xét cuối cùng đó phải có "không" hoặc "không" trong đó.
nategoose

@nategoose, khẳng định của bạn rằng "nếu nó không hoạt động, trình biên dịch không triển khai đúng cách dễ bay hơi" là sai. Đối với một điều, trong số học phần bù của hai, nó sẽ luôn đúng rằng lhs = sum - rhs ngay cả khi xảy ra tràn. Ngay cả khi đó không phải là trường hợp, và mặc dù ví dụ cụ thể này là một bit, ví dụ, trình biên dịch có thể tạo mã thực hiện phép cộng, lưu trữ giá trị kết quả, đọc lại giá trị vào một thanh ghi khác, so sánh giá trị được lưu trữ với giá trị đã đọc giá trị và thông báo rằng chúng giống nhau và do đó giả sử không có tràn nào xảy ra.
davmac

-1

Theo tôi, cách kiểm tra đơn giản nhất sẽ là kiểm tra các dấu hiệu của toán hạng và kết quả.

Hãy kiểm tra tổng: tràn có thể xảy ra theo cả hai hướng, + hoặc -, chỉ khi cả hai toán hạng có cùng dấu. Và, theo lẽ thường, tràn sẽ xảy ra khi dấu của kết quả không giống như dấu của toán hạng.

Vì vậy, một séc như thế này là đủ:

int a, b, sum;
sum = a + b;
if  (((a ^ ~b) & (a ^ sum)) & 0x80000000)
    detect_oveflow();

Chỉnh sửa: như Nils đề xuất, đây là ifđiều kiện chính xác :

((((unsigned int)a ^ ~(unsigned int)b) & ((unsigned int)a ^ (unsigned int)sum)) & 0x80000000)

Và kể từ khi hướng dẫn

add eax, ebx 

dẫn đến hành vi không xác định? Không có điều đó trong sự khác biệt về tập lệnh x86 của Intel ..


2
Bạn đang thiếu điểm ở đây. Dòng mã thứ hai của bạn sum = a + bcó thể mang lại hành vi không xác định.
Kênh 72

nếu bạn cast Tóm lại, a và b để unsigned trong quá trình thử-Ngoài ra bạn mã của bạn sẽ làm việc btw ..
Nils Pipenbrinck

Nó không được xác định không phải vì chương trình sẽ gặp sự cố hoặc sẽ hoạt động khác nhau. Đó là điều chính xác mà bộ xử lý đang làm để tính toán cờ OF. Tiêu chuẩn chỉ đang cố gắng bảo vệ bản thân khỏi các trường hợp phi tiêu chuẩn, nhưng không có nghĩa là bạn không được phép làm điều này.
ruslik

@Nils vâng, tôi muốn làm điều đó, nhưng tôi nghĩ rằng bốn chữ (usngined int)s sẽ khiến nó khó đọc hơn nhiều. (bạn biết đấy, bạn lần đầu tiên đọc nó, và chỉ thử nếu bạn thích nó).
ruslik

1
hành vi không xác định là trong C, chứ không phải sau khi biên dịch để lắp ráp
phuclv
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.