Skocz do zawartości

[Programowanie] Pułapki języka C


Pomocna odpowiedź

osmial, jeszcze jest drugie zastosowanie: kiedy korzystasz z komend preprocesora #ifdef i #ifndef. Można wtedy się np. decydować czy dany blok ma być wykonany np. zawsze raz, po sprawdzeniu warunku czy w pętli.

  • 1 rok później...

Kiedyś miałem problem właśnie w C, i natknąłem się na sztuczkę z liczbami:

uint32_t total = hours * 3600 + minutes * 60 + seconds;

Problem był taki, że zmienna total po wykonaniu tej linijki była typu uint8_t. Pozostałe zmienne hours, minutes i seconds są uint8_t. Próbowałem też dawać rzutowanie tej całej operacji po znaku '=' na uint32_t ale to nic nie pomagało. Chodzi o to, że trzeba było dopisać do tych liczb UL np. 60UL wtedy typ zmiennej total był taki jaki chciałem uint32_t. No tak i tydzień szukania.

Nie trzeba do wszystkich, wystarczy dopisać to do pierwszej z nich i zreorganizować mnożenie, lub rzucić pierwszą zmienną na odpowiedni typ:

uint32_t total = 3600UL * hours + 60 * minutes+ seconds;
uint32_t total = (uint32_t)hours * 3600 + minutes * 60 + seconds; 

Natomiast należy uważać np. na taki zapis:

uint32_t total = 3600 * hours + 60 * minutes+ seconds;

ponieważ, wbrew pozorom, wynik może być różny w zależności od zastosowanej maszyny. Zadziała tutaj mechanizm zwany promocją do int, tj. kompilator rzuci pierwszą liczbę do inta. I tak dla 8 i 16-bitówców int ma zazwyczaj rozmiar 16-bitów, a dla 32-bitówców zazwyczaj 32-bity.

Właśnie chyba tak próbowałem i nie pomagało mi (pierwszy kod). Dopiero ten trick z dopisaniem UL pomógł, jakby to jakiego typu są liczby dawało pierwszeństwo jeśli chodzi o zwracany typ operacji. Albo być może miałem tak jak napisałeś w drugim kodzie i wtedy tak jak mówisz rzutowało mi do inta

Generalnie pierwszy element ma znaczenie.

Natomiast chciałbym kolegom przypomnieć o istotnej pułapce, której zdaje się nie zauważać wielu z nas, a która "zadziała" na maszynach 16- i 32-bitowych. Załóżmy, że mam taki kod:

uint8_t buffer[16];
uint16_t z1;
uint32_t z2;

// tutaj kopiujemy coś do bufora, jakieś memcpy itp.

z1 = buffer[0]; // dobrze
z1 = buffer[1]; // źle
z1 = buffer[2]; // teoretycznie dobrze, zależnie od kompilatora i maszyny
z1 = buffer[3]; // źle

z2 = buffer[0]; // dobrze
z2 = buffer[1]; // źle
z2 = buffer[2]; // raczej źle, zależnie od kompilatora i maszyny
z2 = buffer[3]; // źle

Przy przypisywaniu danych z bufora do zmiennych należy uważać na wyrównanie danych w buforze. Jeżeli dane zaczynają się nieparzyście lub nie od wielokrotności 4 (lub 2 dla 16-bitowców), mogą pojawić się problemy. O wiele bezpieczniejszym sposobem jest wtedy wykorzystanie tradycyjnego memcpy:

memcpy(&z1, &buffer[1], sizeof(z1));

Jednak należy tutaj pamiętać o poprawnym endianesie - do takich operacji przydane jest mieć specjalne makra, które potrafią przepisać dany typ zmiennej bez względu na rodzaj maszyny, tj wykryć go w czasie kompilacji, albo mieć przygotowany zestaw dla big i little endian. Przykładowym moim makrem do kopiowania uint16_t z niewyrównanego bufora (który trzyma dane w big endian) jest:

#define _Int16ToVoidBE(buff, val) {_Lo(buff) = _Hi(val); _Hi(buff) = _Lo(val);}

gdzie makra _Lo i _Hi potrafią się odnieść do danych bajtów względem niewyrównanego adresu.

Tyle smaczków na dziś 🙂

Nie do końca zgodzę się z tym co napisał madman07. Zacznijmy od prostego przykładu:

uint8_t x = 5;
uint16_t y = x;

Chyba wszyscy się zgodzą, że niezależnie od architektury, w zmiennej y powinniśmy otrzymać 5, a nie np. 0x1205 ?

Zdarzają się kompilatory, które w ramach optymalizacji, źle kompilują taki kod (nie zerują wyższych bitów), ale to błędy kompilatorów i o nich nie mówmy.

Wracając więc do przykładu:

uint8_t x[2] = { 5, 10 };
uint16_t y = x[0];

Ten kod niczym nie różni się od poprzedniego! Więc poprawna wartość y to nadal 5, a nie 0x0a05.

Więc to co napisał madman07, to moim zdaniem rezultat niepoprawnego działania jakiegoś kompilatora. Faktycznie takie błędy lubią się kompilatorom przytrafiać - wynika to z silnej optymalizacji.

Użycie memcpy jak w przykładzie to w rzeczywistości odpowiednik:

uint32_t z1 = *((uint32_t*)&buffer[1]);

