¿Las funciones de una biblioteca de C siempre deben esperar la longitud de una cadena?

14

Actualmente estoy trabajando en una biblioteca escrita en C. Muchas funciones de esta biblioteca esperan una cadena como char* o const char* en sus argumentos. Comencé con esas funciones esperando siempre la longitud de la cadena como size_t , por lo que no se requería una terminación nula. Sin embargo, al escribir las pruebas, esto resultó en el uso frecuente de strlen() , así:

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

Confiar en que el usuario pase cadenas correctamente terminadas daría lugar a un código menos seguro, pero más conciso y (en mi opinión) legible:

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

Entonces, ¿cuál es la práctica sensata aquí? Haga que la API sea más complicada de usar, pero ¿obliga al usuario a pensar en su entrada o documenta el requisito de una cadena terminada en nulo y confía en la persona que llama?

    
pregunta Benjamin Kloster 12.06.2012 - 20:42

7 respuestas

4

La longitud más absoluta y absoluta es la longitud . La biblioteca estándar de C se rompe de esta manera, lo que no ha causado ningún dolor al tratar con los desbordamientos de búfer. Este enfoque es el foco de tanto odio y angustia que los compiladores modernos realmente advertirán, se quejarán y se quejarán cuando usen este tipo de funciones de biblioteca estándar.

Es tan malo, que si alguna vez te encuentras con esta pregunta en una entrevista, y tu entrevistador técnico parece tener algunos años de experiencia, el fanatismo puro puede conseguir el trabajo. puede citar el precedente de disparar a alguien que implementa API en busca del terminador de cadena en C

.

Dejando de lado la emoción de todo esto, hay muchas cosas que pueden ir mal con ese NULL al final de la cadena, tanto en su lectura como en su manipulación, además de que es realmente una violación directa de los conceptos de diseño moderno como defensa- en profundidad (no necesariamente aplicado a la seguridad, sino al diseño de la API). Abundan los ejemplos de API de C que tienen la longitud - ej. la API de Windows.

De hecho, este problema se resolvió en algún momento de los años 90, el consenso emergente de hoy es que ni siquiera debes tocar tus cuerdas .

Edición posterior : este es un gran debate en vivo, así que agregaré que confiar en todos los que están debajo y encima de ti para ser agradable y usar las funciones de la biblioteca str * está bien, hasta que veas cosas clásicas como output = malloc(strlen(input)); strcpy(output, input); o while(*src) { *dest=transform(*src); dest++; src++; } . Casi puedo escuchar el Lacrimosa de Mozart en el fondo.

    
respondido por el vski 12.06.2012 - 21:06
17

En C, la expresión idiomática es que las cadenas de caracteres están terminadas en NUL, por lo que tiene sentido cumplir con la práctica común: en realidad es relativamente poco probable que los usuarios de la biblioteca tengan cadenas que no estén terminadas en NUL (ya que son necesarias trabajar para imprimir usando printf y usar en otro contexto). El uso de cualquier otro tipo de cadena no es natural y probablemente sea relativamente raro.

También, dadas las circunstancias, su prueba me parece un poco extraña, ya que para funcionar correctamente (usando strlen), está asumiendo una cadena terminada en NUL en primer lugar. Debería estar probando el caso de cadenas no terminadas en NUL si desea que su biblioteca trabaje con ellas.

    
respondido por el James McLeod 12.06.2012 - 20:53
10

Tu argumento de "seguridad" realmente no se sostiene. Si no confías en que el usuario te entregue una cadena terminada en nulo cuando eso es lo que documentaste (y lo que es "la norma" para la C simple), realmente no puedes confiar en la longitud que te dan (lo que harán). probablemente consiga usar strlen como lo está haciendo si no lo tienen a mano, y que fallará si la "cadena" no era una cadena en primer lugar).

Sin embargo, hay razones válidas para requerir una longitud: si desea que sus funciones trabajen en subcadenas, posiblemente sea mucho más fácil (y eficiente) pasar una longitud que hacer que el usuario haga un poco de magia de copiado para obtener el byte nulo en el lugar correcto (y corre el riesgo de errores off-by-one en el camino).
Ser capaz de manejar codificaciones donde los bytes nulos no son terminaciones, o ser capaz de manejar cadenas que tienen nulos incrustados (a propósito) puede ser útil en algunas circunstancias (depende de lo que exijan sus funciones). También es útil poder manejar datos no terminados en nulo (arreglos de longitud fija).
En resumen: depende de lo que esté haciendo en su biblioteca y del tipo de datos que espera que manejen sus usuarios.

También es posible que haya un aspecto de rendimiento en esto. Si su función necesita conocer la longitud de la cadena de antemano, y espera que sus usuarios al menos usualmente ya conozcan esa información, hacer que la pasen (en lugar de que usted la calcule) podría afeitarse unos pocos ciclos.

