Hoạt động bitwise trong kích thước biến không mong muốn


24

Bối cảnh

Chúng tôi đang chuyển mã C ban đầu được biên dịch bằng trình biên dịch C 8 bit cho vi điều khiển PIC. Một thành ngữ phổ biến đã được sử dụng để ngăn các biến toàn cục không dấu (ví dụ: bộ đếm lỗi) quay trở về 0 là như sau:

if(~counter) counter++;

Toán tử bitwise ở đây đảo ngược tất cả các bit và câu lệnh chỉ đúng nếu counternhỏ hơn giá trị tối đa. Điều quan trọng, điều này hoạt động bất kể kích thước biến.

Vấn đề

Chúng tôi hiện đang nhắm mục tiêu bộ xử lý ARM 32 bit bằng GCC. Chúng tôi đã nhận thấy rằng cùng một mã tạo ra kết quả khác nhau. Theo như chúng tôi có thể nói, có vẻ như hoạt động bổ sung bitwise trả về một giá trị có kích thước khác với mong đợi của chúng tôi. Để tái tạo điều này, chúng tôi biên dịch, trong GCC:

uint8_t i = 0;
int sz;

sz = sizeof(i);
printf("Size of variable: %d\n", sz); // Size of variable: 1

sz = sizeof(~i);
printf("Size of result: %d\n", sz); // Size of result: 4

Trong dòng đầu ra đầu tiên, chúng ta nhận được những gì chúng ta mong đợi: ilà 1 byte. Tuy nhiên, phần bù bit của ithực tế là bốn byte gây ra vấn đề vì so sánh với điều này bây giờ sẽ không cho kết quả như mong đợi. Ví dụ: nếu làm (nơi iđược khởi tạo đúng cách uint8_t):

if(~i) i++;

Chúng ta sẽ thấy i"bao quanh" từ 0xFF trở lại 0x00. Hành vi này khác với GCC so với khi nó được sử dụng để hoạt động như chúng tôi dự định trong trình biên dịch trước đó và vi điều khiển PIC 8 bit.

Chúng tôi biết rằng chúng tôi có thể giải quyết vấn đề này bằng cách sử dụng như vậy:

if((uint8_t)~i) i++;

Hoặc bằng cách

if(i < 0xFF) i++;

Tuy nhiên, trong cả hai cách giải quyết này, kích thước của biến phải được biết và dễ bị lỗi đối với nhà phát triển phần mềm. Những loại kiểm tra giới hạn trên xảy ra trong suốt cơ sở mã. Có rất nhiều kích cỡ của các biến (ví dụ., uint16_tunsigned charvv) và thay đổi này trong một codebase khác làm việc không phải là điều chúng ta mong muốn.

Câu hỏi

Sự hiểu biết của chúng ta về vấn đề có đúng không, và có các tùy chọn có sẵn để giải quyết vấn đề này không yêu cầu truy cập lại từng trường hợp mà chúng ta đã sử dụng thành ngữ này không? Giả định của chúng tôi có đúng không, rằng một hoạt động như bổ sung bitwise sẽ trả về một kết quả có cùng kích thước với toán hạng? Có vẻ như điều này sẽ phá vỡ, tùy thuộc vào kiến ​​trúc bộ xử lý. Tôi cảm thấy như mình đang uống thuốc điên và C nên dễ mang theo hơn một chút so với thứ này. Một lần nữa, sự hiểu biết của chúng tôi về điều này có thể sai.

Nhìn bề ngoài, điều này có vẻ không phải là một vấn đề lớn nhưng thành ngữ hoạt động trước đây này được sử dụng ở hàng trăm địa điểm và chúng tôi rất muốn hiểu điều này trước khi tiến hành những thay đổi đắt giá.


Lưu ý: Có một câu hỏi trùng lặp có vẻ giống nhau nhưng không chính xác ở đây: Thao tác bit trên char cho kết quả 32 bit

Tôi đã không thấy mấu chốt thực sự của vấn đề được thảo luận ở đó, cụ thể là, kích thước kết quả của phần bổ sung bit bit khác với những gì được đưa vào toán tử.


