A konkretnie, co jest niebezpiecznego w rzucaniu wyniku malloc?

86

Zanim ludzie zaczną oznaczać to dupkiem, przeczytałem wszystkie poniższe rzeczy, z których żadna nie dostarcza odpowiedzi, której szukam:

  1. C FAQ: Co jest złego w rzutowaniu wartości zwracanej przez malloc?
  2. SO: Czy powinienem jawnie rzutować wartość zwracaną przez malloc ()?
  3. SO: niepotrzebne rzutowanie wskaźnika w C
  4. SO: Czy mogę rzucić wynik malloc?

Zarówno C FAQ, jak i wiele odpowiedzi na powyższe pytania cytuje tajemniczy błąd, który mallocmoże ukryć wartość zwracana przez rzutowanie ; jednak żaden z nich nie podaje konkretnego przykładu takiego błędu w praktyce. Teraz zwróć uwagę, że powiedziałem błąd , a nie ostrzeżenie .

Teraz otrzymałem następujący kod:

#include <string.h>
#include <stdio.h>
// #include <stdlib.h>

int main(int argc, char** argv) {

    char * p = /*(char*)*/malloc(10);
    strcpy(p, "hello");
    printf("%s\n", p);

    return 0;
}

Kompilowanie powyższego kodu za pomocą gcc 4.2, z rzutowaniem i bez, daje te same ostrzeżenia, a program działa poprawnie i zapewnia te same wyniki w obu przypadkach.

anon@anon:~/$ gcc -Wextra nostdlib_malloc.c -o nostdlib_malloc
nostdlib_malloc.c: In function ‘main’:
nostdlib_malloc.c:7: warning: incompatible implicit declaration of built-in function ‘malloc’
anon@anon:~/$ ./nostdlib_malloc 
hello

Czy zatem ktoś może podać konkretny przykład kodu błędu kompilacji lub wykonania, który może wystąpić z powodu mallocwartości zwracanej rzutowania , czy jest to tylko miejska legenda?

Edytuj Natknąłem się na dwa dobrze napisane argumenty dotyczące tej kwestii:

  1. Za rzutowaniem : CERT Advisory: Natychmiast rzutowanie wyniku wywołania funkcji alokacji pamięci na wskaźnik do przydzielonego typu
  2. Przeciwko przesyłaniu (błąd 404 z 14.02.2012: użyj kopii Internet Archive Wayback Machine z 27.01.2010. {2016-03-18: "Strona nie może zostać zaindeksowana lub wyświetlona z powodu pliku robots.txt."})
Robert S. Barnes
źródło
6
rzutowanie voidwskaźników pozwala skompilować kod w C ++; niektórzy mówią, że to funkcja, powiedziałbym, że to błąd;)
Christoph
1
przeczytaj również komentarze do pierwszego z linków, ponieważ opisuje, co powinieneś zrobić zamiast przesyłać: securecoding.cert.org/confluence/display/seccode/ ...
Christoph
3
Posłużę się radą CERT-ów co do włączenia obsady. Nie zapomnę też nigdy dołączyć stdlib.h. :)
Abhinav
1
Oto przykład SO błędu wykonania kompilacji z powodu mallocwartości zwracanej rzutowania: rzutowanie int*na arch. 64-bitowe.
John_West
1
to pytanie jest oznaczone jako Cnie C++(są to dwa różne języki), więc żadna dyskusja (jak w niektórych odpowiedziach) nie ma związku z tym pytaniem.
user3629249

Odpowiedzi:

66

Nie otrzymasz błędu kompilatora , ale ostrzeżenie kompilatora . Jako źródła zacytowanie głosu (zwłaszcza pierwszy z nich ), użytkownik może uzyskać nieprzewidziany błąd runtime podczas korzystania z obsady bez włączniestdlib.h .

Więc błąd po twojej stronie nie polega na obsadzie, ale na zapominaniu o włączeniu stdlib.h. Kompilatory mogą założyć, że mallocjest to funkcja zwracająca int, dlatego konwertuje void*wskaźnik faktycznie zwracany przez mallocnaint a następnie na typ wskaźnika z powodu jawnego rzutowania. Na niektórych platformach intwskaźniki mogą zajmować różną liczbę bajtów, więc konwersja typów może prowadzić do uszkodzenia danych.

Na szczęście współczesne kompilatory ostrzegają o twoim rzeczywistym błędzie. Zobacz dane gccwyjściowe, które dostarczyłeś: Ostrzega, że niejawna deklaracja ( int malloc(int)) jest niekompatybilna z wbudowaną malloc. Więc gccwydaje się wiedziećmalloc nawet bez stdlib.h.

Pozostawienie obsady, aby zapobiec temu błędowi, jest w większości tego samego powodu co pisanie

if (0 == my_var)

zamiast

if (my_var == 0)

ponieważ ta ostatnia może prowadzić do poważnego błędu, jeśli ktoś się pomyli = i ==, podczas gdy pierwszy z nich doprowadziłby do błędu kompilacji. Osobiście wolę ten drugi styl, ponieważ lepiej odzwierciedla moje zamiary i nie popełniam tego błędu.

