Định nghĩa lại NULL


118

Tôi đang viết mã C cho một hệ thống có địa chỉ 0x0000 hợp lệ và chứa cổng I / O. Do đó, bất kỳ lỗi nào có thể xảy ra khi truy cập vào con trỏ NULL sẽ vẫn không bị phát hiện và đồng thời gây ra hành vi nguy hiểm.

Vì lý do này, tôi muốn xác định lại NULL là một địa chỉ khác, ví dụ như một địa chỉ không hợp lệ. Nếu tôi vô tình truy cập vào một địa chỉ như vậy, tôi sẽ gặp phải sự cố phần cứng mà tôi có thể xử lý lỗi. Tôi tình cờ có quyền truy cập vào stddef.h cho trình biên dịch này, vì vậy tôi thực sự có thể thay đổi tiêu đề chuẩn và xác định lại NULL.

Câu hỏi của tôi là: liệu điều này có mâu thuẫn với tiêu chuẩn C không? Theo như tôi có thể nói từ 7.17 trong tiêu chuẩn, macro được xác định bởi việc triển khai. Có điều gì khác trong tiêu chuẩn nói rằng NULL phải bằng 0 không?

Một vấn đề khác là nhiều trình biên dịch thực hiện khởi tạo tĩnh bằng cách đặt mọi thứ về 0, bất kể kiểu dữ liệu. Mặc dù tiêu chuẩn nói rằng trình biên dịch nên đặt số nguyên thành 0 và con trỏ thành NULL. Nếu tôi định nghĩa lại NULL cho trình biên dịch của mình, thì tôi biết rằng việc khởi tạo tĩnh như vậy sẽ không thành công. Tôi có thể coi đó là hành vi trình biên dịch không chính xác mặc dù tôi đã mạnh dạn thay đổi tiêu đề trình biên dịch theo cách thủ công? Bởi vì tôi biết chắc chắn rằng trình biên dịch cụ thể này không truy cập macro NULL khi thực hiện khởi tạo tĩnh.


3
Đây thực sự là một câu hỏi tốt. Tôi không có câu trả lời cho bạn, nhưng tôi phải hỏi: bạn có chắc là không thể di chuyển nội dung hợp lệ của bạn ở 0x00 và để NULL là địa chỉ không hợp lệ như trong các hệ thống "bình thường" không? Nếu bạn không thể, thì các địa chỉ không hợp lệ an toàn duy nhất để sử dụng sẽ là những địa chỉ bạn có thể chắc chắn rằng mình có thể cấp phát và sau đó mprotectlà bảo mật. Hoặc, nếu nền tảng không có ASLR hoặc tương tự, các địa chỉ nằm ngoài bộ nhớ vật lý của nền tảng. Chúc may mắn.
Borealid

8
Nó sẽ hoạt động như thế nào nếu mã của bạn đang sử dụng if(ptr) { /* do something on ptr*/ }? Nó sẽ hoạt động nếu NULL được định nghĩa khác 0x0?
Xavier T.

3
Con trỏ C không có quan hệ bắt buộc với địa chỉ bộ nhớ. Miễn là các quy tắc của số học con trỏ được tuân thủ, giá trị con trỏ có thể là bất kỳ giá trị nào. Hầu hết các triển khai chọn sử dụng địa chỉ bộ nhớ làm giá trị con trỏ, nhưng chúng có thể sử dụng bất kỳ thứ gì miễn là nó là đẳng cấu.
datenwolf

2
@bdonlan Điều đó cũng sẽ vi phạm các quy tắc (tư vấn) trong MISRA-C.
Lundin

2
@Andreas Yep đó cũng là suy nghĩ của tôi. Mọi người không được phép thiết kế phần cứng mà phần mềm sẽ chạy trong đó! :)
Lundin

Câu trả lời:


84

Tiêu chuẩn C không yêu cầu con trỏ rỗng phải ở địa chỉ của máy là 0. TUY NHIÊN, việc truyền một 0hằng số thành một giá trị con trỏ phải dẫn đến một NULLcon trỏ (§6.3.2.3 / 3) và việc đánh giá con trỏ null dưới dạng boolean phải là false. Đây có thể là một chút vụng về nếu bạn thực sự làm muốn có một địa chỉ không, và NULLkhông phải là địa chỉ không.

