2011-01-02 15 views
20

Estoy comenzando un nuevo proyecto en C simple (c99) que va a funcionar principalmente con texto. Debido a las limitaciones del proyecto externo, este código tiene que ser extremadamente simple y compacto, que consiste en un solo archivo de código fuente sin dependencias externas o bibliotecas a excepción de libc y bibliotecas de sistema ubicuas similares.Prácticas de manejo de cadenas en C

Con este conocimiento, ¿cuáles son algunas de las mejores prácticas, trampas, trucos u otras técnicas que pueden ayudar a que el manejo del proyecto sea más robusto y seguro?

+11

¿Usar C en primer lugar cuenta como un gotcha? '/ * ouch, ¡para! ¡es una broma! */' – delnan

+0

¿Desea utilizar' char * 'o caracteres anchos? – anatolyg

+2

@anatolyg: ¿Por qué alguien querría usar caracteres anchos? Uhg. –

Respuesta

30

sin ninguna información adicional acerca de lo que su código está haciendo, yo recomendaría el diseño de todas sus interfaces como este:

size_t foobar(char *dest, size_t buf_size, /* operands here */) 

con la semántica como snprintf:

  • dest puntos a un buffer de tamaño al menos buf_size.
  • Si buf_size es cero, los punteros nulos/no válidos son aceptables para dest y no se escribirá nada.
  • Si buf_size es distinto de cero, dest es siempre nulo-terminado.
  • Cada función foobar devuelve la longitud de la salida completa no truncada; la salida se ha truncado si buf_size es menor o igual que el valor de retorno.

De esta forma, cuando la persona que llama puede conocer fácilmente el tamaño del búfer de destino que se requiere, se puede obtener un búfer lo suficientemente grande por adelantado. Si la persona que llama no puede saber fácilmente, puede llamar a la función una vez con un argumento cero para buf_size, o con un búfer que sea "probablemente lo suficientemente grande" y solo vuelva a intentarlo si se quedó sin espacio.

También puede hacer una versión envolvente de esas llamadas análoga a la función GNU asprintf, pero si desea que su código sea lo más flexible posible, evitaría hacer cualquier asignación en las funciones de cadena reales. Manejar la posibilidad de falla siempre es más fácil a nivel del llamador, y muchos llamantes pueden asegurarse de que la falla nunca sea posible usando un búfer local o un búfer que se obtuvo mucho antes en el programa para que el éxito o el fracaso de una operación mayor es atómico (lo que simplifica enormemente el manejo de errores).

2

dos centavos:

  1. Utilice siempre el "n" versión de las funciones de cadena: strncpy, strncmp, (o wcsncpy, wcsncmp etc.)
  2. Siempre asignar utilizando el 1 idioma: por ejemplo, char * str [MAX_STR_SIZE + 1], y luego pase MAX_STR_SIZE como el tamaño de la "n" versión de las funciones de cadena y termine con str [MAX_STR_SIZE] = '\ 0'; para asegurarse de que todas las cadenas estén finalizadas correctamente.

El último paso es importante ya que la "n" versión de las funciones de cadena no agregará '\ 0' después de copiar si se alcanzó el tamaño máximo.

+6

-1 s/always/never/for 'strncpy'. Esta función no hace lo que las personas piensan que hace, y casi nunca es útil. –

+0

@R ..: ¿Qué es exactamente lo que crees que está haciendo? ¿Puedes dar un ejemplo concreto? – hillel

+0

Afortunadamente, podemos echar un vistazo a la fuente de la biblioteca para ver lo que realmente está haciendo: http://git.uclibc.org/uClibc/tree/libc/string/strncpy.c. Como todo el resto de C, me parece bien siempre que entiendas que tu cadena DEBE terminar con cero y sigues el consejo de hillel. +1. –

7

Eche un vistazo a strlcpy y strlcat, consulte el original paper para obtener más información.

+5