Pero si su biblioteca espera cadenas de texto ASCII simples y corrientes, y no tiene restricciones de rendimiento insoportables y una muy buena comprensión de cómo sus usuarios interactuarán con su biblioteca, agregar un parámetro de longitud no suena como una buena idea . Si la cadena no está terminada correctamente, es probable que el parámetro de longitud sea tan falso. No creo que ganes mucho con ello.

    
respondido por el Mat 12.06.2012 - 21:06
2

No. Las cadenas siempre terminan en nulo por definición, la longitud de la cadena es redundante.

Los datos de caracteres no terminados en nulo nunca deben llamarse "cadenas". Procesarlo (y tirar longitudes alrededor) debería generalmente estar encapsulado dentro de una biblioteca, y no formar parte de la API. Requerir la longitud como parámetro solo para evitar las llamadas strlen () es probable que sea una optimización prematura.

Confiar en el llamador de una función API no es inseguro ; el comportamiento indefinido está perfectamente bien si no se cumplen las condiciones previas documentadas.

Por supuesto, una API bien diseñada no debería contener errores y debería facilitar su uso correcto. Y esto solo significa que debe ser lo más simple y directo posible, evitando redundancias y siguiendo las convenciones del idioma.

    
respondido por el dpi 09.05.2015 - 00:55
1

Siempre debes mantener tu longitud alrededor. Por un lado, sus usuarios pueden desear contener NULL en ellos. Y en segundo lugar, no olvide que strlen es O (N) y requiere tocar toda la memoria caché de adiós. Y en tercer lugar, hace que sea más fácil pasar los subconjuntos, por ejemplo, podrían dar menos de la longitud real.

    
respondido por el DeadMG 12.06.2012 - 21:11
1

Debes distinguir entre pasar alrededor de una cadena y pasar alrededor de un búfer .

En C, las cadenas son tradicionalmente terminadas en NUL. Es completamente razonable esperar esto. Por lo tanto, generalmente no hay necesidad de pasar alrededor de la longitud de la cuerda; se puede calcular con strlen si es necesario.

Al pasar por un búfer , especialmente uno que está escrito, entonces absolutamente debe pasar el tamaño del búfer. Para un búfer de destino, esto le permite a la persona llamada asegurarse de que no desborda el búfer. Para un búfer de entrada, le permite a la persona que llama evitar leer más allá del final, especialmente si el búfer de entrada contiene datos arbitrarios que se originan de una fuente no confiable.

Quizás haya algo de confusión porque tanto las cadenas como los búferes pueden ser char* y porque muchas de las funciones de cadena generan nuevas cadenas al escribir en los búferes de destino. Algunas personas luego concluyen que las funciones de cadena deben tener longitudes de cadena. Sin embargo, esta es una conclusión inexacta. La práctica de incluir un tamaño con un búfer (ya sea que ese búfer se use para cadenas, matrices de enteros, estructuras, lo que sea) es un mantra más útil y más general.

(En el caso de leer una cadena de una fuente no confiable (por ejemplo, un socket de red), es importante proporcionar una longitud, ya que la entrada podría no estar terminada en NUL. Sin embargo , debería no considera que la entrada es una cadena. Deberías tratarla como un búfer de datos arbitrario que podría contener una cadena (pero no lo sabes hasta que realmente la validas) , así que esto sigue el principio de que los búferes deben tener tamaños asociados y que las cadenas no los necesitan.)

    
respondido por el jamesdlin 22.03.2017 - 11:40
0

Si las funciones se usan principalmente con literales de cadena, el dolor de tratar con longitudes explícitas se puede minimizar al definir algunas macros. Por ejemplo, dada una función API:

void use_string(char *string, int length);

uno podría definir una macro:

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

y luego invocarlo como se muestra en:

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

Si bien es posible idear cosas "creativas" para aprobar eso macro que compilará pero no funcionará realmente, el uso de "" en cualquiera el lado de la cadena dentro de la evaluación de "sizeof" debería detectar intentos accidentales de usar punteros de caracteres distintos de los literales de cadenas descompuestos [en ausencia de esos "" , un intento de pasar un puntero de caracteres daría erróneamente la longitud como tamaño de un puntero, menos uno.

Un enfoque alternativo en C99 sería definir un tipo de estructura de "puntero y longitud" y definir una macro que convierta un literal de cadena en un literal compuesto de ese tipo de estructura. Por ejemplo:

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

Tenga en cuenta que si uno utiliza un enfoque de este tipo, debería pasar dichas estructuras por valor en lugar de pasar por sus direcciones. De lo contrario algo como:

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

puede fallar ya que la vida útil de los literales compuestos terminaría al final de sus declaraciones adjuntas.

    
respondido por el supercat 21.03.2017 - 22:50

Lea otras preguntas en las etiquetas