2015-09-15 25 views
14

Tôi đang ở giữa cuộc thảo luận đang cố gắng tìm hiểu xem truy cập không được gán có cho phép trong C++ thông qua reinterpret_cast hay không. Tôi nghĩ là không, nhưng tôi đang gặp khó khăn trong việc tìm đúng (các) phần của tiêu chuẩn xác nhận hoặc bác bỏ điều đó. Tôi đã xem xét C++ 11, nhưng tôi sẽ ổn với phiên bản khác nếu nó rõ ràng hơn.Truy cập chưa được chỉ định thông qua reinterpret_cast

Truy cập chưa được chỉ định không được xác định trong C11. Phần có liên quan của the C11 standard (§ 6.3.2.3, đoạn 7):

Một con trỏ tới loại đối tượng có thể được chuyển thành con trỏ thành loại đối tượng khác. Nếu con trỏ kết quả không được căn chỉnh chính xác cho loại được tham chiếu, hành vi sẽ không được xác định.

Do hành vi của truy cập chưa được ký là không xác định, một số trình biên dịch (ít nhất GCC) thực hiện điều đó có nghĩa là bạn có thể tạo hướng dẫn yêu cầu dữ liệu liên kết. Hầu hết thời gian mã vẫn hoạt động cho dữ liệu chưa được ký vì hầu hết các lệnh x86 và ARM trong những ngày này làm việc với dữ liệu chưa được ký, nhưng một số thì không. Đặc biệt, một số hướng dẫn vector không, có nghĩa là khi trình biên dịch nhận được tốt hơn khi tạo mã hướng dẫn tối ưu mà làm việc với các phiên bản cũ hơn của trình biên dịch có thể không hoạt động với các phiên bản mới hơn. Và, tất nhiên, một số kiến ​​trúc (like MIPS) không hoạt động tốt với dữ liệu chưa được ký.

C++11 là, tất nhiên, phức tạp hơn. § 5.2.10, đoạn 7 cho biết:

Một con trỏ đối tượng có thể được chuyển đổi thành con trỏ đối tượng thuộc loại khác. Khi giá trị “v”, được chuyển thành loại “con trỏ thành cv T2”, kết quả là static_cast<cv T2*>(static_cast<cv void*>(v)) nếu cả hai T1T2 là loại bố cục tiêu chuẩn (3.9) và yêu cầu căn chỉnh T2 không chặt chẽ hơn của T1 hoặc nếu một trong hai loại là void. Chuyển đổi giá trị của loại “con trỏ thành T1” thành loại “con trỏ thành T2” (trong đó T1T2 là loại đối tượng và yêu cầu căn chỉnh T2 không chặt chẽ hơn so với số T1) và quay lại loại ban đầu. giá trị con trỏ. Kết quả của bất kỳ chuyển đổi con trỏ nào khác không được chỉ định.

Lưu ý rằng từ cuối cùng là "không xác định", không phải "không xác định". § 1.3.25 định nghĩa "hành vi không xác định" như:

hành vi, cho một cấu trúc chương trình cũng như hình thành và dữ liệu chính xác, mà phụ thuộc vào việc thực hiện

[Note: Việc thực hiện không cần phải tài liệu mà hành vi xảy ra. Phạm vi của các hành vi có thể thường được mô tả theo tiêu chuẩn này. - cuối note]

Trừ khi tôi là thiếu cái gì, tiêu chuẩn không thực sự phân định phạm vi của các hành vi có thể trong trường hợp này, mà dường như để chỉ cho tôi rằng một hành vi rất hợp lý là đó là thực hiện cho C (ít nhất là bởi GCC): không hỗ trợ họ. Điều đó có nghĩa là trình biên dịch là miễn phí để giả định truy cập unaligned không xảy ra và phát ra các hướng dẫn mà có thể không làm việc với bộ nhớ unaligned, giống như nó không cho C.

Người tôi đang thảo luận điều này, tuy nhiên, có cách giải thích khác. Họ trích dẫn § 1.9, đoạn 5:

Thực hiện tuân thủ thực hiện một chương trình được định dạng tốt sẽ tạo ra hành vi quan sát tương tự như một trong những thực thi có thể của trường hợp tương ứng của máy trừu tượng với cùng chương trình và giống nhau đầu vào. Tuy nhiên, nếu bất kỳ việc thực thi nào như vậy có chứa một hoạt động không xác định, tiêu chuẩn này không yêu cầu thực thi chương trình đó với đầu vào đó (thậm chí không liên quan đến các hoạt động trước hoạt động không xác định đầu tiên).

Vì không có hành vi undefined, họ cho rằng trình biên dịch C++ không có quyền giả định quyền truy cập chưa được căn chỉnh sẽ không xảy ra.

Vì vậy, các truy cập chưa được ký thông qua reinterpret_cast có an toàn trong C++ không? Ở đâu trong các đặc điểm kỹ thuật (bất kỳ phiên bản) nào nó nói?

Chỉnh sửa: Bằng cách "truy cập", ý tôi là thực sự đang tải và lưu trữ. Một cái gì đó như

void unaligned_cp(void* a, void* b) { 
    *reinterpret_cast<volatile uint32_t*>(a) = 
    *reinterpret_cast<volatile uint32_t*>(b); 
} 

Làm thế nào bộ nhớ được phân bổ là thực sự bên ngoài phạm vi của tôi (đó là một thư viện mà có thể được gọi với dữ liệu từ bất cứ nơi nào), nhưng malloc và một mảng trên stack đều ứng cử viên có khả năng. Tôi không muốn đặt bất kỳ hạn chế nào về cách phân bổ bộ nhớ.

Chỉnh sửa 2: Hãy trích dẫn nguồn (ví dụ: , chuẩn C++, phần và đoạn) trong câu trả lời.

+0

Truy cập có nghĩa là gì? Aliasing, hoặc chỉ cần đúc các loại con trỏ đến và fro? – Columbo

+0

Bí danh — đặc biệt, tôi quan tâm đến tải và cửa hàng để sắp xếp sai 'uint32_t's. – nemequ

+0

Nó có thể giúp thảo luận nếu bạn đăng một số mã mà bạn cho rằng có thể cho phép truy cập chưa được ký. Nếu bạn không thể nghĩ về bất kỳ đoạn mã như vậy, đó là bằng chứng tốt rằng không có. –

Trả lời

7

Nhìn 3.11/1:

loại đối tượng có yêu cầu liên kết (3.9.1, 3.9.2) mà nơi hạn chế về địa chỉ mà tại đó một đối tượng kiểu có thể được phân bổ.

Có một số cuộc tranh luận trong nhận xét về chính xác những gì cấu thành phân bổ đối tượng thuộc loại. Tuy nhiên tôi tin rằng đối số sau đây hoạt động bất kể cách thảo luận đó được giải quyết:

Lấy *reinterpret_cast<uint32_t*>(a) ví dụ. Nếu biểu thức này không gây ra UB, thì (theo quy tắc bí danh nghiêm ngặt) phải có đối tượng thuộc loại uint32_t (hoặc int32_t) tại vị trí đã cho sau tuyên bố này. Cho dù đối tượng đã có ở đó, hoặc viết này tạo ra nó, không quan trọng.

Theo báo giá Tiêu chuẩn ở trên, các đối tượng có yêu cầu căn chỉnh chỉ có thể tồn tại ở trạng thái được căn chỉnh chính xác.

Vì vậy, bất kỳ nỗ lực nào để tạo hoặc viết đối tượng không được căn chỉnh chính xác đều gây ra UB.

+0

Tôi thích câu trả lời này, nhưng tôi nghĩ rằng nó không đầy đủ mà không trả lời câu hỏi về những gì cấu thành phân bổ một đối tượng của một loại. Nếu không ai trả lời, tôi sẽ chấp nhận câu trả lời này và tạo một câu hỏi khác cho vấn đề đó. – nemequ