14
"Giả định của chúng tôi có đúng không, rằng một hoạt động như bổ sung bitwise sẽ trả về một kết quả có cùng kích thước với toán hạng?" Không, điều này là không chính xác, áp dụng khuyến mãi số nguyên.
Thomas Jager

2
Mặc dù chắc chắn có liên quan, tôi không tin đó là những bản sao của câu hỏi cụ thể này, vì chúng không cung cấp giải pháp cho vấn đề.
Cody Grey

3
Tôi cảm thấy như mình đang uống thuốc điên và C nên dễ mang theo hơn một chút so với thứ này. Nếu bạn không nhận được khuyến mãi số nguyên trên các loại 8 bit, thì trình biên dịch của bạn không tương thích chuẩn C. Trong trường hợp đó tôi nghĩ bạn nên trải qua tất cả các tính toán để kiểm tra chúng và sửa chữa nếu cần thiết.
dùng694733

1
Tôi có phải là người duy nhất tự hỏi logic gì, ngoài các bộ đếm thực sự không quan trọng, có thể đưa nó đến "gia tăng nếu có đủ không gian, nếu không thì hãy quên nó đi"? Nếu bạn đang chuyển mã, bạn có thể sử dụng int (4 byte) thay vì uint_8 không? Điều đó sẽ ngăn chặn vấn đề của bạn trong nhiều trường hợp.
puck

1
@puck Bạn nói đúng, chúng tôi có thể thay đổi thành 4 byte, nhưng nó sẽ phá vỡ tính tương thích khi giao tiếp với các hệ thống hiện có. Mục đích là để biết khi nào có bất kỳ lỗi nào và do đó, bộ đếm 1 byte ban đầu là đủ và vẫn còn như vậy.
Charlie Salts

Câu trả lời:


26

Những gì bạn đang thấy là kết quả của chương trình khuyến mãi số nguyên . Trong hầu hết các trường hợp trong đó một giá trị nguyên được sử dụng trong một biểu thức, nếu loại giá trị nhỏ hơn intgiá trị được thăng cấp int. Điều này được ghi lại trong phần 6.3.1.1p2 của tiêu chuẩn C :

Những điều sau đây có thể được sử dụng trong một biểu thức bất cứ nơi nào inthoặc unsigned intcó thể được sử dụng

  • Một đối tượng hoặc biểu thức có loại số nguyên (khác inthoặc unsigned int) có thứ hạng chuyển đổi số nguyên nhỏ hơn hoặc bằng thứ hạng của intunsigned int.
  • Một trường bit loại _Bool, int ,ký int int , orunsign.

Nếu một intcó thể đại diện cho tất cả các giá trị của loại ban đầu (bị giới hạn bởi chiều rộng, đối với trường bit), giá trị được chuyển đổi thành một int; mặt khác, nó được chuyển đổi thành một unsigned int. Chúng được gọi là các chương trình khuyến mãi số nguyên . Tất cả các loại khác không thay đổi bởi các chương trình khuyến mãi số nguyên.

Vì vậy, nếu một biến có loại uint8_tvà giá trị 255, sử dụng bất kỳ toán tử nào khác ngoài ép hoặc gán trên nó trước tiên sẽ chuyển đổi nó thành loại intvới giá trị 255 trước khi thực hiện thao tác. Đây là lý do tại sao sizeof(~i)cung cấp cho bạn 4 thay vì 1.

Mục 6.5.3.3 mô tả rằng các chương trình khuyến mãi số nguyên áp dụng cho ~nhà điều hành:

Kết quả của ~toán tử là phần bù bit của toán hạng (được thăng cấp) của nó ( nghĩa là, mỗi bit trong kết quả được đặt khi và chỉ khi bit tương ứng trong toán hạng được chuyển đổi không được đặt). Các khuyến mãi số nguyên được thực hiện trên toán hạng và kết quả có loại được thăng cấp. Nếu loại được quảng cáo là loại không dấu, biểu thức ~Etương đương với giá trị tối đa có thể biểu thị trong loại trừ đó E.

Vì vậy, giả sử 32 bit int, nếu countercó giá trị 8 bit, 0xffnó được chuyển đổi thành giá trị 32 bit 0x000000ffvà áp dụng ~cho nó 0xffffff00.

