Le funzioni di una libreria C dovrebbero sempre aspettarsi la lunghezza di una stringa?


15

Attualmente sto lavorando a una libreria scritta in C. Molte funzioni di questa libreria prevedono una stringa come char*o const char*nei loro argomenti. Ho iniziato con quelle funzioni aspettandomi sempre la lunghezza della stringa in size_tmodo che non fosse richiesta la terminazione nulla. Tuttavia, quando si scrivono i test, ciò ha comportato un uso frequente di strlen(), in questo modo:

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

Fidarsi che l'utente passi stringhe correttamente terminate porterebbe a un codice meno sicuro, ma più conciso e (secondo me) leggibile:

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

Allora, qual è la pratica sensata qui? Rendere l'API più complicata da usare, ma costringere l'utente a pensare al proprio input o documentare il requisito per una stringa con terminazione null e fidarsi del chiamante?

Risposte:


4

Sicuramente e assolutamente portare la lunghezza in giro . La libreria C standard è stata notoriamente rotta in questo modo, il che non ha causato fine al dolore nel gestire gli overflow del buffer. Questo approccio è al centro di così tanto odio e angoscia che i compilatori moderni avvertiranno, piagnucoleranno e si lamenteranno quando usano questo tipo di funzioni standard della libreria.

È così brutto che se mai ti imbatti in questa domanda durante un'intervista - e il tuo intervistatore tecnico sembra avere qualche anno di esperienza - il puro zelo può far finire il lavoro - puoi davvero andare molto avanti se puoi citare il precedente di sparare a qualcuno che implementa API in cerca del terminatore di stringhe C.

