c++ - studio - ¿Cómo se programa de forma segura fuera de un entorno de código administrado?
cuales son las caracteristicas de clr (9)
Si usted es alguien que programa en C o C ++, sin los beneficios del lenguaje administrado de administración de memoria, verificación de tipos o protección de desbordamiento del búfer, usando la aritmética del puntero, ¿cómo se asegura de que sus programas sean seguros? ¿Usas muchas pruebas unitarias o solo eres un codificador prudente? ¿Tienes otros métodos?
Además de muchos de los buenos consejos que se ofrecen aquí, mi herramienta más importante es SECO - No repetir. No extiendo el código propenso a errores (por ejemplo, para manejar las asignaciones de memoria con malloc () y libre ()) en toda mi base de código. Tengo exactamente una sola ubicación en mi código donde se llaman malloc y free. Está en las funciones de envoltura MemoryAlloc y MemoryFree.
Está todo el control de los argumentos y el manejo de errores inicial que generalmente se da como un código repetitivo dentro de la llamada a malloc. Además, habilita cualquier cosa con la necesidad de modificar solo una ubicación, comenzando con simples comprobaciones de depuración como contar las llamadas exitosas a malloc y liberar y verificar al finalizar el programa que ambos números son iguales, hasta todos los tipos de comprobaciones de seguridad extendidas.
A veces, cuando leo una pregunta como "Siempre tengo que asegurarme de que strncpy finaliza la cadena, ¿hay alguna alternativa?"
strncpy(dst, src, n);
dst[n-1] = ''/0'';
seguidos de días de discusión, siempre me pregunto si el arte de extraer funcionalidades repetidas en funciones es un arte perdido de programación superior que ya no se enseña en las conferencias de programación.
char *my_strncpy (dst, src, n)
{
assert((dst != NULL) && (src != NULL) && (n > 0));
strncpy(dst, src, n);
dst[n-1] = ''/0'';
return dst;
}
Problema principal de duplicación de código resuelto - ahora pensemos si strncpy realmente es la herramienta correcta para el trabajo. ¿Actuación? Optimización prematura! Y una sola ubicación para comenzar después de que se convierta en el cuello de botella.
Escribimos en C para sistemas integrados. Además de utilizar algunas de las técnicas comunes a cualquier lenguaje de programación o entorno, también empleamos:
He estado usando C ++ durante 10 años. He usado C, Perl, Lisp, Delphi, Visual Basic 6, C #, Java y varios otros lenguajes que no puedo recordar en la cima de mi cabeza.
La respuesta a su pregunta es simple: debe saber lo que está haciendo , más que C # / Java. Más de lo que engendra discursos como el de Jeff Atwood con respecto a las "Escuelas de Java" .
La mayoría de sus preguntas, en cierto sentido, no tienen sentido. Los ''problemas'' que plantea son simplemente hechos de cómo funciona realmente el hardware . Me gustaría desafiarlo a escribir una CPU y RAM en VHDL / Verilog y ver cómo funcionan realmente las cosas, incluso cuando realmente se simplifican. Comenzarás a apreciar que el modo C # / Java es una abstracción que se basa en el hardware.
Un desafío más fácil sería programar un sistema operativo elemental para un sistema integrado desde el encendido inicial; también le mostrará lo que necesita saber.
(También escribí C # y Java)
He hecho C ++ y C # y no veo todo el bombo sobre el código administrado.
Oh, claro, hay un recolector de basura para la memoria, eso es útil ... a menos que te abstengas de utilizar simples punteros viejos en C ++, por supuesto, si solo usas smart_pointers, entonces no tienes tantos problemas.
Pero me gustaría saber ... ¿tu recolector de basura te protege de:
- mantener abiertas las conexiones de la base de datos?
- manteniendo bloqueos en los archivos?
- ...
La gestión de los recursos es mucho más que la gestión de la memoria. Lo bueno de C ++ es que aprendes rápidamente lo que significa la gestión de recursos y RAII, de modo que se convierte en un reflejo:
- si quiero un puntero, quiero un auto_ptr, un shared_ptr o un weak_ptr
- si quiero una conexión DB, quiero un objeto ''Conexión''
- si abro un archivo, quiero un objeto ''Archivo''
- ...
En cuanto a los desbordamientos de búfer, bueno, no es como si estuviéramos usando char * y size_t en todas partes. Tenemos algunas cosas que se llaman ''cadena'', ''iostream'' y, por supuesto, el ya mencionado método vector :: at que nos libera de esas restricciones.
Las bibliotecas probadas (stl, boost) son buenas, úsalas y lleva a problemas más funcionales.
La respuesta de Andrew es buena, pero también agregaría disciplina a la lista. Después de haber practicado lo suficiente con C ++, me da una sensación bastante buena de lo que es seguro y lo que mendiga que los velocirraptores vengan a devorarte. Tiende a desarrollar un estilo de codificación que se siente cómodo cuando sigue las prácticas seguras y lo deja con la sensación de ser un heebie-jeebies si intenta, por ejemplo, devolver un puntero inteligente a un puntero sin formato y pasarlo a otra cosa.
Me gusta pensar que es una herramienta eléctrica en una tienda. Es lo suficientemente seguro una vez que haya aprendido a usarlo correctamente y siempre que se asegure de seguir siempre todas las reglas de seguridad. Es cuando piensas que puedes renunciar a las gafas de seguridad que te hacen daño.
C ++ tiene todas las características que mencionas.
Hay administración de memoria. Puede utilizar Smart Pointers para un control muy preciso. O hay un par de recolectores de basura disponibles, aunque no son parte del estándar (pero en la mayoría de los casos, los indicadores inteligentes son más que adecuados).
C ++ es un lenguaje fuertemente tipado. Al igual que C #.
Estamos usando memorias intermedias. Puede optar por usar la versión comprobada de límites de la interfaz. Pero si sabe que no hay un problema, puede utilizar la versión no verificada de la interfaz.
Compare el método en () (marcado) con el operador [] (no seleccionado).
Sí, utilizamos pruebas unitarias. Justo como deberías estar usando en C #.
Sí, somos codificadores prudentes. Justo como deberías estar en C #. La única diferencia es que las trampas son diferentes en los dos idiomas.
Todas las anteriores. Yo suelo:
- Mucha precaución
- Smart Pointers tanto como sea posible
- Estructuras de datos que se han probado, mucha biblioteca estándar
- Pruebas unitarias todo el tiempo
- Herramientas de validación de memoria como MemValidator y AppVerifier
- Reza todas las noches para que no se cuelgue en el sitio del cliente.
En realidad, solo estoy exagerando. No está mal y no es demasiado difícil mantener el control de los recursos si estructura su código correctamente.
Nota interesante Tengo una aplicación grande que usa DCOM y tiene módulos administrados y no administrados. Los módulos no administrados generalmente son más difíciles de depurar durante el desarrollo, pero funcionan muy bien en el sitio del cliente debido a las numerosas pruebas que se ejecutan en él. Los módulos administrados a veces sufren de código incorrecto porque el recolector de basura es tan flexible que los programadores se vuelven perezosos al verificar el uso de los recursos.
Uso muchas y muchas afirmaciones y compilo tanto una versión de "depuración" como una versión de "lanzamiento". Mi versión de depuración corre mucho más lenta que mi versión de lanzamiento, con todos los controles que hace.
Corro frecuentemente bajo Valgrind , y mi código tiene cero pérdidas de memoria. Cero. Es mucho más fácil mantener un programa sin fugas que tomar un programa con errores y reparar todas las fugas.
Además, mi código se compila sin advertencias, a pesar de que tengo el compilador configurado para advertencias adicionales. A veces las advertencias son tontas, pero a veces apuntan directamente a un error, y lo soluciono sin necesidad de encontrarlo en el depurador.
Estoy escribiendo C puro (no puedo usar C ++ en este proyecto), pero estoy haciendo C de una manera muy consistente. Tengo clases orientadas a objetos, con constructores y destructores; Tengo que llamarlos a mano, pero la consistencia ayuda. Y si me olvido de llamar a un destructor, Valgrind me golpea en la cabeza hasta que lo arregle.
Además del constructor y el destructor, escribo una función de autoverificación que examina el objeto y decide si está en su sano juicio o no; por ejemplo, si el identificador de un archivo es nulo pero los datos del archivo asociado no se ponen a cero, esto indica algún tipo de error (ya sea que el identificador haya sido destruido o que el archivo no se haya abierto pero esos campos tengan basura en ellos). Además, la mayoría de mis objetos tienen un campo de "firma" que debe establecerse en un valor específico (específico para cada objeto diferente). Las funciones que usan objetos generalmente afirman que los objetos están en su sano juicio.
Cada vez que malloc()
algo de memoria, mi función llena la memoria con valores 0xDC
. Una estructura que no está completamente inicializada se vuelve obvia: los recuentos son demasiado grandes, los punteros no son válidos ( 0xDCDCDCDC
) y cuando miro la estructura en el depurador es obvio que no está inicializado. Esto es mucho mejor que la memoria de relleno cero al llamar a malloc()
. (Por supuesto, el relleno de 0xDC
solo está en la versión de depuración, no es necesario que la versión de lanzamiento pierda ese tiempo).
Cada vez que libero memoria, borro el puntero. De esa forma, si tengo un error estúpido en el que el código intenta usar un puntero después de liberar su memoria, al instante obtengo una excepción de puntero nulo, que me señala el error. Mis funciones de destructor no toman un puntero a un objeto, toman un puntero a un puntero y golpean el puntero después de la destrucción del objeto. Además, los destructores limpian sus objetos antes de liberarlos, de modo que si algún fragmento de código tiene una copia de un puntero e intenta usar un objeto, el control de cordura se activa al instante.
Valgrind me dirá si algún código cancela el final de un buffer. Si no tuviera eso, habría puesto valores "canarios" después de los extremos de los buffers, y habría hecho que el chequeo de cordura los pruebe. Estos valores canarios, como los valores de firma, serían solo de depuración-compilación, por lo que la versión de lanzamiento no tendría inflado de memoria.
Tengo una colección de pruebas unitarias, y cuando hago algún cambio importante en el código, es muy reconfortante ejecutar las pruebas unitarias y tener algo de confianza. No rompí horriblemente las cosas. Por supuesto, ejecuto las pruebas unitarias en la versión de depuración, así como en la versión de lanzamiento, por lo que todas mis afirmaciones tienen la posibilidad de encontrar problemas.
Poner toda esta estructura en su lugar fue un poco de esfuerzo extra, pero vale la pena todos los días. Y me siento bastante feliz cuando una afirmación dispara y me señala un error, en lugar de tener que ejecutar el error en el depurador. A la larga, es menos trabajo mantener las cosas limpias todo el tiempo.
Finalmente, debo decir que realmente me gusta la notación húngara. Trabajé en Microsoft hace unos años, y al igual que Joel aprendí aplicaciones húngaras y no la variante rota. Realmente hace que el código incorrecto se vea mal .
Igual de relevante: cómo se asegura de que sus archivos y tomas estén cerrados, sus bloqueos se liberan, bla bla bla. La memoria no es el único recurso, y con GC, usted inherentemente pierde la destrucción confiable / oportuna.
Ni GC ni non-GC es automáticamente superior. Cada uno tiene beneficios, cada uno tiene su precio, y un buen programador debería ser capaz de hacer frente a ambos.
Lo dije tanto en una respuesta a esta pregunta .