To samo dotyczy rzutowania wartości zwracanej przez malloc: wolę być jawny w programowaniu i generalnie dwukrotnie sprawdzam, czy pliki nagłówkowe są dołączone do wszystkich funkcji, których używam.

Ferdinand Beyer
źródło
2
Wydawałoby się, że skoro kompilator ostrzega przed niekompatybilną niejawną deklaracją, to nie jest to problem, o ile zwracasz uwagę na ostrzeżenia kompilatora.
Robert S. Barnes
4
@Robert: tak, biorąc pod uwagę pewne założenia dotyczące kompilatora. Kiedy ludzie udzielają porad, jak najlepiej napisać C w ogóle , nie mogą zakładać, że osoba otrzymująca porady używa najnowszej wersji gcc.
Steve Jessop,
4
Aha, a odpowiedź na drugie pytanie jest taka, że ​​dzwoniący zawiera kod, aby pobrać wartość zwracaną (którą uważa za liczbę całkowitą) i przekonwertować ją na T *. Wywoływany po prostu zapisuje wartość zwracaną (jako void *) i zwraca. Więc w zależności od konwencji wywoływania: int zwraca i void * zwraca może, ale nie musi, znajdować się w „tym samym miejscu” (w rejestrze lub slocie stosu); int i void * mogą, ale nie muszą, mieć ten sam rozmiar; Konwersja między nimi może być lub nie być opcją. Więc może to „po prostu działać” lub wartość może zostać uszkodzona (być może utracone niektóre bity), lub wywołujący może odebrać całkowicie niewłaściwą wartość.
Steve Jessop,
1
@ RobertS.Barnes spóźnia się na imprezę, ale: Wartość zwracana zazwyczaj nie jest częścią podpisu funkcji, nawet w C ++. Linker po prostu generuje skok do symbolu, to wszystko.
Peter - Przywróć Monikę
3
Podczas używania rzutowania bez dołączania stdlib.h może wystąpić nieprzewidywalny błąd w czasie wykonywania . To prawda, ale nie dołączanie stdlib.hjest już samym błędem, nawet jeśli otrzymujesz tylko ostrzeżenia o „niejawnej deklaracji”.
Jabberwocky,
45

Jeden z dobrych argumentów wyższego poziomu przeciwko rzutowaniu wyniku mallocjest często pomijany, chociaż moim zdaniem jest on ważniejszy niż dobrze znane problemy niższego poziomu (takie jak obcięcie wskaźnika, gdy brakuje deklaracji).

Dobrą praktyką programistyczną jest pisanie kodu możliwie niezależnego od typu. Oznacza to w szczególności, że nazwy typów powinny być wymieniane w kodzie jak najmniej lub najlepiej nie wspominane w ogóle. Dotyczy to rzutów (unikaj niepotrzebnych rzutów), typów jako argumentów sizeof(unikaj używania nazw typów w sizeof) i ogólnie wszystkich innych odwołań do nazw typów.

Nazwy typów należą do deklaracji. O ile to możliwe, nazwy typów powinny być ograniczone do deklaracji i tylko do deklaracji.

Z tego punktu widzenia ten fragment kodu jest zły

int *p;
...
p = (int*) malloc(n * sizeof(int));

a to jest o wiele lepsze

int *p;
...
p = malloc(n * sizeof *p);

nie tylko dlatego, że "nie mallocrzutuje wyniku ", ale raczej dlatego, że jest niezależny od typu (lub niezależny od typu, jeśli wolisz), ponieważ automatycznie dostosowuje się do dowolnego pzadeklarowanego typu, bez konieczności jakiejkolwiek interwencji ze strony użytkownik.

Mrówka
źródło
Fwiw, myślę, że to mniej więcej ten sam powód, co ten: stackoverflow.com/questions/953112/ ... ale skupił się na niezależności od typu, a nie na majsterkowaniu. Oczywiście pierwszy wynika z drugiego (lub odwrotnie), więc przynajmniej czasami o nim wspomina . :)
zrelaksuj się
5
@unwind najprawdopodobniej masz na myśli DRY, a nie DIY
kratenko
18

Zakłada się, że funkcje nieoprototypowane powrócą int .

Więc rzucasz int do wskaźnika. Jeśli wskaźniki intna Twojej platformie są szersze niż s, jest to bardzo ryzykowne zachowanie.

Plus, oczywiście, że niektórzy ludzie uważają ostrzeżenia za błędy, tj. Kod powinien się kompilować bez nich.

Osobiście uważam, że fakt, że nie trzeba rzutować void *na inny typ wskaźnika, jest funkcją w C i uważam, że kod, który jest uszkodzony.

