R07 (8)


DRAMATURGIA RZEMIOSAA 139
7.
DRAMATURGIA
RZEMIOSAA
Podczas pisania noweli fantastycznej, z pewnością dążyłbyś do tego, by jej treść
była jak najbardziej tajemnicza, niesamowita, by z każdej stronicy wiało grozą, a
włos jeżył się na głowie. Nie mógłbyś napisać ot tak, po prostu  śledzili potwora
przez dwa tygodnie i w końcu go dopadli , bo to banalne i usypiające; czytelnik
raczej powinien czuć przez skórę bicie serca wystraszonego Erroneusa (czy jak mu
tam było...) w miarę, jak zbliżają się do niego jego najwięksi wrogowie  Debu-
ggerzy (czy jakoÅ› tak...).
A czytelnik z zapartym tchem wciąż zadaje sobie pytanie  Uda mu się, czy
nie?
Niespodzianki, suspensy, groza& Faktycznie, to wszystko jest jak najbardziej sto-
sowne w literaturze fantastycznej, lecz programiści powinni o tym zapomnieć, przy-
najmniej w trakcie tworzenia kodu programu. Wbrew pozorom, tak beznamiętny
język jak C (i każdy inny język programowania) również posiada pewne środki
dramaturgiczne (zwane przez profanów po prostu  trikami ), mające rzekomo
świadczyć o doświadczeniu, fantazji, odkrywczości itp. programisty, jednak nie służą
one nijak ostatecznemu celowi, jakim jest stworzenie bezbłędnego programu.  Nud-
ny i monotonny styl wyrazowy programu z pewnością nie znudzi ani nie uśpi bez-
dusznego komputera, może za to zaoszczędzić wielu kłopotów tym, którzy z kom-
puterem tym będą mieć do czynienia.
W niniejszym rozdziale zademonstruję kilka przykładów owej fantazji pro-
gramistycznej. Wszystkie one sÄ… ciekawe, efektowne i nieoczywiste  i wszystkie
zawierają pewne subtelne błędy.
C:\WINDOWS\Pulpit\Szymon\Niezawodność oprogramowania\r07.doc 139
140 NIEZAWODNOŚĆ OPROGRAMOWANIA
SZYBKOŚĆ, SZYBKOŚĆ
Przyjrzyjmy się raz jeszcze funkcji memchr z poprzedniego rozdziału, w jej bez-
błędnej wersji:
void *memchr(void *pv, unsigned char ch, size_t size)
{
unsigned char *pch = (unsigned char *)pv;
while (size-- > 0)
{
if (*pch == ch)
return (pch);
pch++;
}
return (NULL)
}
Każdy  obrót pętli while związany jest z dwoma testami: pierwszy na nieze-
rowość zmiennej size, drugi na równość porównywanych znaków; gdyby dało się
wyeliminować którykolwiek z tych testów, uzyskalibyśmy niemal dwukrotne przy-
spieszenie pętli.
Jedną z najbardziej ulubionych zabaw programistów można by nazwać  Jak to
przyspieszyć? Zabawa taka nie jest wprawdzie niczym nagannym, lecz, jak poka-
zuje to treść poprzedniego rozdziału, potrafi niekiedy wyprowadzić na manowce.
Spróbujmy więc przyspieszyć naszą funkcję memchr. Załóżmy mianowicie, iż w
przeszukiwanym obszarze na pewno znajduje siÄ™ poszukiwany znak i jego znale-
zienie będzie warunkiem zakończenia pętli. Test (size-- > 0) stanie się wówczas
niepotrzebny, a wykonanie pętli istotnie skróci się prawie dwa razy.
Jak jednak zapewnić obecność poszukiwanego znaku w przeszukiwanym ob-
szarze? Należy po prostu umieścić go bezpośrednio za ostatnim bajtem przeszuki-
wanego obszaru i zwiększyć o 1 liczbę przeszukiwanych bajtów. Dziecinnie pro-
ste, nieprawdaż?
void *memchr(void *pv, unsigned char ch, size_t size)
{
unsigned char *pch = (unsigned char *)pv;
unsigned char *pchPlant;
unsigned char chSave;
/* pchPlant wskazuje na bajt następujący bezpośrednio po ostatnim
* bajcie przeszukiwanego obszaru. Pełni on rolę "wartownika"
* gwarantującego, iż poszukiwany znak zawsze zostanie znaleziony.
*/
pchPlant = pch + size;
chSave = *pchPlant; /* zachowaj poprzednią zawartość bajtu
* zajmowanego przez wartownika
*/
140 C:\WINDOWS\Pulpit\Szymon\Niezawodność oprogramowania\r07.doc
DRAMATURGIA RZEMIOSAA 141
*pchPlant = ch; /* umieść wartownika na swoim miejscu */
while (*pch != ch)
pch++;
*pchPlant = pchSave; /* przywróć zawartość zniszczoną przez
* wartownika
*/
return ((pch == pchPlant) ? NULL : pch);
}
Funkcja memchr w swym nowym wcieleniu wyglÄ…da efektownie  nie zapo-
mniano nawet o odtworzeniu zawartości bajtu przeznaczonego chwilowo dla war-
townika. W rzeczywistości jednak ta postać funkcji rodzi więcej wątpliwości, niż
Batman posiada gadżetów. Rozpocznijmy od najważniejszych:
f& jeżeli pchPlant wskazuje na pamięć tylko do odczytu, próba zapisu wartow-
nika, jeżeli nawet nie spowoduje naruszenia ochrony dostępu, na pewno okaże
się nieskuteczna; w rezultacie pętla while może się nie zatrzymać;
f& jeżeli bajt *pchPlant znajduje się w zakresie pamięci związanej ze sprzętem
(np. w pamięci karty graficznej, czy pomocniczej pamięci BIOS-u w obszarze
0×0040:...  przyp. tÅ‚um.), jego zmiana może powodować różne skutki
uboczne, np. zatrzymanie (lub uruchomienie) dyskietki, zniekształcenie wy-
świetlanego obrazu itp.;
f& jeżeli przeszukiwany obszar znajduje się dokładnie na końcu przydzielonej
programowi pamięci, pchPlant wskazywać będzie nieistniejącą (lub: nielegal-
ną) lokalizację; próba zapisania wartownika na pewno okaże się nieskuteczna i
z dużym prawdopodobieństwem spowoduje błąd ochrony dostępu;
f& jeżeli bajt wskazywany przez pchPlant znajduje się w obszarze pamięci
współdzielonym przez różne procesy, zapisanie wartownika (o ile w ogóle bę-
dzie możliwe) może zdezorganizować pracę innych programów, i vice versa 
inne procesy mogą nieoczekiwanie zmienić zawartość wspomnianego bajtu.
Ostatnia z wymienionych okoliczności jest szczególnie dotkliwa, oznacza bo-
wiem zwiększone ryzyko załamania całego systemu  jest ono tym większe, im
więcej jest uruchomionych jednocześnie procesów. Wystarczy na przykład, by za-
pisanie wartownika zniszczyło zawartość bloków sterujących przydziałem pamięci;
jeżeli nie zapobiegnie temu system ochrony, sparaliżowane zostaną wszystkie pro-
cesy. A co się stanie, jeżeli każdy (lub tylko niektóre) z uruchomionych procesów
wykorzystywać będzie funkcję memchr w opisywanym tu wariancie? Podobne
wątpliwości można by mnożyć w nieskończoność.
I pomyśleć, że tych wszystkich kłopotów można łatwo uniknąć, jeżeli prze-
strzegać się będzie jednej podstawowej zasady: nie odwołuj się do pamięci, która
nie została Ci przydzielona. Pod pojęciem  odwołania należy tu rozumieć zarów-
no zapis, jak i odczyt  ten ostatni nie zdezorganizuje raczej pracy innych proce-
sów, lecz może spowodować załamanie programu wskutek błędu ochrony dostępu.
C:\WINDOWS\Pulpit\Szymon\Niezawodność oprogramowania\r07.doc 141
142 NIEZAWODNOŚĆ OPROGRAMOWANIA
Nie odwołuj się do pamięci,
która nie została Ci przydzielona.
ZAODZIEJ OTWIERAJCY ZAMEK KLUCZEM NIE
PRZESTAJE BYĆ ZAODZIEJEM
Poniższy fragment ilustruje kolejny przejaw programistycznej fantazji:
void FreeWindowTree(window *pwndRoot)
{
if (pwndRoot != NULL)
{
window *pwnd;
/* zwolnij okna potomne w stosunku do pwndRoot */
pwnd = pwndRoot->pwndChild;
while (pwnd != NULL)
{
FreeWindowTree(pwnd); /* zwalnia *pwnd */
pwnd = pwnd->pwndSibling;
}
if (pwndRoot->strWndTitle != NULL)
FreeMemory(pwndRoot->strWndTitle);
FrreeMemory(pwndRoot);
}
}
Przywileje zwiÄ…zane z danymi
Na ogół nie pisze się o tym w podręcznikach dla programistów, ale z każdym wy-
korzystywanym w aplikacji fragmentem pamięci związane są pewne implikowane
uprawnienia do odczytu i zapisu. Uprawnienia te majÄ… naturÄ™ czysto koncepcyjnÄ…
 nie są w żaden sposób przydzielane przez system, czy nadawane deklaracjom
