Czy funkcje biblioteki C powinny zawsze oczekiwać długości łańcucha?

15

Obecnie pracuję nad biblioteką napisaną w C. Wiele funkcji tej biblioteki oczekuje łańcucha jako char*lub const char*w swoich argumentach. Zacząłem od tych funkcji, które zawsze oczekiwały długości łańcucha, size_taby nie było wymagane zakończenie zerowe. Jednak podczas pisania testów spowodowało to częste korzystanie z strlen():

const char* string = "Ugh, strlen is tedious";
libFunction(string, strlen(string));

Ufanie użytkownikowi, że przekaże poprawnie zakończone ciągi, prowadziłoby do mniej bezpiecznego, ale bardziej zwięzłego i (moim zdaniem) czytelnego kodu:

libFunction("I hope there's a null-terminator there!");

Jaka jest tutaj rozsądna praktyka? Czy interfejs API jest bardziej skomplikowany w użyciu, ale zmusić użytkownika do przemyślenia jego danych wejściowych, lub udokumentować wymaganie łańcucha zakończonego znakiem zerowym i zaufać programowi wywołującemu?

Benjamin Kloster
źródło

Odpowiedzi:

4

Zdecydowanie i absolutnie noś długość . Standardowa biblioteka C jest w ten sposób niesławnie łamana, co nie spowodowało końca bólu w przypadku przepełnienia bufora. Takie podejście jest przedmiotem tak dużej nienawiści i udręki, że współczesne kompilatory faktycznie ostrzegają, marudzą i narzekają podczas używania tego rodzaju standardowych funkcji bibliotecznych.

Jest tak źle, że jeśli kiedykolwiek spotkasz się z tym pytaniem na rozmowie kwalifikacyjnej - a Twój specjalista ds. Technicznych wygląda na to, że ma kilka lat doświadczenia - czysta gorliwość może wylądować w pracy - możesz naprawdę być daleko, jeśli możesz przytoczyć precedens strzelania do kogoś, kto implementuje interfejsy API szukające terminatora w języku C.

Odkładając na bok emocje, jest wiele rzeczy, które mogą pójść nie tak z tym NULL na końcu łańcucha, zarówno w czytaniu, jak i manipulowaniu nim, a ponadto jest to tak naprawdę sprzeczne z nowoczesnymi koncepcjami projektowymi, takimi jak obrona dogłębna (niekoniecznie dotyczy bezpieczeństwa, ale projektu API). Przykłady C API, które mają dużą długość - np. interfejs API systemu Windows.

W rzeczywistości problem ten został rozwiązany w latach 90., dziś powstaje konsensus, że nie należy nawet dotykać strun .

Późniejsza edycja : jest to dość debata na żywo, więc dodam, że ufanie wszystkim osobom poniżej i powyżej, aby byli mili i korzystali z funkcji biblioteki str * jest OK, dopóki nie zobaczysz klasycznych rzeczy, takich jak output = malloc(strlen(input)); strcpy(output, input);lub while(*src) { *dest=transform(*src); dest++; src++; }. W tle prawie słyszę Lacrimosę Mozarta.

