Niezawodność Oprogramowania, rdodC, 1


C0x08 graphic

Odpowiedzi

Rozdział 1.

  1. Błąd polega na nieuwzględnieniu reguł pierwszeństwa operatorów — przedmiotowa instrukcja interpretowana jest przez kompilator jako:

while (ch = (getchar() != EOF))

Jest to więc próba przypisania wartości do zmiennej ch, którą to próbę kompilator traktuje jako błędne użycie operatora „=” zamiast „==”.

2a. Najprostszym sposobem uchronienia się przed niespodziewanym użyciem liczb ósemkowych jest wprowadzenie opcjonalnego zakazu używania takich liczb za pomocą odpowiedniej opcji kompilatora.

2b. Kompilator mógłby wychwytywać wszystkie przypadki użycia operatorów & oraz | w instrukcji if oraz złożonych porównaniach w sytuacji, gdy brak jest jawnego porównania z wartością 0. Tak więc instrukcja:

if (u & 1) /* czy u jest nieparzyste? */

musiałaby zostać zmieniona na

if ((u & 1) != 0) /* czy u jest nieparzyste? */

2c. W przypadku poszukiwania niezamierzonych początków komentarza podejrzany jest każdy komentarz rozpoczynający się od znaku alfabetycznego albo nawiasu otwierającego. Jeśli nakażesz kompilatorowi wychwytywać wszystkie takie przypadki, łatwo pozbędziesz się ostrzeżeń z jego strony, wprowadzając co najmniej jedną spację pomiędzy gwiazdką w ograniczniku a wspomnianym alfabetykiem lub nawiasem:

/*Ten komentarz powodować będzie generowanie ostrzeżenia */

/* Ten nie, ze względu na spację rozdzielającą */

/*-----Ten komentarz także nie wzbudzi zastrzeżeń-------*/

Należy ponadto eliminować z kodu wątpliwe przypadki i jasno wyrażać swe intencje zapisem:

quot = numer/ *pdenom

albo

quot = numer/(*pdenom)

zamiast