zmiennych za pomocą jakichś klauzul, są natomiast wynikiem określonej koncep-
cji projektowej.
Aby zrozumieć lepiej to zagadnienie, rozpatrzmy przykład abstrakcyjnej umowy
( protokołu ) pomiędzy programistą tworzącym jakąś funkcję, a programistą tę
funkcję wywołującym i jednocześnie deklarującym, co następuje:
Jeżeli ja, Wywołujący, przekazuję tobie, Wywoływanemu, wskaznik do obszaru
wejściowego, ty zobowiązujesz się do zachowania nienaruszalności tego obsza-
ru, czyli do niezapisywania w nim żadnej zawartości.
Jeżeli ja, Wywołujący, przekazuję tobie, Wywoływanemu, wskaznik do obszaru
wyjściowego, ty zobowiązujesz się traktować przekazaną zawartość tego obszaru
jako całkowicie przypadkową i zobowiązujesz się nie odczytywać jej, a jedynie
zapisać w niej informację wynikową.
Wreszcie  ja, Wywołujący, zobowiązuję się do niezmieniania zawartości obsza-
rów zawierających wyprodukowaną przez ciebie, Wywoływanego, informację
wyjściową i określonych jako  tylko do odczytu . Zobowiązuję się ponadto do
nieodwoływania się do wspomnianej informacji w inny sposób, jak tylko za po-
średnictwem odwołań do przechowującej je pamięci.
142 C:\WINDOWS\Pulpit\Szymon\Niezawodność oprogramowania\r07.doc
DRAMATURGIA RZEMIOSAA 143
Czyli krótko  ty nie przeszkadzasz mnie, ja nie przeszkadzam tobie . Naru-
szenie implikowanych uprawnień dostępu zawsze stwarza ryzyko użycia niezgod-
nie z przeznaczeniem kodu, który stworzony został pod warunkiem ich przestrzega-
nia. Programiści przestrzegający tych reguł nie muszą natomiast obawiać się, iż
tworzone przez nich programy będą zachowywać się błędnie w nietypowych wa-
runkach.
W powyższej funkcji brak jest co prawda odwołań do  nie swojej pamięci,
lecz pętla while skrywa inną interesującą osobliwość: wyróżniona pogrubioną
czcionkÄ… instrukcja powoduje m.in. zwolnienie obszaru wskazywanego przez
pwnd, a kolejna instrukcja jak gdyby nigdy nic odwołuje się do jednego z pól tegoż
obszaru.
Swoją drogą trudno mi zrozumieć intencje programistów odwołujących się do
zwolnionych bloków pamięci  czym bowiem różni się to od otwierania zapaso-
wym kluczem pokoju hotelowego, z którego właśnie się wyprowadziłeś lub od
wybierania się na przejażdżkę samochodem, który właśnie sprzedałeś?
Odwołanie takie będzie poprawne tak długo, jak długo zwolniony obszar za-
chowywać będzie swą zawartość. Jednak z punktu widzenia programisty to, co
dzieje się ze zwolnionymi blokami pamięci jest sprawą czystego przypadku 
nawet w środowisku jednozadaniowym procedury gospodarujące pamięcią mogą
zapisywać w zwolnionych blokach własne informacje sterujące1.
Nie odwołuj się do zwolnionych bloków pamięci.
KAŻDEMU WEDAUG POTRZEB
W poprzednim rozdziale zaprezentowałem następującą implementację funkcji Un-
sToStr:
void UnsToStr(unsigned u, char *str)
{
char *strStart = str;
do
1
Tak było między innymi w Turbo Pascalu 6.0 i Borland Pascalu 7.x (w trybie REAL) 
funkcjonujące przez lata programy stworzone za pomocą Turbo Pascala 4.0 załamywały się
po skompilowaniu ich w środowisku 7.0. Po wyeliminowaniu odwołań do zwolnionych
( przed chwilą przecież ) bloków pamięci opisany problem przestał istnieć (przyp. tłum.).
C:\WINDOWS\Pulpit\Szymon\Niezawodność oprogramowania\r07.doc 143
144 NIEZAWODNOŚĆ OPROGRAMOWANIA
*str++ = (u % 10) + '0';
while ((u /= 10) > 0
*str = '\0';
ReverseStr(strStart);
}
Powyższy kod jest całkowicie poprawny i zrozumiały, jednak niektórym pro-
gramistom z pewnością nie spodoba się fakt, iż kolejne cyfry wynikowej reprezenta-
cji generowane są  od tyłu i w związku z tym konieczne jest użycie funkcji Rever-
seStr. Wydaje się to stratą czasu, której można by uniknąć poprzez budowanie
wynikowego łańcucha w odwrotnym kierunku:
void UnsToStr(unsigned u, char *str)
{
char *pch
/* jeśli u znajduje się poza zakresem, użyj UlongToStr */
ASSERT(u <= 65535);
/* zapamiętuj kolejne cyfry w łańcuchu str "od końca".
* rozpocznij od takiej pozycji łańcucha, która uwzględnia
* największą możliwą wartość u
*/
pch = &str[5];
*pch = '\0';
do
*--pch = (u % 10) + '0';
while ((u /= 10) > 0);
strcpy(str, pch);
}
Na pierwszy rzut oka powyższy kod może wydać się bardzo elegancki  jest
przecież bardziej efektywny i łatwiejszy do zrozumienia. strcpy jest przecież
szybsze od ReverseStr, szczególnie jeżeli użyć kompilatora realizującego wywo-
łania funkcji jako rozwinięcia inline. Tak naprawdę to jednak tylko pozory; funk-
cja zawiera bardzo poważny błąd.
Jak myślisz, jak duży jest fragment pamięci wskazywany przez str? Zgodnie
z opisywanym przed chwilą kontraktem pomiędzy Wywołującym, a Wywoływanym
powinien on być dostatecznie duży, aby zmieścić tekstową reprezentację liczby
przekazanej przez parametr u.  Zoptymalizowana wersja funkcji zakłada jednak,
iż jest on dostatecznie duży do pomieszczenia reprezentacji największej możliwej
liczby akceptowalnej przez tę funkcję, czyli 65535. Wywołajmy naszą funkcję w
sposób następujący:
DisplayScore()
{
char strScore[3]; /* UserScore przyjmuje wartości od 0 do 25 */
UnsToStr(UserScore, strScore);
.
144 C:\WINDOWS\Pulpit\Szymon\Niezawodność oprogramowania\r07.doc
DRAMATURGIA RZEMIOSAA 145
.
.
}
No właśnie: skoro UserScore nigdy nie przekracza wartości 25, jej tekstowa
reprezentacja nie będzie nigdy dłuższa niż dwa znaki; jeżeli uwzględnić zerowy
ogranicznik, wystarczająca okazuje się trójelementowa tablica znaków. Opisywana
wersja funkcji wymaga jednak bezwzględnie tablicy sześcioelementowej  w
efekcie powyższe wywołanie dokona zniszczenia zawartości trzech bajtów nastę-
pujących bezpośrednio za tablicą strScore. Na maszynach ze stosem  rosnącym
w dół  jak 80×86  grozi to zniszczeniem ramki wywoÅ‚ania i(lub) adresu po-
wrotu z funkcji DisplayScore. Jeżeli jednak bezpośrednio po tablicy strScore
występowałyby jeszcze inne zmienne lokalne, zniszczeniu uległoby kilka z nich,
powrót z funkcji DisplayScore nastąpiłby zupełnie normalnie i błąd mógłby po-
zostać długo nie zauważony.
Już słyszę kontrargumenty, iż wszystkiemu winien jest autor funkcji Display-
Score, deklarujący tablicę w sposób bezzasadnie oszczędny, uwzględniający tylko
własne potrzeby i nie biorący pod uwagę wymagań funkcji konwertującej. Powi-
nien on raczej przygotować się na najdłuższy łańcuch, jaki funkcja ta jest w stanie
zwrócić  a skoro tego nie czyni, działa na własne ryzyko i (co ważniejsze) prak-
tykuje ryzykowny sposób kodowania.
Tymczasem żadne w tym ryzyko  po prostu żądam wyprodukowania pewnej
informacji wyjściowej i dostarczam bufor wystarczający do jej zmieszczenia. Je-
dyne, co ryzykuję, to oczekiwanie ze strony wywoływanej funkcji, iż to ja dostar-
czę jej buforów roboczych!
Mimo wszystko koncepcję zoptymalizowanej funkcji da się jeszcze uratować,
jeżeli użyje się roboczej tablicy lokalnej, a obszar wskazywany przez str wyko-
rzysta się jedynie do przesłania końcowego wyniku:
void UnsToStr(unsigned u, char *str)
{
char strDigits[6];
char *pch
/* jeśli u znajduje się poza zakresem, użyj UlongToStr */
ASSERT(u <= 65535);
/* zapamiętuj kolejne cyfry w tablicy strDigits "od końca" */
pch = &strDigits[5];
*pch = '\0';
do
*--pch = (u % 10) + '0';
while ((u /= 10) > 0);
strcpy(str, pch);
}
Nie używaj w roli buforów roboczych obszarów
przeznaczonych na informację wyjściową.
C:\WINDOWS\Pulpit\Szymon\Niezawodność oprogramowania\r07.doc 145
146 NIEZAWODNOŚĆ OPROGRAMOWANIA
NIE UZEWNTRZNIAJ PRYWATNYCH
INFORMACJI
Być może niektórym programistom ostatnia z prezentowanych wersji funkcji Un-
sToStr jawi się jeszcze jako nieefektywna. Zamiast przesyłać informację z bufora
roboczego do bufora wynikowego, można by zwrócić wskaznik do łańcucha tkwią-
cego już przecież w buforze roboczym i oszczędzić w ten sposób trochę czasu. Po-
niższy fragment ilustruje tę koncepcję  naturalnie bufor roboczy jest teraz zmien-
nÄ… statycznÄ…:
char *strFromUns(unsigned u);
{
static char *strDigits = '?????';
char *pch
/* jeśli u znajduje się poza zakresem, użyj UlongToStr */
ASSERT(u <= 65535);
/* zapamiętuj kolejne cyfry w tablicy strDigits "od końca" */
pch = &strDigits[5];
ASSERT(*pch == '\0');
do
*--pch = (u % 10) + '0';
while ((u /= 10) > 0);
return(pch);
}
Podstawową wadę powyższej wersji natychmiast demaskuje kilkakrotne jej wy-
wołanie:
strHighScore = strFromUns(HighScore);
strThisScore = strFromUns(Score);
Po zrealizowaniu powyższego fragmentu obydwie zmienne  strHigh
Score i strThisScore  wskazują na ten sam łańcuch, stanowiący reprezenta-
cję zmiennej Score; po łańcuchu stanowiącym reprezentację zmiennej HighSco-
re nie pozostało ani śladu.
Można by w tym momencie bronić koncepcji stwierdzeniem, iż funkcja jest
całkowicie poprawna, a wszystkiemu winien jest programista nie przechowujący w
bezpieczny sposób pierwszego łańcucha; gdy wywołuje funkcję po raz drugi świa-
dom jest przecież jego zniszczenia. Taki punkt widzenia nie jest jednak zgodny z
podstawowym wymaganiem przedstawionym w rozdziale 5.: nie wystarczy, by
funkcja działała prawidłowo; musi ona jeszcze chronić programistów przed popeł-
nianiem oczywistych błędów. W dodatku opisywany błąd nie zawsze występuje w
sposób tak banalny; w poniższym przykładzie:
strHighScore = strFromUns(HighScore);
strThisScore = FormattedScore(Score);
146 C:\WINDOWS\Pulpit\Szymon\Niezawodność oprogramowania\r07.doc
DRAMATURGIA RZEMIOSAA 147
Pierwotny łańcuch zostanie zniszczony, jeżeli funkcja FormattedScore wy-
woływać będzie (bezpośrednio lub pośrednio) funkcję strFromUns.
Najgorsze w tym wszystkim jest jednak to, iż wewnątrz funkcji strFromUns
ukrywa się bomba z opóznionym zapłonem, która niespodziewanie eksploduje na
którymś etapie rozwoju projektu. Aby mianowicie wywołanie funkcji było bez-
pieczne, spełnione muszą być dwa poniższe wymagania:
XXXXXXXXXXXXXXXXXX
Problem związany z funkcją strFromUns jest przykładem szerszego zagadnienia,
mianowicie niekontrolowanego współdzielenia globalnego zasobu. Przykładami
tak wykorzystywanych zasobów są właśnie globalne bufory robocze. Opisana sy-
tuacja nie zmieniłaby się, gdyby bufor wykorzystywany przez funkcję strFro-
mUns usunąć z pamięci statycznej i przydzielić za pomocą funkcji malloc na po-
czątku wykonywania programu  nie zmieniłby się bowiem globalny charakter
tegoż bufora. Wynika stąd kolejna zasada: nie przekazuj danych w globalnych bu-
forach, chyba że jest to absolutnie konieczne.
f& znajdujący się w buforze roboczym łańcuch jest już niepotrzebny, nie jest bo-
wiem wykorzystywany żaden wskaznik do niego  na wszystkich poziomach
wywołania funkcji;
f& tak długo, jak długo istotna będzie zawartość jakiegokolwiek wskaznika do
ostatnio wyprodukowanego łańcucha w buforze roboczym, nie wolno ponownie
wywołać funkcji strFromUns, bezpośrednio ani pośrednio.
Zignorowanie którejkolwiek z tych zasad oznacza ryzyko popełnienia błędu.
Wyobraz sobie teraz pracę programisty rozbudowującego istniejący projekt uży-
wający funkcji strFromUns: gdy zmieni lub doda jakikolwiek łańcuch wywołań,
będzie zmuszony każdorazowo sprawdzać, czy spełnione są wspomniane reguły.
Mniejsza o to, iż niewesoła to perspektywa i raczej poważne utrudnienie pracy,
znacznie ważniejsze jest to, czy wspomniany programista świadom jest istniejącego
zagrożenia? Przecież o samej funkcji strFromUns mógł on nawet nie słyszeć! Po-
nadto  przyczyny tego, iż wskaznik strHighScore nie wskazuje na reprezenta-
cję zmiennej HighScore programista ów będzie raczej poszukiwał wewnątrz funk-
cji strFromUns (wszak to ona wiąże ze sobą wymienione zmienne), gdy
tymczasem leży on zupełnie gdzie indziej, mianowicie w sposobie wykorzystywa-
nia funkcji.
Unikaj przekazywania danych
za pomocą statycznych lub globalnych buforów.
C:\WINDOWS\Pulpit\Szymon\Niezawodność oprogramowania\r07.doc 147
148 NIEZAWODNOŚĆ OPROGRAMOWANIA
FUNKCJE-PASOŻYTY
Gdy stosuje się złą praktykę przekazywania danych w globalnych buforach, można
mimo wszystko uniknąć błędów, jeżeli postępuje się ostrożnie i ma się odrobinę
szczęścia. To, o czym chcę teraz napisać, jest przykładem czegoś na kształt paso-
żytnictwa na gruncie programistycznym.
Mowa tu o funkcjach, które uzależniają swe działanie od (uwaga) wewnętrznych
szczegółów implementacyjnych innych funkcji! To nie tylko przejaw ryzyka, ale
skrajnej nieodpowiedzialności  pasożyt ginie wraz ze śmiercią żywiciela; zmie-
niając więc implementację jednej funkcji unicestwiamy możliwość działania bazują-
cych na niej funkcji-pasożytów.
Najbardziej wyrazisty przykład takiej  pasożytniczej funkcji, jaki utkwił mi
w pamięci, wiąże się z językiem FORTH. Na przełomie lat 70. i 80. grupa robocza
pod nazwą FORTH Interest Group udostępniła freeware ową wersję tego języka
opartą na standardzie pod nazwą FORTH-77. Standard ten definiował trzy funkcje
standardowe: FILL  wypełniającą blok pamięci podanym bajtem, CMOVE 
przesyłającą bajty pomiędzy obszarami w kierunku rosnących adresów i  przesyłającą bajty pomiędzy obszarami w kierunku malejących adresów. Wynik
użycia dwóch ostatnich funkcji jest w oczywisty sposób różny dla obszarów pokrywa-
jących się częściowo lub całkowicie; za prawidłowy wybór jednej z nich odpowie-
dzialny był programista, uwzględniający sposób nakładania się wspomnianych ob-
szarów.
Ze względów efektywności funkcja CMOVE napisana była w zoptymalizowany
sposób w języku asemblera, natomiast funkcję FILL napisano wprost w języku
FORTH. W przełożeniu na C realizacja funkcji CMOVE była raczej oczywista.
/* CMOVE  przesyłanie pomiędzy obszarami
* w kierunku rosnących adresów
*/
void CMOVE(byte *pbFrom, byte *pbTo, size_t size)
{
while (size-- > 0)
{
*pbTo++ = *pbFrom++
}
Realizacja funkcji FILL była za to kompletnym zaskoczeniem:
/* FILL  wypełnianie obszaru pamięci */
void FILL(byte *pb, szie_t size, byte b)
{
if (size > 0)
{
*pb = b;
CMOVE(pb, pb+1, size-1);
}
}
Jak przed chwilą pisaliśmy, w przypadku kopiowania pomiędzy nakładającymi
siÄ™ obszarami istotny jest kierunek poruszania siÄ™ po kopiowanym obszarze: je-
148 C:\WINDOWS\Pulpit\Szymon\Niezawodność oprogramowania\r07.doc
DRAMATURGIA RZEMIOSAA 149
żeli obszar docelowy rozpoczyna się pod wyższym adresem niż obszar zródłowy, ko-
piowanie musi być przeprowadzone w kierunku malejących adresów, w przeciw-
nym razie w obszarze docelowym otrzymamy nie kopię obszaru zródłowego, lecz
powtórzenia jego fragmentów.
W szczególności  jeżeli obszar docelowy położony jest o jeden bajt dalej od
obszaru zródłowego, otrzymamy kopię pierwszego bajtu obszaru zródłowego; tę
właśnie cechę kopiowania wykorzystuje funkcja FILL.
Stwierdzenie zawarte w ostatnim zdaniu jest jednakże prawdziwe tylko pod wa-
runkiem, iż kopiowanie odbywa się bajt po bajcie; gdyby ktoś postanowił zopty-
malizować funkcję CMOVE i przesyłać dane w porcjach dwu- lub czterobajtowych,
nie uzyskalibyśmy żądanego efektu i funkcja FILL przestałaby działać poprawnie.
W efekcie przestałyby poprawnie działać wszystkie funkcje wywołujące funkcję
FILL (bezpośrednio lub pośrednio), mimo iż optymalizacja funkcji CMOVE nie
wprowadziłaby do niej żadnych błędów!
Aby takiej sytuacji zapobiec, należałoby wprowadzić do funkcji CMOVE ko-
mentarz wyjaśniający całą sprawę i zakazujący dokonywania w niej jakiejkolwiek
optymalizacji. To jednak rozwiązałoby problem zaledwie w połowie.
Wyobraz sobie, iż opracowujesz system sterowania robotem przemysłowym.
Urządzenie to posiada cztery stopnie swobody  współrzędne każdej z jego czte-
rech osi mogą przyjmować wartości całkowite z przedziału 0  255. Najprostszy z
możliwych projektów mógłby wykorzystywać 32-bitowe słowo w pamięci wejścia-
wyjścia  każdy z jego bajtów określałby niezależnie współrzędne jednej z osi.
Sprowadzenie wszystkich osi do pozycji  wyjściowej (0,0,0,0) następowało-
by po wyzerowaniu wszystkich czterech bajtów, co można by uczynić w sposób na-
stępujący:
FILL(pbRobotArm, 4, 0); /* robot idzie spać */
I tu spotkałaby Cię niemiła niespodzianka  robot zacząłby zachowywać się
w sposób losowy. Powyższa instrukcja wcale bowiem nie zeruje wszystkich czte-
rech bajtów, lecz jedynie pierwszy z nich, wpisując  śmieci w pozostałe trzy bajty!
Stanie się to jasne, jeżeli przyjrzymy się dokładnie, w jaki sposób działa funk-
cja FILL. Otóż po wpisaniu zera do pierwszego bajtu odczytuje ona ów bajt i ko-
piuje jego zawartość do drugiego bajtu. Owa zawartość wcale nie będzie jednak ze-
rem  lecz aktualną pozycją pierwszej osi, która nie zdążyła jeszcze (w ułamku
sekundy) przyjąć pozycji zerowej! Podobnie ma się rzecz z kopiowaniem pomiędzy
kolejnymi parami bajtów.
Można by powiedzieć, iż wszystkiemu winny jest specyficzny charakter pamięci
wejścia-wyjścia  w przeciwieństwie do pamięci konwencjonalnej nie otrzymuje-
my przy odczycie wartości uprzednio zapisanej. To prawda, lecz opisany problem
w ogóle by nie wystąpił, gdyby funkcja FILL skonstruowana została w sposób bar-
dziej naturalny  ot, choćby tak, jak funkcja memset w swej najprostszej postaci z
rozdziału 2. W swej obecnej postaci funkcja FILL jest przejawem owej fantazji
twórczej, o której pisałem na początku rozdziału; jak wyraznie widać, poza efek-
tem czysto zewnętrznym fantazja ta przynosi raczej opłakane skutki.
Poniższy fragment jest z pewnością banalny, lecz działa nawet w odniesieniu
do pamięci wejścia-wyjścia:
C:\WINDOWS\Pulpit\Szymon\Niezawodność oprogramowania\r07.doc 149
150 NIEZAWODNOŚĆ OPROGRAMOWANIA
void FILL(byte *pb, size_t size, byte b)
{
while (size-- > 0)
*pb++ = b;
}
PROGRAMISTYCZNE ÅšRUBOKRTY
Jednym z najbardziej przydatnych narzędzi przy odnawianiu ścian może okazać się
 śrubokręt. Podważamy nim wieko puszki z farbą, a następnie mieszamy nim
farbę; wiem coś o tym, mam bowiem w domu całą kolekcję różnokolorowych śru-
bokrętów. Dlaczego jednak ludzie używają śrubokrętu do mieszania farby, chociaż
śrubokrętowi nie wychodzi to wcale na zdrowie, a poza tym istnieją efektywniejsze
sposoby mieszania? Otóż śrubokręt ma tę niezaprzeczalną zaletę, iż na ogół zawsze
jest pod ręką.
Podobną rolę spełniają pewne triki programistyczne  wygodne w użyciu,
pewnie działające i używane w celach zupełnie innych niż te, do których zostały
stworzone. Spójrz na poniższy fragment, wykorzystujący wynik porównania jako
część obliczanego wyrażenia:
unsigned atou(char *str) /* bezznakowa wersja atoi */
/* atoi  konwertuje łańcuch znaków ASCII na liczbę typu int */
int atoi(char *str)
{
/* str ma następujący format:
*
* "[białe znaki][+/-]cyfry"
*
*/
while (isspace(*str))
str++;
if (*str == '-')
return (-(int)atou(str+1));
/* pomiń ewentualny znak '+' */
return ((int)atou(str + (*str == '+')));
}
W ostatniej instrukcji return widzimy pominięcie ewentualnego znaku + po-
przez dodanie wyniku porównania do wskazania na łańcuch docelowy. Ponieważ
zgodnie z normą ANSI wynikiem każdego operatora relacyjnego może być tylko 0
lub 1, więc w zależności od tego, czy znak + występuje, czy też nie, wyrażenie:
str + (*str == '+')
równe jest (odpowiednio) str+1 albo str+0, czyli str.
150 C:\WINDOWS\Pulpit\Szymon\Niezawodność oprogramowania\r07.doc
DRAMATURGIA RZEMIOSAA 151
Programiści stosujący podobne triki, powołując się na normę ANSI, nie
uświadamiają sobie tej oczywistej prawdy, iż norma ta nie stanowi wyroczni w
każdej sprawie, między innymi w kwestii niezawodnego programowania  po-
dobnie jak tabela podatkowa nie zawiera żadnych wskazówek odnośnie tego, skąd
masz zdobyć pieniądze na zapłacenie należnego podatku i czy zabieranie Ci ostat-
nich pieniędzy jest społecznie uzasadnione. Zarówno bowiem norma ANSI, jak i
tabela podatkowa, stanowią przejaw litery prawa, jednocześnie abstrahując od jego
ducha.
Prawdziwy problem nie leży jednak w samym kodzie, lecz w pewnej pozie
programistów, niekiedy wręcz snobujących się na tak kuriozalne konstrukcje; czy
utrwalanie takich nawyków nie jest szkodliwe? Jak mają się podobne nawyki do
idei programowania defensywnego?
Nie nadużywaj języka programowania.
Standardy siÄ™ zmieniajÄ…
Gdy na rynku pojawiła się wersja 83 języka FORTH (FORTH-83), wielu progra-
mistów skonstatowało, iż ich programy, stworzone w zgodzie ze standardem
FORTH-77, przestały poprawnie pracować. Przyczyna tego stanu była jasna  z
różnych względów technologicznych zmieniono reprezentację wartości TRUE z 1
na  1. Miało to opłakane skutki dla programów zakładających, iż TRUE tożsame
jest z jedynkÄ….
Programiści używający FORTHA nie byli pod tym względem odosobnieni. Po-
dobna niespodzianka spotkała użytkowników popularnego w latach 70. i 80.
UCDS Pascala, gdy język ten wkroczył na arenę mikrokomputerów  programiści
otrzymali uaktualnienie kompilatora, po którego zastosowaniu wiele programów
odmówiło współpracy, właśnie ze względu na implementację wartości TRUE.
Powinno to stanowić pewną przestrogę dla użytkowników języka C, polegają-
cych na konkretnej reprezentacji wybranych wartości  kto wie, jakie zmiany
czekają nas w przyszłych wersjach?
SYNDROM APL2
Programiści nie do końca świadomi tego, w jaki sposób kod w języku C tłumaczo-
ny jest na język maszynowy, dążąc do nadania przekładowi maksymalnej zwięzło-
ści starają się minimalizować objętość kodu zródłowego. Fakt, iż mniejszy objęto-
ściowo kod zródłowy oznacza na ogół mniejszy rozmiar kodu wynikowego,
2
APL (ang. A Programming Language)  popularny w latach 60. i 70. konwersacyjny język
służący do wykonywania szybkich obliczeń na maszynach IBM/360, charakteryzujący się
ekstremalną zwięzłością zapisu podyktowaną względami jak największej efektywności
(przyp. tłum.).
C:\WINDOWS\Pulpit\Szymon\Niezawodność oprogramowania\r07.doc 151
152 NIEZAWODNOŚĆ OPROGRAMOWANIA
jednakże relacja ta niekoniecznie musi obowiązywać na poziomie poszczególnych
linii.
Czy pamiętasz  zwięzłą funkcję uCycleCheckBox z rozdziału 6.?
unsigned uCycleCheckBox(unsigned uCur)
{
return ((uCur<=1) ? (uCur?0:1) : (uCur==4)?2:(uCur+1));
}
Nie dość, że jest ona bardzo nieczytelna, to dodatkowo generuje daleki od
efektywności kod maszynowy (co wcześniej wyjaśniłem). W cytowanej przed
chwilÄ… instrukcji:
return ((int)atou(str + (*str == '+')));
dodanie do wskaznika liczby stanowiącej konwersję wyniku porównania może
spowodować wygenerowanie efektywnego kodu pod warunkiem, że odnośna ma-
szyna jest w stanie produkować bezpośrednio (tj. bez żadnych skoków) wartości 0
albo 1 w wyniku porównania3. W przeciwnym razie przekład maszynowy przypo-
minał będzie raczej tłumaczenie instrukcji:
return ((int)atou(str + ((*str == '+') ? 1 : 0 )));
Jako że operator ?: jest w swej istocie inną postacią zapisu instrukcji if, wy-
produkowany kod maszynowy będzie miał jakość gorszą od tej, którą można by
uzyskać, przy programowaniu swych intencji wyraznie, po prostu, bez żadnych efek-
tów specjalnych:
/* pomiń ewentualny znak '+' */
if (*str == '+')
str++;
return ((int)atou(str));
Innym sposobem uzyskania optymalnego kodu jest wykorzystanie częściowe-
go wartościowania (ang. short-circuit evaluation) operacji boolowskiej, w szcze-
gólności alternatywy  jeżeli mianowicie pierwsze z wyrażeń połączonych operato-
rem || ma wartość TRUE, wynik alternatywy jest przesądzony i drugie wyrażenie
nie jest w ogóle wartościowane. Jest tak w poniższym przykładzie:
(*str != '+') || str++; /* pomiń ewentualny znak '+' */
return ((int)atou(str));
Nie gwarantuje to jednak otrzymania kodu bardziej optymalnego niż przy uży-
ciu instrukcji if. Oczywistość konstrukcji też jest tu mocno problematyczna.
Wszak zasadniczym przeznaczeniem operatora || są wyrażenia boolowskie, zaś
operatora ?:  wyrażenia warunkowe; jeżeli chcemy warunkowo wykonać pewien
fragment kodu, należy po prostu użyć instrukcji if.
3
W procesorach 80386 i lepszych możliwość taką dają instrukcje SETxx (przyp. tłum.).
152 C:\WINDOWS\Pulpit\Szymon\Niezawodność oprogramowania\r07.doc
DRAMATURGIA RZEMIOSAA 153
Jak więc widać, bardziej efektywny kod wynikowy daje się często uzyskać za
pomocą mniej zwięzłych w zapisie instrukcji.
BEZ UDZIWNIEC, PROSZ
Niektórzy  eksperci od komputerów mają skłonność do  okrągłego formułowa-
nia swych myśli  zamiast powiedzieć po prostu  Takie błędy mogą zawieszać
system , piszą  Tego rodzaju defekty oprogramowania mogą powodować utratę
kontroli nad systemem albo wymuszać zakończenie jego pracy . Używają termi-
nów typu  aksjomatyczna weryfikacja programu albo  taksonomia błędów , jak
gdyby stanowiły one element codziennego słownika programistów. W efekcie za-
sadnicza treść przesłania zostaje uwikłana w dziwaczną terminologię.
Podobne zjawisko daje się zaobserwować również na kanwie programistycz-
nej. Imponujący (w zamyśle autora) kod staje się jedynie kodem nieczytelnym, jak
w poniższym przykładzie:
void *memmove(void *pvTo, void *pvFrom, size_t size)
{
byte *pbTo = (byte *)pvTo;
byte *pbFrom = (byte *)pvFrom;
((pbTo > pbFrom) ? tailmove : headmove)(pbTo, pbFrom, size);
return (pvTo);
}
Powyższy kod jest całkowicie poprawny z punktu widzenia języka C, stanowi
jednak przykład kodu tyleż zgrabnego, co trudnego do zrozumienia i konserwacji.
Czyż nie prościej byłoby napisać to w taki sposób:
void *memmove(void *pvTo, void *pvFrom, size_t size)
{
byte *pbTo = (byte *)pvTo;
byte *pbFrom = (byte *)pvFrom;
if ((pbTo > pbFrom)
tailmove(pbTo, pbFrom, size);
else
headmove(pbTo, pbFrom, size);
return (pvTo);
}
Oto inny przykład kodu mogącego wywołać wątpliwości programisty:
while (wyrażenie)
{
int i = 33; /* deklaracje zmiennych lokalnych */
vchar str[20];
.
.
.
C:\WINDOWS\Pulpit\Szymon\Niezawodność oprogramowania\r07.doc 153
154 NIEZAWODNOŚĆ OPROGRAMOWANIA
}
Czy wartość 33 nadawana jest zmiennej i przy każdym obrocie pętli, czy tylko
przy jej rozpoczynaniu? Bywa, iż nawet doświadczeni programiści muszą się chwilę
zastanowić, by poprawnie odpowiedzieć na to pytanie.
Kwestia ta staje się jednak bezprzedmiotowa po przepisaniu pętli w następujący
sposób:
while (wyrażenie)
{
int i; /* deklaracje zmiennych lokalnych */
vchar str[20];
i = 33;
.
.
.
}
Programiści nazbyt często nie uświadamiają sobie podstawowej prawdy, iż oprócz
tzw. użytkowników końcowych (ang. end users) istnieje również druga grupa od-
biorców tworzonego przez nich oprogramowania  grupę tę tworzą inni programi-
ści, których zadaniem będzie utrzymywanie i rozwijanie otrzymanego kodu zródło-
wego, nieraz przez długie lata.
Kim są programiści konserwujący oprogramowanie?
Zgodnie z przyjętą w firmie Microsoft praktyką, rozmiar kodu nowo tworzonego
przez danego programistę jest wprost proporcjonalny do znajomości produktu, któ-
rego kod ów dotyczy  i oczywiście ogólnych kwalifikacji programistycznych.
Większa ilość samodzielnie tworzonego kodu oznacza jednocześnie mniejsze za-
angażowanie w konserwację kodu tworzonego przez innych programistów. Pro-
gramiści znający nowy produkt słabo lub nie znający go wcale spędzają więc
większość czasu na czytaniu cudzego kodu, poprawianiu cudzych błędów i wpro-
wadzaniu drobnych poprawek  gdy nie zna siÄ™ dobrze nowego produktu, trudno
decydować o jego generalnych zmianach.
Wydaje się to rozsądną praktyką, wszak programiści o większych kwalifika-
cjach i lepszej znajomości produktu ponoszą większą odpowiedzialność za jego
powstawanie. Programiści zajmujący się konserwacją oprogramowania stworzone-
go przez swoich  bardziej wykwalifikowanych kolegów muszą być jednak w sta-
nie je zrozumieć, a to wymaga zrozumiałego kodowania, wolnego od wszelkich
trików, udziwnień i niejasności.
Podstawowym wymogiem, dyktowanym przez względy niezawodności opro-
gramowania, jest tworzenie kodu zródłowego w taki sposób, by jego rozwijanie nie
napotykało na niepotrzebne trudności i nie stwarzało łatwych okazji do popełniania
błędów. Wydaje się to oczywiste  mniej oczywiste jest natomiast to, iż jeżeli kod
programu będzie zrozumiały jedynie dla ekspertów od programowania, nie będzie
154 C:\WINDOWS\Pulpit\Szymon\Niezawodność oprogramowania\r07.doc
DRAMATURGIA RZEMIOSAA 155
on z pewnością łatwy w utrzymaniu (tym bardziej, iż zadanie konserwacji kodu
powierza siÄ™ raczej programistom mniej wykwalifikowanym, a nie ekspertom).
Formułuj kod programu w taki sposób,
by jego zrozumienie nie wymagało kwalifikacji eksperta.
NA ÅšMIETNIK Z TYMI WSZYSTKIMI TRIKAMI
W niniejszym rozdziale przyjrzeliśmy się kilku przykładom kodowania wyglądają-
cym efektownie na pierwszy rzut oka, lecz gdy spogląda się na te przykłady po raz
drugi (czy nawet  piąty) niełatwo jest dostrzec czające się w nich subtelne błędy
lub efekty uboczne. Mimo zewnętrznej efektowności praktyczna przydatność tak
stworzonego kodu staje się wątpliwa, jeżeli wziąć pod uwagę względy jego nieza-
wodności i koszty przyszłego utrzymania.
Jeżeli więc tworzony przez Ciebie kod wyda Ci się w pewnym momencie nie-
co  trikowy , zatrzymaj się na chwilę i spróbuj poszukać innego rozwiązania. Jeżeli
bowiem dany fragment kodu faktycznie produkuje żądane wyniki, fakt ten musi
być widoczny w sposób oczywisty. W kodzie, którego poprawność jest w jakimś
stopniu zakamuflowana, mogą bowiem czaić się równie głęboko zakamuflowane
błędy.
I to właśnie jest najważniejszym powodem tworzenia kodu prostego, łatwego
do zrozumienia, pozbawionego efektownych  wodotrysków . Postępując zgodnie
z tą ideą ułatwiasz pracę i sobie, i innym.
PODSUMOWANIE
f& Nie zapisuj danych (nawet tymczasowo) do pamięci, która nie jest przydzielo-
na do Twojego programu. Jeżeli sądzisz, że odczytywanie danych z takiej pa-
mięci może być sensowne, przypomnij sobie kłopoty z kopiowaniem pomię-
dzy komórkami pamięci wejścia-wyjścia.
f& Nie odwołuj się do bloku pamięci po jego zwolnieniu  jego zawartość mogła
ulec zniszczeniu przez inne programy lub procedury zarządzające pamięcią.
f& Przekazywanie danych za pomocą globalnych buforów może być niekiedy
uzasadnione względami efektywności, lecz wiąże się z wieloma trudnościami.
Tworzone przez wywoływaną funkcję dane, użyteczne dla funkcji wywołują-
cej, powinny być przekazywane bezpośrednio do tej ostatniej. Jeżeli jednak dane
te rezydują w globalnych buforach, należy chronić je przed zniszczeniem tak dłu-
go, jak długo są potrzebne.
f& Nie uzależniaj poprawnego działania tworzonej funkcji od konkretnych szcze-
gółów implementacyjnych innych funkcji.
C:\WINDOWS\Pulpit\Szymon\Niezawodność oprogramowania\r07.doc 155
156 NIEZAWODNOŚĆ OPROGRAMOWANIA
f& Używaj języka programowania zgodnie z przeznaczeniem, posługując się jego
naturalnymi konstrukcjami i unikając niejasnych idiomów  nawet wówczas,
gdy standardy języka gwarantują ich poprawne działanie. Pamiętaj, iż standar-
dy nie są wieczne i mogą się zmieniać.
f& To nieprawda, iż zwięzłe konstrukcje językowe powodują generowanie równie
zwięzłego przekładu. Należy więc dobrze się zastanowić przed przystąpieniem
do przekształcania czytelnej instrukcji, zajmującej kilka linii kodu w nieczytel-
ną konstrukcję mieszczącą się w jednej linii. Efektywność przekładu może nic
na tym nie zyskać i przysłowiowa skórka okaże się niewarta wyprawki.
f& Unikaj tworzenia kodu przypominającego kontrakty pisane przez prawników 
nie można wymagać, by zrozumienie programu wymagało kwalifikacji eksper-
ta; musi on być zrozumiały przez przeciętnego programistę.
POMYÅšL O TYM
1. Programiści bardzo często modyfikują argumenty wywołania funkcji (w jej
treści  przyp. tłum.). Dlaczego nie kłóci się to z implikowanymi regułami do-
stępu do danych wejściowych?
2. Pamiętając o ryzyku związanym z używaniem globalnego bufora przez funkcję
strFromUns zastanów się, czy poniższa wersja używająca globalnego wskaz-
nika stwarza jakieś dodatkowe niebezpieczeństwo?
char *strFromUns(unsigned u);
{
static char *strDigits = '?????';
char *pch
/* jeśli u znajduje się poza zakresem, użyj UlongToStr */
ASSERT(u <= 65535);
/* zapamiętuj kolejne cyfry w tablicy strDigits "od końca" */
pch = &strDigits[5];
ASSERT(*pch == '\0');
do
*--pch = (u % 10) + '0';
while ((u /= 10) > 0);
return(pch);
}
3. Napotkałem kiedyś kod dokonujący szybkiego zerowania wszystkich zmien-
nych lokalnych w następujący sposób:
void DoSomething(...)
{
int i;
int j;
int k;
156 C:\WINDOWS\Pulpit\Szymon\Niezawodność oprogramowania\r07.doc
DRAMATURGIA RZEMIOSAA 157
memset(&k, 0, 3*sizeof(int)); /* wyzeruj i, j oraz k */
&
}
Ten kod może poprawnie funkcjonować w niektórych implementacjach, ale
podobnych konstrukcji należy generalnie unikać. Dlaczego?
4. Mimo iż część systemu operacyjnego komputera może być zapisana w pamięci
tylko do odczytu, bezpośrednie odwoływanie się do tej pamięci z pominięciem
interfejsu systemowego niesie ze sobÄ… pewne ryzyko. Dlaczego?
5. Język C umożliwia pomijanie niektórych argumentów funkcji w jej wywołaniu,
na przykład:
.
.
.
DoOperation(opNegAcc); /* nie ma potrzeby przekazywania
* argumentu "val"
*/
.
.
.
void DoOperation(operation op, int val)
{
switch (op)
{
case opNegAcc:
accumulator = - accumulator;
break;
case opAddVal:
accumulator += val;
break;
.
.
.
}
Dlaczego mimo wszystko nie należy tej możliwości wykorzystywać, mimo iż
może ona poprawić efektywność programu?
6. Co w istocie weryfikuje poniższa asercja i jaka jest jej bardziej czytelna po-
stać?
Przypomnij sobie poniższy fragment funkcji memmove:
((pbTo > pbFrom) ? tailmove : headmove)(pbTo, pbFrom, size);
W jaki sposób poprawić jej czytelność, z zachowaniem koncepcji prezento-
wanej przez autora?
7. Poniższy fragment w języku asemblera pokazuje najczęstszy sposób wywoły-
wania funkcji. Na czym polega ryzyko zwiÄ…zane z tego rodzaju konstrukcja-
mi?
C:\WINDOWS\Pulpit\Szymon\Niezawodność oprogramowania\r07.doc 157
158 NIEZAWODNOŚĆ OPROGRAMOWANIA
move r0,#PRINTER
call Print+4
.
.
.
Print: move r0,#DISPLAY ; (instrukcja 4-bajtowa)
; r0 zawiera identyfikator urzÄ…dzenia
.
.
.
8. Poniższy fragment kodu, podobnie jak fragment z poprzedniego ćwiczenia,
zależny jest od wewnętrznej implementacji funkcji Print, lecz ma poza tym
jeszcze jedną niepożądaną cechę. Jaką?
instClearR0 = 0x36A2 ; kod zerujÄ…cy rejestr r0
.
.
.
call Print+2 ; wyjście na drukarkę
.
.
.
Print: move r0,#instClearR0 ; (instrukcja 4-bajtowa)
comp r0,#0 ; 0 - drukarka, `" 0  ekran
.
.
.
158 C:\WINDOWS\Pulpit\Szymon\Niezawodność oprogramowania\r07.doc


Wyszukiwarka

Podobne podstrony:
r07 04 ojqz7ezhsgylnmtmxg4rpafsz7zr6cfrij52jhi
r07 0001
r07 01
r07 03
r07
r07
R07 (5)
r07 02
R07
R07 (19)

więcej podobnych podstron