Lasciando da parte l'emozione di tutto ciò, c'è molto che può andare storto con quel NULL alla fine della tua stringa, sia nella lettura che nella manipolazione - inoltre è davvero una violazione diretta di concetti di design moderni come la difesa in profondità (non necessariamente applicato alla sicurezza, ma alla progettazione dell'API). Esempi di API C che portano la lunghezza abbondano - es. l'API di Windows.

In realtà, questo problema è stato risolto negli anni '90, il consenso emergente di oggi è che non dovresti nemmeno toccare le corde .

Modifica successiva : questo è un dibattito piuttosto vivo, quindi aggiungerò che fidarsi di tutti sotto e sopra di te per essere gentile e usare le funzioni str * della libreria va bene, fino a quando vedi cose classiche come output = malloc(strlen(input)); strcpy(output, input);o while(*src) { *dest=transform(*src); dest++; src++; }. Riesco quasi a sentire la Lacrimosa di Mozart in sottofondo.


1
Non capisco il tuo esempio dell'API di Windows che richiede al chiamante di fornire la lunghezza delle stringhe. Ad esempio, una tipica funzione API Win32 come CreateFileaccetta un LPTCSTR lpFileNameparametro come input. Non è prevista alcuna lunghezza della stringa dal chiamante. In effetti, l'uso di stringhe con terminazione NUL è così radicato che la documentazione non menziona nemmeno che il nome del file deve essere terminato con NUL (ma ovviamente deve esserlo).
Greg Hewgill,

1
In realtà in Win32, il LPSTRtipo dice che le stringhe possono essere NUL-terminate e, in caso contrario , ciò sarà indicato nelle specifiche associate. Pertanto, se non diversamente specificato, si prevede che tali stringhe in Win32 vengano terminate con NUL.
Greg Hewgill,

Ottimo punto, ero impreciso. Considera che CreateFile e il suo gruppo sono in circolazione da Windows NT 3.1 (primi anni '90); l'attuale API (ovvero dall'introduzione di Strsafe.h in XP SP2 - con le scuse pubbliche di Microsoft) ha esplicitamente deprecato tutte le cose terminate con NULL che potrebbe. La prima volta che Microsoft si sentì davvero dispiaciuto per l'utilizzo di stringhe con terminazione NULL fu in realtà molto prima, quando dovettero introdurre il BSTR nella specifica OLE 2.0, al fine di portare in qualche modo VB, COM e il vecchio WINAPI nella stessa barca.
vski,

1
Anche StringCbCatper esempio, solo la destinazione ha un buffer massimo, il che ha senso. La sorgente è ancora una normale stringa C terminata con NUL. Forse potresti migliorare la tua risposta chiarendo la differenza tra un parametro di input e un parametro di output . I parametri di output devono sempre avere una lunghezza massima del buffer; i parametri di input sono normalmente chiusi da NUL (ci sono eccezioni, ma raro nella mia esperienza).
Greg Hewgill,

1
Sì. Le stringhe sono immutabili sia su JVM / Dalvik sia su .NET CLR a livello di piattaforma, nonché in molte altre lingue. Andrei così lontano e speculerei sul fatto che il mondo nativo non può ancora farlo (lo standard C ++ 11) a causa di a) eredità (in realtà non guadagni molto avendo solo una parte delle tue stringhe immutabili) eb ) hai davvero bisogno di un GC e di una tabella di stringhe per farlo funzionare, gli allocatori con ambito in C ++ 11 non riescono a tagliarlo.
vski,

16

In C, il linguaggio è che le stringhe di caratteri sono terminate con NUL, quindi ha senso attenersi alla pratica comune - in realtà è relativamente improbabile che gli utenti della libreria dispongano di stringhe senza terminazione NUL (poiché queste hanno bisogno di ulteriore lavoro per stampare usando printf e usalo in un altro contesto). L'uso di qualsiasi altro tipo di stringa è innaturale e probabilmente relativamente raro.

Inoltre, date le circostanze, il tuo test mi sembra un po 'strano, dal momento che per funzionare correttamente (usando strlen), stai assumendo in primo luogo una stringa terminata con NUL. Dovresti testare il caso di stringhe non terminate con NUL se intendi che la tua libreria funzioni con esse.


-1, mi dispiace, questo è semplicemente sconsigliato.
vski,

Ai vecchi tempi, questo non era sempre vero. Ho lavorato molto con i protocolli binari che inserivano i dati delle stringhe in campi a lunghezza fissa che non erano terminati NULL. In questi casi, è stato molto utile lavorare con funzioni che hanno richiesto molto tempo. Non ho mai usato C da un decennio.
Gort the Robot,

4
@vski, in che modo forzare l'utente a chiamare 'strlen' prima di chiamare la funzione target fa qualcosa per evitare problemi di overflow del buffer? Almeno se controlli tu stesso la lunghezza all'interno della funzione target puoi essere sicuro di quale senso della lunghezza viene usato (incluso il terminale null o no).
Charles E. Grant,

@Charles E. Grant: vedi il commento sopra su StringCbCat e StringCbCatN in Strsafe.h. Se hai solo un carattere * e nessuna lunghezza, allora davvero non hai altra scelta se non quella di usare le funzioni str *, ma il punto è di portare la lunghezza, quindi diventa un'opzione tra str * e strn * funzioni delle quali sono preferite queste ultime.
vski,

2
@vski Non è necessario passare per la lunghezza di una stringa . V'è la necessità di passare intorno ad un buffer di lunghezza s'. Non tutti i buffer sono stringhe e non tutte le stringhe sono buffer.
jamesdlin,

10

Il tuo argomento di "sicurezza" non regge. Se non ti fidi che l'utente ti consegni una stringa con terminazione nulla quando questo è ciò che hai documentato (e qual è "la norma" per la semplice C), non puoi davvero fidarti della lunghezza che ti danno (che probabilmente strlenlo farai usando esattamente come stai facendo se non lo hanno a portata di mano, e ciò fallirà se la "stringa" non era una stringa in primo luogo).

Esistono motivi validi per richiedere una lunghezza: se vuoi che le tue funzioni lavorino su sottostringhe, è probabilmente molto più semplice (ed efficiente) passare una lunghezza piuttosto che far fare all'utente un po 'di copia magica avanti e indietro per ottenere il byte null nel posto giusto (e rischiano errori off-by-one lungo la strada).
Essere in grado di gestire codifiche in cui i byte null non sono terminazioni o essere in grado di gestire stringhe con null incorporati (apposta) può essere utile in alcune circostanze (dipende da cosa esattamente fanno le tue funzioni).
Essere in grado di gestire dati senza terminazione null (array a lunghezza fissa) è anche utile.
In breve: dipende da cosa stai facendo nella tua libreria e dal tipo di dati che prevedi vengano gestiti dagli utenti.

C'è anche forse un aspetto prestazionale in questo. Se la tua funzione ha bisogno di conoscere in anticipo la lunghezza della stringa e ti aspetti che i tuoi utenti sappiano almeno di solito già tali informazioni, farle passare (piuttosto che calcolarle) potrebbe radere alcuni cicli.

Ma se la tua biblioteca si aspetta normali stringhe di testo ASCII normali e non hai vincoli di prestazioni lancinanti e una buona comprensione di come gli utenti interagiranno con la tua biblioteca, l'aggiunta di un parametro di lunghezza non sembra una buona idea. Se la stringa non viene terminata correttamente, è probabile che il parametro length sia altrettanto falso. Non penso che guadagnerai molto con esso.


Sono in forte disaccordo con questo approccio. Non fidarti mai dei tuoi chiamanti, specialmente dietro un'API della libreria, fai del tuo meglio per mettere in discussione le cose che ti danno e fallire con grazia. Portare la lunghezza maledetta, lavorare con stringhe con terminazione NULL non è ciò che significa "essere libero con i chiamanti e rigoroso con i tuoi callees".
vski,

2
Concordo principalmente con la tua posizione, ma sembri fidarti molto di quell'argomentazione lunga - non c'è motivo per cui dovrebbe essere affidabile del null null. La mia posizione è che dipende da ciò che fa la biblioteca.
Mat

C'è molto di più che può andare storto con il terminatore NULL nelle stringhe che con la lunghezza passata per valore. In C, l'unica ragione per cui ci si fiderebbe della lunghezza è perché sarebbe irragionevole e poco pratico non farlo - portare la lunghezza del buffer non è una buona risposta, è solo la migliore considerando le alternative. È uno dei motivi per cui le stringhe (e i buffer in generale) sono ben confezionati e incapsulati nei linguaggi RAD.
vski,

2

No. Le stringhe sono sempre nulle per definizione, la lunghezza della stringa è ridondante.

I dati dei caratteri non terminati con null non devono mai essere chiamati "string". L'elaborazione (e il lancio di lunghezze) dovrebbe in genere essere incapsulata all'interno di una libreria e non parte dell'API. Richiedere la lunghezza come parametro solo per evitare singole chiamate strlen () è probabilmente un'ottimizzazione precoce.

Fidarsi del chiamante di una funzione API non è pericoloso ; un comportamento indefinito è perfettamente accettabile se non sono soddisfatte le condizioni documentate.

Naturalmente, un'API ben progettata non dovrebbe contenere insidie ​​e dovrebbe facilitarne l'uso corretto. E questo significa solo che dovrebbe essere il più semplice e diretto possibile, evitando licenziamenti e seguendo le convenzioni del linguaggio.


non solo perfettamente ok, ma in realtà inevitabile a meno che non si passi a un linguaggio a thread singolo sicuro per la memoria. Potrebbero aver lasciato cadere altre restrizioni necessarie ...
Deduplicatore

1

Dovresti sempre mantenere la tua lunghezza. Per uno, i tuoi utenti potrebbero voler contenere NULL in essi. E in secondo luogo, non dimenticare che strlenè O (N) e richiede di toccare l'intera cache ciao ciao. E in terzo luogo, facilita il passaggio tra i sottoinsiemi, ad esempio, potrebbero fornire meno della lunghezza effettiva.


4
Se la funzione di libreria si occupa di NULL incorporati nelle stringhe deve essere ben documentato. La maggior parte delle funzioni della libreria C si interrompe a NULL o lunghezza, a seconda di quale sia la prima. (E se scritti con competenza, quelli che non impiegano molto tempo non lo usano mai strlenin un test ad anello.)
Gort the Robot

1

È necessario distinguere tra il passaggio di una stringa e il passaggio di un buffer .

In C, le stringhe sono tradizionalmente terminate con NUL. È del tutto ragionevole aspettarselo. Pertanto, di solito non è necessario passare per la lunghezza della stringa; può essere calcolato strlense necessario.

Quando si passa attorno a un buffer , in particolare a quello in cui è stato scritto, è necessario passare assolutamente la dimensione del buffer. Per un buffer di destinazione, ciò consente al chiamante di assicurarsi che non trabocchi il buffer. Per un buffer di input, consente al chiamante di evitare la lettura oltre la fine, specialmente se il buffer di input contiene dati arbitrari provenienti da una fonte non attendibile.

C'è forse un po 'di confusione perché potrebbero essere sia le stringhe che i buffer char*e perché molte funzioni di stringa generano nuove stringhe scrivendo nei buffer di destinazione. Alcune persone concludono quindi che le funzioni di stringa dovrebbero richiedere lunghezze di stringa. Tuttavia, questa è una conclusione inaccurata. La pratica di includere una dimensione con un buffer (sia che quel buffer sia usato per stringhe, matrici di numeri interi, strutture, qualunque cosa) è un mantra più utile e più generale.

(Nel caso di lettura di una stringa da una fonte non attendibile (ad esempio una presa di rete), è importante fornire una lunghezza quanto l'ingresso potrebbe non essere NUL-terminato. Tuttavia , si dovrebbe non considera l'ingresso come una stringa. È dovrebbe trattarlo come un buffer di dati arbitrario che potrebbe contenere una stringa (ma non lo sai fino a quando non lo convalidi effettivamente), quindi questo segue ancora il principio che i buffer dovrebbero avere dimensioni associate e che le stringhe non ne hanno bisogno.)


Questo è esattamente ciò che manca alla domanda e alle altre risposte.
Blrfl,

0

Se le funzioni vengono utilizzate principalmente con i letterali stringa, il dolore di gestire lunghezze esplicite può essere ridotto al minimo definendo alcune macro. Ad esempio, data una funzione API:

void use_string(char *string, int length);

si potrebbe definire una macro:

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

e quindi invocarlo come mostrato in:

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

Mentre potrebbe essere possibile trovare cose "creative" per passare quella macro che verrà compilata ma non funzionerà effettivamente, l'uso di ""su entrambi i lati della stringa nella valutazione di "sizeof" dovrebbe catturare tentativi accidentali di usare il carattere puntatori diversi dai letterali di stringa decomposti [in assenza di quelli "", un tentativo di passare un puntatore a carattere darebbe erroneamente la lunghezza della dimensione di un puntatore, meno uno.

Un approccio alternativo in C99 sarebbe quello di definire un tipo di struttura "puntatore e lunghezza" e definire una macro che converte una stringa letterale in un letterale composto di quel tipo di struttura. Per esempio:

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

Si noti che se si utilizza un tale approccio, è necessario passare tali strutture per valore anziché passare attorno ai loro indirizzi. Altrimenti qualcosa di simile:

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

potrebbe fallire poiché la vita dei letterali composti terminerebbe alla fine delle loro dichiarazioni allegate.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.