Có lẽ cách đơn giản nhất để xử lý việc này là không cần phải biết loại là kiểm tra xem giá trị có phải là 0 sau khi tăng hay không và nếu có thì giảm đi không.

if (!++counter) counter--;

Gói số nguyên không dấu hoạt động theo cả hai hướng, do đó, việc giảm giá trị 0 mang lại cho bạn giá trị dương lớn nhất.


1
if (!++counter) --counter;có thể ít lạ đối với một số lập trình viên hơn là sử dụng toán tử dấu phẩy.
Eric Postpischil

1
Một cách khác là ++counter; counter -= !counter;.
Eric Postpischil

@EricPostpischil Thật ra, tôi thích lựa chọn đầu tiên của bạn hơn. Đã chỉnh sửa.
dbush

15
Điều này là xấu xí và không thể đọc được cho dù bạn viết nó như thế nào. Nếu bạn phải sử dụng một thành ngữ như thế này, hãy ưu tiên mọi lập trình viên bảo trì và gói nó thành một hàm nội tuyến : một cái gì đó như increment_unsigned_without_wraparoundhoặc increment_with_saturation. Cá nhân, tôi sẽ sử dụng một hàm ba toán hạng chung clamp.
Cody Grey

5
Ngoài ra, bạn không thể biến điều này thành một hàm, bởi vì nó phải hoạt động khác nhau đối với các loại đối số khác nhau. Bạn sẽ phải sử dụng macro chung loại .
user2357112 hỗ trợ Monica

7

trong sizeof (i); bạn yêu cầu kích thước của biến i , vì vậy 1

trong sizeof (~ i); bạn yêu cầu kích thước của loại biểu thức, là một số nguyên , trong trường hợp của bạn 4


Để sử dụng

nếu (~ i)

để biết nếu tôi không đánh giá 255 (trong trường hợp của bạn với uint8_t) thì không thể đọc được, chỉ cần làm

if (i != 255)

và bạn sẽ có một mã di động và dễ đọc


Có nhiều kích thước của biến (ví dụ: uint16_t và char không dấu, v.v.)

Để quản lý bất kỳ kích thước không dấu:

if (i != (((uintmax_t) 2 << (sizeof(i)*CHAR_BIT-1)) - 1))

Biểu thức là hằng số, do đó được tính toán tại thời gian biên dịch.

#include <giới hạn.h> cho CHAR_BIT#include <stdint.h> cho uintmax_t


3
Câu hỏi nói rõ rằng họ có nhiều kích cỡ để giải quyết, vì vậy != 255không đầy đủ.
Eric Postpischil

@EricPostpischil ah vâng, tôi quên điều đó, vì vậy "if (i! = ((1u << sizeof (i) * 8) - 1))" giả sử luôn luôn không dấu?
bruno

1
Điều đó sẽ không được xác định cho unsignedcác đối tượng vì các dịch chuyển của chiều rộng đối tượng đầy đủ không được xác định bởi tiêu chuẩn C, nhưng nó có thể được sửa bằng (2u << sizeof(i)*CHAR_BIT-1) - 1.
Eric Postpischil

ồ vâng, CHAR_BIT, xấu của tôi
bruno

2
Để an toàn với các loại rộng hơn, người ta có thể sử dụng ((uintmax_t) 2 << sizeof(i)*CHAR_BIT-1) - 1.
Eric Postpischil

5

