Piszę kod C dla systemu, w którym adres 0x0000 jest prawidłowy i zawiera port I / O. Dlatego wszelkie możliwe błędy, które uzyskują dostęp do wskaźnika NULL, pozostaną niewykryte i jednocześnie spowodują niebezpieczne zachowanie.
Z tego powodu chcę przedefiniować NULL na inny adres, na przykład na adres, który jest nieprawidłowy. Jeśli przypadkowo uzyskam dostęp do takiego adresu, otrzymam przerwanie sprzętowe, w którym mogę obsłużyć błąd. Tak się składa, że mam dostęp do stddef.h dla tego kompilatora, więc mogę faktycznie zmienić standardowy nagłówek i przedefiniować NULL.
Moje pytanie brzmi: czy będzie to sprzeczne ze standardem C? O ile wiem z 7.17 w standardzie, makro jest zdefiniowane w implementacji. Czy w normie jest coś, co mówi, że NULL musi wynosić 0?
Inną kwestią jest to, że wiele kompilatorów przeprowadza statyczną inicjalizację, ustawiając wszystko na zero, bez względu na typ danych. Mimo że standard mówi, że kompilator powinien ustawić liczby całkowite na zero, a wskaźniki na NULL. Gdybym przedefiniował NULL dla mojego kompilatora, to wiem, że taka statyczna inicjalizacja się nie powiedzie. Czy mogę to uznać za nieprawidłowe zachowanie kompilatora, mimo że odważnie ręcznie zmieniłem nagłówki kompilatora? Ponieważ wiem na pewno, że ten konkretny kompilator nie ma dostępu do makra NULL podczas wykonywania statycznej inicjalizacji.
mprotect
zabezpieczyć. Lub, jeśli platforma nie ma ASLR lub podobnego, adresy poza fizyczną pamięcią platformy. Powodzenia.if(ptr) { /* do something on ptr*/ }
? Czy zadziała, jeśli NULL jest zdefiniowane inaczej niż 0x0?Odpowiedzi:
Standard C nie wymaga, aby wskaźniki zerowe znajdowały się pod adresem zerowym maszyny. JEDNAK rzutowanie
0
stałej na wartość wskaźnika musi skutkowaćNULL
wskaźnikiem (§6.3.2.3 / 3), a obliczanie wskaźnika pustego jako wartości logicznej musi być fałszywe. To może być nieco niewygodne, jeśli naprawdę nie chcesz adres zerowy, aNULL
nie jest adresem zero.Niemniej jednak, przy (ciężkich) modyfikacjach kompilatora i biblioteki standardowej, nie jest niemożliwe, aby
NULL
być reprezentowanym za pomocą alternatywnego wzoru bitowego, przy jednoczesnym zachowaniu ścisłej zgodności z biblioteką standardową. To nie wystarczy po prostu zmienić definicjęNULL
jednak sama, jak wtedyNULL
będzie oceniać true.W szczególności musisz:
-1
.0
aby zamiast tego sprawdzić magiczną wartość (§6.5.9 / 6)Jest kilka rzeczy, którymi nie musisz się zajmować. Na przykład:
Po tym
p
NIE gwarantuje się, że wskaźnik będzie pusty. Obsługiwane są tylko stałe przypisania (jest to dobre podejście do dostępu do prawdziwego adresu zero). Również:Również:
x
nie jest gwarantowane0
.Krótko mówiąc, ten sam warunek był najwyraźniej rozważany przez komitet językowy C i rozważany dla tych, którzy wybraliby alternatywną reprezentację dla NULL. Wszystko, co musisz teraz zrobić, to wprowadzić duże zmiany w kompilatorze i hej, gotowe :)
Na marginesie, może być możliwe zaimplementowanie tych zmian na etapie transformacji kodu źródłowego przed właściwym kompilatorem. Oznacza to, że zamiast normalnego przepływu preprocesora -> kompilatora -> assemblera -> konsolidatora, można dodać preprocesor -> transformację NULL -> kompilator -> asembler -> linker. Następnie możesz wykonać transformacje, takie jak:
Wymagałoby to pełnego parsera C, a także parsera typów i analizy typów definicji typów i deklaracji zmiennych w celu określenia, które identyfikatory odpowiadają wskaźnikom. Jednak w ten sposób można uniknąć konieczności wprowadzania zmian w częściach generowania kodu przez kompilator. clang może być przydatny do realizacji tego - rozumiem, że został zaprojektowany z myślą o takich transformacjach. Oczywiście nadal prawdopodobnie będziesz musiał wprowadzić zmiany w bibliotece standardowej.
źródło
NULL
.Standard stwierdza, że wyrażenie stałe będące liczbą całkowitą o wartości 0 lub takie wyrażenie przekonwertowane na
void *
typ jest stałą wskaźnika o wartości null. Oznacza to, że(void *)0
zawsze jest wskaźnikiem NULL, ale biorąc pod uwagęint i = 0;
,(void *)i
nie musi być.Implementacja C składa się z kompilatora wraz z jego nagłówkami. Jeśli zmodyfikujesz nagłówki, aby przedefiniować
NULL
, ale nie zmodyfikujesz kompilatora w celu naprawienia statycznych inicjalizacji, oznacza to, że utworzyłeś niezgodną implementację. To cała implementacja razem wzięta ma niepoprawne zachowanie, a jeśli ją zepsujesz, naprawdę nie masz nikogo innego do zarzucenia;)Musisz naprawić ponad Initialisations tylko statycznych, oczywiście - biorąc pod uwagę wskaźnik
p
,if (p)
jest równoznaczne zif (p != NULL)
powodu powyższej reguły.źródło
Jeśli używasz biblioteki std C, napotkasz problemy z funkcjami, które mogą zwracać NULL. Na przykład dokumentacja malloc stwierdza:
Ponieważ malloc i powiązane funkcje są już skompilowane do plików binarnych z określoną wartością NULL, jeśli przedefiniujesz NULL, nie będziesz mógł bezpośrednio używać biblioteki std C, chyba że będziesz mógł przebudować cały łańcuch narzędzi, w tym biblioteki std C.
Również ze względu na użycie wartości NULL przez bibliotekę std, jeśli ponownie zdefiniujesz NULL przed dołączeniem nagłówków std, możesz nadpisać definicję NULL wymienioną w nagłówkach. Cokolwiek wstawione byłoby niespójne w skompilowanych obiektach.
Zamiast tego zdefiniowałbym twój własny NULL, „MYPRODUCT_NULL”, do własnych celów i albo unikałbym, albo tłumaczył z / do biblioteki std C.
źródło
Pozostaw NULL w spokoju i traktuj IO do portu 0x0000 jako przypadek specjalny, być może używając procedury napisanej w asemblerze, a zatem nie podlegającej standardowej semantyce C. IOW, nie redefiniuj NULL, przedefiniuj port 0x00000.
Zwróć uwagę, że jeśli piszesz lub modyfikujesz kompilator C, praca wymagana do uniknięcia wyłuskiwania NULL (zakładając, że w twoim przypadku procesor nie pomaga) jest taka sama bez względu na to, jak zdefiniowano NULL, więc łatwiej jest pozostawić zdefiniowaną NULL jako zero i upewnij się, że zero nigdy nie zostanie wyłuskane z C.
źródło
*p
,p[]
lubp()
, więc tylko kompilator musi troszczyć się o tych, w celu ochrony portu IO 0x0000.Biorąc pod uwagę ogromną trudność w redefiniowaniu NULL, o czym wspominali inni, być może łatwiej będzie przedefiniować dereferencje dla dobrze znanych adresów sprzętowych. Podczas tworzenia adresu dodaj 1 do każdego dobrze znanego adresu, aby Twój dobrze znany port IO wyglądał następująco:
Jeśli adresy, które Cię interesują, są zgrupowane razem i możesz czuć się bezpiecznie, że dodanie 1 do adresu nie będzie z niczym kolidować (co nie powinno w większości przypadków), możesz to zrobić bezpiecznie. A potem nie musisz się martwić o przebudowę łańcucha narzędzi / biblioteki standardowej i wyrażeń w postaci:
wciąż pracuję
Wiem, że to szalone, ale pomyślałem, że wyrzucę tam pomysł :).
źródło
Wzorzec bitowy dla wskaźnika zerowego może nie być taki sam jak wzorzec bitowy dla liczby całkowitej 0. Ale rozwinięcie makra NULL musi być stałą wskaźnika zerowego, to znaczy stałą liczbą całkowitą o wartości 0, która może być rzutowana do (void *).
Aby osiągnąć pożądany efekt, zachowując zgodność, będziesz musiał zmodyfikować (lub może skonfigurować) swój łańcuch narzędzi, ale jest to osiągalne.
źródło
Prosisz o kłopoty. Przedefiniowanie
NULL
do wartości innej niż null spowoduje przerwanie tego kodu:źródło