Chciałbym przygotować małe narzędzie edukacyjne dla SO, które powinno pomóc początkującym (i średnio zaawansowanym) programistom rozpoznać i zakwestionować ich nieuzasadnione założenia w C, C ++ i ich platformach.
Przykłady:
- „zawijanie liczb całkowitych”
- „każdy ma ASCII”
- „Mogę przechowywać wskaźnik funkcji w pustej przestrzeni *”
Doszedłem do wniosku, że mały program testowy można uruchomić na różnych platformach, zgodnie z "wiarygodnymi" założeniami, które z naszego doświadczenia w SO są zwykle poczynione przez wielu niedoświadczonych / częściowo doświadczonych programistów głównego nurtu i rejestrują sposoby ich łamania na różnych maszynach.
Celem tego nie jest udowodnienie, że można coś zrobić „bezpiecznie” (co byłoby niemożliwe, testy dowodzą tylko wszystkiego, jeśli się zepsują), ale zamiast tego zademonstrowanie nawet najbardziej niezrozumiałej osobie, jak najbardziej niepozorne wyrażenie przerwa na innym komputerze, jeśli ma niezdefiniowane lub zdefiniowane w implementacji zachowanie. .
Aby to osiągnąć, chciałbym Cię zapytać:
- Jak można ulepszyć ten pomysł?
- Które testy byłyby dobre i jak powinny wyglądać?
- Czy przeprowadziłbyś testy na platformach, na których możesz dostać, i opublikował wyniki, abyśmy otrzymali bazę danych platform, czym się różnią i dlaczego ta różnica jest dozwolona?
Oto aktualna wersja zabawki testowej:
#include <stdio.h>
#include <limits.h>
#include <stdlib.h>
#include <stddef.h>
int count=0;
int total=0;
void expect(const char *info, const char *expr)
{
printf("..%s\n but '%s' is false.\n",info,expr);
fflush(stdout);
count++;
}
#define EXPECT(INFO,EXPR) if (total++,!(EXPR)) expect(INFO,#EXPR)
/* stack check..How can I do this better? */
ptrdiff_t check_grow(int k, int *p)
{
if (p==0) p=&k;
if (k==0) return &k-p;
else return check_grow(k-1,p);
}
#define BITS_PER_INT (sizeof(int)*CHAR_BIT)
int bits_per_int=BITS_PER_INT;
int int_max=INT_MAX;
int int_min=INT_MIN;
/* for 21 - left to right */
int ltr_result=0;
unsigned ltr_fun(int k)
{
ltr_result=ltr_result*10+k;
return 1;
}
int main()
{
printf("We like to think that:\n");
/* characters */
EXPECT("00 we have ASCII",('A'==65));
EXPECT("01 A-Z is in a block",('Z'-'A')+1==26);
EXPECT("02 big letters come before small letters",('A'<'a'));
EXPECT("03 a char is 8 bits",CHAR_BIT==8);
EXPECT("04 a char is signed",CHAR_MIN==SCHAR_MIN);
/* integers */
EXPECT("05 int has the size of pointers",sizeof(int)==sizeof(void*));
/* not true for Windows-64 */
EXPECT("05a long has at least the size of pointers",sizeof(long)>=sizeof(void*));
EXPECT("06 integers are 2-complement and wrap around",(int_max+1)==(int_min));
EXPECT("07 integers are 2-complement and *always* wrap around",(INT_MAX+1)==(INT_MIN));
EXPECT("08 overshifting is okay",(1<<bits_per_int)==0);
EXPECT("09 overshifting is *always* okay",(1<<BITS_PER_INT)==0);
{
int t;
EXPECT("09a minus shifts backwards",(t=-1,(15<<t)==7));
}
/* pointers */
/* Suggested by jalf */
EXPECT("10 void* can store function pointers",sizeof(void*)>=sizeof(void(*)()));
/* execution */
EXPECT("11 Detecting how the stack grows is easy",check_grow(5,0)!=0);
EXPECT("12 the stack grows downwards",check_grow(5,0)<0);
{
int t;
/* suggested by jk */
EXPECT("13 The smallest bits always come first",(t=0x1234,0x34==*(char*)&t));
}
{
/* Suggested by S.Lott */
int a[2]={0,0};
int i=0;
EXPECT("14 i++ is strictly left to right",(i=0,a[i++]=i,a[0]==1));
}
{
struct {
char c;
int i;
} char_int;
EXPECT("15 structs are packed",sizeof(char_int)==(sizeof(char)+sizeof(int)));
}
{
EXPECT("16 malloc()=NULL means out of memory",(malloc(0)!=NULL));
}
/* suggested by David Thornley */
EXPECT("17 size_t is unsigned int",sizeof(size_t)==sizeof(unsigned int));
/* this is true for C99, but not for C90. */
EXPECT("18 a%b has the same sign as a",((-10%3)==-1) && ((10%-3)==1));
/* suggested by nos */
EXPECT("19-1 char<short",sizeof(char)<sizeof(short));
EXPECT("19-2 short<int",sizeof(short)<sizeof(int));
EXPECT("19-3 int<long",sizeof(int)<sizeof(long));
EXPECT("20 ptrdiff_t and size_t have the same size",(sizeof(ptrdiff_t)==sizeof(size_t)));
#if 0
{
/* suggested by R. */
/* this crashed on TC 3.0++, compact. */
char buf[10];
EXPECT("21 You can use snprintf to append a string",
(snprintf(buf,10,"OK"),snprintf(buf,10,"%s!!",buf),strcmp(buf,"OK!!")==0));
}
#endif
EXPECT("21 Evaluation is left to right",
(ltr_fun(1)*ltr_fun(2)*ltr_fun(3)*ltr_fun(4),ltr_result==1234));
{
#ifdef __STDC_IEC_559__
int STDC_IEC_559_is_defined=1;
#else
/* This either means, there is no FP support
*or* the compiler is not C99 enough to define __STDC_IEC_559__
*or* the FP support is not IEEE compliant. */
int STDC_IEC_559_is_defined=0;
#endif
EXPECT("22 floating point is always IEEE",STDC_IEC_559_is_defined);
}
printf("From what I can say with my puny test cases, you are %d%% mainstream\n",100-(100*count)/total);
return 0;
}
Aha, i stworzyłem tę wspólnotową wiki od samego początku, ponieważ pomyślałem, że ludzie chcą edytować moją paplaninę, kiedy to czytają.
AKTUALIZACJA Dzięki za wkład. Dodałem kilka przypadków z twoich odpowiedzi i zobaczę, czy mogę założyć github, jak zasugerował Greg.
AKTUALIZACJA : W tym celu utworzyłem repozytorium github, plik to „gotcha.c”:
Prosimy o przesłanie tutaj poprawek lub nowych pomysłów, aby można je było omówić lub wyjaśnić tutaj. W takim razie połączę je w gotcha.c.
źródło
dlsym()
zwraca void *, ale jest przeznaczony zarówno dla wskaźników danych, jak i funkcji. Dlatego poleganie na tym może nie być takie złe.Odpowiedzi:
Kolejność oceny podwyrażeń, w tym
+
,-
,=
,*
,/
), z wyjątkiem:&&
i||
),?:
) i,
)jest nieokreślony
Na przykład
źródło
boost::spirit
)+
operatora jest nieokreślona (autorzy kompilatora nie muszą dokumentować zachowania). Nie narusza żadnej zasady dotyczącej punktów sekwencji jako takiej.sdcc 29.7 / ucSim / Z80
printf ulega awarii. „O_O”
gcc 4.4@x86_64-suse-linux
gcc 4.4@x86_64-suse-linux (-O2)
clang 2.7@x86_64-suse-linux
open64 4.2.3@x86_64-suse-linux
intel 11.1@x86_64-suse-linux
Turbo C ++ / DOS / Mała pamięć
Turbo C ++ / DOS / Medium Memory
Turbo C ++ / DOS / pamięć kompaktowa
cl65 @ Commodore PET (wice emulator)
Zaktualizuję te później:
Borland C ++ Builder 6.0 w systemie Windows XP
Visual Studio Express 2010 C ++ CLR, Windows 7 64-bitowy
(musi być skompilowany jako C ++, ponieważ kompilator CLR nie obsługuje czystego C)
MINGW64 (wersja wstępna gcc-4.5.2)
- http://mingw-w64.sourceforge.net/
64-bitowy system Windows używa modelu LLP64: oba
int
ilong
są zdefiniowane jako 32-bitowe, co oznacza, że żaden z nich nie jest wystarczająco długi na wskaźnik.avr-gcc 4.3.2 / ATmega168 (Arduino Diecimila)
Błędne założenia to:
Atmega168 ma 16-bitowy komputer PC, ale kod i dane znajdują się w oddzielnych przestrzeniach adresowych. Większe Atmegas mają 22-bitowy PC !.
gcc 4.2.1 na MacOSX 10.6, skompilowany z -arch ppc
źródło
sizeof(void*)>=sizeof(void(*)())
byłoby bardziej odpowiednie niż ==. Jedyne, na czym nam zależy, to „czy możemy przechowywać wskaźnik funkcji w void pointer”, więc założeniem, które musisz przetestować, jest to, czy avoid*
jest co najmniej tak duże jak wskaźnik funkcji.sizeof(void*)>=sizeof(void(*)())
- zobacz opengroup.org/onlinepubs/009695399/functions/dlsym.htmlDawno temu uczyłem C z podręcznika, który miał
jako przykładowe pytanie. Nie udało się to uczniowi, ponieważ
sizeof
daje wartości typusize_t
nieint
,int
w tej implementacji było 16 bitów isize_t
32, a było to big-endian. (Platformą była Lightspeed C na Macintoshach z 680x0. Powiedziałem, że to było dawno temu).źródło
unsigned long long
tam size_t . Dodano jako test 17.z
modyfikatora dlasize_t
liczb całkowitych o rozmiarze ilong long
nie jest również obsługiwane na niektórych platformach. Nie ma więc bezpiecznego, przenośnego sposobu formatowania lub rzutowania rozmiaru wydruku obiektu.Musisz uwzględnić przyjęte przez ludzi założenia
++
i--
.Na przykład jest legalny składniowo, ale daje różne wyniki w zależności od zbyt wielu rzeczy, aby je uzasadnić.
Każde stwierdzenie, które ma
++
(lub--
) i zmienną, która występuje więcej niż raz, stanowi problem.źródło
Bardzo interesujące!
Inne rzeczy, które przychodzą mi do głowy, mogą być przydatne do sprawdzenia:
czy wskaźniki funkcji i wskaźniki danych istnieją w tej samej przestrzeni adresowej? (Przerwy w maszynach o architekturze Harvardu, takich jak mały tryb DOS. Nie wiem jednak, jak to przetestować).
jeśli weźmiesz wskaźnik danych NULL i rzucisz go na odpowiedni typ liczby całkowitej, czy ma on wartość liczbową 0? (Przerwy na niektórych naprawdę starych maszynach - patrz http://c-faq.com/null/machexamp.html .) Podobnie ze wskaźnikiem funkcji. Mogą też mieć różne wartości.
czy inkrementacja wskaźnika poza koniec odpowiadającego mu obiektu pamięci, a następnie z powrotem, daje sensowne wyniki? (Nie znam żadnych maszyn, na których to faktycznie się psuje, ale uważam, że specyfikacja C nie pozwala nawet myśleć o wskaźnikach, które nie wskazują ani (a) zawartości tablicy, ani (b) elementu bezpośrednio po tablicy lub (c) NULL. Zobacz http://c-faq.com/aryptr/non0based.html .)
czy porównanie dwóch wskaźników z różnymi obiektami pamięci za pomocą <i> daje spójne wyniki? (Mogę sobie wyobrazić to zerwanie na egzotycznych maszynach opartych na segmentach; specyfikacja zabrania takich porównań, więc kompilator byłby uprawniony do porównania tylko przesuniętej części wskaźnika, a nie części segmentu).
Hmm. Spróbuję pomyśleć o czymś więcej.
Edycja: Dodano kilka wyjaśniających linków do doskonałego C FAQ.
źródło
Myślę, że powinieneś postarać się rozróżnić dwie bardzo różne klasy „niepoprawnych” założeń. Dobra połowa (przesunięcie w prawo i rozszerzenie znaku, kodowanie zgodne z ASCII, pamięć jest liniowa, wskaźniki danych i funkcji są zgodne itp.) To całkiem rozsądne założenia dla większości koderów C, a nawet mogą być włączone jako część standardu gdyby C projektowano dzisiaj i gdybyśmy nie mieli odziedziczonych po IBM śmieci. Druga połowa (rzeczy związane z aliasowaniem pamięci, zachowanie funkcji bibliotecznych w przypadku nakładania się pamięci wejściowej i wyjściowej, 32-bitowe założenia, takie jak te wskaźniki
int
lub których możesz użyćmalloc
bez prototypu, ta konwencja wywoływania jest identyczna dla funkcji wariadycznych i nie-wariadycznych, ...) albo koliduje z optymalizacjami, które chcą wykonać nowoczesne kompilatory, albo z migracją na maszyny 64-bitowe lub inną nową technologię.źródło
malloc
bez prototypu oznacza nieuwzględnianie<stdlib.h>
, co powodujemalloc
domyślnieint malloc(int)
, nie-nie, jeśli chcesz obsługiwać 64-bit.<stdlib.h>
o ile dołączysz inny nagłówek, który definiuje,size_t
a następnie sam zadeklarujeszmalloc
z poprawnym prototypem.Oto fajny: Co jest nie tak z tą funkcją?
[Odpowiedź (rot13): Inevnqvp nethzragf borl gur byq X&E cebzbgvba ehyrf, juvpu zrnaf lbh pnaabg hfr 'sybng' (be 'pune' be 'fubeg') va in_net! Naq gur pbzcvyre vf erdhverq abg gb gerng guvf nf n pbzcvyr-gvzr reebe. (TPP qbrf rzvg n jneavat, gubhtu.)]
źródło
Inny dotyczy trybu tekstowego w
fopen
. Większość programistów zakłada, że tekst i plik binarny są takie same (Unix) lub że tryb tekstowy dodaje\r
znaki (Windows). Ale C został przeniesiony do systemów, które używają rekordów o stałej szerokości, na którychfputc('\n', file)
w pliku tekstowym oznacza się dodanie spacji lub czegoś podobnego, dopóki rozmiar pliku nie będzie wielokrotnością długości rekordu.A oto moje wyniki:
gcc (Ubuntu 4.4.3-4ubuntu5) 4.4.3 na x86-64
źródło
pow(2, n)
z operacjami bitowymi.Niektórych z nich nie da się łatwo przetestować z poziomu C, ponieważ program prawdopodobnie ulegnie awarii w implementacjach, których założenie nie jest spełnione.
„Można cokolwiek zrobić ze zmienną o wartości wskaźnika. Musi ona zawierać prawidłową wartość wskaźnika tylko wtedy, gdy ją wyłuskujesz”.
To samo dotyczy typów całkowitych i zmiennoprzecinkowych (innych niż
unsigned char
), które mogą mieć reprezentacje pułapek.„Obliczenia liczb całkowitych zawijają się. Więc ten program wypisuje dużą ujemną liczbę całkowitą”.
(Tylko C89.) „Możesz spaść z końca
main
.”źródło
gcc -ftrapv -O
, po wyjściuWe like to think that:
następujeAborted
main
z bez wartości: program jest poprawny, ale zwraca niezdefiniowany stan zakończenia (C89 §2.1.2.2). W przypadku wielu implementacji (takich jak gcc i starsze kompilatory unixowe) otrzymujesz wszystko, co było w pewnym rejestrze w tym momencie. Program zazwyczaj działa, dopóki nie zostanie użyty w pliku makefile lub innym środowisku, które sprawdza stan zakończenia.Cóż, klasyczne założenia dotyczące przenośności, o których jeszcze nie wspomniano, są
źródło
short
wartości fedcab9876543210 (czyli 16 cyfr binarnych) jako dwóch bajtów 0248ace i fdb97531.Błędy dyskretyzacji spowodowane reprezentacją zmiennoprzecinkową. Na przykład, jeśli użyjesz standardowej formuły do rozwiązywania równań kwadratowych lub skończonych różnic do przybliżonych pochodnych lub standardowej formuły do obliczania wariancji, precyzja zostanie utracona z powodu obliczania różnic między podobnymi liczbami. Algorytm Gaußa do rozwiązywania układów liniowych jest zły, ponieważ gromadzą się błędy zaokrągleń, dlatego stosuje się dekompozycję QR lub LU, dekompozycję Choleskiego, SVD itp. Dodawanie liczb zmiennoprzecinkowych nie jest asocjacyjne. Istnieją wartości denormalne, nieskończone i NaN. a + b - a ≠ b .
Ciągi znaków: różnica między znakami, punktami kodowymi i jednostkami kodu. Sposób implementacji Unicode w różnych systemach operacyjnych; Kodowanie Unicode. Otwarcie pliku z dowolną nazwą pliku Unicode nie jest możliwe w C ++ w sposób przenośny.
Warunki wyścigu, nawet bez wątków: jeśli przetestujesz, czy plik istnieje, wynik może stać się nieważny w dowolnym momencie.
ERROR_SUCCESS
= 0źródło
Uwzględnij sprawdzenie rozmiarów całkowitych. Większość ludzi zakłada, że int jest większe niż short jest większe niż char. Jednak wszystko to może być fałszywe:
sizeof(char) < sizeof(int); sizeof(short) < sizeof(int); sizeof(char) < sizeof(short)
Ten kod może się nie powieść (awarie przy dostępie bez wyrównania)
źródło
int *p = (int*)&buf[1];
w c ++, ludzie oczekują, że to też zadziała.sizeof(char) < sizeof(int)
jest wymagane. Na przykład fgetc () zwraca wartość znaku jako unsigned char przekonwertowany na int lubEOF
będący wartością ujemną.unsigned char
może nie mieć bitów wypełniających, więc jedynym sposobem na to jest uczynienie wartości int większym niż char. Ponadto (większość wersji) specyfikacji C wymaga, aby każda wartość z zakresu -32767..32767 mogła być przechowywana w int.Kilka rzeczy o wbudowanych typach danych:
char
isigned char
są w rzeczywistości dwoma różnymi typami (w przeciwieństwieint
isigned int
które odnoszą się do tego samego typu liczb całkowitych ze znakiem).-3/5
może powrócić0
lub-1
. Zaokrąglanie w kierunku zera w przypadku, gdy jeden operand był ujemny, jest gwarantowane tylko w C99 w górę i C ++ 0x w górę.int
ma co najmniej 16 bitów, along
ma co najmniej 32 bity, along long
ma co najmniej 64 bity. Afloat
może poprawnie reprezentować przynajmniej 6 najbardziej znaczących cyfr dziesiętnych. Adouble
może poprawnie reprezentować przynajmniej 10 najbardziej znaczących cyfr dziesiętnych.Trzeba przyznać, że na większości maszyn będziemy mieć dwa uzupełnienia i zmiennoprzecinkowe IEEE 754.
źródło
int mult(int a,int b) { return (long)a*b;}
[np. Jeśliint
ma 32 bity, ale rejestruje ilong
jest 64]. Bez takiego wymagania „naturalne” zachowanie najszybszej implementacjilong l=mult(1000000,1000000);
byłobyl
równe1000000000000
, mimo że jest to wartość „niemożliwa” dla plikuint
.A co z tym:
Żaden wskaźnik danych nie może być taki sam jak prawidłowy wskaźnik funkcji.
Jest to PRAWDA dla wszystkich płaskich modeli, modeli MS-DOS TINY, LARGE i HUGE, fałszywa dla modelu MS-DOS SMALL i prawie zawsze fałszywa dla modeli MEDIUM i COMPACT (w zależności od adresu ładowania, będziesz potrzebować naprawdę starego DOS uczynić to prawdziwym).
Nie mogę na to napisać testu
I gorzej: można porównać wskaźniki rzutowane na ptrdiff_t. Nie dotyczy to modelu MS-DOS LARGE (jedyna różnica między LARGE i HUGE polega na tym, że HUGE dodaje kod kompilatora w celu znormalizowania wskaźników).
Nie mogę napisać testu, ponieważ środowisko, w którym te bomby są trudne, nie przydzieli bufora większego niż 64K, więc kod, który to demonstruje, zawiesiłby się na innych platformach.
Ten konkretny test przeszedłby na jeden nieistniejący już system (zauważ, że zależy to od elementów wewnętrznych malloc):
źródło
EDYCJA: Zaktualizowano do ostatniej wersji programu
Solaris-SPARC
gcc 3.4.6 w wersji 32-bitowej
gcc 3.4.6 w wersji 64-bitowej
i 32-bitowym SUNStudio 11
oraz z SUNStudio 11 w wersji 64-bitowej
źródło
Możesz użyć trybu tekstowego (
fopen("filename", "r")
) do czytania dowolnego rodzaju pliku tekstowego.Chociaż teoretycznie powinno to działać dobrze, jeśli używasz również
ftell()
w swoim kodzie, a plik tekstowy ma zakończenia linii w stylu UNIX, w niektórych wersjach biblioteki standardowej Windowsftell()
często zwraca nieprawidłowe wartości. Rozwiązaniem jest użycie trybu binarnego (fopen("filename", "rb")
).źródło
gcc 3.3.2 w systemie AIX 5.3 (tak, musimy zaktualizować gcc)
źródło
Założenie, które niektórzy mogą zrobić w C ++, jest takie, że a
struct
jest ograniczone do tego, co może zrobić w C. Faktem jest, że w C ++ astruct
jest podobne do a,class
z tym wyjątkiem, że domyślnie ma wszystko publiczne.Struktura C ++:
źródło
Standardowe funkcje matematyczne w różnych systemach nie dają identycznych wyników.
źródło
Visual Studio Express 2010 na 32-bitowej architekturze x86.
źródło
Poprzez Codepad.org (
C++: g++ 4.1.2 flags: -O -std=c++98 -pedantic-errors -Wfatal-errors -Werror -Wall -Wextra -Wno-missing-field-initializers -Wwrite-strings -Wno-deprecated -Wno-unused -Wno-non-virtual-dtor -Wno-variadic-macros -fmessage-length=0 -ftemplate-depth-128 -fno-merge-constants -fno-nonansi-builtins -fno-gnu-keywords -fno-elide-constructors -fstrict-aliasing -fstack-protector-all -Winvalid-pch
).Zwróć uwagę, że Codepad nie miał
stddef.h
. Usunąłem test 9, ponieważ kodowanie używało ostrzeżeń jako błędów. Zmieniłem też nazwęcount
zmiennej, ponieważ z jakiegoś powodu została już zdefiniowana.źródło
A co z przesunięciem w prawo o nadmierne kwoty - czy jest to dozwolone przez normę, czy też warte sprawdzenia?
Czy Standard C określa zachowanie następującego programu:
Na przynajmniej jednym kompilatorze, którego używam, ten kod nie powiedzie się, chyba że argument print_string to "char const *". Czy norma dopuszcza takie ograniczenie?
Niektóre systemy pozwalają na tworzenie wskaźników do niewyrównanych int, a inne nie. Może warto przetestować.
źródło
<<
i>>
). C99 ma identyczny język w §6.5.7-3.putch
(dlaczego nie użyłeś standarduputchar
?), Nie widzę w Twoim programie żadnego niezdefiniowanego zachowania. C89 §3.1.4 określa, że „literał ciągu znaków ma typ […] 'tablica znaków'” (uwaga: nieconst
) oraz że „jeśli program próbuje zmodyfikować literał ciągu znaków […], zachowanie jest niezdefiniowane” . Co to za kompilator i jak tłumaczy ten program?Do Twojej wiadomości, dla tych, którzy muszą przetłumaczyć swoje umiejętności C na Javę, oto kilka pułapek.
W Javie znak char jest 16-bitowy i podpisany. bajt jest 8-bitowy i podpisany.
long jest zawsze 64-bitowe, odwołania mogą być 32-bitowe lub 64-bitowe (jeśli masz więcej niż aplikację o pojemności większej niż 32 GB) 64-bitowe maszyny JVM zwykle używają odwołań 32-bitowych.
Przesunięcie jest zamaskowane, więc i << 64 == i == i << -64, i << 63 == i << -1
ByteOrder.nativeOrder () może mieć wartość BIG_ENDIAN lub LITTLE_ENDIAN
i = i++
nigdy się nie zmieniai
Rozmiar kolekcji i tablic jest zawsze 32-bitowy, niezależnie od tego, czy maszyna JVM jest 32-bitowa, czy 64-bitowa.
char to 16-bit, short to 16-bit, int to 32-bit, a long to 64-bit.
źródło