Khi hỏi về hành vi không xác định phổ biến trong C , đôi khi người ta đề cập đến quy tắc răng cưa nghiêm ngặt.
Bọn họ đang nói gì thế?
Khi hỏi về hành vi không xác định phổ biến trong C , đôi khi người ta đề cập đến quy tắc răng cưa nghiêm ngặt.
Bọn họ đang nói gì thế?
Câu trả lời:
Một tình huống điển hình khi bạn gặp phải các vấn đề răng cưa nghiêm ngặt là khi chồng một cấu trúc (như thông điệp thiết bị / mạng) lên bộ đệm có kích thước từ của hệ thống của bạn (như con trỏ tới uint32_t
s hoặc uint16_t
s). Khi bạn phủ một cấu trúc lên một bộ đệm như vậy hoặc một bộ đệm lên một cấu trúc như vậy thông qua việc đúc con trỏ, bạn có thể dễ dàng vi phạm các quy tắc bí danh nghiêm ngặt.
Vì vậy, trong kiểu thiết lập này, nếu tôi muốn gửi tin nhắn đến một thứ gì đó, tôi phải có hai con trỏ không tương thích trỏ đến cùng một đoạn bộ nhớ. Sau đó, tôi có thể ngây thơ mã hóa một cái gì đó như thế này (trên một hệ thống với sizeof(int) == 2
):
typedef struct Msg
{
unsigned int a;
unsigned int b;
} Msg;
void SendWord(uint32_t);
int main(void)
{
// Get a 32-bit buffer from the system
uint32_t* buff = malloc(sizeof(Msg));
// Alias that buffer through message
Msg* msg = (Msg*)(buff);
// Send a bunch of messages
for (int i =0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendWord(buff[0]);
SendWord(buff[1]);
}
}
Quy tắc bí danh nghiêm ngặt làm cho thiết lập này trở thành bất hợp pháp: hủy bỏ một con trỏ bí danh một đối tượng không phải là loại tương thích hoặc một trong các loại khác được cho phép bởi C 2011 6.5 đoạn 7 1 là hành vi không xác định. Thật không may, bạn vẫn có thể mã theo cách này, có thể nhận được một số cảnh báo, biên dịch tốt, chỉ có hành vi bất ngờ kỳ lạ khi bạn chạy mã.
(GCC có vẻ không nhất quán trong khả năng đưa ra các cảnh báo răng cưa, đôi khi đưa ra cảnh báo thân thiện và đôi khi không.)
Để xem tại sao hành vi này không được xác định, chúng ta phải suy nghĩ về những gì quy tắc bí danh nghiêm ngặt mua trình biên dịch. Về cơ bản, với quy tắc này, không cần phải suy nghĩ về việc chèn hướng dẫn để làm mới nội dung của buff
mỗi lần chạy vòng lặp. Thay vào đó, khi tối ưu hóa, với một số giả định khó chịu về bí danh, nó có thể bỏ qua các hướng dẫn đó, tải buff[0]
và buff[1
] vào các thanh ghi CPU một lần trước khi vòng lặp được chạy và tăng tốc phần thân của vòng lặp. Trước khi bí danh nghiêm ngặt được giới thiệu, trình biên dịch phải sống trong trạng thái hoang tưởng rằng nội dung buff
có thể thay đổi bất cứ lúc nào từ bất cứ nơi nào bởi bất cứ ai. Vì vậy, để có được lợi thế hiệu suất cao hơn và giả sử hầu hết mọi người không gõ kiểu con trỏ, quy tắc bí danh nghiêm ngặt đã được đưa ra.
Hãy ghi nhớ, nếu bạn nghĩ rằng ví dụ này bị chiếm đoạt, điều này thậm chí có thể xảy ra nếu bạn chuyển một bộ đệm sang một chức năng khác thực hiện việc gửi cho bạn, nếu thay vào đó bạn có.
void SendMessage(uint32_t* buff, size_t size32)
{
for (int i = 0; i < size32; ++i)
{
SendWord(buff[i]);
}
}
Và viết lại vòng lặp trước đó của chúng tôi để tận dụng chức năng tiện lợi này
for (int i = 0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendMessage(buff, 2);
}
Trình biên dịch có thể hoặc không thể hoặc đủ thông minh để thử gửi SendMessage nội tuyến và nó có thể hoặc không thể quyết định tải hoặc không tải lại buff. Nếu SendMessage
là một phần của API khác được biên dịch riêng, thì nó có thể có hướng dẫn để tải nội dung của buff. Sau đó, một lần nữa, có thể bạn đang ở trong C ++ và đây là một số tiêu đề chỉ thực hiện mà trình biên dịch nghĩ rằng nó có thể nội tuyến. Hoặc có thể đó chỉ là thứ bạn đã viết trong tệp .c để thuận tiện cho chính bạn. Dù sao hành vi không xác định vẫn có thể xảy ra. Ngay cả khi chúng tôi biết một số điều xảy ra dưới mui xe, nó vẫn vi phạm quy tắc nên không có hành vi được xác định rõ nào được đảm bảo. Vì vậy, chỉ bằng cách gói trong một hàm có bộ đệm phân cách từ của chúng tôi không nhất thiết phải giúp đỡ.
Vì vậy, làm thế nào để tôi có được xung quanh này?
Sử dụng một công đoàn. Hầu hết các trình biên dịch hỗ trợ điều này mà không phàn nàn về răng cưa nghiêm ngặt. Điều này được cho phép trong C99 và được cho phép rõ ràng trong C11.
union {
Msg msg;
unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
};
Bạn có thể vô hiệu hóa bí danh nghiêm ngặt trong trình biên dịch của mình ( f [no-] aliasing aliasing trong gcc))
Bạn có thể sử dụng char*
cho răng cưa thay vì từ hệ thống của bạn. Các quy tắc cho phép một ngoại lệ cho char*
(bao gồm signed char
và unsigned char
). Nó luôn luôn giả định rằng char*
bí danh các loại khác. Tuy nhiên, điều này sẽ không hoạt động theo cách khác: không có giả định rằng cấu trúc của bạn bí danh một bộ đệm ký tự.
Người mới bắt đầu hãy cẩn thận
Đây chỉ là một mỏ khai thác tiềm năng khi chồng hai loại lên nhau. Bạn cũng nên tìm hiểu về endianness , căn chỉnh từ và cách xử lý các vấn đề căn chỉnh thông qua các cấu trúc đóng gói chính xác.
1 Các loại mà C 2011 6.5 7 cho phép một giá trị truy cập là:
unsigned char*
được sử dụng xa hơn char*
? Tôi có xu hướng sử dụng unsigned char
chứ không phải char
là loại cơ bản byte
vì các byte của tôi không được ký và tôi không muốn sự kỳ lạ của hành vi đã ký (đáng chú ý là bị tràn)
unsigned char *
là được.
uint32_t* buff = malloc(sizeof(Msg));
công đoàn của bạn và sau đó unsigned int asBuffer[sizeof(Msg)];
sẽ có kích thước khác nhau và không đúng. Cuộc malloc
gọi dựa vào căn chỉnh 4 byte dưới mui xe (không thực hiện) và liên kết sẽ lớn hơn 4 lần so với yêu cầu ... Tôi hiểu rằng đó là sự rõ ràng nhưng nó không gây ra lỗi gì cho tôi - ít hơn ...
Lời giải thích tốt nhất mà tôi đã tìm thấy là của Mike Acton, Hiểu về bí danh nghiêm ngặt . Nó tập trung một chút vào phát triển PS3, nhưng về cơ bản đó chỉ là GCC.
Từ bài viết:
"Bí danh nghiêm ngặt là một giả định, được tạo bởi trình biên dịch C (hoặc C ++), rằng các con trỏ hội thảo cho các đối tượng thuộc các loại khác nhau sẽ không bao giờ đề cập đến cùng một vị trí bộ nhớ (nghĩa là bí danh lẫn nhau.)"
Vì vậy, về cơ bản nếu bạn int*
chỉ vào một số bộ nhớ có chứa một bộ nhớ int
và sau đó bạn trỏ float*
đến bộ nhớ đó và sử dụng nó làm bộ nhớ float
bạn phá vỡ quy tắc. Nếu mã của bạn không tôn trọng điều này, thì trình tối ưu hóa của trình biên dịch rất có thể sẽ phá vỡ mã của bạn.
Ngoại lệ cho quy tắc là a char*
, được phép trỏ đến bất kỳ loại nào.
Đây là quy tắc răng cưa nghiêm ngặt, được tìm thấy trong phần 3.10 của tiêu chuẩn C ++ 03 (các câu trả lời khác cung cấp giải thích tốt, nhưng không có quy tắc nào cung cấp chính quy tắc này):
Nếu một chương trình cố gắng truy cập giá trị được lưu trữ của một đối tượng thông qua một giá trị khác với một trong các loại sau đây thì hành vi không được xác định:
- loại động của đối tượng,
- một phiên bản đủ điều kiện cv của loại động của đối tượng,
- một loại là loại đã ký hoặc không dấu tương ứng với loại động của đối tượng,
- một loại là loại đã ký hoặc không dấu tương ứng với phiên bản đủ điều kiện cv của loại động của đối tượng,
- một loại tổng hợp hoặc liên minh bao gồm một trong các loại đã nói ở trên trong số các thành viên của nó (bao gồm, đệ quy, một thành viên của một phân nhóm hoặc liên kết có chứa),
- một loại là loại lớp cơ sở (có thể đủ điều kiện cv) của loại động của đối tượng,
- một
char
hoặcunsigned char
loại.
Từ ngữ C ++ 11 và C ++ 14 (những thay đổi được nhấn mạnh):
Nếu một nỗ lực chương trình để truy cập giá trị được lưu trữ của một đối tượng thông qua một glvalue của khác hơn là một trong các loại sau đây hành vi này là không xác định:
- loại động của đối tượng,
- một phiên bản đủ điều kiện cv của loại động của đối tượng,
- một loại tương tự (như được định nghĩa trong 4.4) với loại động của đối tượng,
- một loại là loại đã ký hoặc không dấu tương ứng với loại động của đối tượng,
- một loại là loại đã ký hoặc không dấu tương ứng với phiên bản đủ điều kiện cv của loại động của đối tượng,
- một loại tổng hợp hoặc kết hợp bao gồm một trong các loại đã nói ở trên trong số các thành phần của nó hoặc các thành viên dữ liệu không tĩnh (bao gồm, đệ quy, một thành phần hoặc thành viên dữ liệu không tĩnh của một phân nhóm hoặc liên kết có chứa),
- một loại là loại lớp cơ sở (có thể đủ điều kiện cv) của loại động của đối tượng,
- một
char
hoặcunsigned char
loại.
Hai thay đổi là nhỏ: glvalue thay vì lvalue và làm rõ trường hợp tổng hợp / liên minh.
Thay đổi thứ ba làm cho một sự bảo đảm mạnh mẽ hơn (nới lỏng quy tắc bí danh mạnh mẽ): Khái niệm mới về các loại tương tự hiện an toàn với bí danh.
Ngoài ra, từ ngữ C (C99; ISO / IEC 9899: 1999 6.5 / 7; từ ngữ chính xác được sử dụng trong ISO / IEC 9899: 2011 §6.5 7):
Một đối tượng sẽ có giá trị được lưu trữ chỉ được truy cập bằng biểu thức lvalue có một trong các loại sau 73) hoặc 88) :
- một loại tương thích với loại hiệu quả của đối tượng,
- phiên bản quali của một loại tương thích với loại hiệu quả của đối tượng,
- một loại là loại đã ký hoặc không dấu tương ứng với loại hiệu quả của đối tượng,
- một loại là loại đã ký hoặc không dấu tương ứng với phiên bản quali của loại đối tượng hiệu quả,
- một loại tổng hợp hoặc liên minh bao gồm một trong các loại đã nói ở trên giữa các thành viên của nó (bao gồm, đệ quy, một thành viên của liên hiệp hoặc phân nhóm), hoặc
- một kiểu nhân vật.
73) hoặc 88) Mục đích của danh sách này là chỉ định những trường hợp trong đó một đối tượng có thể hoặc không được đặt bí danh.
wow(&u->s1,&u->s2)
sẽ cần phải hợp pháp ngay cả khi một con trỏ được sử dụng để sửa đổi u
và điều đó sẽ phủ nhận hầu hết các tối ưu hóa rằng quy tắc răng cưa được thiết kế để tạo điều kiện.
Điều này được trích từ "Quy tắc bí danh nghiêm ngặt của tôi là gì và tại sao chúng ta quan tâm?" hãy viết ra giấy.
Trong bí danh C và C ++ có liên quan đến loại biểu thức nào chúng ta được phép truy cập các giá trị được lưu trữ thông qua. Trong cả C và C ++, tiêu chuẩn chỉ định loại biểu thức nào được phép đặt bí danh cho loại nào. Trình biên dịch và trình tối ưu hóa được phép giả định rằng chúng tôi tuân thủ nghiêm ngặt các quy tắc răng cưa, do đó thuật ngữ quy tắc răng cưa nghiêm ngặt . Nếu chúng tôi cố gắng truy cập một giá trị bằng cách sử dụng một loại không được phép, nó được phân loại là hành vi không xác định ( UB ). Khi chúng tôi có hành vi không xác định, tất cả các cược đã tắt, kết quả của chương trình của chúng tôi không còn đáng tin cậy nữa.
Thật không may với các vi phạm bí danh nghiêm ngặt, chúng tôi sẽ thường nhận được kết quả mà chúng tôi mong đợi, để lại khả năng phiên bản tương lai của trình biên dịch với tối ưu hóa mới sẽ phá vỡ mã mà chúng tôi cho là hợp lệ. Điều này là không mong muốn và nó là một mục tiêu đáng giá để hiểu các quy tắc bí danh nghiêm ngặt và làm thế nào để tránh vi phạm chúng.
Để hiểu thêm về lý do tại sao chúng tôi quan tâm, chúng tôi sẽ thảo luận về các vấn đề phát sinh khi vi phạm các quy tắc răng cưa nghiêm ngặt, loại xảo quyệt vì các kỹ thuật phổ biến được sử dụng trong loại picky thường vi phạm các quy tắc bí danh nghiêm ngặt và cách gõ chữ đúng.
Chúng ta hãy xem xét một số ví dụ, sau đó chúng ta có thể nói về chính xác những gì các tiêu chuẩn nói, kiểm tra một số ví dụ khác và sau đó xem làm thế nào để tránh bí danh nghiêm ngặt và bắt vi phạm mà chúng ta đã bỏ lỡ. Đây là một ví dụ không đáng ngạc nhiên ( ví dụ trực tiếp ):
int x = 10;
int *ip = &x;
std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";
Chúng ta có một int * trỏ đến bộ nhớ bị chiếm bởi một int và đây là một bí danh hợp lệ. Trình tối ưu hóa phải cho rằng các bài tập thông qua ip có thể cập nhật giá trị chiếm dụng bởi x .
Ví dụ tiếp theo cho thấy răng cưa dẫn đến hành vi không xác định ( ví dụ trực tiếp ):
int foo( float *f, int *i ) {
*i = 1;
*f = 0.f;
return *i;
}
int main() {
int x = 0;
std::cout << x << "\n"; // Expect 0
x = foo(reinterpret_cast<float*>(&x), &x);
std::cout << x << "\n"; // Expect 0?
}
Trong hàm foo, chúng ta lấy một int * và float * , trong ví dụ này, chúng ta gọi foo và đặt cả hai tham số để trỏ đến cùng một vị trí bộ nhớ mà trong ví dụ này chứa một int . Lưu ý, reinterpret_cast đang báo cho trình biên dịch xử lý biểu thức như thể nó có kiểu được chỉ định bởi tham số mẫu của nó. Trong trường hợp này, chúng tôi đang bảo nó xử lý biểu thức & x như thể nó có kiểu float * . Chúng tôi có thể ngây thơ mong đợi kết quả của cout thứ hai là 0 nhưng với tối ưu hóa được kích hoạt bằng cách sử dụng -O2 cả gcc và clang tạo ra kết quả như sau:
0
1
Điều này có thể không được mong đợi nhưng hoàn toàn hợp lệ vì chúng tôi đã gọi hành vi không xác định. Một float không thể hợp lệ bí danh một đối tượng int . Do đó, trình tối ưu hóa có thể giả sử hằng số 1 được lưu trữ khi hội thảo i sẽ là giá trị trả về do một cửa hàng qua f không thể ảnh hưởng hợp lệ đến một đối tượng int . Việc cắm mã trong Compiler Explorer cho thấy đây chính xác là những gì đang xảy ra ( ví dụ trực tiếp ):
foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1
mov dword ptr [rdi], 0
mov eax, 1
ret
Trình tối ưu hóa sử dụng Phân tích bí danh dựa trên loại (TBAA) giả định 1 sẽ được trả về và trực tiếp di chuyển giá trị không đổi vào thanh ghi eax mang giá trị trả về. TBAA sử dụng các quy tắc ngôn ngữ về loại nào được phép đặt bí danh để tối ưu hóa tải và lưu trữ. Trong trường hợp này, TBAA biết rằng một float không thể bí danh và int và tối ưu hóa tải của i .
Chính xác thì tiêu chuẩn nói chúng ta được phép và không được phép làm gì? Ngôn ngữ tiêu chuẩn không đơn giản, vì vậy đối với mỗi mục tôi sẽ cố gắng cung cấp các ví dụ mã thể hiện ý nghĩa.
Các C11 tiêu chuẩn nói sau đây trong phần 6,5 Expressions đoạn 7 :
Một đối tượng sẽ có giá trị được lưu trữ chỉ được truy cập bằng biểu thức lvalue có một trong các loại sau: 88) - một loại tương thích với loại hiệu quả của đối tượng,
int x = 1;
int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int
- phiên bản đủ điều kiện của loại tương thích với loại đối tượng hiệu quả,
int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int
- một loại là loại đã ký hoặc không dấu tương ứng với loại hiệu quả của đối tượng,
int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to
// the effective type of the object
gcc / clang có một phần mở rộng và cũng cho phép gán int * không dấu cho int * mặc dù chúng không phải là loại tương thích.
- một loại là loại đã ký hoặc không dấu tương ứng với một phiên bản đủ điều kiện của loại đối tượng hiệu quả,
int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type
// that corresponds with to a qualified verison of the effective type of the object
- một loại tổng hợp hoặc liên minh bao gồm một trong các loại đã nói ở trên trong số các thành viên của nó (bao gồm, đệ quy, một thành viên của một tập hợp phụ hoặc có liên kết), hoặc
struct foo {
int x;
};
void foobar( struct foo *fp, int *ip ); // struct foo is an aggregate that includes int among its members so it can
// can alias with *ip
foo f;
foobar( &f, &f.x );
- một kiểu nhân vật.
int x = 65;
char *p = (char *)&x;
printf("%c\n", *p ); // *p gives us an lvalue expression of type char which is a character type.
// The results are not portable due to endianness issues.
Tiêu chuẩn dự thảo C ++ 17 trong phần [basic.lval] đoạn 11 nói:
Nếu một chương trình cố gắng truy cập giá trị được lưu trữ của một đối tượng thông qua một giá trị khác với một trong các loại sau đây thì hành vi không được xác định: 63 (11.1) - loại động của đối tượng,
void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0}; // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n"; // *ip gives us a glvalue expression of type int which matches the dynamic type
// of the allocated object
(11.2) - phiên bản đủ điều kiện cv của loại động của đối tượng,
int x = 1;
const int *cip = &x;
std::cout << *cip << "\n"; // *cip gives us a glvalue expression of type const int which is a cv-qualified
// version of the dynamic type of x
(11.3) - một loại tương tự (như được định nghĩa trong 7.5) với loại động của đối tượng,
(11.4) - một loại là loại đã ký hoặc không dấu tương ứng với loại động của đối tượng,
// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
si = 1;
ui = 2;
return si;
}
(11,5) - một loại là loại đã ký hoặc không dấu tương ứng với phiên bản đủ điều kiện cv của loại động của đối tượng,
signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing
(11.6) - một loại tổng hợp hoặc kết hợp bao gồm một trong các loại đã nói ở trên trong số các thành phần hoặc thành viên dữ liệu không phải là dữ liệu (bao gồm, đệ quy, một thành phần hoặc thành viên dữ liệu không tĩnh của liên kết gộp hoặc chứa)
struct foo {
int x;
};
// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
fp.x = 1;
ip = 2;
return fp.x;
}
foo f;
foobar( f, f.x );
(11.7) - một loại là loại lớp cơ sở (có thể đủ điều kiện cv) của loại động của đối tượng,
struct foo { int x ; };
struct bar : public foo {};
int foobar( foo &f, bar &b ) {
f.x = 1;
b.x = 2;
return f.x;
}
(11.8) - một kiểu char, unsign char hoặc std :: byte.
int foo( std::byte &b, uint32_t &ui ) {
b = static_cast<std::byte>('a');
ui = 0xFFFFFFFF;
return std::to_integer<int>( b ); // b gives us a glvalue expression of type std::byte which can alias
// an object of type uint32_t
}
Đáng chú ý char đã ký không được bao gồm trong danh sách trên, đây là một sự khác biệt đáng chú ý từ C mà nói một loại ký tự .
Chúng tôi đã đi đến điểm này và chúng tôi có thể tự hỏi, tại sao chúng tôi muốn bí danh cho? Câu trả lời thường là gõ chữ , thường các phương thức được sử dụng vi phạm các quy tắc răng cưa nghiêm ngặt.
Đôi khi chúng tôi muốn phá vỡ hệ thống loại và giải thích một đối tượng là một loại khác. Điều này được gọi là loại pucky , để diễn giải lại một đoạn bộ nhớ như một loại khác. Kiểu pucky rất hữu ích cho các tác vụ muốn truy cập vào biểu diễn cơ bản của một đối tượng để xem, vận chuyển hoặc thao tác. Các lĩnh vực điển hình mà chúng tôi thấy loại pucky đang được sử dụng là trình biên dịch, tuần tự hóa, mã mạng, v.v.
Theo truyền thống, điều này đã được thực hiện bằng cách lấy địa chỉ của đối tượng, chuyển nó thành một con trỏ của loại mà chúng ta muốn diễn giải lại nó và sau đó truy cập giá trị, hay nói cách khác bằng cách đặt bí danh. Ví dụ:
int x = 1 ;
// In C
float *fp = (float*)&x ; // Not a valid aliasing
// In C++
float *fp = reinterpret_cast<float*>(&x) ; // Not a valid aliasing
printf( "%f\n", *fp ) ;
Như chúng ta đã thấy trước đây, đây không phải là một bí danh hợp lệ, vì vậy chúng ta đang gọi hành vi không xác định. Nhưng các trình biên dịch truyền thống đã không tận dụng các quy tắc bí danh nghiêm ngặt và loại mã này thường chỉ hoạt động, các nhà phát triển đã không may làm quen với việc làm theo cách này. Một phương pháp thay thế phổ biến cho loại picky là thông qua các hiệp hội, có giá trị trong C nhưng hành vi không xác định trong C ++ ( xem ví dụ trực tiếp ):
union u1
{
int n;
float f;
} ;
union u1 u;
u.f = 1.0f;
printf( "%d\n”, u.n ); // UB in C++ n is not the active member
Điều này không hợp lệ trong C ++ và một số người coi mục đích của các công đoàn chỉ là để thực hiện các loại biến thể và cảm thấy việc sử dụng các công đoàn cho loại picky là một sự lạm dụng.
Phương pháp tiêu chuẩn để loại pucky trong cả C và C ++ là memcpy . Điều này có vẻ hơi nặng tay nhưng trình tối ưu hóa sẽ nhận ra việc sử dụng memcpy để loại pucky và tối ưu hóa nó đi và tạo ra một đăng ký để đăng ký di chuyển. Ví dụ: nếu chúng ta biết int64_t có cùng kích thước với double :
static_assert( sizeof( double ) == sizeof( int64_t ) ); // C++17 does not require a message
chúng ta có thể sử dụng memcpy :
void func1( double d ) {
std::int64_t n;
std::memcpy(&n, &d, sizeof d);
//...
Ở mức tối ưu hóa đủ, bất kỳ trình biên dịch hiện đại phong nha nào cũng tạo ra mã giống hệt với phương thức reinterpret_cast hoặc phương thức hợp nhất đã đề cập trước đó để loại picky . Kiểm tra mã được tạo, chúng tôi thấy nó chỉ sử dụng đăng ký Mov ( Ví dụ trình biên dịch trình biên dịch trực tiếp ).
Trong C ++ 20, chúng tôi có thể nhận được bit_cast ( triển khai có sẵn trong liên kết từ đề xuất ), điều này mang lại một cách đơn giản và an toàn để chơi chữ cũng như có thể sử dụng được trong ngữ cảnh constexpr.
Sau đây là một ví dụ về cách sử dụng bit_cast để gõ chữ int unsced để nổi , ( xem trực tiếp ):
std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)
Trong trường hợp các loại To và From không có cùng kích thước, nó yêu cầu chúng ta sử dụng một cấu trúc trung gian15. Chúng ta sẽ sử dụng một cấu trúc chứa một mảng ký tự sizeof (unsign int) ( giả sử 4 byte unsign int ) là kiểu From và unsign int như kiểu To .:
struct uint_chars {
unsigned char arr[sizeof( unsigned int )] = {} ; // Assume sizeof( unsigned int ) == 4
};
// Assume len is a multiple of 4
int bar( unsigned char *p, size_t len ) {
int result = 0;
for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
uint_chars f;
std::memcpy( f.arr, &p[index], sizeof(unsigned int));
unsigned int result = bit_cast<unsigned int>(f);
result += foo( result );
}
return result ;
}
Thật không may là chúng ta cần loại trung gian này nhưng đó là ràng buộc hiện tại của bit_cast .
Chúng tôi không có nhiều công cụ tốt để bắt răng cưa nghiêm ngặt trong C ++, các công cụ chúng tôi có sẽ bắt được một số trường hợp vi phạm bí danh nghiêm ngặt và một số trường hợp tải và lưu trữ sai.
gcc sử dụng hàm răng cưa -fstrict-aliasing và -Wstrict-aliasing có thể bắt được một số trường hợp mặc dù không phải không có dương / âm sai. Ví dụ: các trường hợp sau sẽ tạo cảnh báo trong gcc ( xem trực tiếp ):
int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught
// it was being accessed w/ an indeterminate value below
printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));
mặc dù nó sẽ không bắt được trường hợp bổ sung này ( xem trực tiếp ):
int *p;
p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));
Mặc dù clang cho phép những lá cờ này nhưng dường như nó không thực sự cảnh báo.
Một công cụ khác mà chúng tôi có sẵn cho chúng tôi là ASan có thể bắt các tải và cửa hàng bị sai lệch. Mặc dù đây không phải là những vi phạm răng cưa trực tiếp nghiêm ngặt nhưng chúng là kết quả chung của các vi phạm răng cưa nghiêm ngặt. Ví dụ: các trường hợp sau sẽ tạo ra lỗi thời gian chạy khi được tạo bằng clang bằng cách sử dụng -fsanitize = address
int *x = new int[2]; // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6); // regardless of alignment of x this will not be an aligned address
*u = 1; // Access to range [6-9]
printf( "%d\n", *u ); // Access to range [6-9]
Công cụ cuối cùng tôi sẽ giới thiệu là C ++ cụ thể và không hoàn toàn là một công cụ mà là thực hành mã hóa, không cho phép các kiểu phôi C. Cả gcc và clang sẽ tạo ra chẩn đoán cho các kiểu phôi C bằng cách sử dụng -Wold-style-cast . Điều này sẽ buộc mọi kiểu chơi chữ không xác định sử dụng reinterpret_cast, nói chung reinterpret_cast phải là một cờ để xem xét mã gần hơn. Nó cũng dễ dàng hơn để tìm kiếm cơ sở mã của bạn cho reinterpret_cast để thực hiện kiểm toán.
Đối với C, chúng tôi có tất cả các công cụ đã được trình bày và chúng tôi cũng có trình thông dịch tis, một bộ phân tích tĩnh phân tích triệt để một chương trình cho một tập hợp con lớn của ngôn ngữ C. Đưa ra một câu C của ví dụ trước đó trong đó việc sử dụng -fstrict-aliasing bỏ lỡ một trường hợp ( xem trực tiếp )
int a = 1;
short j;
float f = 1.0 ;
printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));
int *p;
p=&a;
printf("%i\n", j = *((short*)p));
tis-interpeter có thể bắt được cả ba, ví dụ sau đây gọi tis-kernal là tis-phiên dịch (đầu ra được chỉnh sửa cho ngắn gọn):
./bin/tis-kernel -sa example1.c
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
rules by accessing a cell with effective type int.
...
example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
accessing a cell with effective type float.
Callstack: main
...
example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
accessing a cell with effective type int.
Cuối cùng là TySan hiện đang được phát triển. Trình khử trùng này thêm thông tin kiểm tra loại trong phân đoạn bộ nhớ bóng và kiểm tra truy cập để xem liệu chúng có vi phạm quy tắc răng cưa hay không. Công cụ có khả năng có thể bắt được tất cả các vi phạm răng cưa nhưng có thể có chi phí hoạt động lớn.
reinterpret_cast
có thể làm hoặc cout
có nghĩa là gì . (Bạn hoàn toàn có thể đề cập đến C ++ nhưng câu hỏi ban đầu là về C và IIUC, những ví dụ này có thể được viết một cách hợp lệ bằng C.)
Bí danh nghiêm ngặt không chỉ đề cập đến con trỏ, nó cũng ảnh hưởng đến các tài liệu tham khảo, tôi đã viết một bài báo về nó cho wiki nhà phát triển thúc đẩy và nó đã được đón nhận đến mức tôi đã biến nó thành một trang trên trang web tư vấn của mình. Nó giải thích hoàn toàn nó là gì, tại sao nó làm mọi người bối rối và phải làm gì với nó. Giấy trắng răng cưa nghiêm ngặt . Cụ thể, nó giải thích tại sao các công đoàn là hành vi rủi ro cho C ++ và tại sao sử dụng memcpy là bản sửa lỗi duy nhất có thể di chuyển trên cả C và C ++. Hy vọng điều này là hữu ích.
Như phần phụ lục cho những gì Doug T. đã viết, đây là một trường hợp thử nghiệm đơn giản có thể kích hoạt nó với gcc:
kiểm tra
#include <stdio.h>
void check(short *h,long *k)
{
*h=5;
*k=6;
if (*h == 5)
printf("strict aliasing problem\n");
}
int main(void)
{
long k[1];
check((short *)k,k);
return 0;
}
Biên dịch với gcc -O2 -o check check.c
. Thông thường (với hầu hết các phiên bản gcc tôi đã thử), điều này dẫn đến "vấn đề răng cưa nghiêm ngặt", bởi vì trình biên dịch giả định rằng "h" không thể có cùng địa chỉ với "k" trong chức năng "kiểm tra". Do đó, trình biên dịch tối ưu hóa if (*h == 5)
đi và luôn gọi printf.
Đối với những người quan tâm ở đây là mã trình biên dịch x64, được sản xuất bởi gcc 4.6.3, chạy trên Ubuntu 12.04.2 cho x64:
movw $5, (%rdi)
movq $6, (%rsi)
movl $.LC0, %edi
jmp puts
Vì vậy, điều kiện if hoàn toàn biến mất khỏi mã trình biên dịch chương trình.
long long*
và int64_t
*). Người ta có thể mong đợi rằng một trình biên dịch lành mạnh sẽ nhận ra rằng a long long*
và int64_t*
có thể truy cập cùng một bộ lưu trữ nếu chúng được lưu trữ giống hệt nhau, nhưng cách xử lý như vậy không còn hợp thời nữa.
Gõ picky thông qua các con trỏ phôi (trái ngược với việc sử dụng một liên minh) là một ví dụ chính về việc phá vỡ bí danh nghiêm ngặt.
fpsync()
giữa viết dưới dạng fp và đọc dưới dạng int hoặc ngược lại [trên các triển khai với các đường dẫn và bộ đệm số nguyên và FPU riêng biệt , một lệnh như vậy có thể tốn kém, nhưng không tốn kém như việc trình biên dịch thực hiện đồng bộ hóa như vậy trên mỗi lần truy cập liên minh]. Hoặc việc triển khai có thể chỉ định rằng giá trị kết quả sẽ không bao giờ có thể sử dụng được ngoại trừ trong các trường hợp sử dụng Chuỗi ban đầu chung.
Theo lý do C89, các tác giả của Tiêu chuẩn không muốn yêu cầu các trình biên dịch đưa ra mã như:
int x;
int test(double *p)
{
x=5;
*p = 1.0;
return x;
}
nên được yêu cầu tải lại giá trị x
giữa câu lệnh gán và trả về để cho phép khả năng p
có thể trỏ đến x
và do đó việc gán *p
có thể thay đổi giá trị của x
. Quan điểm cho rằng một trình biên dịch nên có quyền cho rằng sẽ không có bí danh trong các tình huống như trên là không gây tranh cãi.
Thật không may, các tác giả của C89 đã viết quy tắc của họ theo cách mà nếu đọc theo nghĩa đen, sẽ làm cho ngay cả hàm sau gọi ra Hành vi không xác định:
void test(void)
{
struct S {int x;} s;
s.x = 1;
}
bởi vì nó sử dụng một giá trị loại int
để truy cập một đối tượng của loại struct S
và int
không nằm trong số các loại có thể được sử dụng khi truy cập a struct S
. Bởi vì thật phi lý khi coi tất cả việc sử dụng các thành viên không thuộc loại cấu trúc của các cấu trúc và công đoàn là Hành vi không xác định, nên hầu như mọi người đều nhận ra rằng có ít nhất một số trường hợp có thể sử dụng một loại giá trị để truy cập vào một đối tượng thuộc loại khác . Thật không may, Ủy ban Tiêu chuẩn C đã không xác định được những trường hợp đó là gì.
Phần lớn vấn đề là kết quả của Báo cáo lỗi # 028, đã hỏi về hành vi của một chương trình như:
int test(int *ip, double *dp)
{
*ip = 1;
*dp = 1.23;
return *ip;
}
int test2(void)
{
union U { int i; double d; } u;
return test(&u.i, &u.d);
}
Báo cáo khiếm khuyết # 28 nói rằng chương trình gọi Hành vi không xác định vì hành động viết một thành viên công đoàn loại "kép" và đọc một loại "int" gọi hành vi Xác định thực hiện. Lý luận như vậy là vô nghĩa, nhưng tạo cơ sở cho các quy tắc Loại hiệu quả, điều này làm phức tạp ngôn ngữ trong khi không làm gì để giải quyết vấn đề ban đầu.
Cách tốt nhất để giải quyết vấn đề ban đầu có lẽ là xử lý chú thích về mục đích của quy tắc như thể nó là quy phạm và làm cho quy tắc không thể thực thi được trừ khi trong trường hợp thực sự liên quan đến truy cập xung đột bằng cách sử dụng bí danh. Đưa ra một cái gì đó như:
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
s.x = 1;
p = &s.x;
inc_int(p);
return s.x;
}
Không có xung đột bên trong inc_int
bởi vì tất cả các truy cập vào bộ lưu trữ được truy cập *p
đều được thực hiện với một giá trị loại int
và không có xung đột test
vì p
có nguồn gốc rõ ràng từ một struct S
, và vào lần tiếp theo s
được sử dụng, tất cả các quyền truy cập vào bộ lưu trữ đó sẽ được thực hiện thông qua p
sẽ đã xảy ra.
Nếu mã được thay đổi một chút ...
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
p = &s.x;
s.x = 1; // !!*!!
*p += 1;
return s.x;
}
Ở đây, có một xung đột răng cưa giữa p
và quyền truy cập s.x
vào dòng được đánh dấu bởi vì tại thời điểm đó trong thực thi, một tham chiếu khác tồn tại sẽ được sử dụng để truy cập vào cùng một bộ lưu trữ .
Báo cáo khiếm khuyết 028 cho biết ví dụ ban đầu đã gọi UB vì sự chồng chéo giữa việc tạo và sử dụng hai con trỏ, điều đó sẽ làm cho mọi thứ rõ ràng hơn rất nhiều mà không cần phải thêm "Loại hiệu quả" hoặc sự phức tạp khác.
Sau khi đọc nhiều câu trả lời, tôi cảm thấy cần phải thêm một cái gì đó:
Bí danh nghiêm ngặt (mà tôi sẽ mô tả một chút) rất quan trọng vì :
Truy cập bộ nhớ có thể tốn kém (hiệu năng khôn ngoan), đó là lý do tại sao dữ liệu được thao tác trong các thanh ghi CPU trước khi được ghi lại vào bộ nhớ vật lý.
Nếu dữ liệu trong hai thanh ghi CPU khác nhau sẽ được ghi vào cùng một không gian bộ nhớ, chúng ta không thể dự đoán dữ liệu nào sẽ "tồn tại" khi chúng ta viết mã trong C.
Trong quá trình lắp ráp, nơi chúng tôi mã hóa việc tải và dỡ các thanh ghi CPU theo cách thủ công, chúng tôi sẽ biết dữ liệu nào còn nguyên vẹn. Nhưng C (rất may) trừu tượng hóa chi tiết này đi.
Vì hai con trỏ có thể trỏ đến cùng một vị trí trong bộ nhớ, điều này có thể dẫn đến mã phức tạp xử lý các va chạm có thể xảy ra .
Mã bổ sung này chậm và làm tổn thương hiệu năng vì nó thực hiện các hoạt động đọc / ghi bộ nhớ thêm, cả hai đều chậm hơn và (có thể) không cần thiết.
Các quy tắc nghiêm ngặt răng cưa cho phép chúng tôi để tránh mã máy dự phòng trong những trường hợp trong đó nó nên được an toàn để giả định rằng hai con trỏ không trỏ đến cùng một khối nhớ (xem thêm các restrict
từ khoá).
Các răng cưa nghiêm ngặt cho rằng an toàn khi giả định rằng các con trỏ tới các loại khác nhau trỏ đến các vị trí khác nhau trong bộ nhớ.
Nếu một trình biên dịch thông báo rằng hai con trỏ trỏ đến các loại khác nhau (ví dụ: int *
a và a float *
), nó sẽ giả sử địa chỉ bộ nhớ là khác nhau và nó sẽ không bảo vệ chống va chạm địa chỉ bộ nhớ, dẫn đến mã máy nhanh hơn.
Ví dụ :
Cho phép đảm nhận chức năng sau:
void merge_two_ints(int *a, int *b) {
*b += *a;
*a += *b;
}
Để xử lý trường hợp a == b
(cả hai con trỏ trỏ đến cùng một bộ nhớ), chúng ta cần đặt hàng và kiểm tra cách chúng ta tải dữ liệu từ bộ nhớ vào các thanh ghi CPU, vì vậy mã có thể kết thúc như thế này:
tải a
và b
từ bộ nhớ.
thêm a
vào b
.
lưu b
và tải lại a
.
(lưu từ thanh ghi CPU vào bộ nhớ và tải từ bộ nhớ vào thanh ghi CPU).
thêm b
vào a
.
lưu a
(từ thanh ghi CPU) vào bộ nhớ.
Bước 3 rất chậm vì cần truy cập vào bộ nhớ vật lý. Tuy nhiên, cần phải bảo vệ chống lại các trường hợp ở đó a
và b
trỏ đến cùng một địa chỉ bộ nhớ.
Bí danh nghiêm ngặt sẽ cho phép chúng tôi ngăn chặn điều này bằng cách thông báo cho trình biên dịch rằng các địa chỉ bộ nhớ này khác nhau rõ ràng (trong trường hợp này, sẽ cho phép tối ưu hóa hơn nữa không thể được thực hiện nếu con trỏ chia sẻ địa chỉ bộ nhớ).
Điều này có thể được nói với trình biên dịch theo hai cách, bằng cách sử dụng các loại khác nhau để trỏ đến. I E:
void merge_two_numbers(int *a, long *b) {...}
Sử dụng restrict
từ khóa. I E:
void merge_two_ints(int * restrict a, int * restrict b) {...}
Bây giờ, bằng cách thỏa mãn quy tắc Stias Aliasing, bước 3 có thể tránh được và mã sẽ chạy nhanh hơn đáng kể.
Trong thực tế, bằng cách thêm restrict
từ khóa, toàn bộ chức năng có thể được tối ưu hóa thành:
tải a
và b
từ bộ nhớ.
thêm a
vào b
.
lưu kết quả cả đến a
và b
.
Việc tối ưu hóa này không thể được thực hiện trước đây, vì có thể xảy ra va chạm (nơi a
và b
sẽ tăng gấp ba thay vì tăng gấp đôi).
b
(không tải lại) và tải lại a
. Tôi hy vọng nó rõ ràng hơn bây giờ.
restrict
, nhưng tôi nghĩ rằng trong hầu hết các trường hợp sẽ có hiệu quả hơn và nới lỏng một số ràng buộc register
sẽ cho phép nó điền vào một số trường hợp restrict
không giúp được gì. Tôi không chắc chắn việc coi Tiêu chuẩn là mô tả đầy đủ tất cả các trường hợp mà các lập trình viên nên mong đợi trình biên dịch nhận ra bằng chứng về răng cưa, thay vì chỉ mô tả những nơi mà trình biên dịch phải giả định ngay cả khi không có bằng chứng cụ thể nào về nó .
restrict
từ khóa giảm thiểu không chỉ tốc độ của các hoạt động mà cả số lượng của chúng, điều đó có thể có ý nghĩa ... Ý tôi là, hoạt động nhanh nhất hoàn toàn không phải là hoạt động :)
Bí danh nghiêm ngặt không cho phép các loại con trỏ khác nhau vào cùng một dữ liệu.
Bài viết này sẽ giúp bạn hiểu vấn đề một cách chi tiết.
int
và một cấu trúc có chứa một int
).
Về mặt kỹ thuật trong C ++, quy tắc răng cưa nghiêm ngặt có lẽ không bao giờ được áp dụng.
Lưu ý định nghĩa của toán tử ( * toán tử ):
Toán tử unary * thực hiện cảm ứng: biểu thức được áp dụng sẽ là một con trỏ tới một loại đối tượng hoặc một con trỏ tới một loại hàm và kết quả là một giá trị tham chiếu đến đối tượng hoặc hàm mà biểu thức chỉ ra .
Cũng từ định nghĩa của glvalue
Glvalue là một biểu thức có đánh giá xác định danh tính của một đối tượng, (... snip)
Vì vậy, trong bất kỳ dấu vết chương trình được xác định rõ, một giá trị đề cập đến một đối tượng. Vì vậy, cái gọi là quy tắc răng cưa nghiêm ngặt không bao giờ được áp dụng. Đây có thể không phải là những gì các nhà thiết kế muốn.
int foo;
, những gì được truy cập bởi biểu thức lvalue *(char*)&foo
? Đó có phải là một đối tượng của loại char
? Liệu đối tượng đó có tồn tại cùng lúc với foo
? Sẽ viết để foo
thay đổi giá trị được lưu trữ của loại đối tượng nói trên char
? Nếu vậy, có quy tắc nào cho phép giá trị được lưu trữ của một đối tượng loại char
được truy cập bằng cách sử dụng một giá trị của loại int
không?
int i;
tạo ra bốn đối tượng của mỗi loại ký tự in addition to one of type
int ? I see no way to apply a consistent definition of "object" which would allow for operations on both
* (char *) & i` và i
. Cuối cùng, không có gì trong Tiêu chuẩn cho phép ngay cả một volatile
con trỏ đủ tiêu chuẩn truy cập vào các thanh ghi phần cứng không đáp ứng định nghĩa về "đối tượng".
c
vàc++faq
.