quot = numer/*pdenom

2d. Kompilator mógłby na przykład kwestionować wszelkie przypadki łączenia operatorów bitowych z arytmetycznymi, jeżeli brak jest nawiasów wyznaczających explicite kolejność obliczeń. Instrukcja:

word = bHigh << 8 + bLow;

musiałaby wówczas zostać zapisana w jednej z poniższych postaci:

word = (bHigh << 8) + bLow;

word = bHigh << (8 + bLow);

zależnie od faktycznych intencji programisty.

Na podobnej zasadzie można by wykrywać łączenie dwóch operatorów o różniącym się priorytecie, bez użycia dodatkowych nawiasów. Zasada ta jest oczywiście zbyt prymitywna, by stosować ją w praktyce in extenso, jednakże sama idea jest warta uwagi i wzbogacenie jej o jakąś dodatkową heurystykę mogłoby przydać jej znaczenia praktycznego. Heurystyka ta nie powinna być oczywiście zbyt prymitywna — w szczególności nie powinny być kwestionowane konstrukcje takie, jak:

word = bHigh*256 + bLow;

if (ch == ' ' || ch == '\t' || ch == '\n')

  1. Kompilator mógłby generować ostrzeżenie w przypadku napotkania frazy else poprzedzonej dwiema instrukcjami if, jak w poniższych przykładach:

  2. if (wyrażenie1)

    if (wyrażenie2)

    .
    .
    .
    else

    .
    .
    .

    if (wyrażenie1)

    if (wyrażenie2)

    .
    .
    .
    else

    .
    .
    .

    Aby uniknąć niepotrzebnych ostrzeżeń, należałoby ujmować w nawiasy klamrowe każdą uwarunkowaną instrukcję:

    if (wyrażenie1)

    {

    if (wyrażenie2)

    .
    .
    .
    }

    else

    .
    .
    .

    if (wyrażenie1)

    {

    if (wyrażenie2)

    .
    .
    .
    else

    .
    .
    .

    }

    1. Pożyteczna skądinąd zasada umieszczania stałych i wyrażeń po lewej stronie operatora porównania staje się bezużyteczna, jeżeli obydwa porównywane operandy są zmiennymi. Gdyby, za pomocą stosownej opcji, zmusić kompilator do kwestionowania każdej instrukcji przypisania mogącej stanowić wynik błędnego użycia operatora „=” zamiast „==”, po prostu otrzymywalibyśmy zazwyczaj zbyt wiele ostrzeżeń.

    2. Najprostszym sposobem wykrywania niezdefiniowanych makr jest wyposażenie kompilatora w przełącznik powodujący, iż preprocesor będzie kwestionował użycie takich makr. Domyślne utożsamianie niezdefiniowanych makr z zerem nie jest specjalnie użyteczne w kompilatorach honorujących zarówno starą dyrektywę #ifdef, jak i nowy operator jednoargumentowy defined w wyrażeniach #if. Przykładową sekwencję:

    /* ustalenie platformy wynikowej */

    #if INTEL8080

    #elif INTEL80x86

    #elif MC680x0

    #endif

    należałoby wówczas zmienić do następującej postaci:

    /* ustalenie platformy wynikowej */

    #if defined(INTEL8080)

    #elif defined(INTEL80x86)

    #elif defined(MC680x0)

    #endif

    Jednocześnie wspomniany przełącznik nie powinien powodować generowania ostrzeżenia w przypadku napotkania niezdefiniowanego makra w instrukcji #ifdef.

    Rozdział 2.

    1. Jednym z rozwiązań jest makro ASSERTMSG posiadające dwa parametry: testowane wyrażenie oraz komunikat wyświetlany w sytuacji, gdy wyrażenie to ma wartość FALSE. W przypadku wykrycia, iż kopiowane bloki nakładają się, wywołanie makra mogłoby być następujące:

    ASSERTMSG(pbTo >= pbFrom+size || pbFrom >= pbTo+size,

    "memcpy: bloki nakładają się");

    Prezentowana na następnej stronie implementacja makra ASSERTMSG powinna znaleźć się w dołączonym pliku nagłówkowym:

    #ifdef DEBUG

    void _AssertMsg(char *strMessage); /* prototyp */

    #define ASSERTMSG(f,str) \

    if (f) \

    {} \

    else \

    _AssertMsg(str)

    #else

    #define ASSERTMSG(f,str)

    #endif

    Wykorzystywana funkcja _AssertMsg ma następującą definicję — powinna ona znaleźć się w konwencjonalnym pliku źródłowym:

    #ifdef DEBUG

    void _AssertMsg(char *strMessage)

    {

    fflush(NULL);

    fprintf(stderr, "\n\nNiespełniona asercja - %s\n",

    strMessage );

    fflush(stderr);

    abort()

    }

    #endif

    1. Niektóre kompilatory (być może opcjonalnie) optymalizują kod w taki sposób, iż określony łańcuch, używany w wielu miejscach kodu, tworzony jest tylko w jednym egzemplarzu — wszystkie odwołania do niego współdzielą tę samą kopię. Wówczas niezależnie od liczby asercji używających nazwy pliku, nazwa ta zapamiętana zostanie w kodzie tylko jednokrotnie. Problem w tym, że taka „oszczędnościowa” polityka dotyczyć będzie wszystkich łańcuchów używanych w programie, czego programista mógłby sobie z pewnych względów nie życzyć.

    Alternatywne rozwiązanie polega na takim zaimplementowaniu makra ASSERT, by wszystkie jego wywołania w danym pliku posługiwały się w zamierzony sposób tą samą kopią łańcucha zawierającego nazwę pliku. Należy w związku z tym zdefiniować jeszcze jedno makro — ASSERTFILE — i wywoływać je jednokrotnie na początku każdego pliku źródłowego:

    #include <stdio.h>

    .

    .

    .

    #include <debug.h>

    ASSERTFILE(__FILE__)

    .

    .

    .

    void *memcpy(void *pvTo, void *pvFrom, size_t size)

    {

    byte *pbTo = (byte *)pvTo;

    byte *pbFrom = (byte *)pvFrom;

    ASSERT(pvTo != NULL && pvFrom != NULL);

    .

    .

    .

    Jak widać, sposób wywołania makra ASSERT nie zmienił się, zmieniła się natomiast jego definicja:

    #ifdef DEBUG

    #define ASSERTFILE(str) \

    static char strAssertFile[] = str;

    #define ASSERT(f) \

    if (f) \

    {} \

    else \

    _Assert(strAssertFile, __LINE__)

    #else

    #define ASSERTFILE(str)

    #define ASSERT(f)

    #endif

    Gdy używa się makra ASSERT w tej postaci, można niekiedy zaoszczędzić sporo pamięci — podczas testowania przykładowego kodu niniejszej książki udało mi się zaoszczędzić 3 kB danych.

    Problem z użyciem asercji w prezentowanym przykładzie polega na tym, iż testowane wyrażenie musi być wartościowane także w handlowej wersji programu. Obecna wersja, przy niezdefiniowanym symbolu DEBUG wpadnie w długotrwałą pętlę kończącą się w sposób losowy, gdy bajt wskazywany przez pch zawierać będzie znak nowej linii. By uniknąć tej przykrej pułapki, należałoby przepisać funkcję getline w sposób następujący:

    void getline(char *pch)

    {

    int ch; /* ch musi być zadeklarowane jako int */

    do

    {

    ch = getchar();

    ASSERT(ch != EOF);

    }

    while ((*pch++ = ch) != '\n');

    }

    Jednym ze sposobów jest umieszczenie „fałszywej” asercji w domyślnej części instrukcji switch, do której sterowanie nie ma prawa trafić — o ile oczywiście każdy możliwy wariant selektora obsługiwany jest w sposób jawny:

    .

    .

    .

    default:

    ASSERT(FALSE); /* bezwarunkowe załamanie */

    break;

    }

    Wzorzec dla każdej pozycji tablicy powinien zawierać się w pewnej stowarzyszonej z nią masce; jeżeli przykładowo maska ma postać 0×FF00, w młodszym bajcie wzorca wszystkie bity muszą być zerowe, w przeciwnym razie dopasowanie wzorca dla dowolnej instrukcji nie będzie możliwe. W związku z tym funkcja CheckIdInst musi zostać uzupełniona o stosowną weryfikację:

    void CheckIdInst(void)

    {

    indenfity *pid, *pidEarlier;

    instruction inst;

    for (pid = &idInst[0]; pid->mask != 0; pid++)

    {

    /* upewnij się, że wzorzec jest zgodny z maską */

    ASSERT((pid->pat & pid->mask) == pid->pat);

    .

    .

    .

    Należy zweryfikować, za pomocą odpowiednich asercji, poprawność ustawień związanych z instrukcją reprezentowaną przez inst:

    instruction *pcDecodeEOR(instruction inst, instruction *pc,

    opcode *popc)

    {

    /* czy przypadkowo nie jest to instrukcja CMPM lub CMPA.L ? */

    ASSERT(eamode(inst) != 1 && mode(inst) != 3);

    /* jeśli tryb nierejestrowy, zezwolenie tylko na tryb

    * abs word i abs long

    */

    ASSERT(eamode(inst) != 7 ||

    eareg(inst) == 0 || eareg(inst) == 1));

    .

    .

    .

    Generalnie sposobem sprawdzenia poprawności jednego algorytmu jest użycie innego algorytmu — do danych oryginalnych i(lub) do danych wyprodukowanych przez pierwszy algorytm. I tak poprawność sortowania szybkiego można zweryfikować, przez kontrolę uporządkowania danych wynikowych — sposób owej „kontroli” zdecydowanie różni się od sortowania, mamy więc istotnie do czynienia z zupełnie innym algorytmem. Sortowanie binarne można zweryfikować za pomocą prostego szukania liniowego — należy sprawdzić, czy obydwa algorytmy dają identyczny wynik. W przypadku funkcji itoa dokonującej konwersji liczby całkowitej na jej reprezentację znakową, należy otrzymany łańcuch znaków przekształcić z powrotem na postać binarną i porównać wynik z wartością oryginalną.

    Rozdział 3.

    1. Przez zastosowanie dwóch różnych wzorców wypełniania — innego dla pamięci nowo przydzielonej i innego dla pamięci właśnie zwalnianej:

    #define bNewGarbage 0xA3

    #define bFreeGarbage 0xA5

    Funkcja fResizeMemory może dokonywać zarówno przydziału, jak i zwalniania pamięci, stosowane więc będą obydwa powyższe wzorce — chyba, żeby zdefiniować na tę okazję jeszcze dwa inne.

    1. Jednym ze sposobów jest periodyczne testowanie bajtów następujących bezpośrednio za przydzielonym blokiem w celu upewnienia się, iż ich zawartość nie uległa zmianie. Należy w tym celu zwiększyć rozmiar każdego przydzielanego bloku o wielkość owego obszaru testowego i ustalić wypełniający go wzorzec.

    I tak na przykład przy ustaleniu wielkości obszaru testowego na 1 bajt i zapotrzebowaniu na blok o rozmiarze 36 bajtów, należy w rzeczywistości przydzielić 37 bajtów i ostatni bajt wypełnić żądanym wzorcem; podobnie należy postąpić w przypadku realokacji bloku. Doskonałą okazją do wykonywania wspomnianego testu na niezmienność dodatkowego obszaru są wywołania funkcji sizeofBlock, fValid Pointer, FreeBlockInfo, NoteMemoryRef i CheckMemoryRefs — fakt przekroczenia legalnych granic pamięci można by wykrywać za pomocą stosownych asercji.

    Oto jeden z przykładów implementacji przedstawionej idei. Wartość sizeofDebugByte określa wielkość obszaru testowego (tu: jeden bajt), natomiast wartość bDebugByte — wypełniający go wzorzec. Przede­finiowaniu podlegają funkcje fNewMemory i fResizeMemory:

    #define bDebugByte 0xE1

    /* obszar testowy istnieje tylko w wersji testowej programu */

    #ifdef debug

    #define sizeofDebugByte 1

    #else

    #define sizeofDebugByte 0

    #endif

    .

    .

    .

    flag fNewMemory(void **ppv, size_t size)

    {

    byte **ppb = (byte **)ppv;

    ASSERT(ppv != NULL && size != 0);

    *ppb = (byte *)malloc(size + sizeofDebugByte);

    #ifdef DEBUG

    {

    if (*ppb != NULL)

    {

    *(*ppb + size) = bDebugByte;

    memset(*ppb, bGarbage, size);

    .

    .

    .

    flag fResizeMemory(void **ppv, size_t sizeNew)

    {

    byte **ppb = (byte **)ppv;

    byte *pbNew;

    .

    .

    .

    pbNew = (byte *)realloc(*ppb, sizeNew = sizeofDebugByte);

    if (pbNew != NULL)

    {

    #ifdef DEBUG

    {

    *(pbNew + sizeNew) = bDebugByte;

    UpdateBlockInfo(*ppb, pbNew, sizeNew);

    .

    .

    .

    Wspomniana asercja testująca niezmienność obszaru testowego mogłaby mieć następującą postać:

    ASSERT(*(pbi->pb + pbi->size) == dDebugByte);

    Pełna implementacja funkcji sizeofBlock, fValidPointer, FreeBlockInfo, NoteMemoryRef i CheckMemoryRefs znajduje się w dodatku B.

    1. Jednym ze sposobów zapobiegania błędom tego typu jest uruchomienie dodatkowego mechanizmu gospodarowania pamięcią. Polega on na niezwalnianiu wykorzystanych bloków, lecz składowaniu ich w puli bloków zwolnionych. Bloki te pozostają przydzielone z punktu widzenia systemu operacyjnego, lecz nie są wykorzystywane przez aplikację, która w razie zapotrzebowania na pamięć pobiera ją „normalnie” od systemu. Pula bloków zwolnionych utrzymywana jest tak długo, aż możliwe stanie się zweryfikowanie poprawności całej gospodarki pamięcią za pomocą wywołania funkcji CheckMemoryRefs. Po przeprowadzeniu weryfikacji funkcja CheckMemoryRefs zwraca systemowi wszystkie bloki znajdujące się w puli bloków zwolnionych.

    Opisany scenariusz wymaga modyfikacji funkcji CheckMemoryRefs i FreeMemory i powinien być stosowany jedynie wtedy, gdy faktycznie w aplikacji występują tego rodzaju problemy z niewyzerowanymi wskaźnikami. Opisane rozwiązanie łamie bowiem zasadę, zgodnie z którą wersja testowa programu powinna różnić się od wersji handlowej jedynie obecnością dodatkowego kodu, nie zaś modyfikacjami w wykonywanym kodzie.

    1. Należy rozróżnić dwa rodzaje wskaźników: wskaźniki do całych bloków i wskaźniki do subalokacji wewnątrz bloku. Dla wskaźników wskazujących na kompletne bloki najbardziej wiarygodny test, jaki można przeprowadzić, to sprawdzenia, czy istotnie wskaźnik wskazuje na początek bloku i czy jego rozmiar zgodny jest z wartością zwracaną przez sizeofBlock. Dla subalokacji możliwe jest jedynie sprawdzenie, czy wskazywany obszar znajduje się wewnątrz jakiegoś bloku i czy jego rozmiar nie wykracza poza górną granicę tegoż bloku.

    Tak więc zamiast pojedynczej funkcji NoteMemoryRef rejestrującej odwołania do obszarów pamięci w ogólności, należałoby użyć dwóch funkcji, przeznaczonych (odpowiednio) dla całych bloków i dla subalokacji:

    /* ----------------------------------------------------------

    * NoteMemoryRef(pv, size)

    *

    * NoteMemoryRef rejestruje odwołanie do obszaru pamięci

    * o rozmiarze size identyfikowanego przez wskaźnik pv.

    * pv nie musi wskazywać na początek bloku, może wskazywać

    * na obszar stanowiący subalokację wewnątrz bloku.

    *

    * Dla zarejestrowania odwołania do całego bloku należy

    * wykorzystać funkcję NoteMemoryBlock

    */

    void NoteMemoryRef(void *pv, size_t size);

    .

    .

    .

    /* ----------------------------------------------------------

    * NoteMemoryBlock(pv, size)

    *

    * NoteMemoryBlock rejestruje odwołanie do kompletnego bloku

    * pamięci identyfikowanego przez wskaźnik pv i posiadającego

    * długość size.

    * pv musi wskazywać na początek bloku

    *

    */

    void NoteMemoryRef(void *pv, size_t size)

    .

    .

    .

    1. Należy zastąpić flagę fReferenced w strukturze blockinfo licznikiem nReferenced i zamiast rejestrować odwołania, zliczać je. Z weryfikacją integralności odwołań, dokonywaną przez funkcję CheckMemoryRefs, sprawa jest nieco trudniejsza, należy bowiem odróżnić legalne wielokrotne odwołania od odwołań niedozwolonych.

    Jedno z możliwych rozwiązań polega na wprowadzeniu do struktury blockinfo znacznika (tag) określającego bliżej charakter bloku. Procedura CheckMemoryRefs dokonywałaby konfrontacji tego znacznika z wartością licznika odwołań i rozstrzygała o jej legalności. Poniżej prezentujemy zarys implementacji tej idei; w dodatku B znajdują się szczegółowe komentarze wyjaśniające znaczenie poszczególnych parametrów odnośnych funkcji.

    /* blocktag jest typem uwzględniającym wszelkie możliwe typy bloków

    * w kontekście dozwolonych wartości licznika odwołań.

    * Funkcja ClearMemoryRefs ustawia tę wartość na tagNone dla każdego

    * bloku; funkcja NoteMemoryRef ustawia ten wskaźnik zgodnie

    * z drugim parametrem wywołania

    */

    typedef enum

    {

    tagNone,

    tagSymName,

    tagSymList,

    tagListNode, /* węzeł listy musi mieć licznik odwołań

    * ustawiony dokładnie na 2

    */

    .

    .

    .

    } blocktag;

    void ClearMemoryRefs(void)

    {

    blockinfo *pbi;

    for (pbi = pbiHead; pbi != NULL; pbi = pbi->pbiNext);

    pbi->nReferenced = 0

    pbi->tag = tagNone

    }

    void NoteMemoryRef(void *pv)

    {

    blockinfo *pbi;

    pbi= pbiGetBlockInfo((byte *)pv);

    pbi->nReferenced++;

    /* nie można zmieniać typu bloku */

    ASSERT(pbi->tag == tagNone || pbi->tag == tag);

    pbi->tag = tag

    }

    void CheckMemoryRefs(void)

    {

    blockinfo *pbi;

    for (pbi = pbiHead; pbi != NULL; pbi = pbi->pbiNext)

    {

    /* prosty test na poprawność wskazywanego bloku */

    ASSERT(pbi->pb != NULL && pbi->size != 0);

    /* test na wyciek pamięci. Jeśli licznik odwołań do bloku

    * jest równy zero, aplikacja prawdopodobnie utraciła

    * łączność z nim. Kryterium legalności wartości

    * wskazywanej przez licznik odwołań jest specyficzne

    * dla konkretnego typu bloku

    */

    switch (pbi->tag)

    {

    default:

    /* dla większości bloków legalne jest tylko

    * pojedyncze odwołanie

    */

    ASSERT(pbi->nReferenced == 1);

    break;

    case tagListNode:

    ASSERT(pbi->nReferenced == 2);

    break;

    .

    .

    .

    }

    }

    }

    1. W środowisku MS-DOS, Windows i na Macintoshu programiści symulują sytuację niedostatku pamięci przez uruchomienie, równolegle z testowaną aplikacją, jakiegoś programu, którego głównym zadaniem jest „pożeranie” pamięci. Technika ta nie jest jednak specjalnie użyteczna, jeżeli testowaniu podlega wybrana cecha aplikacji. Lepszym rozwiązaniem jest wówczas wbudowanie symulacji niedostatku pamięci bezpośrednio w menedżer pamięci.

    Oprócz błędów pamięci aplikacja narażona jest jednak na wiele innych błędów — błędy dyskowe, brak papieru w drukarce, zajęta linia telefoniczna, itp. Tak naprawdę potrzebne więc jest coś na kształt „uogólnionego” symulatora błędów. Symulator taki mógłby być reprezentowany przez strukturę failureinfo określającą szczegóły pozorowanego błędu; samo symulowanie błędnej sytuacji powierzone zostałoby specjalnej funkcji fFakeFailure. I tak na przykład funkcje fNewMemory i fResizeMemory wyposażone w taką możliwość prezentowałyby się następująco:

    flag fNewMemory(void **ppv, size_t size)

    {

    byte **ppb = (byte **)ppv;

    #ifdef DEBUG

    if fFakeFailure(&fiMemory))

    {

    *ppb = NULL;

    return (FALSE);

    }

    #endif

    .

    .

    .

    }

    flag fResizeMemory(void **ppv, size_t sizeNew)

    {

    byte **ppb = (byte **)ppv;

    byte *pbNew;

    #ifdef DEBUG

    if fFakeFailure(&fiMemory))

    {

    return (FALSE);

    }

    #endif

    .

    .

    .

    }

    fiMemory jest tutaj globalną strukturą typu failureinfo, za pośrednictwem której tester prowadzący testowanie jednostek (unit testing) określa (mówiąc skrótowo) scenariusz symulowania błędów. Najprostszy z takich scenariuszy mógłby zakładać pewną liczbę poprawnych wywołań, po których następowałaby pewna liczba wywołań kończących się (symulowanym błędem); funkcja ustalająca parametry scenariusza mogłaby być wywołana na przykład w taki sposób:

    SetFailures(&fiMemory, 5, 7);

    co oczywiście oznaczałoby, iż w pięciu najbliższych wywołaniach funkcja fFakeFailure zwróci wartość FALSE (czyli testowany kod pozostawiony zostanie własnemu losowi), natomiast siedem następnych zakończy się zwróceniem wartości TRUE. Wywołanie:

    SetFailures(&fiMemory, 0, UINT_MAX);

    oznacza przy tych założeniach permanentne symulowanie błędów, zaś wywołanie:

    SetFailures(&fiMemory, UINT_MAX, 0);

    powoduje całkowite pozostawienie testowanego kodu własnemu losowi.

    Mechanizm symulowania błędów mógłby też być całkowicie dezaktywowany na pewien czas, kiedy testowany program wykonuje fragmenty kodu nie sprawiające żadnych kłopotów; do selektywnego aktywowania i dezaktywowania służyłyby funkcje DisableFailures i EnableFailures:

    DisableFailures(&fiMemory);

    ... /* bezproblemowy fragment kodu */

    EnableFailures(&fiMemory);

    Poniżej przedstawiamy przykładową implementację czterech wspomnianych funkcji:

    typedef struct

    {

    unsigned nSucceed; /* liczba bezbłędnych wywołań */

    unsigned nFail; /* liczba wywołań z symulowanym błędem */

    unsigned nTries; /* liczba dotychczasowych wywołań */

    int lock; /* gdy dodatnie, symulator jest wyłączony */

    } failureinfo;

    void SetFailures(failureinfo *pfi, unsigned nSucceed,

    unsigned nFail);

    {

    /* jeśli nFail == 0, wymagane jest nSucceed = UINT_MAX */

    ASSERT(nFail != 0 || nSucceed == UINT_MAX);

    pfi->nSucceed = nSucceed;

    pfi->nFail = nFail;

    pfi->nTries = 0;

    pfi->lock = 0;

    }

    void EnableFailures(failureinfo *pfi)

    {

    ASSERT(pfi->lock > 0);

    pfi->lock--;

    }

    void DisableFailures(failureinfo *pfi)

    {

    ASSERT(pfi->lock >= 0 && pfi->lock < INT_MAX);

    pfi->lock++;

    }

    flag fFakeFailure(failureinfo *pfi)

    {

    ASSERT(pfi != NULL);

    if (pfi->lock > 0)

    return(FALSE);

    /* uaktualnij licznik wywołań */

    if (pfi->nTries != UINT_MAX)

    pfi->nTries++;

    if (pfi->nTries <= pfi->nSucceed)

    return (FALSE);

    if (pfi->nTries - pfi->nSucceed <= pfi->nFail)

    return (TRUE);

    return(FALSE);

    }

    Rozdział 5.

    1. Funkcja strdup cierpi na tę samą przypadłość, co funkcja malloc — błąd sygnalizowany jest przez zwrócenie wartości NULL zamiast poprawnego wyniku. Wartość ta może być omyłkowo potraktowana na równi z użytecznym wynikiem, jeżeli użytkownik nie uwzględni jej wystąpienia. Lekarstwem na tę przypadłość jest oddzielenie informacji o błędzie od adresu nowo utworzonego łańcucha:

    char strDup /* wskaźnik do kopiowanego łańcucha */

    if (fStrDup(&strDup, str ToCopy))

    // udało się, strDup wskazuje na nowy łańcuch

    else

    // nie udało się, strDup ma wartość NULL

    1. Funkcja zwracająca znak z wejścia standardowego będzie jeszcze bardziej intuicyjna od funkcji fGetChar, jeżeli zamiast „dualnej” wartości TRUE - FALSE zwracać będzie numeryczny kod błędu. Oto przykład:

    /* kody przykładowych błędów, które sygnalizować może funkcja errGetChar */

    typedef enum

    {

    errNone = 0,

    errEOF,

    errBadRead,

    .

    .

    .

    } error;

    void ReadSomeStuff(void)

    {

    char ch;

    error err;

    if ((err = errGetChar(&ch)) == errNone)

    // udało się, ch zawiera kolejny znak z wejścia

    else

    // nie udało się, err zawiera kod błędu

    .

    .

    .

    W ten oto sposób zamiast kategorycznej informacji „dobrze ­-­ źle”, otrzymu­jemy szczegółową informację o statusie operacji. Jeżeli mimo wszystko tak szczegółowy poziom informacji nie jest w danej sytuacji potrzebny, można zadowolić się jedynie testem na okoliczność zgodności kodu tego statusu z wartością errNone:

    if (errGetChar(&ch) == errNone)

    // udało się, ch zawiera kolejny znak z wejścia

    else

    // nie udało się, brak dokładniejszych informacji o błędzie

    .

    .

    .

    1. Problem z wykorzystaniem funkcji strncpy sprowadza się do niespójności jej zachowania. Zwracany przez nią łańcuch niekiedy zakończony jest zerowym ogranicznikiem, niekiedy nie. Ponieważ funkcja strncpy wymieniana jest często w towarzystwie funkcji ogólnego przeznaczenia (general purpose), niektórzy użytkownicy również mogą uważać ją za funkcję tej kategorii. Jest to opinia błędna — jedynym powodem, dla którego funkcja ta w ogóle brana jest pod uwagę, jest jej powszechne używanie stanowiące spadek po implementacjach C poprzedzających normę ANSI.

    2. Funkcje wstawialne, będące równie efektywnymi w użyciu jak makra (pod warunkiem używania dobrego kompilatora), wolne są jednocześnie od efektów ubocznych związanych z wartościowaniem parametrów wywołania.

    3. Niebezpieczeństwem związanym z parametrem przekazywanym przez referencję jest fakt, iż programista wywołujący funkcję posiadającą taki parametr może nie zdawać sobie sprawy z możliwości zmiany tego parametru przez ciało funkcji. W poniższej konstrukcji:

    if (fResizeMemory(pb, sizeNew))

    // udało się, pb wskazuje na nowy blok

    przez referencję przekazywany jest parametr pb. Programista przyzwyczajony do tego, iż w „tradycyjnym” C wszystkie parametry przekazywane były przez wartość, może nawet nie domyślać się, iż wartość wskaźnika pb mogła ulec zmianie.

    Wprowadzenie operatora referencji parametru wymaga również większej dyscypliny od programistów tworzących funkcje. W „tradycyjnym” C wszystkie parametry przekazywane były przez wartość, więc programiści, wolni od podobnych obaw, mogli w nieskrępowany sposób wykorzystywać parametry funkcji w charakterze zmiennych roboczych; w przypadku przekazania parametru przez referencję wszelkie jego zmiany odzwierciedlają się w parametrze aktualnym.

    1. Z punktu widzenia użytkownika wywołującego funkcję strcmp konwencja zwracania wyniku porównania jest mało czytelna. Przede wszystkim nie można określić konkretnej wartości zwracanej przez funkcję w przypadku nierówności łańcuchów; usunięcie tej niedogodności pozwoliłoby zdefiniować synonimy STR_LESS, STR_GREATER i STR_ EQUAL oznaczające (kolejno) pierwszeństwo (alfabetyczne) pierwszego łańcucha, pierwszeństwo drugiego łańcucha i równość łańcuchów.

    W sytuacji, gdy nie badamy relacji pomiędzy łańcuchami, lecz testujemy konkretną relację (np. równość), użyteczne może okazać się zaimplementowanie trzech odrębnych funkcji porównujących fStrLess, fStrGreater i fStrEqual:

    if (fStrLess(strLeft, strRight))

    if (fStrGreater(strLeft, strRight))

    if (fStrEqual(strLeft, strRight))

    Rozwiązanie to ma tę zaletę, iż wspomniane funkcje mogą być zdefinio­wane jako makra stanowiące otoczki funkcji strcmp:

    #define fStrLess(strLeft, strRight) \

    (strcmp(strLeft, strRight) < 0)

    #define fStrGreater(strLeft, strRight) \

    (strcmp(strLeft, strRight) > 0)

    #define fStrEqual(strLeft, strRight) \

    (strcmp(strLeft, strRight) = 0)

    Ideę tę można rozwinąć przez zdefiniowanie funkcji fStrLessOrEqual, fStrGreaterOrEqual i fStrNotEqual odpowiadających operatorom <=, >= i !=. Zyskujemy w ten sposób na czytelności bez jakiejkolwiek straty rozmiaru i efektywności kodu.

    Rozdział 6.

    1. Przenośny zakres jednobitowego pola to 0 - 0, co nie jest specjalnie użyteczne. Kłopot bowiem w tym, iż pojedynczy bit „1” może być — zależnie od implementacji — traktowany jako „liczba ze znakiem” i wówczas ma on wartość -1, bądź też jako „liczba bez znaku” i wówczas traktowany jest jako 1. Jeżeli więc chcemy potraktować tę sytuację „w sposób przenośny”, to można to uczynić jedynie w kategoriach „zerowy — niezerowy” i ograniczyć wszelkie porównania wyłącznie do porównań z wartością 0. Poniższe konstrukcje są więc konstrukcjami przenośnymi:

    if (psw.carry == 0)

    if (psw.carry != 0)

    if (!psw.carry)

    if (psw.carry)

    Rezultat poniższych porównań jest natomiast zależny od konkretnej implementacji:

    if (psw.carry == 1)

    if (psw.carry != 1)

    if (psw.carry == -1)

    if (psw.carry != -1)

    1. Funkcje zwracające wartość boolowską podobne są w pewnym sensie do pola jednobitowego — nie można bowiem określić, czym w danej implementacji jest wartość TRUE. Można bezpiecznie przyjąć, iż wewnętrzną reprezentacją wartości FALSE jest zero. Jeśli chodzi o wartość TRUE, to w roli tej może wystąpić każda niezerowa wartość, niekoniecznie równa (pod względem reprezentacji) wartości TRUE w danej implementacji. Podobnie więc jak w przypadku pola bitowego, przenośność funkcji zwracającej wynik boolowski można postrzegać jedynie w kategoriach „FALSE-inny niż FALSE” — poniższe testy są więc konstrukcjami przenośnymi:

    if (fNewMemory(…) == FALSE)

    if (fNewMemory(…) != FALSE)

    if (!fNewMemory(…)) /* zalecane */

    if (fNewMemory(…)) /* zalecane */

    Wynik poniższego testu jest jednak zależny od implementacji:

    if (fNewMemory(…) == TRUE) /* ryzykowne */

    Stąd wniosek, iż należy unikać porównywania wartości boolowskich z wartością TRUE.

    1. Uczynienie korzenia drzewa strukturą globalną ma tę niedogodność, iż nadaje mu specyficzny status, wymagający specjalnego traktowania. Spójrzmy na przykład na poniższą funkcję, zwalniającą poddrzewo o wskazanym korzeniu:

    void FreeWindowTree(window *pwndRoot)

    {

    if (pwndRoot != NULL)

    {

    window *pwnd, *pwndNext;

    ASSERT(fValidWindow(pwndRoot));

    for (pwnd = pwndRoot->pwndChild; pwnd != NULL; pwnd = pwndNext)

    {

    /* przechowaj wskaźnik przed zwolnieniem bloku pamięci */

    pwndNext = pwnd->pwndSibling;

    FreeWindowTree(pwnd);

    }

    if (pwndRoot->strWndTitle != NULL)

    FreeMemory(pwndRoot->strWndTitle);

    FreeMemory(pwndRoot);

    }

    }

    Powyższa funkcja, po zwolnieniu całego poddrzewa, zwalnia w końcu jego korzeń — i tu pojawia się problem: korzeń całej struktury, jako statyczny, nie może zostać zwolniony, nie jest więc możliwe zwol­nienie całej struktury okien za pomocą pojedynczego wywołania

    FreeWindowTree(&wndDisplay)

    (wndDisplay jest nazwą przedmiotowego korzenia). Aby uczynić funkcję FreeWindowTree uniwersalną, należałoby jej ostatnią instrukcję zmienić następująco:

    if (pwndRoot != &wndDisplay)

    FreeMemory(pwndRoot);

    1. Zgodnie z treścią rozdziału 6., jest to postępowanie zdecydowanie napiętnowane z punktu widzenia niezawodnego programowania — oznacza bowiem konieczność uwzględnienia „specjalnych” wartości argumentu.

    Druga wersja kodu jest o tyle ryzykowna, iż zawiera powielone fragmenty kodu: wyrażenie, A i D. W pierwszej wersji fragmenty te wykonywane są niezależnie od wartości f; w drugiej wersji A i D muszą być testowane oddzielnie, co (z wyjątkiem przypadku, gdy są one identyczne) zwiększa prawdopodobieństwo popełnienia błędu.

    Ponadto, mimo iż z punktu widzenia kodu źródłowego fragmenty A i D stanowią powielone ciągi instrukcji, to jednak inaczej może to wyglądać z punktu widzenia kompilatora tłumaczącego i optymalizującego ciągi ABD i ACD jako całość. Identyczne fragmenty A i D są wówczas testowane w różnych warunkach, co w praktyce oznacza podwójną robotę.

    1. Druga wersja stwarza także problemy z utrzymaniem i rozbudową kodu, pod względem znajdowania i usuwania błędów — zmiany w zakresie fragmentów A i D muszą być wprowadzane do kodu źródłowego dwukrotnie. Wynika stąd kolejna wskazówka dotycząca niezawodnego programowania: minimalizuj różnice przez maksymalizowanie fragmentów wspólnego kodu.

    „Pomylenie” dwóch podobnie brzmiących nazw nie jest zazwyczaj wychwytywane przez kompilator, jeżeli opatrywane tymi nazwami obiekty są tego samego rodzaju. Poniższy fragment:

    int strcmp(const char *s1, const char *s2)

    {

    for ( ; *s1 == *s2; s1++, s2++)

    {

    if (*s1 == '\0') /* koniec? */

    retunr (0);

    }

    return ((*(unsigned char *)s2 < *(unsigned char *)s1) ? -1 : 1);

    }

    jest błędny, ponieważ w ostatniej instrukcji return nazwy s1 i s2 zamienione są miejscami, jednak ze względu na ich podobieństwo trudno jest ten fakt zauważyć. Ich zamiana na jakieś bardziej znaczące odpowiedniki — na przykład strLeft i strRight — uczyniłaby kod bardziej czytelnym i zmniejszyła prawdopodobieństwo opisanej pomyłki.

    1. Standard ANSI gwarantuje adresowalność bajtu następującego bezpośrednio po nazwanej tablicy, nie daje jednak analogicznej gwarancji dotyczącej bajtu poprzedzającego tablicę. Nie można więc zapewnić adresowalności bajtu poprzedzającego blok pamięci przydzielony przez funkcję malloc.

    I tak na przykład procesory serii 80×86 adresują pamięć dwuczłonowo, w postaci segment:przesunięcie, gdzie przesunięcie jest dwu- lub czterobajtową liczbą całkowitą bez znaku. Inkrementacja i dekrementacja adresu sprowadza się do inkrementacji (dekrementacji) przesunięcia — jeżeli więc funkcja malloc przydzieli blok na granicy segmentu (czyli z przesunięciem równym 0), to adres „poprzedzającego” ten blok bajtu będzie miał postać segment:0×FFFF (względnie segment: 0×FFFFFFFF). „Dekrementacja” wskaźnika (pch) wskazującego na początek bloku (pchStart) nie będzie więc w istocie dekrementacją, lecz gigantyczną inkrementacją i pch nigdy nie będzie mniejsze od pchStart. Błąd ten jest o tyle zdradliwy, iż nie wystąpi w przypadku, gdy przydzielony blok ma niezerowe przesunięcie.

    7a. printf(str) i printf("%s",str) dają różne wyniki w sytuacji, gdy str zawiera znaki %, zostają one bowiem potraktowane jako specyfikatory formatu.

    7b. Użycie f = 1-f zamiast f = !f zakłada milcząco, iż f może przyjmować wyłącznie wartości 0 albo 1 — tylko wówczas nastąpi „przełączenie” boolowskiej wartości f. Instrukcja f = !f zapewnia przełączenie przy dowolnej wartości.

    7c. Podstawowe ryzyko wynikające z wielokrotnych przypisań wiąże się z nieoczekiwanymi konwersjami danych. W poniższej instrukcji:

    ch = *str++ = getchar();

    nawet jeżeli programista przezornie zadeklaruje ch jako int, to i tak wynik zwracany przez getchar przypisany będzie najpierw łańcuchowi, a to oznacza konwersję tego wyniku na typ char. Jeżeli więc funkcja getchar zwróci wynik EOF, nie zostanie on przypisany poprawnie zmiennej ch. Notabene to jeszcze jeden argument przeciwko funkcjom łączącym użyteczną informację z informacjami diagnostycznymi.

    1. Generalnie tablice przeglądowe przyczyniają się do zmniejszenia objętości kodu i poprawy jego efektywności, wprowadzają też do kodu pewną tendencję „unifikacyjną” — każda wielkość, niezależnie od swego charakteru, może być traktowana jednolicie jako „element tablicy”. Używanie tablic przeglądowych wymaga jednak dodatkowej pamięci na same tablice, ponadto duże i złożone tablice przeglądowe stanowią doskonałą okazję do popełniania błędów — dotyczy to nie tyle pięcioelementowej tablicy używanej przez funkcję uCycleCheckBox, lecz raczej tablicy sterującej pracą deasemblera z rozdziału 2. Używanie tablic przeglądowych jest więc bezpieczne tylko wówczas, gdy istnieje możliwość weryfikacji zawartych w nich danych.

    1. Jeżeli twórcy konkretnego kompilatora nie zaimplementowali w nim optymalizowania mnożeń i dzieleń za pomocą przesunięć bitowych, to prawdopodobnie zabieg taki byłby niecelowy z punktu widzenia danej platformy sprzętowej. A jeżeli tak, to „ręczne” optymalizowanie kodu w ten sposób niczego w nim nie poprawi, a może wprowadzić doń trudne do wykrycia błędy. Jeżeli podejrzewasz, iż brak automatycznej optymalizacji jest wynikiem kiepskiej jakości kompilatora, należy po prostu zaopatrzyć się w lepszy kompilator.

    2. Aby zagwarantować bezpieczeństwo zawartości pliku, należy dokonać przydziału wspomnianego bufora jeszcze przed otwarciem pliku, a w każdym razie przed wprowadzeniem do niego jakichkolwiek zmian. W przypadku niemożności przydziału bufora, należy otworzyć plik tylko do odczytu albo w ogóle zrezygnować z otwarcia. Jeżeli aplikacja przetwarza kilka ważnych plików, opisany bufor należy przydzielić na początku realizacji programu, w rozmiarze wystarczającym na zaspokojenie wszystkich potrzeb związanych z buforowaniem zmian. I nie ma co ubolewać nad ewentualnym bezproduktywnym zajęciem pamięci z tego tytułu, gdyż jest ono niczym wobec groźby utraty cennych danych.

    Rozdział 7.

    1. Poniższa funkcja modyfikuje wartość obydwu swych argumentów, stanowiących wskaźniki do informacji wejściowej:

    char *strcpy(char *pchTo, char *pchFrom)

    {

    char *pchStart = pchTo;

    while (*pchto++ = *pchFrom++)

    {}

    retunr (pchStart);

    }

    Modyfikacja ma jednak miejsce tylko z punktu widzenia samej funkcji — parametry pchTo i pchFrom przekazywane są przez wartość i tak naprawdę modyfikowane są ich lokalne kopie tworzone ad hoc przez funkcję. Z punktu widzenia funkcji wywołującej żadna modyfikacja więc nie następuje. Niektóre języki programowania — na przykład FORTRAN — nie stosują przekazywania parametrów przez wartość, więc zmiana tych parametrów w ciele wywoływanej funkcji widoczna jest także dla funkcji wywołujących.

    1. Podstawowy problem wynika tu z pewnego zabiegu optymalizacyjnego stosowanego przez kompilatory, nazywanego składaniem stałych (ang. constant folding). Polega on na tym, iż wszystkie odwołania do określonej stałej w całym kompilowanym kodzie odnoszą się do jej pojedynczej kopii. Jeżeli więc kilka funkcji deklaruje w swym wnętrzu wskaźnik do łańcucha "?????", każdy z tych wskaźników wskazywać będzie to samo miejsce w pamięci i modyfikacja wskazywanego łańcucha przez którąkolwiek z tych funkcji stanie się nieoczekiwanie widoczna dla funkcji pozostałych.

    Niektóre kompilatory posuwają się w optymalizacji stałych łańcuchów jeszcze dalej, nie tworząc osobnego łańcucha, jeżeli jest on końcówką łańcucha już istniejącego — i tak na przykład na potrzeby kodu używającego łańcuchów „pascal”, „scal” i „cal” wystarczy utworzyć jeden łańcuch — „pascal”. Nieoczekiwana zmiana łańcuchów wskazywanych przez deklarowane wskaźniki daje wówczas jeszcze bardziej zdumiewające efekty.

    Aby uniknąć opisanego efektu, należy deklarować tablice znaków, nie zaś wskaźniki do łańcuchów:

    char *strFromUns(unsigned u)

    {

    static char strDigits[] = '?????';

    Nawet jednak to nie jest całkowicie bezpieczne, bowiem przy wpisywaniu konkretnej liczby znaków łatwo o pomyłkę — chyba, że używa się do tego celu różnych znaków, na przykład „12345”zamiast „?????”. Należy także uwzględnić fakt, iż zerowy ogranicznik łańcucha może zostać przypadkowo zniszczony. Zamiast więc deklarować konkretną zawartość bufora, należy raczej zadeklarować jego konkretny rozmiar i explicite zapisywać zerowy ogranicznik:

    char *strFromUns(unsigned u);

    {

    static char strDigits[6]; /* 5 cyfr + '\0' */

    .

    .

    .

    pch = &strDigits[5];

    *pch == '\0';

    1. Przedstawiony przykład wykorzystuje fakt sąsiadującego ułożenia w pamięci kolejno deklarowanych zmiennych, a więc cechę konkretnej implementacji i to jest głównym czynnikiem związanego z nim ryzyka. Całą operację należałoby wykonać po prostu tak:

    i = 0;

    j = 0;

    k = 0;

    albo tak:

    i = j = k = 0;

    Założenie, iż lokalne zmienne sąsiadują ze sobą w pamięci operacyjnej jest bardzo ryzykowne ze względu na różnego rodzaju optymalizację wykonywaną przez kompilator — niektóre z tych zmiennych mogą zostać umieszczone nie w pamięci, lecz w rejestrach procesora. Kod, w zamyśle programisty zerujący zmienne lokalne, w rzeczywistości zerować może ważne informacje organizacyjne, na przykład kod powrotu do funkcji wywołującej. Spełnienie wspomnianego założenia staje się wówczas kwestią nie konkretnej implementacji, ale wręcz… konkretnej funkcji!

    Ponadto, jeżeli intencją programisty stosującego przedstawiony trik było polepszenie efektywności kodu, to jego usiłowania okazują się chybione ze względu na to, iż operacje pomocnicze związane z wywołaniem funkcji memset i wartościowaniem jej argumentów są daleko bardziej czasochłonne niż wyzerowanie trzech zmiennych.

    1. To, iż zawartości pamięci ROM nie można zmienić w danym egzemplarzu komputera, nie oznacza, iż będzie ona identyczna w innym (na przykład nowszym) komputerze, na którym przyjdzie nam uruchamiać przedmiotowy program. Ponadto w miarę wykrywania ewentualnych błędów w oprogramowaniu zapisanym w pamięci ROM producenci opracowują zazwyczaj stosowne łaty (ang. patches) ładowane w sposób rezydentny do pamięci RAM i dostępne za pośrednictwem interfejsu systemowego. Odwoływanie się do pamięci ROM w sposób bezpośredni oznacza konsekwentne ignorowanie tych łat.

    2. Jeżeli „oprogramowanie stałe” (np. BIOS) zapisane jest w pamięci EPROM, to jego uaktualnienie może spowodować, iż program pracujący dotąd bezproblemowo odmówi współpracy — i to na tym samym komputerze! W przeciwieństwie bowiem do strategii „łatania” wymazana zostaje z pamięci ta wersja BIOS-u, od której uzależniony jest wspomniany program.

    Założenie o możliwości opuszczenia parametru val jest tego samego rodzaju, co założenie przyjmowane przez funkcję FILL odnośnie określonego sposobu realizacji funkcji CMOVE. Załóżmy mianowicie, iż programista zmienił funkcję DoOperation w sposób następujący:

    void DoOperation(operation op, int val)

    {

    if (op < opPrimaryOps)

    DoPrimaryOps(op, val);

    else if (op < opFloatsOps)

    DoFloatOps(op, val);

    else

    .

    .

    .

    }

    Niezależnie od parametru op parametr val jest teraz zawsze wykorzystywany, lecz niektóre funkcje wywołujące funkcję DoOperation nie są na to przygotowane. Co więc nastąpi, gdy funkcja odwoła się do parametru val, który nie został przekazany w wywołaniu? To oczywiście zależy od konkretnej implementacji, w szczególności może wystąpić błąd ochrony dostępu przy próbie modyfikowania chronionej przed zapisem ramki wywołania na stosie.

    Zamiast więc pomijać nieistotne parametry, należy raczej nadawać im wartość 0 i uwzględniać w wywołaniu. Wartość ta może być weryfikowana za pomocą stosownych asercji, na przykład:

    case opNegAcc:

    ASSERT(val == 0);

    accumulator = - accumulator;

    break;

    1. Asercja ta weryfikuje fakt posiadania przez zmienną f jednej z wartości odpowiadających w danej implementacji stałym TRUE i FALSE. Trudno zrozumieć intencje programisty optymalizującego ją w sposób tak wyśrubowany w kodzie testowym — w dodatku kosztem kompletnego zaciemnienia czytelności — w każdym razie powinna ona być zapisana w najbardziej naturalnej postaci:

    ASSERT(f == TRUE || f == FALSE);

    1. Intencją programisty był wybór jednej z dwóch funkcji wywoływanej z tym samym zestawem parametrów. Ideę tę zrealizujesz bardziej czytelnie, przypisując wybrany adres wskaźnikowi roboczemu i używając tego wskaźnika do ostatecznego wywołania żądanej funkcji:

    void *memmove(void *pvTo, void *pvFrom, size_t size)

    {

    void (*pfnMove)(byte *, byte *, size_t);

    byte *pbTo = (byte *)pvTo;

    byte *pbFrom = (byte *)pvFrom;

    pfnMove = (pbTo > pbFrom) ? tailmove : headmove;

    (*pfnMove)(pbTo, pbFrom, size);

    return (pvTo);

    }

    1. Mówiąc prosto, prezentowany przykład bazuje na konkretnej implementacji funkcji Print, dokładniej — na czynnościach wykonywanych w efekcie skoku 4 bajty wyżej od jej punktu wejścia. Jeżeli już nie da się uniknąć skakania do wnętrza funkcji, to fakt ten powinien być przynajmniej oczywisty dla programisty konserwującego kod — należy wówczas zdefiniować etykietę w miejscu, do którego odbywa się skok:

    move r0,#PRINTER

    call PrintDevice

    .

    .

    .

    PrintDisplay: move r0,#DISPLAY

    PrintDevice: ; w r0 znajduje się

    ; identyfikator urządzenia

    .

    .

    .

    1. Zamiast skoku do wnętrza funkcji mamy teraz do czynienia ze skokiem do wnętrza instrukcji! Jest to uzasadnione jedynie na komputerach z tak małą pamięcią, iż każdy jej bajt jest na wagę złota. Ale z pewnością nie jest to sposób na uniknięcie błędów w tworzonych programach!

    196 Niezawodność oprogramowania

    Odpowiedzi 195

    196 D:\Roboczy\Niezawodność oprogramowania\9 po skladzie 1\rdodC.doc

    D:\Roboczy\Niezawodność oprogramowania\9 po skladzie 1\rdodC.doc 195



    Wyszukiwarka

    Podobne podstrony:
    Niezawodność Oprogramowania, R07, 1
    Niezawodność Oprogramowania, R08, 1
    Niezawodność Oprogramowania, rdodA, 1
    Niezawodność Oprogramowania, R00-2, 1
    Niezawodność Oprogramowania, rdodB, 1
    Niezawodnosc oprogramowania nieopr

    więcej podobnych podstron