Sollten Funktionen einer C-Bibliothek immer die Länge eines Strings erwarten?

15

Ich arbeite derzeit an einer Bibliothek, die in C geschrieben ist. Viele Funktionen dieser Bibliothek erwarten einen String als char*oder const char*in ihren Argumenten. Ich habe mit diesen Funktionen begonnen und immer die Länge des Strings als erwartet, size_tso dass keine Null-Terminierung erforderlich war. Beim Schreiben von Tests wurde jedoch häufig Folgendes verwendet strlen():

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

Das Vertrauen des Benutzers, ordnungsgemäß terminierte Zeichenfolgen zu übergeben, würde zu weniger sicherem, aber präziserem und (meiner Meinung nach) lesbarem Code führen:

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

Also, was ist die vernünftige Praxis hier? Machen Sie die Verwendung der API komplizierter, aber zwingen Sie den Benutzer, über ihre Eingaben nachzudenken, oder dokumentieren Sie die Anforderung einer nullterminierten Zeichenfolge und vertrauen Sie dem Aufrufer?

Benjamin Kloster
quelle

Antworten:

4

Tragen Sie auf jeden Fall und absolut die Länge herum . Die Standard-C-Bibliothek wird auf diese Weise infamös zerstört, was beim Umgang mit Pufferüberläufen keine Schmerzen verursacht hat. Dieser Ansatz ist der Mittelpunkt von so viel Hass und Angst, dass moderne Compiler tatsächlich warnen, jammern und sich beschweren, wenn sie diese Art von Standardbibliotheksfunktionen verwenden.

Es ist so schlimm, dass Sie, wenn Sie bei einem Interview auf diese Frage stoßen - und Ihr technischer Interviewer sieht aus, als hätte er ein paar Jahre Erfahrung -, dass pure Eifersucht vielleicht den Job erobert -, tatsächlich ziemlich weit kommen können, wenn Sie das zitieren können aufschiebenden Schießen jemand Umsetzung APIs für den C - String - Terminator suchen.

Abgesehen von den Emotionen kann mit diesem NULL-Wert am Ende des Strings viel schief gehen, sowohl beim Lesen als auch beim Manipulieren. Außerdem ist dies eine direkte Verletzung moderner Designkonzepte wie der Tiefenverteidigung (Dies gilt nicht unbedingt für die Sicherheit, sondern für das API-Design.) Beispiele für C-APIs, die die Länge im Überfluss tragen - z. die Windows-API.

Tatsächlich wurde dieses Problem irgendwann in den 90er Jahren gelöst. Der heutige Konsens ist, dass Sie nicht einmal Ihre Saiten berühren sollten .

Später bearbeiten : Dies ist eine ziemliche Live-Debatte. Daher möchte ich hinzufügen, dass es in Ordnung ist, allen unter und über Ihnen zu vertrauen, dass sie nett sind und die Funktionen der Bibliothek str * verwenden, bis Sie klassische Dinge wie output = malloc(strlen(input)); strcpy(output, input);oder sehen while(*src) { *dest=transform(*src); dest++; src++; }. Im Hintergrund ist fast Mozarts Lacrimosa zu hören.

vski
quelle
1
Ich verstehe Ihr Beispiel für die Windows-API nicht, bei der der Aufrufer die Länge der Zeichenfolgen angeben muss. Beispielsweise verwendet eine typische Win32-API-Funktion wie CreateFileeinen LPTCSTR lpFileNameParameter als Eingabe. Vom Aufrufer wird keine Länge der Zeichenfolge erwartet. Tatsächlich ist die Verwendung von mit NUL abgeschlossenen Zeichenfolgen so tief verwurzelt, dass in der Dokumentation nicht einmal erwähnt wird, dass der Dateiname mit NUL abgeschlossen sein muss (aber natürlich muss dies der Fall sein).
Greg Hewgill
1
Eigentlich in Win32, der LPSTRTyp sagt , dass Strings kann NUL-terminiert sein, und wenn nicht , wird diese in der zugehörigen Beschreibung angegeben werden. Sofern nicht ausdrücklich anders angegeben, wird erwartet, dass solche Zeichenfolgen in Win32 NUL-terminiert sind.
Greg Hewgill
Toller Punkt, ich war ungenau. Bedenken Sie, dass CreateFile und seine Produkte seit Windows NT 3.1 (Anfang der 90er Jahre) verfügbar sind. Die aktuelle API (dh seit der Einführung von Strsafe.h in XP SP2 - mit der öffentlichen Entschuldigung von Microsoft) hat alle NULL-terminierten Inhalte explizit verworfen. Das erste Mal, dass Microsoft die Verwendung von Strings mit NULL-Endung wirklich leid tat, war tatsächlich viel früher, als sie das BSTR in der OLE 2.0-Spezifikation einführen mussten, um VB, COM und das alte WINAPI irgendwie in dasselbe Boot zu bringen.
vski
1
Selbst in StringCbCatzum Beispiel hat nur das Ziel einen maximalen Puffer, was Sinn macht. Die Quelle ist immer noch eine gewöhnliche NUL-terminierte C-Zeichenfolge. Vielleicht können Sie Ihre Antwort verbessern, indem Sie den Unterschied zwischen einem Eingabeparameter und einem Ausgabeparameter verdeutlichen . Ausgabeparameter sollten immer eine maximale Pufferlänge haben. Eingabeparameter sind normalerweise NUL-terminiert (es gibt Ausnahmen, aber meiner Erfahrung nach selten).
Greg Hewgill
1
Ja. Zeichenfolgen sind sowohl auf der JVM / Dalvik- als auch auf der .NET-CLR-Plattformebene sowie in vielen anderen Sprachen unveränderlich. Ich würde so weit gehen und spekulieren, dass die einheimische Welt dies noch nicht ganz kann (der C ++ 11-Standard), weil a) das Erbe (Sie gewinnen nicht wirklich viel, wenn Sie nur einen Teil Ihrer Zeichenfolgen unveränderlich haben) und b ) Sie brauchen wirklich einen GC und eine String-Tabelle, damit dies funktioniert.
vski
16