vski
źródło
1
Nie rozumiem twojego przykładu Windows API wymagającego od osoby dzwoniącej podania długości ciągów. Na przykład typowa funkcja API Win32, taka jak CreateFileprzyjmowanie LPTCSTR lpFileNameparametru jako danych wejściowych. Od osoby dzwoniącej nie oczekuje się długości ciągu. W rzeczywistości użycie ciągów zakończonych przez NUL jest tak zakorzenione, że dokumentacja nawet nie wspomina, że nazwa pliku musi być zakończona przez NUL (ale oczywiście tak musi być).
Greg Hewgill,
1
Właściwie w Win32, LPSTRtyp mówi, że łańcuchy mogą być zakończone NUL, a jeśli nie , będzie to wskazane w powiązanej specyfikacji. Tak więc, chyba że wyraźnie zaznaczono inaczej, oczekuje się, że takie łańcuchy w Win32 będą kończyć się na NUL.
Greg Hewgill,
Świetna uwaga, byłem nieprecyzyjny. Weź pod uwagę, że CreateFile i jego paczka istnieją już od Windows NT 3.1 (wczesne lata 90-te); obecny interfejs API (tj. od czasu wprowadzenia Strsafe.h w XP SP2 - z publicznymi przeprosinami Microsoftu) wyraźnie zdezaktualizował wszystkie rzeczy zakończone przez NULL, jakie mógł. Pierwszy raz, kiedy Microsoft naprawdę czuł się naprawdę przykro z powodu użycia ciągów zakończonych zerami NULL, był właściwie znacznie wcześniej, kiedy musieli wprowadzić BSTR w specyfikacji OLE 2.0, aby jakoś wprowadzić VB, COM i stary WINAPI do tej samej łodzi.
vski
1
Nawet StringCbCatna przykład tylko miejsce docelowe ma maksymalny bufor, co ma sens. Źródłem jest nadal zwykłym znakiem NUL C ciąg. Być może mógłbyś poprawić swoją odpowiedź, wyjaśniając różnicę między parametrem wejściowym a parametrem wyjściowym . Parametry wyjściowe powinny zawsze mieć maksymalną długość bufora; parametry wejściowe są zwykle zakończone NUL (są wyjątki, ale z mojego doświadczenia rzadko).
Greg Hewgill
1
Tak. Ciągi są niezmienne zarówno w JVM / Dalvik, jak i .NET CLR na poziomie platformy, a także w wielu innych językach. Posunąłbym się tak daleko i spekuluję, że rodzimy świat nie jest w stanie tego jeszcze zrobić (standard C ++ 11) z powodu a) dziedzictwa (tak naprawdę nie zyskujesz tak dużo, mając tylko część swoich ciągów niezmiennych) ib ) naprawdę potrzebujesz GC i tabeli ciągów, aby to zadziałało, alokatory o ograniczonym zasięgu w C ++ 11 nie mogą tego całkiem wyciąć.
vski
16

W języku C idiomem jest to, że ciągi znaków są zakończone NUL, więc sensowne jest przestrzeganie powszechnej praktyki - w rzeczywistości jest stosunkowo mało prawdopodobne, aby użytkownicy biblioteki mieli ciągi nie zakończone NUL (ponieważ wymagają one dodatkowej pracy do wydrukowania używając printf i używaj w innym kontekście). Używanie dowolnego innego ciągu jest nienaturalne i prawdopodobnie stosunkowo rzadkie.

Ponadto, w tych okolicznościach, twoje testowanie wygląda dla mnie trochę dziwnie, ponieważ do poprawnej pracy (przy użyciu strlen), w pierwszej kolejności zakładasz łańcuch zakończony znakiem NUL. Powinieneś testować ciągi znaków nie zakończone NUL, jeśli zamierzasz z nimi pracować w bibliotece.

James McLeod
źródło
-1, przepraszam, to jest po prostu odradzane.
vski
W dawnych czasach nie zawsze było to prawdą. Dużo pracowałem z protokołami binarnymi, które umieszczają dane ciągów w polach o stałej długości, które nie zostały zakończone wartością NULL. W takich przypadkach bardzo przydatna była praca z funkcjami, które zajmowały dużo czasu. Jednak nie ukończyłem C od dekady.
Gort the Robot
4
@vski, w jaki sposób zmuszanie użytkownika do wywołania „strlen” przed wywołaniem funkcji docelowej robi cokolwiek, aby uniknąć problemów z przepełnieniem bufora? Przynajmniej jeśli sam sprawdzisz długość w ramach funkcji docelowej, możesz być pewien, jakie poczucie długości jest używane (w tym terminal zerowy, czy nie).
Charles E. Grant,
@Charles E. Grant: Zobacz komentarz powyżej o StringCbCat i StringCbCatN w Strsafe.h. Jeśli masz po prostu znak * i brak długości, to rzeczywiście nie masz rzeczywistego wyboru, jak tylko użyć funkcji str *, ale chodzi o to, aby nosić długość, więc staje się ona opcją między str * i strn * funkcje, z których te ostatnie są preferowane.
vski
2
@vski Nie ma potrzeby omijania długości łańcucha . Konieczne jest podanie długości bufora . Nie wszystkie bufory są łańcuchami i nie wszystkie łańcuchy są buforami.
jamesdlin
10