Tuy nhiên, với các sửa đổi (nặng) đối với trình biên dịch và thư viện tiêu chuẩn, không thể không NULLđược biểu diễn bằng một mẫu bit thay thế trong khi vẫn tuân thủ nghiêm ngặt thư viện tiêu chuẩn. Tuy nhiên, chỉ đơn giản là thay đổi định nghĩa của chính nó là không đủ NULL, vì sau đó NULLsẽ đánh giá thành true.

Cụ thể, bạn cần phải:

  • Sắp xếp các số không theo nghĩa đen trong các phép gán cho con trỏ (hoặc phôi tới con trỏ) được chuyển đổi thành một số giá trị ma thuật khác chẳng hạn như -1.
  • Sắp xếp các bài kiểm tra bằng nhau giữa con trỏ và một số nguyên không đổi 0để kiểm tra giá trị ma thuật thay thế (§6.5.9 / 6)
  • Sắp xếp cho tất cả các ngữ cảnh trong đó kiểu con trỏ được đánh giá là boolean để kiểm tra sự bình đẳng với giá trị ma thuật thay vì kiểm tra bằng 0. Điều này tuân theo ngữ nghĩa kiểm tra bình đẳng, nhưng trình biên dịch có thể triển khai nó theo cách khác nhau trong nội bộ. Xem §6.5.13 / 3, §6.5.14 / 3, §6.5.15 / 4, §6.5.3.3 / 5, §6.8.4.1 / 2, §6.8.5 / 4
  • Như caf đã chỉ ra, hãy cập nhật ngữ nghĩa để khởi tạo đối tượng tĩnh (§6.7.8 / 10) và khởi tạo phức hợp một phần (§6.7.8 / 21) để phản ánh biểu diễn con trỏ null mới.
  • Tạo một cách thay thế để truy cập số không địa chỉ thực.

Có một số việc bạn không phải xử lý. Ví dụ:

int x = 0;
void *p = (void*)x;

Sau đó, pKHÔNG được đảm bảo là một con trỏ null. Chỉ các phép gán hằng số mới cần được xử lý (đây là một cách tiếp cận tốt để truy cập địa chỉ thực số 0). Tương tự như vậy:

int x = 0;
assert(x == (void*)0); // CAN BE FALSE

Cũng thế:

void *p = NULL;
int x = (int)p;

xkhông được đảm bảo là 0.

Nói tóm lại, điều kiện này rõ ràng đã được xem xét bởi ủy ban ngôn ngữ C, và những cân nhắc được thực hiện đối với những người sẽ chọn đại diện thay thế cho NULL. Tất cả những gì bạn phải làm bây giờ là thực hiện các thay đổi lớn đối với trình biên dịch của mình và chào bạn trước khi hoàn thành :)

Lưu ý thêm, có thể thực hiện những thay đổi này với giai đoạn chuyển đổi mã nguồn trước khi trình biên dịch thích hợp. Có nghĩa là, thay vì dòng thông thường của bộ tiền xử lý -> trình biên dịch -> trình hợp dịch -> trình liên kết, bạn sẽ thêm một bộ tiền xử lý -> chuyển đổi NULL -> trình biên dịch -> trình hợp dịch -> trình liên kết. Sau đó, bạn có thể thực hiện các phép biến đổi như:

p = 0;
if (p) { ... }
/* becomes */
p = (void*)-1;
if ((void*)(p) != (void*)(-1)) { ... }

Điều này sẽ yêu cầu trình phân tích cú pháp C đầy đủ, cũng như trình phân tích cú pháp kiểu và phân tích typedef và khai báo biến để xác định số nhận dạng nào tương ứng với con trỏ. Tuy nhiên, bằng cách làm này, bạn có thể tránh phải thực hiện các thay đổi đối với các phần tạo mã của trình biên dịch thích hợp. clang có thể hữu ích cho việc thực hiện điều này - tôi hiểu rằng nó được thiết kế với các phép biến đổi như thế này. Tất nhiên, bạn vẫn có thể cần thực hiện các thay đổi đối với thư viện chuẩn.