In C ist die Redewendung, dass Zeichenketten NUL-terminiert sind. Daher ist es sinnvoll, sich an die gängige Praxis zu halten. Es ist relativ unwahrscheinlich, dass Benutzer der Bibliothek nicht NUL-terminierte Zeichenketten haben (da diese zusätzliche Arbeit zum Drucken benötigen) Verwendung von printf und Verwendung in einem anderen Kontext). Die Verwendung einer anderen Saite ist unnatürlich und wahrscheinlich relativ selten.

Unter diesen Umständen erscheint mir Ihr Test auch etwas seltsam, da Sie für die korrekte Funktion (mit strlen) zunächst von einem NUL-terminierten String ausgehen. Sie sollten den Fall von nicht NUL-terminierten Zeichenfolgen testen, wenn Sie beabsichtigen, dass Ihre Bibliothek damit arbeitet.

James McLeod
quelle
-1, tut mir leid, das ist einfach schlecht beraten.
vski
Früher stimmte das nicht immer. Ich habe viel mit binären Protokollen gearbeitet, die Zeichenfolgendaten in Felder mit fester Länge setzen, die nicht mit NULL abgeschlossen wurden. In solchen Fällen war es sehr umständlich, mit Funktionen zu arbeiten, die viel Zeit in Anspruch nahmen. Ich habe aber seit einem Jahrzehnt kein C mehr gemacht.
Gort the Robot
4
@vski, wie kann der Benutzer gezwungen werden, 'strlen' aufzurufen, bevor die Zielfunktion aufgerufen wird, um Probleme mit dem Pufferüberlauf zu vermeiden? Zumindest wenn Sie die Länge innerhalb der Zielfunktion selbst überprüfen, können Sie sicher sein, welches Längenmaß verwendet wird (einschließlich Terminal null oder nicht).
Charles E. Grant
@Charles E. Grant: Siehe obigen Kommentar zu StringCbCat und StringCbCatN in Strsafe.h. Wenn Sie nur ein Zeichen * und keine Länge haben, haben Sie in der Tat keine andere Wahl, als die str * -Funktionen zu verwenden, aber der Punkt ist, die Länge herumzutragen, so dass es eine Option zwischen str * und strn * wird. Funktionen, von denen letztere bevorzugt sind.
Vski
2
@vski Es ist nicht erforderlich, die Länge einer Zeichenfolge zu übergeben . Es ist eine Notwendigkeit , eine passieren um Puffer ‚s Länge. Nicht alle Puffer sind Zeichenfolgen, und nicht alle Zeichenfolgen sind Puffer.
Jamesdlin
10

Dein "Sicherheits" -Argument ist nicht wirklich gültig. Wenn Sie dem Benutzer nicht vertrauen, dass er Ihnen eine nullterminierte Zeichenfolge übergibt, wenn Sie dies dokumentiert haben (und was "die Norm" für einfaches C ist), können Sie der Länge, die sie Ihnen geben, auch nicht wirklich vertrauen (was sie auch tun werden) Wahrscheinlich kommen strlenSie damit zurecht, wie Sie es tun, wenn sie es nicht zur Hand haben.

