¿Deben las funciones de una biblioteca C siempre esperar la longitud de una cadena?

15

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 siempre esperando la longitud de la cadena size_tpara que no se requiera una terminación nula. Sin embargo, al escribir pruebas, esto resultó en el uso frecuente de strlen(), así:

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

Confiar en el usuario para que pase cadenas terminadas correctamente conduciría 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í? ¿Hacer que la API sea más complicada de usar, pero obligar al usuario a pensar en su entrada o documentar el requisito de una cadena terminada en nulo y confiar en la persona que llama?

Benjamin Kloster
fuente

Respuestas:

4

Definitivamente y absolutamente llevan la longitud . La biblioteca estándar de C se rompe infamemente de esta manera, lo que ha causado un sinfín de dolores al tratar con 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 al usar 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 que tiene algunos años de experiencia, el fanatismo puro puede conseguir el trabajo, en realidad puedes avanzar bastante si puedes citar el precedente de disparar a alguien que implementa APIs buscando el terminador de cadena C.

Dejando a un lado la emoción de todo esto, hay muchas cosas que pueden salir mal con ese NULL al final de su cadena, tanto al leerlo como al manipularlo, además de que está realmente en violación directa de los conceptos de diseño modernos, como la defensa en profundidad. (no necesariamente se aplica a la seguridad, sino al diseño de la API). Los ejemplos de API de C que llevan la longitud abundan - 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 hilos .

Edición posterior : este es un debate en vivo, así que agregaré que confiar en que todos los que están debajo y encima de ti sean amables y usen las funciones str * de la biblioteca 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 la Lacrimosa de Mozart en el fondo.

vski
fuente
1
No entiendo su ejemplo de la API de Windows que requiere que la persona que llama proporcione la longitud de las cadenas. Por ejemplo, una función típica de la API Win32, como CreateFiletoma un LPTCSTR lpFileNameparámetro como entrada. No se espera ninguna longitud de la cadena de la persona que llama. De hecho, el uso de cadenas terminadas en NUL está tan arraigado que la documentación ni siquiera menciona que el nombre del archivo debe estar terminado en NUL (pero, por supuesto, debe estarlo).
Greg Hewgill
1
En realidad, en Win32, el LPSTRtipo dice que las cadenas pueden tener terminación NUL, y si no , eso se indicará en la especificación asociada. Entonces, a menos que se indique específicamente lo contrario, se espera que tales cadenas en Win32 terminen en NUL.
Greg Hewgill
Gran punto, fui impreciso. Considere que CreateFile y su grupo existen desde Windows NT 3.1 (principios de los 90); la API actual (es decir, desde la introducción de Strsafe.h en XP SP2, con las disculpas públicas de Microsoft) desaprobó explícitamente todas las cosas terminadas en NULL que pudo. La primera vez que Microsoft sintió lástima por usar cadenas terminadas en NULL fue en realidad mucho antes, cuando tuvieron que introducir el BSTR en la especificación OLE 2.0, para traer de alguna manera VB, COM y el antiguo WINAPI en el mismo barco.
vski
1
Incluso, StringCbCatpor ejemplo, solo el destino tiene un búfer máximo, lo que tiene sentido. La fuente sigue siendo una cadena C común terminada en NUL. Quizás podría mejorar su respuesta aclarando la diferencia entre un parámetro de entrada y un parámetro de salida . Los parámetros de salida siempre deben tener una longitud máxima de búfer; Los parámetros de entrada generalmente están terminados en NUL (hay excepciones, pero es raro en mi experiencia).
Greg Hewgill
1
Si. Las cadenas son inmutables tanto en JVM / Dalvik como en .NET CLR a nivel de plataforma, así como en muchos otros idiomas. Llegaría tan lejos y especularía que el mundo nativo todavía no puede hacer esto (el estándar C ++ 11) debido a a) legado (realmente no gana tanto al tener solo una parte de sus cadenas inmutables) yb ) realmente necesita un GC y una tabla de cadenas para que esto funcione, los asignadores de ámbito en C ++ 11 no pueden cortarlo.
vski
16

