Kulturált sztringmásoló függvény
A C nyelvnek egyszerre előnye és hátránya is az, hogy a tömbök használatakor nem ellenőrzi futási időben a túlindexelést. Előnye azért, mert gyors: ha tudjuk, hogy a program helyes, akkor felesleges futási időben minden indexeléskor ellenőrizni a határokat. (Nehéz is lenne, tekintve a nyelv tömbmodelljét.) Hátránya pedig azért, mert ha hibázunk, az nem mindig derül ki egyértelműen, determinisztikusan. Olyankor a legváltozatosabb hibajelenségeket tapasztalhatjuk: memóriaelérési hibák, helytelen eredmények, program lefagyások, a rendszer biztonságának sérülése. Vagy egyszerűen az is előfordulhat, hogy semmi hatását nem észleljük a hibának.
Bár a gondolat csábító, hogy a program írásakor mindig biztosítani lehessen valahogy a helyes tömbméret megválasztását, ez sokszor nem egyszerű. A probléma talán a sztringeknél érzékelhető a legjobban: bármekkorára is választjuk a karaktertömb méretét, mindig előfordulhat, hogy a felhasználó hosszabb sort vagy szót ad meg, mint amire gondoltunk. Emiatt a programokban a tömbök túlindexelése egyébként gyakoribb szokott lenni, mint az alulindexelés. A két leghírhedtebb, Interneten terjedő féregprogram, a Code Red és a Slammer is éppen ilyen túlindexelési problémát – tulajdonképpen figyelmetlenül megírt programot – használt ki.
Nincs persze szó arról, hogy elvi akadály miatt megoldhatatlan probléma lenne ez. A félév második felében szereplő dinamikus memóriakezelés és dinamikus adatszerkezetek témakör éppen azt mutatja be, hogyan léphetünk túl a fix méretű tömbökön. Ez is egy megoldás lehet, csak éppen ezt sokszor egyszerűen a lustaság akadályozza meg. Ha maradunk a rögzített méretű tömböknél, akkor is el tudjuk kerülni az ilyen jellegű hibákat. Azonban sajnos a szabványos C függvénykönyvtár sztringkezelő függvényei sem túl szerencsésen lettek megtervezve. Nézzük meg, miért; utána pedig azt, hogyan lehetne javítani a helyzeten.
1 A C könyvtár sztringkezelő függvényei
Idézzük csak fel a C sztringek lelkivilágát! A C sztringek nullával lezárt karaktertömbök. Adott a következő, száz elemű tömb:
char sztring[100];
Ebben a tömbben egy legfeljebb 99 karakterből álló szöveget
tárolhatunk, hiszen legkésőbb a tömb utolsó elemének, sztring[99]-nek
a lezáró nullát kell tartalmaznia. Ezt az elvárást a C sztringkezelő
függvényei néha nem teljesítik, máskor pedig nagyon körülményesen kell
felparaméterezni őket:
strcpy(cél, forrás). Astrcpy()függvény a cél tömb méretére való tekintet nélkül bemásolja a pointer által mutatótt memóriaterülettől kezdődően a forrás sztringet. Vagyis ha a forrás sztring egy 1000 karakteres szöveget tartalmaz, akkor a függvény 1001 karaktert másol a cél tömbbe, akkor is, ha az csak tíz elemű.strncpy(cél, forrás, maxbájt). Ez kicsit jobbnak tűnik, mint az előző, hiszen meg lehet neki adni, hogy maximum hány bájtot másoljon. Csakhogy van egy kis probléma: a másolandó bájtok számába astrncpy()beleérti a lezáró nullát is, és nem kezeli azt különlegesen. Azaz ha a forrás sztring elsőmaxbájtkaraktere között nincsen lezáró nulla, akkor a cél sztring sem lesz nullával lezárva! Ez horror. Így lehetne megbízhatóvá tenni:strncpy(cel, forras, cel_tomb_merete); cel[cel_tomb_merete-1] = '\0';
Tudva persze, hogy ha nem fér bele a tömbbe a sztring, akkor le lesz vágva. A másik furcsaság, amit astrncpy()csinál, az az, hogy a tömb fennmaradó részeit (ha a forrás sztring rövidebb, mint a cél tömb) nullákkal tölti ki, ami pedig általában felesleges, és időbe telik.- A
strcat(cél, forrás)függvénnyel két sztringet fűzhetünk össze, cél+=forrás módon. A cél tömb méretére ez sincs tekintettel. Nem is lehet, hiszen nem kapja paraméterként, nem tudja miből megállapítani – nekünk kell figyelnünk arra, hogy a tömb legalább strlen(cél)+strlen(forrás)+1 bájt méretű legyen. - A
strncat(cél, forrás, maxbájt)függvény talán egy fokkal jobban használható, mint a'cpypárja, hiszen ez legalább biztosan lezárja nullával a cél tömböt. A baj csak az, hogy a maxbájt paraméter által megadott méretet a másolt bájtok számára érti, ráadásul a lezáró nulla még efölé jön. Arról nem is beszélve, hogy a maximum másolható bájtok tényleges száma az összefűzés miatt nem csak a forrás sztring méretétől függ, hanem a cél sztring eredeti tartalmától is. Ezt valahogy így lehetne megbízhatóvá tenni:strncat(cel, forras, cel_tomb_merete-strlen(forras)-1);
2 Sztringmásoló függvény – elvárások
Mit várnánk egy sztringet másoló függvénytől?
- Ne írja túl a cél tömböt.
- Ha a cél tömbbe nem fér bele az a sztring, amit bele szeretnénk másolni, akkor azt jelezze valahogyan.
- Könnyű legyen használni.
- Az utóbbi leginkább azt jelenti, hogy a cél tömb méretét kelljen neki megadni. És pontosan azt, ne −1-et stb., mert azt úgyis előbb-utóbb el fogjuk felejteni.
- Mindezek vonatkoznak az összefűzést végző függvényre is: ott is a méretet meghatározó paraméter a cél tömb mérete legyen, és ne függjön annak eredeti tartalmától.
- És végül: garantáltan zárja le a cél tömböt nullával.
Ilyen tulajdonságokkal rendelkezik a könyvtári
snprintf() függvény. Ennek első paramétere a cél tömb,
a második paramétere pedig a cél tömb mérete kell legyen. (A többi
paramétere a printf()-éhez hasonló.) Figyelembe veszi az
írás közben azt, hogy csak méret-1 karaktert írhat, és akármi is
történik, lezárja nullával a tömböt. A visszatérési értéke pedig
annak a sztringnek a hossza, aminek az előállítására kértük – még
akkor is, ha az a cél tömbbe nem fért bele. Ehhez hasonlóan
viselkedő 'cpy és 'cat függvényeket több
operációs rendszer C könyvtára is tartalmaz, általában
strlcpy()
és strlcat()
néven.
3 Miért kell az ilyen paraméterezés?
Tegyük fel a fent linkelt cikk nyomán, hogy egy fájl elérési útját szeretnénk összebarkácsolni egy sztringben, saját_mappa + / + fájl.txt módon:
char eleresiut[100]; strcpy(eleresiut, sajat_mappa); strcat(eleresiut, "/"); strcat(eleresiut, "fajl.txt");
Ez potenciálisan több túlindexelést is tartalmaz. A saját mappa neve különböző
lehet, hiszen a felhasználó határozza meg. A könyvtári strncpy() és strncat()
függvényekkel a következőképpen lehetne biztonságossá tenni a kódrészletet:
char eleresiut[100]; strncpy(eleresiut, sajat_mappa, sizeof(eleresiut) - 1); eleresiut[sizeof(eleresiut) - 1] = '\0'; strncat(eleresiut, "/", sizeof(eleresiut) - strlen(eleresiut) - 1); strncat(eleresiut, "fajl.txt", sizeof(eleresiut) - strlen(eleresiut) - 1);
Ezen már minden látszik, csak az nem, hogy mit csinál. Ha a sztringmásoló és -összefűző függvényünk az előbb említett tulajdonságokkal rendelkezik, akkor a biztonságossá tétel sokkal egyszerűbb, sőt triviális:
char eleresiut[100]; strlcpy(eleresiut, sajat_mappa, sizeof(eleresiut)); strlcat(eleresiut, "/", sizeof(eleresiut)); strlcat(eleresiut, "fajl.txt", sizeof(eleresiut));
Ezek a függvények egyébként a snprintf()-hez
hasonlóan azt a sztringhosszt szokták visszaadni, amekkora a
másolt/keletkező sztring lett volna. Így bármelyik pillanatban, ha
azt látjuk, hogy a visszatérési értékük ≥ sizeof(elérésiút),
akkor tudjuk, hogy le kellett vágni a sztringet, mert nem fért bele
a cél tömbbe.
4 Sztringmásoló függvény – egy konkrét implementáció
Az OpenBSD strlcpy() függvénye így néz ki, magyarra fordított megjegyzésekkel:
/*
* src-t dst sztringbe másolja, ahol az utóbbi tömb mérete siz.
* Legfeljebb siz-1 karakter kerül másolásra.
* Mindig lezárja 0-val a dst tömböt (kivétel ha siz == 0).
* strlen(src)-vel tér vissza; ha ez >= siz, akkor a cél sztring
* le lett vágva.
*/
size_t
strlcpy(char *dst, const char *src, size_t siz)
{
char *d = dst;
const char *s = src;
size_t n = siz;
/* Annyi bájtot másol, amennyi belefér */
if (n != 0) {
while (--n != 0) {
if ((*d++ = *s++) == '\0')
break;
}
}
/* Nincs elég hely dst-ben: nullával lezárás és src végének megkeresése */
if (n == 0) {
if (siz != 0)
*d = '\0'; /* dst 0-val lezárása */
while (*s++)
;
}
return s - src - 1; /* a méretbe nem számít bele a lezáró 0 */
}
Így néz ki az a függvény, amely figyel a sztringek játékszabályaira, és egyúttal könnyű is használni.