Es gibt triftige Gründe, eine Länge zu fordern: Wenn Sie möchten, dass Ihre Funktionen mit Teilzeichenfolgen arbeiten, ist es möglicherweise viel einfacher (und effizienter), eine Länge zu übergeben, als dass der Benutzer etwas hin und her kopieren muss, um das Null-Byte zu erhalten am richtigen Ort (und riskieren dabei einzelne Fehler).
Die Möglichkeit, Codierungen zu verarbeiten, bei denen Nullbytes keine Abschlusszeichen sind, oder Zeichenfolgen mit eingebetteten Nullen (absichtlich), kann unter bestimmten Umständen hilfreich sein (hängt davon ab, was genau Ihre Funktionen tun).
Es ist auch praktisch, Daten (Arrays fester Länge) verarbeiten zu können, die nicht mit Nullen terminiert sind.
Kurz gesagt: Hängt davon ab, was Sie in Ihrer Bibliothek tun und welche Art von Daten Sie von Ihren Benutzern erwarten.

Dies hat möglicherweise auch einen Leistungsaspekt. Wenn Ihre Funktion die Länge der Zeichenfolge im Voraus kennen muss und Sie erwarten, dass Ihre Benutzer diese Informationen zumindest normalerweise bereits kennen, kann es einige Zyklen ersparen, wenn sie diese Informationen weitergeben (anstatt sie zu berechnen).

Wenn Ihre Bibliothek jedoch normale reine ASCII-Textzeichenfolgen erwartet und Sie keine übermäßigen Leistungseinschränkungen und ein sehr gutes Verständnis für die Interaktion Ihrer Benutzer mit Ihrer Bibliothek haben, ist das Hinzufügen eines Längenparameters keine gute Idee. Wenn der String nicht richtig terminiert ist, ist der Längenparameter wahrscheinlich genauso falsch. Ich denke nicht, dass Sie viel damit gewinnen werden.

Matte
quelle
Stimme diesem Ansatz überhaupt nicht zu. Vertraue niemals deinen Anrufern, besonders nicht hinter einer Bibliotheks-API. Bemühe dich nach Kräften, die Dinge, die sie dir geben, in Frage zu stellen und versage mit Würde. Tragen Sie die verdammte Länge, das Arbeiten mit NULL-terminierten Zeichenfolgen ist nicht das, was "mit Ihren Anrufern locker und mit Ihren Anrufen streng sein" bedeutet.
Vski
2
Ich stimme größtenteils Ihrer Position zu, aber Sie scheinen diesem Argument der Länge sehr zu vertrauen - es gibt keinen Grund, warum es als Nullterminator zuverlässig sein sollte. Meine Position ist, dass es davon abhängt, was die Bibliothek tut.
Mat
Es gibt viel mehr Probleme mit dem NULL-Terminator in Strings als mit der als Wert übergebenen Länge. In C ist der einzige Grund, warum man der Länge vertrauen würde, der, dass es unangemessen und unpraktisch wäre, keine Pufferlänge zu tragen - eine gute Antwort, die in Anbetracht der Alternativen einfach die beste ist. Dies ist einer der Gründe, warum Zeichenfolgen (und Puffer im Allgemeinen) ordentlich gepackt und in RAD-Sprachen eingekapselt sind.
Vski
2

Nein. Strings sind per Definition immer nullterminiert, die Stringlänge ist redundant.

Nicht nullterminierte Zeichendaten sollten niemals als "Zeichenfolge" bezeichnet werden. Die Verarbeitung (und das Herumwerfen von Längen) sollte normalerweise in einer Bibliothek gekapselt werden und nicht Teil der API. Es ist wahrscheinlich, dass die Länge als Parameter erforderlich ist, um einzelne strlen () -Aufrufe zu vermeiden.

Es ist nicht unsicher , dem Aufrufer einer API-Funktion zu vertrauen . undefiniertes Verhalten ist völlig in Ordnung, wenn dokumentierte Voraussetzungen nicht erfüllt sind.

Natürlich sollte eine gut gestaltete API keine Fallstricke enthalten und die korrekte Verwendung erleichtern. Und das bedeutet nur, dass es so einfach und unkompliziert wie möglich sein sollte, Redundanzen vermieden werden und die Konventionen der Sprache befolgt werden.

dpi
quelle
nicht nur vollkommen in Ordnung, sondern auch unvermeidlich, es sei denn, man wechselt zu einer speichersicheren Single-Thread-Sprache. Vielleicht ein paar mehr notwendig Einschränkungen fallen gelassen haben ...
Deduplicator
1