Taki kod nie tylko że jest brzydki, ale może faktycznie źle działać i to nawet na 32-bitowych architekturach. W ARM7TDMI można odwoływać się do pamięci tylko adresami podzielnymi przez 4 - inne odczyty to w rzeczywistości pobranie 4 bajtów i przesunięcie bitowe. Więc taki kod spowodowałby wyjątek dostępu do pamięci.

Mogliśmy się nie zrozumieć 😉 Pierwszy kod jest okej. Natomiast, taki kod (odwołując się do Twojego drugiego przykładu):

uint16_t y = x[0]; // y = 5
uint16_t y = x[1]; // y nie musi być równe 10

Tutaj dochodzimy do konkluzji, czyli tego, jak kompilator rzeczywiście to potraktuje. A potraktuje to tak, jak napisałeś (i co pokazuję poniżej). Użycie memcpy (tj kopiowanie bajt po bajcie) ABSOLUTNIE nie jest odpowiednikiem takiego kodu:

uint32_t z1 = *((uint32_t*)&buffer[1]);

który to kompilator spróbuje przetłumaczyć na instrukcje typu Load-Save zamiast kopiowania bajt po bajcie. W efekcie, dokładnie jak piszesz, mogą stać się niesprecyzowane rzeczy czy nawet wyjątek dostępu do pamięci. To już zależy pewnie od architektury.

Z takim przypadkiem nigdy się nie spotkałem:

uint16_t y = x[1]; // y nie musi być równe 10

Zgodnie ze standardem C kompilator może inaczej rozmieszczać elementy w obrębie struktury i wtedy faktycznie adresy pól będą inne w zależności od wyrównania. Ale tablice, o ile wiem, muszą mieć kolejne elementy w następujących po sobie lokalizacjach.

Praktycznie każdy procesor potrafi odczytywać bajty z dowolnego adresu - trudniej w przypadku słów lub półsłow. Tak jak pisałem - czasem optymalizator "zapomni" wyzerować górne bity, ale to tylko błąd kompilatora.

Czym Twoim zdaniem różni się kopiowanie bajt-po-bajcie od instrukcji Load-Store? O ile wiem ARM nie mają innych instrukcji dostępu do pamięci niż Load-Store... Więc różnica jest tylko taka, że odpowiednie LDR/STR będą wywołane w kodzie memcpy, a nie włączone do programu bezpośrednio.

Jeszcze jedna sprawa - memcpy nie musi kopiować bajt po bajcie. Jej definicja mówi, że kopiuje bloki bajtów, ale optymalizowane implementacje mogą kopiować większe bloki niż jeden bajt na raz. Oczywiście dbając o początkowe i końcowe bajty niezbędne do wyrównania.

Faktycznie, z tablicą się zagalopowałem, przepraszam za wprowadzanie w błąd 😉 Powinienem to zapisać tak, jak jest tutaj poniżej to zapisane.

Natomiast jest różnica pomiędzy

uint32_t z1 = *((uint32_t*)&buffer[1]);

a

memcpy(&z1, &buffer[1], sizeof(z1));

gdy buffer jest tablicą np. typu uint8_t. Jak sam powiedziałeś, memcpy skopiuje dane dbając o wyrównanie (czy implementacja standardowa uwzględnia optymalizację pod postacią kopiowania np. bloków 4 bajtowych?), natomiast rzutowanie i przypisanie tego nie zrobi.

Kod który podałem jest oczywiście anty-przykładem. Tak nie należy robić.

Co do optymalizacji memcpy, to polecam fajny artykuł: http://www.embedded.com/design/configurable-systems/4024961/Optimizing-Memcpy-improves-speed

Natomiast co do implementacji memcpy, to wszystko zależy od wersji. Nie ma czegoś takiego jak standardowa implementacja. Każdy kompilator ma swoją - niektóre optymalizowaną.

Przykładowo:

* stary Apple (optymalizacja dla 16-bitowców) - http://www.opensource.apple.com/source/xnu/xnu-2050.18.24/libsyscall/wrappers/memcpy.c

* Microblaze - https://stuff.mit.edu/afs/sipb/contrib/linux/arch/microblaze/lib/memcpy.c

* Linux - http://lxr.free-electrons.com/source/arch/alpha/lib/memcpy.c

Optymalizacja memcpy to wbrew pozorom ważna sprawa. Kopiowanie "bajt-po-bajcie" potrafi być niesamowicie nieefektywne. Ale to chyba trochę offtop się zrobił 🙂

  • 1 rok później...

Cenne spostrzeżenie z tymi typami zmiennoprzecinkowymi. Co do pułapek języka C czy C++ warto używać narzędzi do sprawdzania walidacji sprawdzania jakości kodu w czasie rzeczywistym. Np. do VS jest dużo wtyczek które pomagają wychwycić tego typu błędy czy złe praktyki.

Generalnie, wyznaję zasadę, że najważniejsza w kodzie jest czytelność i nie należy iść na skróty. Dużo robią wcięcia w kodzie, dla wielu osób jest to, mam nadzieję oczywiste.

  • Lubię! 1

Bądź aktywny - zaloguj się lub utwórz konto!

Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony

Utwórz konto w ~20 sekund!

Zarejestruj nowe konto, to proste!

Zarejestruj się »

Zaloguj się

Posiadasz własne konto? Użyj go!

Zaloguj się »
×
×
  • Utwórz nowe...