La concatenación es casi siempre una mala expresión (en términos de seguridad, rendimiento y complejidad). –

0
  • trabajo con matrices en la pila siempre que sea posible e inicializar correctamente. No tiene que hacer un seguimiento de las asignaciones, tamaños e inicializaciones.

    char myCopy[] = { "the interesting string" }; 
    
  • Para cadenas de tamaño medio C99 tiene VLA. Son un poco menos utilizables ya que no puede inicializarlos. Pero todavía tiene las dos primeras ventajas anteriores de .

    char myBuffer[n]; 
    myBuffer[0] = '\0'; 
    
+6

VLA es un ** desbordamiento de pila ** esperando a suceder. (El peor tipo de SO, no del tipo bueno que hace su tarea para usted. :-) –

+0

@R, sí, si uno hace mal uso de la función. Pero hay usos apropiados, con las verificaciones necesarias. Después de todo, tampoco harías un 'malloc' desde la entrada del usuario sin marcar. –

+0

@Matthew: El único buen uso para VLA que he encontrado requería alguna invención seria, y sigue siendo solo una clase teórica de problemas: problemas recursivos donde se sabe que el uso total de memoria en todos los niveles de recursión es 'O (n) 'pero donde cualquier nivel puede usar hasta' C * n' espacio (obviamente, solo un número finito de niveles realmente podría usar tanto). VLA es la única herramienta que lo hace posible sin 'malloc', suponiendo que' O (n^2) 'no cabe en la pila. Pero para cada caso del mundo real que he visto, una dimensión constante moderadamente grande es tan buena como VLA. –

0

también algunas precauciones importantes son:

  • En C, no hay ninguna relación en absoluto entre la longitud de cadena y el tamaño de búfer. Una cadena siempre se ejecuta hasta (e incluye) el primer '\0' -caracter. Es su responsabilidad como programador asegurarse de que este carácter se encuentre dentro del buffer reservado para esa cadena.
  • Siempre haga un seguimiento explícito de los tamaños del búfer. El compilador realiza un seguimiento del tamaño de las matrices, pero esa información se perderá antes de que te des cuenta.
10

Algunos pensamientos de un desarrollador de largo plazo incrustado, la mayoría de los cuales elaborada en su requisito para la simplicidad y no son C-específica:

  • Decidir qué cadena de manejo de las funciones que necesita, y mantener ese conjunto lo más pequeño posible para minimizar los puntos de falla.

  • Siga la sugerencia de R. para definir una interfaz clara que sea coherente en todos los manejadores de cadenas. Un conjunto de reglas estricto, pequeño pero detallado, le permite utilizar la coincidencia de patrones como una herramienta de depuración: puede sospechar de cualquier código que se vea diferente al resto.

  • Como notó Bart van Ingen Schenau, rastree la longitud del búfer independientemente de la longitud de la cuerda. Si va a siempre trabajando con texto, es seguro usar el carácter nulo estándar para indicar el final de la cadena, pero depende de usted asegurarse de que el texto + nulo se ajuste al búfer.

  • garantizar un comportamiento coherente en todos los controladores de cadena, en particular cuando se carece de las funciones estándar: truncamiento, entradas nulas, null de terminación, relleno, etc .

  • Si es absolutamente necesario que infrinja alguna de sus reglas, cree una función separada para ese fin y asígnele un nombre apropiado. En otras palabras, otorgue a cada función un único comportamiento inequívoco. Por lo tanto, puede usar str_copy_and_pad() para una función que siempre atenúa su objetivo con valores nulos.

  • Siempre que sea posible, el uso seguro funciones integradas (por ejemplo memmove() por Jonathan Leffler) para hacer el trabajo pesado. ¡Pero pruébalos para asegurarte de que están haciendo lo que crees que están haciendo!

  • Compruebe errores lo antes posible.Los desbordamientos de búfer no detectados pueden provocar errores de "rebote" que son notoriamente difíciles de localizar.

  • Escribir pruebas para cada función para garantizar que cumpla con su contrato. Asegúrese de cubrir las mayúsculas (desactivado en 1, cadenas nulas/vacías, superposición de origen/destino, , etc.) Y esto puede sonar obvio, pero asegúrese de comprender cómo crear y detectar un desbordamiento/desbordamiento de memoria intermedia, luego escribir pruebas que generen explícitamente y verifiquen esos problemas. (Mi gente de control de calidad son probablemente harto de escuchar mis instrucciones al pie "no se limite a una prueba para asegurarse de que funciona;. De prueba para asegurarse de que no se rompe")