2
Ok, tôi đã không tìm thấy văn bản trong §6.3.2.3, nhưng tôi nghi ngờ sẽ có một tuyên bố như vậy ở đâu đó :). Tôi đoán điều này trả lời câu hỏi của tôi, theo tiêu chuẩn, tôi không được phép định nghĩa lại NULL trừ khi tôi thích viết một trình biên dịch C mới để sao lưu cho tôi :)
Lundin

2
Một mẹo hay là hack trình biên dịch để con trỏ <-> chuyển đổi số nguyên XOR một giá trị cụ thể là một con trỏ không hợp lệ và vẫn đủ tầm thường để kiến ​​trúc mục tiêu có thể làm điều đó với giá rẻ (thông thường, đó sẽ là một giá trị với một tập hợp bit , chẳng hạn như 0x20000000).
Simon Richter

2
Một điều khác mà bạn sẽ cần thay đổi trong trình biên dịch là khởi tạo các đối tượng có kiểu phức hợp - nếu một đối tượng được khởi tạo một phần, thì bất kỳ con trỏ nào không có trình tạo biểu tượng rõ ràng phải được khởi tạo NULL.
caf

20

Tiêu chuẩn nói rằng một biểu thức hằng số nguyên có giá trị 0, hoặc một biểu thức như vậy được chuyển đổi thành void *kiểu, là một hằng con trỏ null. Điều này có nghĩa là nó (void *)0luôn luôn là một con trỏ null, nhưng đã cho int i = 0;, (void *)ikhông cần thiết.

Việc triển khai C bao gồm trình biên dịch cùng với các tiêu đề của nó. Nếu bạn sửa đổi các tiêu đề để xác định lại NULL, nhưng không sửa đổi trình biên dịch để sửa các khởi tạo tĩnh, thì bạn đã tạo một triển khai không phù hợp. Đó là toàn bộ quá trình thực hiện cùng nhau có hành vi không chính xác, và nếu bạn phá vỡ nó, bạn thực sự không có ai khác để đổ lỗi;)

Tất nhiên, bạn phải sửa nhiều thứ hơn là chỉ khởi tạo tĩnh - cho trước một con trỏ p, if (p)tương đương với if (p != NULL), do quy tắc trên.


8

Nếu bạn sử dụng thư viện C std, bạn sẽ gặp sự cố với các hàm có thể trả về NULL. Ví dụ, tài liệu malloc cho biết:

Nếu hàm không thể cấp phát khối bộ nhớ được yêu cầu, một con trỏ rỗng sẽ được trả về.

Bởi vì malloc và các hàm liên quan đã được biên dịch thành các tệp nhị phân với giá trị NULL cụ thể, nếu bạn xác định lại NULL, bạn sẽ không thể sử dụng trực tiếp thư viện C std trừ khi bạn có thể xây dựng lại toàn bộ chuỗi công cụ của mình, bao gồm cả C std libs.

Cũng vì việc sử dụng NULL của thư viện std, nếu bạn xác định lại NULL trước khi bao gồm tiêu đề std, bạn có thể ghi đè định nghĩa NULL được liệt kê trong tiêu đề. Bất kỳ thứ gì nội dòng sẽ không nhất quán từ các đối tượng được biên dịch.

Thay vào đó, tôi sẽ định nghĩa NULL của riêng bạn, "MYPRODUCT_NULL", cho mục đích sử dụng của riêng bạn và tránh hoặc dịch từ / sang thư viện C std.


6

Để nguyên NULL và coi IO đến cổng 0x0000 là một trường hợp đặc biệt, có thể sử dụng một quy trình được viết bằng trình hợp dịch và do đó không tuân theo ngữ nghĩa C tiêu chuẩn. IOW, không xác định lại NULL, xác định lại cổng 0x00000.