En C, el idioma 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 sin terminación NUL (ya que necesitan trabajo adicional para imprimir usando printf y uso en otro contexto). Usar cualquier otro tipo de cadena no es natural y probablemente sea relativamente raro.

Además, bajo las circunstancias, su prueba me parece un poco extraña, ya que para funcionar correctamente (usando strlen), en primer lugar, está asumiendo una cadena terminada en NUL. Debería probar el caso de cadenas sin terminación NUL si desea que su biblioteca trabaje con ellas.

James McLeod
fuente
-1, lo siento, esto es simplemente mal aconsejado.
vski
En los viejos tiempos, esto no siempre era cierto. Trabajé mucho con protocolos binarios que pusieron datos de cadena en campos de longitud fija que no estaban terminados en NULL. En tales casos, era muy difícil trabajar con funciones que tomaban mucho tiempo. Sin embargo, no he hecho C en una década.
Gort the Robot
44
@vski, ¿de qué manera obligar al usuario a llamar 'strlen' antes de llamar a la función de destino hace algo para evitar problemas de desbordamiento del búfer? Al menos si comprueba la longitud usted mismo dentro de la función de destino, puede estar seguro de qué sentido de longitud se está utilizando (incluyendo terminal nulo o no).
Charles E. Grant
@Charles E. Grant: Vea el comentario anterior sobre StringCbCat y StringCbCatN en Strsafe.h. Si solo tiene un char * y no tiene longitud, entonces no tiene más remedio que usar las funciones str *, pero el objetivo es llevar la longitud, por lo que se convierte en una opción entre str * y strn * funciones de las cuales se prefieren estas últimas.
vski
2
@vski No hay necesidad de pasar la longitud de una cadena . No es necesario pasar alrededor de un búfer de longitud 's. No todas las memorias intermedias son cadenas, y no todas las cadenas son memorias intermedias.
jamesdlin
10

Su argumento de "seguridad" realmente no es válido. Si no confías en el usuario para que te entregue una cadena terminada en nulo cuando eso es lo que documentaste (y cuál es "la norma" para C simple), tampoco puedes confiar en la longitud que te darán (que probablemente lo usen strlencomo lo están haciendo si no lo tienen a mano, y eso fallará si la "cadena" no era una cadena en primer lugar).

Sin embargo, existen razones válidas para exigir una longitud: si desea que sus funciones funcionen 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 copia de un lado a otro para obtener el byte nulo en el lugar correcto (y corre el riesgo de errores ocasionales 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 hagan exactamente sus funciones).
También es útil poder manejar datos no terminados en nulo (matrices de longitud fija).
En resumen: depende de lo que esté haciendo en su biblioteca y de qué tipo de datos espera que manejen sus usuarios.

Posiblemente también haya un aspecto de rendimiento en esto. Si su función necesita conocer de antemano la longitud de la cadena y espera que sus usuarios al menos ya conozcan esa información, hacer que la pasen (en lugar de calcularla) podría reducir algunos ciclos.

Pero si su biblioteca espera cadenas de texto ASCII simples y ordinarias, 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 parece una buena idea. Si la cadena no se termina correctamente, es probable que el parámetro de longitud sea igual de falso. No creo que ganes mucho con eso.