+1

@nemequ sẽ là một vấn đề riêng biệt. Đọc qua [câu hỏi này] (http://stackoverflow.com/questions/30114397/constructing-a-trivially-copyable-object-with-memcpy) trước tiên. –

3

EDIT Điều này trả lời câu hỏi ban đầu của OP, đó là "đang truy cập con trỏ không được gán dấu an toàn". OP từ đó đã chỉnh sửa câu hỏi của họ thành "là dereferencing một con trỏ lệch hướng an toàn", một câu hỏi thực tế hơn và ít thú vị hơn.


Các khứ hồi cast kết quả của giá trị con trỏ là không xác định dưới những hoàn cảnh nào. Trong một số trường hợp hạn chế (liên quan đến căn chỉnh), chuyển đổi con trỏ thành A thành con trỏ thành B, và sau đó quay lại, dẫn đến con trỏ ban đầu, ngay cả khi bạn không có B ở vị trí đó.

Nếu yêu cầu căn chỉnh không được đáp ứng, so với chuyến đi khứ hồi đó - con trỏ đến A đến con trỏ đến B đến con trỏ đến A kết quả trong con trỏ có giá trị không xác định.

Vì có giá trị con trỏ không hợp lệ, dereferencing một con trỏ có giá trị không xác định có thể dẫn đến hành vi không xác định. Nó không khác với *(int*)0xDEADBEEF theo nghĩa nào đó.

Chỉ cần lưu trữ con trỏ đó, tuy nhiên, hành vi không xác định.

Không có báo giá nào trên C++ nói về thực sự sử dụng con trỏ đến A làm con trỏ đến B. Sử dụng một con trỏ đến "loại sai" trong tất cả, nhưng một số trường hợp rất hạn chế là hành vi không xác định, thời gian.

Ví dụ về việc này bao gồm việc tạo std::aligned_storage_t<sizeof(T), alignof(T)>. Bạn có thể tạo T ở vị trí đó và nó sẽ sống hạnh phúc, mặc dù nó "thực sự" là aligned_storage_t<sizeof(T), alignof(T)>. (Tuy nhiên, bạn có thể sử dụng con trỏ được trả lại từ vị trí new để tuân thủ đầy đủ tiêu chuẩn; Tôi không chắc chắn. Xem bí danh nghiêm ngặt.)

Đáng buồn thay, tiêu chuẩn này thiếu một chút về tuổi thọ của đối tượng. Nó đề cập đến nó, nhưng không xác định nó đủ tốt cuối cùng tôi đã kiểm tra. Bạn chỉ có thể sử dụng một số T tại một vị trí cụ thể trong khi số T sống ở đó, nhưng điều đó có nghĩa là không được thực hiện rõ ràng trong mọi trường hợp.

+2

Nếu một hệ thống sử dụng một từ để giữ một 'int *' nhưng hai từ để giữ một 'char *' hoặc 'void *' [ví dụ: một số hệ thống sử dụng các địa chỉ từ, nhưng bao gồm một lệnh để truy cập một nửa từ tại một byte bù trừ nhất định từ một địa chỉ từ đã cho], hệ thống có được phép bẫy không nếu cố gắng tạo thành 'int *' a 'char * 'với một bù trừ lẻ, hoặc hệ thống sẽ được yêu cầu để có dàn diễn viên mang lại một con trỏ có thể được chỉ định (mặc dù không nhất thiết phải dereferenced) mà không bị mắc kẹt? – supercat

+0

@supercat câu hỏi hay; Tôi sẽ phải kiểm tra những gì tiêu chuẩn nói về đại diện bẫy của con trỏ mình. Có thể sao chép một con trỏ với giá trị không xác định gây ra một cái bẫy? – Yakk

+0

Cho 'int foo; int * x, * y; ', câu lệnh' x = (int *) (((char *) & foo) +1); 'có thể hợp pháp gây ra' x' để giữ biểu diễn bẫy, và nếu nó làm như vậy, ' y = x; 'sẽ có hành vi không xác định. Có gì ít rõ ràng hơn là việc lưu trữ một giá trị được xác định hoặc không xác định thực hiện * trực tiếp * từ một biểu thức tạo ra nó có hành vi được xác định. Tôi thấy không có lợi ích khi cấm các trình biên dịch khỏi bẫy khi cố gắng tạo * các giá trị không hợp lệ, nhưng điều đó sẽ biểu hiện sự khác biệt giữa "tạo giá trị không xác định" hoặc "tạo giá trị được xác định thực hiện [có thể là bẫy ...]" .. – supercat

0

Tất cả các trích dẫn của bạn đều nói về giá trị con trỏ, chứ không phải hành động của dereferencing.

5.2.10, đoạn 7 nói rằng, giả sử int có một sự liên kết chặt chẽ hơn char, sau đó chuyến đi vòng char* để int* để char* tạo ra một giá trị không xác định cho kết quả char*.

Mặt khác, nếu bạn chuyển đổi int* để char* để int*, bạn được đảm bảo để lấy lại chính xác cùng một con trỏ khi bạn bắt đầu với.

Nó không nói về những gì bạn nhận được khi bạn dereference nói con trỏ. Nó chỉ đơn giản nói rằng trong một trường hợp, bạn phải có khả năng đi vòng. Nó rửa tay theo cách khác.


Giả sử bạn có một số kiểu int, và alignof(int) > 1:

int some_ints[3] ={0}; 

sau đó bạn có một con trỏ int được bù đắp:

int* some_ptr = (int*)(((char*)&some_ints[0])+1); 

Chúng tôi sẽ đoán rằng sao chép con trỏ này lệch doesn không gây ra hành vi không xác định cho bây giờ.

Giá trị của some_ptr không được chỉ định theo tiêu chuẩn. Chúng tôi sẽ hào phóng và cho rằng nó thực sự trỏ đến một số byte trong phạm vi some_bytes.

Bây giờ chúng tôi có một số int* trỏ đến một nơi không thể phân bổ int (3.11/1). Dưới (3.8) việc sử dụng con trỏ đến int bị hạn chế theo một số cách. Việc sử dụng thông thường bị hạn chế đối với con trỏ đến số T mà thời gian sử dụng của chúng đã được phân bổ đúng (/ 3). Một số sử dụng hạn chế được cho phép trên một con trỏ đến một số T đã được cấp phát đúng cách, nhưng thời gian tồn tại của chúng chưa bắt đầu (/ 5 và/6).

Không có cách nào để tạo đối tượng int không tuân thủ các hạn chế căn chỉnh của int trong tiêu chuẩn.

Vì vậy, lý thuyết int* mà tuyên bố trỏ đến một int không thẳng hàng không trỏ đến một int. Không có giới hạn nào được đặt vào hành vi của con trỏ đã nói khi bị bỏ qua; các quy tắc dereferencing thông thường cung cấp hành vi của một con trỏ hợp lệ tới một đối tượng (bao gồm một số int) và cách nó hoạt động.


Và bây giờ các giả định khác của chúng tôi. Không có giới hạn nào về giá trị của some_ptr ở đây được thực hiện theo tiêu chuẩn: int* some_ptr = (int*)(((char*)&some_ints[0])+1);.

Đây không phải là con trỏ đến số int, giống như (int*)nullptr không phải là con trỏ đến số int. Vòng cắt nó trở lại một kết quả char* trong một con trỏ với giá trị không xác định (nó có thể là 0xbaadf00d hoặc nullptr) một cách rõ ràng trong tiêu chuẩn.

Tiêu chuẩn xác định những gì bạn phải làm. Có (gần như? Tôi đoán đánh giá nó trong một bối cảnh boolean phải trả về một bool) không có yêu cầu đặt trên hành vi của some_ptr theo tiêu chuẩn, khác hơn là chuyển đổi nó trở lại char* kết quả trong một giá trị không xác định (của con trỏ).

Các vấn đề liên quan