Twój argument „bezpieczeństwa” tak naprawdę nie ma zastosowania. Jeśli nie ufasz, że użytkownik poda ci ciąg zakończony zerem, gdy to jest to, co udokumentowałeś (i co jest „normą” dla zwykłego C), nie możesz tak naprawdę ufać długości, którą ci daje (co zrobi prawdopodobnie skorzystasz z tego, strlenco robisz, jeśli nie mają go pod ręką, a to się nie powiedzie, jeśli „ciąg” nie był ciągiem w pierwszej kolejności.

Istnieją jednak uzasadnione powody, aby wymagać długości: jeśli chcesz, aby twoje funkcje działały na podciągach, być może znacznie łatwiej (i wydajniej) jest podać długość niż pozwolić użytkownikowi na wykonanie magii kopiowania tam iz powrotem, aby uzyskać bajt zerowy we właściwym miejscu (i ryzykuj po drodze błędy popełniane po drodze).
Możliwość obsługi kodowań, w których bajty zerowe nie są zakończeniami, lub obsługa łańcuchów z osadzonymi wartościami zerowymi (celowo) może być przydatna w niektórych okolicznościach (zależy od tego, co dokładnie robią twoje funkcje).
Przydaje się również możliwość obsługi danych, które nie są zakończone zerem (tablice o stałej długości).
Krótko mówiąc: zależy od tego, co robisz w bibliotece i jakiego rodzaju danych oczekujesz od użytkowników.

Być może wiąże się to również z aspektem wydajności. Jeśli Twoja funkcja musi znać długość łańcucha z wyprzedzeniem, a oczekujesz, że użytkownicy przynajmniej zwykle już znają tę informację, przekazanie jej (a nie jej obliczenie) może ogolić kilka cykli.

Ale jeśli twoja biblioteka oczekuje zwykłych zwykłych ciągów tekstowych ASCII, a nie masz rozdzierających ograniczeń wydajności i bardzo dobrego zrozumienia, w jaki sposób użytkownicy będą wchodzić w interakcje z twoją biblioteką, dodanie parametru długości nie wydaje się dobrym pomysłem. Jeśli łańcuch nie zostanie poprawnie zakończony, szanse na to, że parametr długości będzie równie fałszywy. Nie sądzę, że zyskasz na tym dużo.

Mata
źródło
Zdecydowanie nie zgadzam się z tym podejściem. Nigdy nie ufaj swoim dzwoniącym, zwłaszcza za biblioteką API, dołóż wszelkich starań, aby zakwestionować to, co ci dają i z wdziękiem ponieść porażkę. Noś cholerną długość, praca z ciągami zakończonymi NULL nie jest tym, co oznacza „bądź luźny w stosunku do swoich rozmówców i surowy wobec swoich rozmówców”.
vski
2
Zgadzam się głównie z twoim stanowiskiem, ale wydaje się, że pokładasz dużo zaufania w tym argumentie długości - nie ma powodu, dla którego powinien być niezawodny niż terminator zerowy. Moje stanowisko jest takie, że zależy to od tego, co robi biblioteka.
Mat.
Jest o wiele więcej rzeczy, które mogą pójść nie tak z terminatorem NULL w ciągach niż z długością przekazywaną przez wartość. W C jedynym powodem, dla którego można ufać długości, jest to, że byłoby nierozsądne i niepraktyczne, aby nie nosić długości bufora nie jest dobrą odpowiedzią, jest to po prostu najlepsza biorąc pod uwagę alternatywy. Jest to jeden z powodów, dla których ciągi (i ogólnie bufory) są starannie zapakowane i enkapsulowane w językach RAD.
vski
2

Nie. Ciągi znaków są z definicji zawsze zakończone zerem, długość ciągu jest zbędna.

Dane znakowe nie zakończone znakiem null nigdy nie powinny być nazywane „ciągiem”. Przetwarzanie go (i wyrzucanie długości) powinno zwykle być zawarte w bibliotece, a nie w części interfejsu API. Wymaganie długości jako parametru tylko w celu uniknięcia pojedynczych wywołań strlen () jest prawdopodobnie przedwczesną optymalizacją.

Ufanie dzwoniącemu funkcji API nie jest niebezpieczne ; niezdefiniowane zachowanie jest całkowicie poprawne, jeśli nie są spełnione udokumentowane warunki wstępne.

Oczywiście dobrze zaprojektowany interfejs API nie powinien zawierać pułapek i powinien ułatwiać prawidłowe korzystanie z niego. A to oznacza po prostu, że powinno to być tak proste i jednoznaczne, jak to możliwe, unikając redundancji i przestrzegając konwencji języka.

dpi
źródło
nie tylko doskonale ok, ale w rzeczywistości nie da się tego uniknąć, chyba że przejdzie się do bezpiecznego dla języka, jednowątkowego języka. Może porzuciłem trochę więcej niezbędnych ograniczeń ...
Deduplicator
1

Zawsze powinieneś zachować swoją długość. Po pierwsze, użytkownicy mogą chcieć zawierać w nich wartości NULL. Po drugie, nie zapominaj, że strlenjest to O (N) i wymaga dotknięcia całej pamięci podręcznej bye bye cache. Po trzecie, łatwiej jest ominąć podzbiory - na przykład mogą dać mniej niż rzeczywista długość.

DeadMG
źródło
4
To, czy funkcja biblioteki zajmuje się osadzonymi wartościami NULL w ciągach, musi być bardzo dobrze udokumentowane. Większość funkcji biblioteki C zatrzymuje się na NULL lub na długości, w zależności od tego, co nastąpi pierwsze. (A jeśli napisane kompetentnie, te, które nie zajmują długości, nigdy nie używają strlenw teście pętli.)
Gort the Robot
1

Należy rozróżnić między przekazywaniem ciągu a przekazywaniem bufora .

W C łańcuchy są tradycyjnie zakończone NUL. Jest całkowicie uzasadnione, aby się tego spodziewać. Dlatego zwykle nie ma potrzeby omijania długości łańcucha; w strlenrazie potrzeby można to obliczyć .

Podczas przekazywania bufora , zwłaszcza takiego, do którego jest napisane, absolutnie powinieneś podać rozmiar bufora. W przypadku bufora docelowego pozwala to odbiorcy upewnić się, że nie przepełnia bufora. W przypadku bufora wejściowego pozwala to odbiorcy uniknąć odczytu poza koniec, szczególnie jeśli bufor wejściowy zawiera dowolne dane pochodzące z niezaufanego źródła.

Być może istnieje pewne zamieszanie, ponieważ zarówno łańcuchy, jak i bufory mogą być, char*i ponieważ wiele funkcji łańcuchowych generuje nowe łańcuchy, pisząc do buforów docelowych. Niektóre osoby dochodzą następnie do wniosku, że funkcje łańcuchowe powinny mieć długości łańcuchowe. Jest to jednak niedokładny wniosek. Praktyka dołączania rozmiaru z buforem (niezależnie od tego, czy bufor ten ma być używany do łańcuchów, tablic liczb całkowitych, struktur itp.) Jest bardziej przydatną i bardziej ogólną mantrą.

(W przypadku odczytu ciągu z niezaufanego źródła (np. Gniazda sieciowego) ważne jest podanie długości, ponieważ dane wejściowe mogą nie być zakończone NUL. Nie należy jednak traktować danych wejściowych jako ciągów. powinien traktować go jako dowolny bufor danych, który może zawierać ciąg znaków (ale nie wiesz, dopóki go nie zweryfikujesz), więc nadal jest to zgodne z zasadą, że bufory powinny mieć powiązane rozmiary i łańcuchy ich nie potrzebują.)

jamesdlin
źródło
Właśnie tego brakowało pytanie i inne odpowiedzi.
Blrfl,
0

Jeśli funkcje są używane głównie z literałami ciągów, ból związany z jawnymi długościami można zminimalizować poprzez zdefiniowanie niektórych makr. Na przykład, biorąc pod uwagę funkcję API:

void use_string(char *string, int length);

można zdefiniować makro:

#define use_strlit(x) use_string(x, sizeof ("" x "")-1)

a następnie wywołaj go, jak pokazano na:

void test(void)
{
  use_strlit("Hello");
}

Chociaż może być możliwe wymyślenie „kreatywnych” rzeczy do przekazania makra, które się skompiluje, ale tak naprawdę nie zadziała, użycie ""po obu stronach łańcucha w ramach oceny „sizeof” powinno wyłapać przypadkowe próby użycia znaku wskaźniki inne niż rozłożone literały łańcuchowe [w przypadku ich braku ""próba przekazania wskaźnika znaku błędnie dałaby długość jako rozmiar wskaźnika minus jeden.

Alternatywnym podejściem w C99 byłoby zdefiniowanie typu struktury „wskaźnik i długość” oraz zdefiniowanie makra, które konwertuje literał łańcuchowy na literał złożony tego typu struktury. Na przykład:

struct lstring { char const *ptr; int length; };
#define as_lstring(x) \
  (( struct lstring const) {x, sizeof("" x "")-1})

Zauważ, że jeśli zastosujemy takie podejście, należy przekazywać takie struktury pod względem wartości, a nie przekazywać ich adresy. W przeciwnym razie coś takiego:

struct lstring *p;
if (foo)
{
  p = &as_lstring("Hello");
}
else
{
  p = &as_lstring("Goodbye!");
}
use_lstring(p);

może zawieść, ponieważ czas życia literałów złożonych kończy się na końcu ich załączających instrukcji.

supercat
źródło