Sie sollten immer Ihre Länge um halten. Zum einen möchten Ihre Benutzer möglicherweise NULL-Werte enthalten. Und zweitens: Vergessen Sie nicht, dass dies strlenO (N) ist und dass Sie den gesamten String-Bye-Bye-Cache berühren müssen. Und drittens erleichtert dies das Weitergeben von Teilmengen - sie könnten beispielsweise weniger als die tatsächliche Länge ergeben.

DeadMG
quelle
4
Ob die Bibliotheksfunktion mit eingebetteten NULL-Werten in Strings umgeht, muss sehr gut dokumentiert werden. Die meisten C-Bibliotheksfunktionen enden bei NULL oder der Länge, je nachdem, was zuerst eintritt. (Und wenn sie kompetent geschrieben sind, werden diejenigen, die keine Länge haben, niemals strlenin einem Schleifentest verwendet.)
Gort the Robot,
1

Sie sollten zwischen dem Umgehen eines Strings und dem Umgehen eines Puffers unterscheiden .

In C werden Strings traditionell mit NUL terminiert. Es ist völlig vernünftig, dies zu erwarten. Daher ist es normalerweise nicht erforderlich, die Länge der Zeichenfolge zu überschreiten. es kann mit berechnet werdenstrlen werden.

Beim Umfahren eines Puffers , insbesondere einen, in den geschrieben wird, sollten Sie die Puffergröße unbedingt mitgeben. Bei einem Zielpuffer kann der Angerufene so sicherstellen, dass der Puffer nicht überläuft. Bei einem Eingabepuffer kann der Angerufene vermeiden, über das Ende hinaus zu lesen, insbesondere wenn der Eingabepuffer beliebige Daten enthält, die von einer nicht vertrauenswürdigen Quelle stammen.

Es besteht möglicherweise eine gewisse Verwirrung, da sowohl Zeichenfolgen als auch Puffer vorhanden sein können char*und viele Zeichenfolgenfunktionen neue Zeichenfolgen erzeugen, indem sie in Zielpuffer schreiben. Einige Leute schließen daraus, dass String-Funktionen String-Längen annehmen sollten. Dies ist jedoch eine ungenaue Schlussfolgerung. Das Einfügen einer Größe in einen Puffer (unabhängig davon, ob dieser Puffer für Zeichenfolgen, Arrays von Ganzzahlen oder Strukturen verwendet wird) ist ein nützlicheres und allgemeineres Mantra.

(Im Falle eines Strings aus einer nicht vertrauenswürdigen Quelle zu lesen (zB eine Netzwerkbuchse), ist es wichtig , eine Länge zu liefern , da der Eingang möglicherweise nicht NUL-terminiert werden. Allerdings sollten Sie nicht die Eingabe betrachten eine Zeichenfolge sein. Sie sollte es als willkürlicher Datenpuffer behandeln, der eine Zeichenfolge enthalten könnte (aber Sie wissen es nicht, bis Sie es tatsächlich validieren), daher folgt dies weiterhin dem Prinzip, dass Puffer zugeordnete Größen haben sollten und dass Zeichenfolgen sie nicht benötigen.)

jamesdlin
quelle
Genau das haben die Frage und andere Antworten verpasst.
Blrfl
0

Wenn Funktionen hauptsächlich mit String-Literalen verwendet werden, kann der Aufwand für den Umgang mit expliziten Längen durch das Definieren einiger Makros minimiert werden. Beispiel für eine gegebene API-Funktion:

void use_string(char *string, int length);

man könnte ein Makro definieren:

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

und rufen Sie es dann wie folgt auf:

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

Während es möglich sein mag, "kreative" Dinge zu entwickeln, die das zu kompilierende Makro übergehen, aber nicht funktionieren, sollte die Verwendung von ""auf beiden Seiten der Zeichenkette bei der Bewertung von "sizeof" versehentliche Versuche zur Verwendung von Zeichen auffangen andere Zeiger als zerlegte String-Literale [ ""Ohne diese würde ein Versuch, einen Zeichenzeiger zu übergeben, fälschlicherweise die Länge als Größe eines Zeigers minus eins angeben.

Ein alternativer Ansatz in C99 wäre, einen Strukturtyp "Zeiger und Länge" zu definieren und ein Makro zu definieren, das ein Zeichenfolgenliteral in ein zusammengesetztes Literal dieses Strukturtyps umwandelt. Beispielsweise:

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

Beachten Sie, dass Sie bei Verwendung eines solchen Ansatzes solche Strukturen als Wert übergeben sollten, anstatt ihre Adressen weiterzugeben. Ansonsten so etwas wie:

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

kann fehlschlagen, da die Lebensdauer von zusammengesetzten Literalen am Ende ihrer beigefügten Anweisungen enden würde.

Superkatze
quelle