rozwijać
źródło
14
Mam przekonanie, że kompilator wie więcej o języku niż ja, więc jeśli mnie o czymś ostrzega, zwracam uwagę.
György Andrasek
3
W wielu projektach kod C jest kompilowany jako C ++, w którym trzeba rzutować void*.
laalto
nit: " domyślnie zakłada się, że zwracane są funkcje nieoprototypowane int." - Czy masz na myśli, że można zmienić typ zwracanych funkcji nie-prototypowych?
pmg
1
@laalto - Tak, ale nie powinno. C to C, a nie C ++, i powinno być kompilowane za pomocą kompilatora C, a nie kompilatora C ++. Nie ma wymówki: GCC (jeden z najlepszych kompilatorów C) działa na prawie każdej możliwej platformie (i generuje również wysoce zoptymalizowany kod). Jakie powody możesz mieć skompilować C za pomocą kompilatora C ++, inne niż lenistwo i luźne standardy?
Chris Lutz,
3
Przykład kodu, który może chcesz skompilować zarówno C i C ++ #ifdef __cplusplus \nextern "C" { \n#endif static inline uint16_t swb(uint16_t a) {return ((a << 8) | ((a >> 8) & 0xFF); } \n#ifdef __cplusplus\n } \n#endif. Teraz, dlaczego chcesz wywołać malloc w statycznej funkcji wbudowanej, naprawdę nie wiem, ale nagłówki, które działają w obu, są prawie niespotykane.
Steve Jessop,
11

Jeśli zrobisz to podczas kompilacji w trybie 64-bitowym, zwrócony wskaźnik zostanie obcięty do 32-bitów.

EDYCJA: Przepraszam, że jestem zbyt krótki. Oto przykładowy fragment kodu do celów dyskusji.

Główny()
{
   char * c = (char *) malloc (2);
   printf ("% p", c);
}

Załóżmy, że zwrócony wskaźnik sterty jest czymś większym niż to, co można przedstawić w int, powiedzmy 0xAB00000000.

Jeśli malloc nie ma prototypu, aby zwrócić wskaźnik, zwrócona wartość int będzie początkowo znajdować się w jakimś rejestrze ze wszystkimi ustawionymi znaczącymi bitami. Teraz kompilator powie: „OK, jak mogę przekonwertować i int na wskaźnik”. Będzie to albo rozszerzenie znaku, albo rozszerzenie zerowe 32-bitowych najmniej znaczących bitów, o których powiedziano, że malloc „zwraca” przez pominięcie prototypu. Ponieważ int jest podpisany, myślę, że konwersja będzie rozszerzeniem znaku, które w tym przypadku konwertuje wartość na zero. Zwracając wartość 0xABF0000000, otrzymasz niezerowy wskaźnik, który również sprawi trochę radości, gdy spróbujesz go wyłuskać.

Peeter Joot
źródło
1
Czy mógłbyś szczegółowo wyjaśnić, jak to się stanie?
Robert S. Barnes
5
Wydaje mi się, że Peeter Joot doszedł do wniosku, że „Domyślnie zakłada się, że funkcje nie-prototypowane zwracają int” bez uwzględnienia stdlib.h, a sizeof (int) to 32 bity, podczas gdy sizeof (ptr) to 64.
Test
4

Reguła dotycząca oprogramowania wielokrotnego użytku:

W przypadku pisania funkcji inline, w której użyto malloc (), aby umożliwić jej wielokrotne użycie również dla kodu C ++, należy wykonać jawne rzutowanie typu (np. (Char *)); w przeciwnym razie kompilator będzie narzekał.

Test
źródło
miejmy nadzieję, że wraz z (niedawnym) włączeniem optymalizacji czasu łącza do gcc (patrz gcc.gnu.org/ml/gcc/2009-10/msg00060.html ), deklarowanie funkcji inline w plikach nagłówkowych nie będzie już konieczne
Christoph
masz złe pomysły. Czy zdajesz sobie sprawę z tego, co jest przenośne i wieloplatformowe wśród różnych kompilatorów / wersji / architektur? ok, nie możesz. co to znaczy wielokrotnego użytku?
Test
2
pisząc w C ++, malloc / free NIE jest właściwą metodologią. Raczej użyj nowego / usuń. IE nie powinno być żadnych / nada / zero wywołań do malloc / free w kodzie C ++
user3629249
3
@ user3629249: Pisząc funkcję, która musi być użyteczny od wewnątrz obu kodu C lub C ++ kod, używając malloc/ freedla obu jest w stanie lepiej niż stara się wykorzystać mallocw C i newC ++, zwłaszcza jeśli struktury danych są dzielone między C i C ++ kod i istnieje możliwość, że obiekt może zostać utworzony w kodzie C i wydany w kodzie C ++ lub odwrotnie.
supercat
3

Wskaźnik void w C można przypisać do dowolnego wskaźnika bez jawnego rzutowania. Kompilator wyświetli ostrzeżenie, ale może być ponownie użyty w C ++ według rzutowania typu malloc()na odpowiedni typ. Bez rzutowania typu out może być również używany w C , ponieważ C nie jest ścisłym sprawdzaniem typu . Ale C ++ służy wyłącznie do sprawdzania typów, więc wymagane jest rzucanie typów malloc()w C ++.

Yogeesh HT
źródło
Jeśli używasz malloc w C ++, lepiej miej cholernie dobry powód! ; p
antred