Lưu ý rằng nếu bạn đang viết hoặc sửa đổi trình biên dịch C, công việc cần thiết để tránh tham chiếu NULL (giả sử rằng trong trường hợp của bạn là CPU không trợ giúp) là giống nhau cho dù NULL được định nghĩa như thế nào, vì vậy việc xác định NULL sẽ dễ dàng hơn bằng 0 và đảm bảo rằng số 0 không bao giờ được tham chiếu từ C.


Vấn đề sẽ chỉ phát sinh khi NULL được truy cập một cách tình cờ, không phải khi cổng được cố tình truy cập. Tại sao tôi phải xác định lại cổng I / O cho lúc đó? Nó đã hoạt động như bình thường.
Lundin

2
@Lundin Vô tình hay không, NULL có thể chỉ được dereferenced trong một chương trình C sử dụng *p, p[]hoặc p(), vì vậy trình biên dịch chỉ cần quan tâm đến những người bảo vệ cổng IO 0x0000.
Apalala

@Lundin Phần thứ hai trong câu hỏi của bạn: Khi bạn hạn chế quyền truy cập vào địa chỉ 0 từ bên trong C, bạn cần một cách khác để truy cập cổng 0x0000. Một hàm được viết bằng trình hợp ngữ có thể làm được điều đó. Từ bên trong C, cổng có thể được ánh xạ thành 0xFFFF hoặc bất cứ thứ gì, nhưng tốt nhất bạn nên sử dụng một hàm và quên số cổng.
Apalala

3

Xem xét sự khó khăn cực kỳ trong việc xác định lại NULL như những người khác đã đề cập, có thể dễ dàng hơn để xác định lại hội nghị đối với các địa chỉ phần cứng nổi tiếng. Khi tạo địa chỉ, hãy thêm 1 vào mọi địa chỉ nổi tiếng để cổng IO nổi tiếng của bạn sẽ là:

  #define CREATE_HW_ADDR(x)(x+1)
  #define DEREFERENCE_HW_ADDR(x)(*(x-1))

  int* wellKnownIoPort = CREATE_HW_ADDR(0x00000000);

  printf("IoPortIs" DEREFERENCE_HW_ADDR(wellKnownIoPort));

Nếu các địa chỉ bạn lo ngại được nhóm lại với nhau và bạn có thể cảm thấy an toàn khi thêm 1 vào địa chỉ sẽ không xung đột với bất kỳ điều gì (điều này không nên xảy ra trong hầu hết các trường hợp), bạn có thể thực hiện việc này một cách an toàn. Và sau đó bạn không cần phải lo lắng về việc xây dựng lại chuỗi công cụ / std lib và các biểu thức của mình ở dạng:

  if (pointer)
  {
     ...
  }

vẫn làm việc

Thật điên rồ, tôi biết, nhưng chỉ nghĩ rằng tôi sẽ ném ý tưởng ra khỏi đó :).


Vấn đề sẽ chỉ phát sinh khi NULL được truy cập một cách tình cờ, không phải khi cổng được cố tình truy cập. Tại sao tôi phải xác định lại cổng I / O cho lúc đó? Nó đã hoạt động như bình thường.
Lundin

@LundIn Tôi đoán bạn phải chọn cái nào khó hơn, điều chỉnh việc xây dựng lại toàn bộ chuỗi công cụ hoặc thay đổi một phần này trong mã của bạn.
Doug T.

2

Mẫu bit cho con trỏ null có thể không giống với mẫu bit cho số nguyên 0. Nhưng việc mở rộng macro NULL phải là hằng số con trỏ null, đó là một số nguyên không đổi của giá trị 0 có thể được chuyển thành (void *).

Để đạt được kết quả bạn muốn trong khi vẫn tuân thủ, bạn sẽ phải sửa đổi (hoặc có thể định cấu hình) chuỗi công cụ của mình, nhưng nó có thể đạt được.


1

Bạn đang yêu cầu rắc rối. Việc xác định lại NULLthành giá trị không phải null sẽ phá vỡ mã này:

   nếu (myPointer)
   {
      // myPointer không rỗ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.