Dưới đây là một số tùy chọn để triển khai Thêm Add 1 vào xnhưng kẹp ở giá trị đại diện tối đa, được đưa ra xlà một số kiểu không dấu:

  1. Thêm một khi và chỉ khi xnhỏ hơn giá trị tối đa có thể biểu thị trong loại của nó:

    x += x < Maximum(x);

    Xem các mục sau đây cho định nghĩa của Maximum. Phương pháp này có cơ hội tốt để được trình biên dịch tối ưu hóa thành các hướng dẫn hiệu quả như so sánh, một số dạng tập hợp hoặc di chuyển có điều kiện và thêm.

  2. So sánh với giá trị lớn nhất của loại:

    if (x < ((uintmax_t) 2u << sizeof x * CHAR_BIT - 1) - 1) ++x

    (Điều này tính toán 2 N , trong đó N là số bit trong x, bằng cách dịch chuyển 2 bởi N 1 bit. Chúng tôi làm điều này thay vì dịch chuyển 1 bit N vì sự thay đổi số lượng bit trong một loại không được xác định bởi C tiêu chuẩn. CHAR_BITMacro có thể xa lạ với một số người, đó là số bit trong một byte, vì vậy sizeof x * CHAR_BITsố lượng bit trong loại x.)

    Điều này có thể được bọc trong một macro như mong muốn cho tính thẩm mỹ và rõ ràng:

    #define Maximum(x) (((uintmax_t) 2u << sizeof (x) * CHAR_BIT - 1) - 1)
    if (x < Maximum(x)) ++x;
  3. Tăng xvà sửa nếu nó kết thúc bằng 0, sử dụng mộtif :

    if (!++x) --x; // !++x is true if ++x wraps to zero.
  4. Tăng x và sửa nếu nó kết thúc bằng 0, sử dụng biểu thức:

    ++x; x -= !x;

    Điều này là không phân nhánh (đôi khi có lợi cho hiệu suất), nhưng trình biên dịch có thể thực hiện nó giống như trên, sử dụng một nhánh nếu cần nhưng có thể với các hướng dẫn vô điều kiện nếu kiến ​​trúc đích có các hướng dẫn phù hợp.

  5. Một tùy chọn không phân nhánh, sử dụng macro trên, là:

    x += 1 - x/Maximum(x);

    Nếu xlà tối đa của loại của nó, điều này ước tính x += 1-1. Nếu không, nó là x += 1-0. Tuy nhiên, sự phân chia có phần chậm trên nhiều kiến ​​trúc. Một trình biên dịch có thể tối ưu hóa điều này thành các hướng dẫn mà không cần phân chia, tùy thuộc vào trình biên dịch và kiến ​​trúc đích.


1
Tôi chỉ không thể tự mình đưa ra một câu trả lời khuyên bạn nên sử dụng macro. C có chức năng nội tuyến. Bạn không làm bất cứ điều gì bên trong định nghĩa vĩ mô mà không thể thực hiện dễ dàng bên trong hàm nội tuyến. Và nếu bạn sẽ sử dụng macro, hãy đảm bảo rằng bạn có dấu ngoặc đơn chiến lược cho rõ ràng: toán tử << có mức độ ưu tiên rất thấp. Clang cảnh báo về điều này với -Wshift-op-parentheses. Tin tốt là, một trình biên dịch tối ưu hóa sẽ không tạo ra một bộ phận ở đây, vì vậy bạn không phải lo lắng về việc nó bị chậm.
Cody Grey

1
@CodyGray, nếu bạn nghĩ bạn có thể làm điều này với một hàm, hãy viết câu trả lời.
Carsten S

2
@CodyGray: sizeof xkhông thể được triển khai bên trong hàm C vì xsẽ phải là một tham số (hoặc biểu thức khác) với một số loại cố định. Nó không thể tạo ra kích thước của bất kỳ loại đối số nào mà người gọi sử dụng. Một vĩ mô có thể.
Eric Postpischil

2

Trước stdint.h các kích thước biến có thể thay đổi từ trình biên dịch sang trình biên dịch và các loại biến thực tế trong C vẫn là int, dài, v.v. và vẫn được xác định bởi tác giả trình biên dịch về kích thước của chúng. Không phải một số giả định tiêu chuẩn cũng như mục tiêu cụ thể. Sau đó, tác giả cần tạo stdint.h để ánh xạ hai thế giới, đó là mục đích của stdint.h để ánh xạ uint_this đó thành int, dài, ngắn.

Nếu bạn đang chuyển mã từ một trình biên dịch khác và nó sử dụng char, short, int, long thì bạn phải đi qua từng loại và tự thực hiện cổng, không có cách nào xung quanh nó. Và hoặc bạn kết thúc với kích thước phù hợp cho biến, khai báo thay đổi nhưng mã như văn bản hoạt động ....

if(~counter) counter++;

hoặc ... cung cấp trực tiếp mặt nạ hoặc typecast