Estas son algunas de las técnicas que han funcionado para mí:

  • crear contenedores para sus rutinas de administración de memoria que asignan "bytes" de la cerca en cada extremo de los tampones durante la asignación y que no estén sobre cancelación de asignación. También puede verificarlos dentro de los manejadores de cadenas, tal vez cuando se establece una macro STR_DEBUG. Advertencia: tendrá que probar sus diagnósticos a fondo, para que no creen puntos de falla adicionales.

  • Crea una estructura de datos que encapsula tanto el búfer como su longitud. (También puede contener los bytes de cerca si los usa). Advertencia: ahora tiene una estructura de datos no estándar que toda la base de códigos debe administrar, lo que puede significar una reescritura sustancial (y por lo tanto puntos de falla adicionales))

  • Haga que sus manipuladores de cadena validen sus entradas. Si una función prohíbe los punteros nulos, verifíquelos explícitamente. Si requiere una cadena válida (como strlen()) y conoce la longitud del búfer, verifique que el búfer contenga un carácter nulo. En otras palabras, verifique cualquier suposición que pueda estar haciendo sobre el código o los datos.

  • Escriba sus pruebas primero. Eso lo ayudará a comprender el contrato de cada función, exactamente lo que espera de la persona que llama y lo que la persona que llama debe esperar de él. Se encontrará pensando en las formas en que lo usará, las formas en que podría romperse y los casos límite que debe manejar.

¡Muchas gracias por hacer esta pregunta! Desearía que más desarrolladores pensaran en estos problemas, especialmente antes de, comiencen a codificar. ¡Buena suerte y mis mejores deseos para un producto robusto y exitoso!

+2

+1 para la primera mitad y el último párrafo. Son lo suficientemente buenos como para no juzgar la segunda mitad. :-) La prueba de punteros nulos es uno de mis Considerados Dañinos y aunque las estructuras sofisticadas pueden ayudarlo a depurar, también hacen que su código sea mucho más difícil de usar e integrar con otros códigos. En su lugar, trabajaría en probar rigurosamente tus funciones para satisfacer sus contratos, y luego no necesitarías ningún otro desorden de comprobación en el tiempo de ejecución. –

+0

@R .: Estoy de acuerdo en que las pruebas exhaustivas son clave, y que las "estructuras sofisticadas" a menudo hacen más daño que bien. Pero los simples, usados ​​correctamente, también pueden aumentar la robustez y la capacidad de prueba, especialmente si son parte integral del diseño. ¿Puedes explicar tu aversión a las pruebas con puntero nulo? Soy fanático de probar todo lo que puedo, pero a menudo uso macros (por ejemplo, #ifdef TEST) para rodear las pruebas que pueden ser costosas o redundantes. ¡Gracias por tus pensamientos! –

+1

Mi parte en una larga discusión sobre los méritos o deméritos de la prueba de punteros NULL está aquí: http://stackoverflow.com/questions/4390007/in-either-c-or-c-should-i-check-pointer- parameters-for-null/4390210 # 4390210 –

0

Cuando se trata de tiempo vs espacio, no se olvide de recoger el bit estándar twiddling de here

Durante mis primeros proyectos de firmware, he utilizado la tablas de consulta para contar el bit establecido en un O (1) eficiencia de operación.