do
Historia
Moja żona odziedziczyła kota od rodziny. † Niestety jestem bardzo uczulony na zwierzęta. Kot był już daleko od swojej świetności i powinien był zostać uśmiercony, zanim jeszcze go dostaliśmy, ale nie mogła się zmusić do pozbycia się go ze względu na jego sentymentalną wartość. I wykluły plan, aby zakończyć mój Jego cierpienia.
Jechaliśmy na dłuższy urlop, ale ona nie chciała wejść na pokład kota w gabinecie weterynarza. Martwiła się tym, że zapada na nią choroba lub jest źle traktowana. Stworzyłem automatyczny karmnik dla kotów, abyśmy mogli zostawić go w domu. Napisałem oprogramowanie mikrokontrolera w C. Zawierający plik main
wyglądał podobnie do poniższego kodu.
Jednak moja żona jest również programistą i znała moje uczucia do kota, dlatego nalegała na weryfikację kodu, zanim zgodziła się zostawić go w domu bez opieki. Miała kilka obaw, w tym:
main
nie ma podpisu zgodnego ze standardami (dla hostowanej implementacji)
main
nie zwraca wartości
tempTm
jest używany niezainicjowany, ponieważ malloc
został wywołany zamiastcalloc
- zwracana wartość
malloc
nie powinna być rzutowana
- czas mikrokontrolera może być niedokładny lub przewracany (podobnie jak problemy z czasem Y2K lub Unix 2038)
elapsedTime
zmienna może nie mieć wystarczającego zakresu
Potrzeba było wielu przekonujących, ale w końcu zgodziła się, że tezy nie były problemami z różnych powodów (nie bolało, że spóźniliśmy się już na nasz lot). Ponieważ nie było czasu na testy na żywo, zatwierdziła kod i pojechaliśmy na wakacje. Kiedy wróciliśmy kilka tygodni później, moja nędza mojego kota się skończyła (chociaż w rezultacie mam teraz dużo więcej).
† Całkowicie fikcyjny scenariusz, bez obaw.
Kod
#include <time.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
//#include "feedcat.h"
// contains extern void FeedCat(struct tm *);
// implemented in feedcat.c
// stub included here for demonstration only
#include <stdio.h>
// passed by pointer to avoid putting large structure on stack (which is very limited)
void FeedCat(struct tm *amPm)
{
if(amPm->tm_hour >= 12)
printf("Feeding cat dinner portion\n");
else
printf("Feeding cat breakfast portion\n");
}
// fallback value calculated based on MCU clock rate and average CPI
const uintmax_t FALLBACK_COUNTER_LIMIT = UINTMAX_MAX;
int main (void (*irqVector)(void))
{
// small stack variables
// seconds since last feed
int elapsedTime = 0;
// fallback fail-safe counter
uintmax_t loopIterationsSinceFeed = 0;
// last time cat was fed
time_t lastFeedingTime;
// current time
time_t nowTime;
// large struct on the heap
// stores converted calendar time to help determine how much food to
// dispense (morning vs. evening)
struct tm * tempTm = (struct tm *)malloc(sizeof(struct tm));
// assume the cat hasn't been fed for a long time (in case, for instance,
// the feeder lost power), so make sure it's fed the first time through
lastFeedingTime = (size_t)(-1);
while(1)
{
// increment fallback counter to protect in case of time loss
// or other anomaly
loopIterationsSinceFeed++;
// get current time, write into to nowTime
time(&nowTime);
// calculate time since last feeding
elapsedTime = (int)difftime(nowTime, lastFeedingTime);
// get calendar time, write into tempTm since localtime uses an
// internal static variable
memcpy(&tempTm, localtime(&nowTime), sizeof(struct tm));
// feed the cat if 12 hours have elapsed or if our fallback
// counter reaches the limit
if( elapsedTime >= 12*60*60 ||
loopIterationsSinceFeed >= FALLBACK_COUNTER_LIMIT)
{
// dispense food
FeedCat(tempTm);
// update last feeding time
time(&lastFeedingTime);
// reset fallback counter
loopIterationsSinceFeed = 0;
}
}
}
Niezdefiniowane zachowanie:
Dla tych, którzy nie chcą zawracać sobie głowy znalezieniem samego UB:
W tym kodzie jest zdecydowanie zachowanie lokalne, nieokreślone i zdefiniowane w implementacji, ale wszystko powinno działać poprawnie. Problem tkwi w następujących wierszach kodu:
struct tm * tempTm //...
//...
memcpy(&tempTm, localtime(&nowTime), sizeof(struct tm));
memcpy
zastępuje tempTM
wskaźnik zamiast obiektu, na który wskazuje, niszcząc stos. To zastępuje, oprócz innych rzeczy, elapsedTime
i loopIterationsSinceFeed
. Oto przykładowy bieg, w którym wydrukowałem wartości:
pre-smash : elapsedTime=1394210441 loopIterationsSinceFeed=1
post-smash : elapsedTime=65 loopIterationsSinceFeed=0
Prawdopodobieństwo zabicia kota:
- Biorąc pod uwagę ograniczone środowisko wykonywania i łańcuch kompilacji, zawsze występuje niezdefiniowane zachowanie.
- Podobnie, niezdefiniowane zachowanie zawsze uniemożliwia karmnikowi działanie zgodnie z przeznaczeniem (a raczej pozwala mu „pracować” zgodnie z przeznaczeniem).
- Jeśli karmnik nie działa, jest bardzo prawdopodobne, że kot umrze. To nie jest kot, który może sobie poradzić, a ja nie poprosiłem sąsiada, żeby zajrzał na niego.
Szacuję, że kot umiera z prawdopodobieństwem 0,995 .