if((~counter)&0xFF) counter++;
if((uint_8)(~counter)) counter++;

Vào cuối ngày, nếu bạn muốn mã này hoạt động, bạn phải chuyển nó sang nền tảng mới. Sự lựa chọn của bạn như thế nào. Có, bạn phải dành thời gian đánh từng trường hợp và thực hiện đúng, nếu không bạn sẽ tiếp tục quay lại mã này thậm chí còn đắt hơn.

Nếu bạn cô lập các loại biến trên mã trước khi chuyển và kích thước của các loại biến đó, thì hãy cách ly các biến làm điều này (nên dễ grep) và thay đổi khai báo của chúng bằng định nghĩa stdint.h hy vọng sẽ không thay đổi trong tương lai, và bạn sẽ ngạc nhiên nhưng đôi khi các tiêu đề sai được sử dụng vì vậy thậm chí đặt séc vào để bạn có thể ngủ ngon hơn vào ban đêm

if(sizeof(uint_8)!=1) return(FAIL);

Và trong khi kiểu mã hóa đó hoạt động (if (~ counter) counter ++;), đối với tính di động mong muốn ngay bây giờ và trong tương lai, tốt nhất là sử dụng mặt nạ để giới hạn kích thước cụ thể (và không dựa vào khai báo), hãy làm điều này khi mã được viết ở vị trí đầu tiên hoặc chỉ hoàn thành cổng và sau đó bạn sẽ không phải chuyển lại cổng vào một ngày khác. Hoặc để làm cho mã dễ đọc hơn, hãy thực hiện nếu x <0xFF sau đó hoặc x! = 0xFF hoặc một cái gì đó tương tự sau đó trình biên dịch có thể tối ưu hóa nó thành cùng một mã cho bất kỳ giải pháp nào trong số này, chỉ làm cho nó dễ đọc hơn và ít rủi ro hơn ...

Phụ thuộc vào mức độ quan trọng của sản phẩm hoặc số lần bạn muốn gửi các bản vá / cập nhật hoặc lăn một chiếc xe tải hoặc đi bộ đến phòng thí nghiệm để khắc phục sự cố xem bạn có cố gắng tìm giải pháp nhanh chóng hay chỉ cần chạm vào các dòng mã bị ảnh hưởng. nếu nó chỉ là một trăm hoặc một vài cái mà không phải là một cổng lớn.


0
6.5.3.3 Toán tử số học đơn phương
...
4 Kết quả của ~toán tử là phần bù bit của toán hạng (được thăng cấp) của nó (nghĩa là, mỗi bit trong kết quả được đặt khi và chỉ khi bit tương ứng trong toán hạng được chuyển đổi không được đặt ). Các khuyến mãi số nguyên được thực hiện trên toán hạng và kết quả có loại được thăng cấp . Nếu loại được quảng cáo là loại không dấu, biểu thức ~Etương đương với giá trị tối đa có thể biểu thị trong loại trừ đó E.

Dự thảo trực tuyến C 2011

Vấn đề là toán hạng của ~đang được thăng cấp inttrước khi toán tử được áp dụng.

Thật không may, tôi không nghĩ rằng có một cách dễ dàng để thoát khỏi điều này. Viết

if ( counter + 1 ) counter++;

Sẽ không có ích vì chương trình khuyến mãi cũng được áp dụng. Điều duy nhất tôi có thể đề xuất là tạo một số hằng số tượng trưng cho giá trị tối đa bạn muốn đối tượng đó đại diện và kiểm tra đối với điều đó:

#define MAX_COUNTER 255
...
if ( counter < MAX_COUNTER-1 ) counter++;

Tôi đánh giá cao quan điểm về quảng cáo số nguyên - có vẻ như đây là vấn đề chúng tôi đang gặp phải. Tuy nhiên, điều đáng nói là trong mẫu mã thứ hai của bạn, điều -1không cần thiết, vì điều này sẽ khiến bộ đếm giải quyết ở mức 254 (0xFE). Trong mọi trường hợp, cách tiếp cận này, như được đề cập trong câu hỏi của tôi, không lý tưởng do các kích thước biến khác nhau trong cơ sở mã tham gia vào thành ngữ này.
Charlie Salts
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.