Estera
fuente
Muy en desacuerdo con este enfoque. Nunca confíes en las personas que llaman, especialmente detrás de una API de biblioteca, haz tu mejor esfuerzo para cuestionar las cosas que te dan y fallar con gracia. Lleve el largo zurcido, trabajar con cuerdas terminadas en NULL no es lo que significa "ser suelto con sus llamantes y estricto con sus llamadas".
vski
2
Estoy de acuerdo principalmente con su posición, pero parece que confía mucho en ese argumento extenso: no hay razón para que sea confiable que el terminador nulo. Mi posición es que depende de lo que haga la biblioteca.
Mat
Hay mucho más que puede salir mal con el terminador NULL en cadenas que con la longitud pasada por valor. En C, la única razón por la que uno confiaría en la longitud es porque sería irrazonable y poco práctico no hacerlo: llevar la longitud del búfer no es una buena respuesta, es la mejor teniendo en cuenta las alternativas. Es una de las razones por las que las cadenas (y los buffers en general) están perfectamente empaquetados y encapsulados en lenguajes RAD.
vski
2

No. Las cadenas siempre tienen terminación nula por definición, la longitud de la cadena es redundante.

Los datos de caracteres no terminados en nulo nunca deben llamarse una "cadena". Procesarlo (y arrojar longitudes) generalmente debe encapsularse dentro de una biblioteca y no formar parte de la API. Requerir la longitud como parámetro solo para evitar 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 trampas 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 lenguaje.

ppp
fuente
no solo está perfectamente bien, sino que es inevitable a menos que uno se mueva a un lenguaje de un solo subproceso seguro para la memoria. Podría haber eliminado algunas restricciones más necesarias ...
Deduplicator
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 strlenes O (N) y requiere tocar todo el caché de bye de cadena. Y en tercer lugar, facilita el paso de subconjuntos; por ejemplo, podrían dar menos de la longitud real.

DeadMG
fuente
44
Si la función de biblioteca trata con NULL incrustados en cadenas debe estar muy bien documentado. La mayoría de las funciones de la biblioteca C se detienen en NULL o length, lo que ocurra primero. (Y si se escriben de manera competente, los que no toman mucho tiempo nunca se usan strlenen una prueba de bucle.)
Gort the Robot
1

Debe distinguir entre pasar una cadena y pasar 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 la longitud de la cadena; se puede calcular con strlensi es necesario.

Al pasar alrededor de un búfer , especialmente uno en el que está escrito, entonces absolutamente debe pasar el tamaño del búfer. Para un búfer de destino, esto permite a la persona que llama asegurarse de que no desborde el búfer. Para un búfer de entrada, permite que la persona que llama evite leer más allá del final, especialmente si el búfer de entrada contiene datos arbitrarios procedentes de una fuente no confiable.

Quizás haya cierta confusión porque las cadenas y los buffers podrían serlo char*y porque muchas funciones de cadena generan nuevas cadenas al escribir en los buffers de destino. Algunas personas concluyen que las funciones de cadena deben tomar 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 la lectura de una cadena a partir de una fuente no confiable (por ejemplo, un conector de red), es importante para suministrar una longitud ya que no podría ser terminado NUL-la entrada. Sin embargo , usted debe no tenemos en cuenta la entrada a ser una cadena. Usted debe tratarlo como un búfer de datos arbitrario que podría contener una cadena (pero no lo sabrá hasta que realmente lo valide), por lo que esto sigue el principio de que los búferes deben tener tamaños asociados y que las cadenas no los necesitan).

jamesdlin
fuente
Esto es exactamente lo que se perdió la pregunta y otras respuestas.
Blrfl
0

Si las funciones se usan principalmente con literales de cadena, el dolor de tratar con longitudes explícitas se puede minimizar definiendo 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 llegar a cosas "creativas" para pasar esa macro que se compilará pero que en realidad no funcionará, el uso de ""cualquier lado de la cadena dentro de la evaluación de "sizeof" debería detectar intentos accidentales de usar caracteres punteros que no sean literales de cadena descompuestos [en ausencia de ellos "", un intento de pasar un puntero de caracteres daría erróneamente la longitud como el 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 usa dicho enfoque, debe pasar dichas estructuras por valor en lugar de pasar 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 en los extremos de sus declaraciones adjuntas.

Super gato
fuente