assembly - Desentrañar código de espagueti de lenguaje ensamblador
coding-style embedded (11)
He heredado un programa de 10K líneas escrito en lenguaje ensamblador 8051 que requiere algunos cambios. Desafortunadamente está escrito en las mejores tradiciones del código de espagueti. El programa, escrito como un solo archivo, es un laberinto de sentencias CALL y LJMP (aproximadamente 1200 en total), con subrutinas que tienen múltiples puntos de entrada y / o salida, si se pueden identificar como subrutinas. Todas las variables son globales. Hay comentarios; algunos son correctos No hay pruebas existentes, y no hay presupuesto para refactorización.
Un poco de historia sobre la aplicación: el código controla un centro de comunicaciones en una aplicación de venta que actualmente se implementa internacionalmente. Maneja dos transmisiones en serie simultáneamente (con la ayuda de un procesador de comunicaciones separado) y puede comunicarse con hasta cuatro dispositivos físicos diferentes, cada uno de un proveedor diferente. El fabricante de uno de los dispositivos recientemente realizó un cambio ("Sí, hicimos un cambio, ¡pero el software es absolutamente el mismo!") Lo que hace que algunas configuraciones del sistema ya no funcionen y no está interesado en cambiarlo (sea lo que sea) no cambiaron).
El programa fue originalmente escrito por otra compañía, transferido a mi cliente, y luego modificado hace nueve años por otro consultor. Ni la compañía original, ni el consultor, están disponibles como recursos.
Basado en el análisis del tráfico en uno de los buses serie, se me ocurrió un truco, que parece funcionar, pero es feo y no aborda la causa raíz. Si tuviera una mejor comprensión del programa, creo que podría abordar el problema real. Tengo aproximadamente una semana más antes de que se congele el código para admitir una fecha de envío de fin de mes.
Pregunta original: Necesito entender el programa lo suficientemente bien como para hacer los cambios sin romperse. ¿Alguien ha desarrollado técnicas para trabajar con este tipo de desorden?
Veo algunas sugerencias geniales aquí, pero estoy limitado por el tiempo. Sin embargo, es posible que tenga otra oportunidad en el futuro para seguir algunos de los cursos de acción más complejos.
¿En qué medida entiende la plataforma de hardware en la que se ejecuta este código?
¿Se ha puesto en modo de apagado (Pcon = 2) para ahorrar energía? Si es así, ¿cómo se ha activado? (un reinicio o en la interrupción de hardware)
¿Tiene que esperar a que el oscilador llegue a los establos después de un encendido antes de realizar las comunicaciones en serie?
Se ha puesto en modo de reposo (Pcon = 1)
¿Hay diferentes versiones del hardware en el campo?
Asegúrese de tener todas las diferentes variaciones de hardware para probar.
No pierda el tiempo con un simulador: es muy difícil trabajar con él y tiene que hacer muchas suposiciones sobre el hardware. Consiga un emulador de circuito (ICE) y ejecútelo en el hardware.
El software fue escrito en ensamblador por una razón por la que necesita averiguar por qué. es decir - restricciones de memoria - restricciones de velocidad
Puede haber una razón por la que este código es un desastre
Eche un vistazo al archivo de enlaces para:
XDATA SPACE, IDATA SPACE y CODE SPACE:
Si no hay espacio de código libre o Xdata o Idata?
El autor original puede haberlo optimizado para que quepa en el espacio de memoria disponible.
Si ese es el caso , necesita hablar con el desarrollador original para averiguar lo que hizo .
Cortarlo en trozos.
Encuentra otro trabajo, en serio! De lo contrario, el libro "trabajar efectivamente con el código heredado" podría ayudar, aunque creo que se refiere al código heredado como código sin pruebas unitarias.
Esta es una de las pocas veces en que le recomendaré que ponga en práctica sus habilidades blandas y le presente a su PM / Gerente / CXO su razonamiento detrás de una reescritura, y el ahorro de tiempo / costo que implica tal empresa.
He hecho este tipo de cosas un par de veces. Algunas recomendaciones:
- Comience por revisar el esquema, esto lo ayudará a comprender qué puertos y pines impactan sus cambios deseados.
- Usa grep para encontrar todas las llamadas, ramas, saltos y devoluciones. Esto puede ayudar a comprender el flujo e identificar los trozos de código.
- Mire el vector de reinicio y la tabla de interrupciones para identificar las líneas principales.
- Use grep para crear una referencia cruzada para todas las etiquetas de código y referencias de datos (si sus herramientas de ensamblador no pueden hacer esto por usted).
Tenga en cuenta la Ley de Hofstadter: siempre lleva más tiempo de lo que espera, incluso si tiene en cuenta la Ley de Hofstadter .
Buena suerte.
Me temo que no hay una bala mágica para este tipo de problema. Creo que la única solución es imprimir el archivo ASM para luego ir a un lugar silencioso y simular la ejecución del programa línea por línea en su mente (mientras escribe los contenidos de los registros y las ubicaciones de la memoria en un bloc de notas). Después de un tiempo, encuentras que esto no toma tanto tiempo como cabría esperar. Esté preparado para pasar muchas horas haciendo esto y beber galones de café. Después de un tiempo, comprenderá lo que está haciendo y puede considerar los cambios.
¿El 8051 tiene algún puerto IO no utilizado? Si lo hace y no puede calcular cuándo se están llamando ciertas rutinas, agregue un código para enviar estos puertos de reserva altos o bajos. Luego, cuando el programa se está ejecutando, observe estos puertos con un osciloscopio.
Buena suerte
No necesita un presupuesto especial para la refactorización y las pruebas; le ahorran dinero y le permiten trabajar más rápido. Es la técnica que debe usar para agregar cambios al código heredado heredado porque es la forma más barata de hacerlo sin "sin roturas".
La mayoría de las veces, creo que hay una compensación en la que obtienes más calidad a cambio de pasar más tiempo, pero con el código heredado con el que no estás familiarizado, creo que es más rápido realizar pruebas. Debes ejecutar el código antes. usted lo envía, ¿verdad?
Primero, intentaría ponerme en contacto con aquellas personas que originalmente desarrollaron el código o que al menos lo mantuvieron antes que yo, con la esperanza de obtener suficiente información para obtener una comprensión básica del código en general, para que pueda comenzar a agregar comentarios útiles a eso.
Tal vez incluso pueda conseguir que alguien describa las API más importantes (incluyendo su firma, valores de devolución y propósito) para el código. Si el estado global es modificado por una función, esto también debería hacerse explícito. De manera similar, comience a diferenciar entre funciones y procedimientos, así como registros de entrada / salida.
Debe dejarle muy claro a su empleador que esta información es necesaria, si no le creen, haga que se sienten con usted frente a este código mientras describe lo que se supone que debe hacer y cómo debe hacerlo. it (ingeniería inversa). ¡Tener un empleador con experiencia en informática y programación será realmente útil en ese caso!
Si su empleador no tiene una formación técnica de este tipo, pídale que traiga a otro programador / colega para explicarle sus pasos, ya que esto le demostrará que es serio y honesto al respecto, porque es un problema real, no solo desde su punto de vista (asegúrese de tener colegas que conozcan este ''proyecto'').
Si está disponible y es factible, también dejaría en claro que contratar (o al menos contactar) a los desarrolladores / mantenedores anteriores (si es que ya no están trabajando para su empresa) para ayudar a documentar este código sería un requisito previo. -requisito para mejorar el código de manera realista en un lapso de tiempo corto y para garantizar que se pueda mantener más fácilmente en el futuro.
Haga hincapié en que toda esta situación se debe a deficiencias en el proceso de desarrollo de software anterior y que estos pasos ayudarán a mejorar la base del código. Por lo tanto, el código base en su forma actual es un problema creciente y todo lo que se haga ahora para manejar este problema es una inversión para el futuro.
Esto en sí mismo también es importante para ayudarles a evaluar y comprender su situación: hacer lo que se supone que debe hacer ahora está lejos de ser trivial, y ellos deben saberlo, aunque solo sea para establecer sus expectativas con claridad (por ejemplo, con respecto a los plazos y la complejidad de la tarea).
Además, personalmente, comenzaría a agregar pruebas unitarias para aquellas partes que entiendo lo suficientemente bien, de modo que pueda comenzar lentamente a refactorizar / reescribir algún código.
En otras palabras, una buena documentación y comentarios sobre el código fuente son una cosa, pero tener un conjunto de pruebas completo es otra cosa importante, nadie puede realmente esperar que modifique una base de código desconocida sin una forma establecida de probar la funcionalidad clave.
Dado que el código es 10K, también buscaría factorizar las subrutinas en archivos separados para hacer que los componentes sean más identificables, preferiblemente usando envoltorios de acceso en lugar de variables globales y también nombres de archivos intuitivos.
Además, buscaría pasos para mejorar aún más la legibilidad del código fuente al disminuir la complejidad, tener sub rutinas con múltiples puntos de entrada (¿y posiblemente incluso firmas de parámetros diferentes?) Parece una forma segura de ofuscar el código innecesariamente.
De manera similar, las sub rutinas enormes también se pueden refactorizar en otras más pequeñas para ayudar a mejorar la legibilidad.
Entonces, una de las primeras cosas que estudiaría sería determinar aquellas cosas que hacen que sea realmente complicado desarrollar la base del código y luego volver a trabajar esas partes, por ejemplo, dividiendo enormes sub rutinas con múltiples puntos de entrada en distintos sub rutinas que se llaman entre sí. Si esto no se puede hacer debido a razones de rendimiento o sobrecarga de llamadas, use macros en su lugar.
Además, si es una opción viable, consideraría volver a escribir partes del código de manera incremental utilizando un lenguaje de nivel más alto, ya sea utilizando un subconjunto de C, o al menos haciendo un uso excesivo de macros de ensamblaje para ayudar a estandarizar el código Base, sino también para ayudar a localizar posibles errores.
Si una reescritura incremental en C es una opción viable, una forma posible de comenzar sería convertir todas las funciones obvias en funciones C cuyos cuerpos se copian / pegan al principio desde el archivo de ensamblaje, para que termine con C Funciones con mucho ensamblaje en línea.
Personalmente, también intentaría ejecutar el código en un simulator/emulator para avanzar fácilmente a través del código y, con suerte, comenzar a entender los bloques de construcción más importantes (al examinar el registro y el uso de la pila), un buen simulador 8051 con un depurador incorporado debería ser disponible para usted si realmente tiene que hacer esto en gran parte por su cuenta.
Esto también le ayudaría a encontrar la secuencia de inicialización y la estructura del bucle principal, así como un gráfico de llamadas.
Tal vez, incluso puede encontrar un buen simulador de código abierto 80851 que se pueda modificar fácilmente para que también proporcione un gráfico de llamadas completo automáticamente, solo haciendo una búsqueda rápida, encontré gsim51 , pero obviamente hay varias otras opciones, varias también propietarias.
Si estuviera en su situación, incluso consideraría subcontratar el esfuerzo de modificar mis herramientas para simplificar el trabajo con este código fuente, es decir, muchos proyectos de Sourceforge aceptan donaciones y quizás pueda convencer a su empleador para que patrocine dicha modificación.
Si no es financieramente, ¿quizás por proporcionarle los parches correspondientes?
Si ya está utilizando un producto patentado, incluso podría hablar con el fabricante de este software y detallar sus requisitos y preguntarles si están dispuestos a mejorar este producto de esa manera o si al menos pueden exponer una interfaz para permitir clientes que realicen dichas personalizaciones (alguna forma de API interna o quizás incluso simples scripts de pegamento).
Si no responden, indique que su empleador ha estado pensando en usar un producto diferente desde hace algún tiempo y que usted fue el único que insistió en que se usara ese producto en particular ... ;-)
Si el software espera ciertos periféricos y hardware de E / S, es posible que incluso desee escribir un bucle de simulación de hardware correspondiente para ejecutar el software en un emulador.
En última instancia, sé a ciencia cierta que personalmente disfrutaría mucho más el proceso de personalizar otro software para que me ayude a entender un monstruo de código espagueti, que pasar el código manualmente y jugar al emulador yo mismo, sin importar cuántos galones de café pueda. obtener.
Obtener un gráfico de llamadas utilizable de un emulador 8051 de código abierto no debería tomar mucho más tiempo que un fin de semana (a lo sumo), porque en su mayoría significa buscar códigos de operación CALL y registrar sus direcciones (posición y destino), de modo que todo se descargue a un archivo para su posterior inspección.
Tener acceso a las partes internas de un emulador también sería una excelente manera de inspeccionar más el código, por ejemplo, para encontrar patrones recurrentes de códigos de operación (por ejemplo, 20-50 +), que pueden incluirse en funciones / procedimientos independientes, esto podría realmente Ayuda a disminuir aún más el tamaño y la complejidad de la base de código.
El siguiente paso probablemente sería examinar el uso de la pila y el registro. Y para determinar el tipo / tamaño de los parámetros de función utilizados, así como su rango de valores, para que pueda concebir las pruebas unitarias correspondientes.
Usar herramientas como dot / graphviz para visualizar la estructura de la secuencia de inicialización y el bucle principal en sí mismo, será una pura alegría en comparación con hacer todo esto manualmente.
Además, en realidad terminará con datos y documentos útiles que pueden servir de base para una mejor documentación a largo plazo.
Sé que esto suena loco ... pero estoy desempleado (elegí el momento equivocado para decirle al socio de la ciudad que se vaya al infierno) y tengo algo de tiempo libre. Estaría dispuesto a echarle un vistazo. Solía escribir el ensamblaje para la manzana] [y la PC original. Si pudiera jugar con su código en el simulador durante un par de horas, podría darle una idea si tengo la oportunidad de documentarlo por usted (sin ejecutar mis vacaciones no planificadas). Como no sé nada acerca de 8051, esto podría no ser posible para alguien como yo, pero el simulador parecía prometedor. No querría dinero para hacer esto. Es suficiente solo para exponerse al desarrollo incrustado 8051. Te dije que esto sonaría loco.
Tuve un problema muy similar con un software 8052. Así que la compañía heredó tal bestia, el código ROM completo (64Kbytes), aproximadamente 1,5 megas de módulos de espagueti de ensamblaje más dos módulos PL / M de 3000 líneas formaron esta monstruosidad de codificación. Los desarrolladores originales del software murieron hace mucho tiempo (esto no significa que no hubiera nadie, pero de hecho nadie lo entendería en su totalidad), los compiladores que los compilaban eran de la mitad de los 80 que se ejecutaban en un emulador MDS-70, y varios críticos Los módulos estaban en los límites de estos compiladores. Como agregar un símbolo global más, y el enlazador se bloquearía. Agregue un símbolo más a un archivo ASM, y el compilador se bloquearía.
Entonces, ¿cómo se podría empezar a cortar esto?
Primero necesitarás herramientas. Notepad ++, por ejemplo, es algo muy bueno, ya que se puede usar para realizar búsquedas cruzadas en varios archivos a la vez, ideal para encontrar qué módulos se refieren a un símbolo global. Este es probablemente el elemento más crucial.
Si es posible, obtenga cualquier documento que pueda encontrar en el software. El problema más inmediato para resolver con estas bestias es entender cómo están compuestas aproximadamente, cuál es su arquitectura. Por lo general, esto no se incluye en el software en sí, ni siquiera si está debidamente comentado.
Para obtener la arquitectura usted mismo, primero puede intentar construir un gráfico de llamadas . Es más simple de hacer que un gráfico de flujo de datos, ya que generalmente hay menos llamadas y saltos entre archivos que las variables globales. Para esta llamada, los gráficos solo consideran símbolos globales suponiendo que los archivos de origen deben ser módulos (lo que no es necesariamente cierto, pero generalmente deberían serlo).
Para hacer esto, use su herramienta para la búsqueda cruzada de archivos, cree una lista grande (por ejemplo, en OpenOffice Calc) donde recopile qué símbolo está definido en qué archivo y qué archivos se refieren a este símbolo que lo llama.
Luego roba algunas hojas grandes (!) Del trazador y comienza a dibujar. Si tiene mucha habilidad en algún software de gráficos, puede usarlo, pero a menos que sea así, es más probable que lo retenga. Entonces dibuje un gráfico de llamadas que muestre qué archivo tiene llamadas a qué otros archivos (sin mostrar los símbolos en sí mismos, con más o menos 50 archivos, no podría administrarlo).
Lo más probable es que el resultado de esto sea un espagueti. El objetivo es enderezar esto para obtener un árbol jerárquico con una raíz (que será el archivo que contiene el punto de entrada del programa) sin bucles. Puedes devorar varias hojas durante este proceso para enderezar iterativamente a la bestia. También es posible que ciertos archivos estén tan entrelazados que no puedan representarse sin bucles. En este caso, lo más probable es que un solo "módulo" se haya separado de alguna manera en dos archivos, o se hayan enredado más módulos conceptuales. Regrese a su lista de llamadas y agrupe los símbolos para cortar los archivos problemáticos en unidades independientes más pequeñas (también tendrá que revisar el archivo para ver los saltos locales aquí para ver si es posible el corte).
Al final, a menos que ya esté trabajando en otro lugar por su propio bien, obtendrá un gráfico de llamadas jerárquico con módulos conceptuales. A partir de esto, es posible deducir la arquitectura intencional del software y seguir trabajando.
El próximo objetivo es la arquitectura . Con su mapa previamente creado, tendrá que navegar por el software, descubrir sus subprocesos (tareas de interrupción y del programa principal) y los propósitos generales de cada uno de los módulos / archivos fuente. Cómo puede hacer esto y lo que obtiene aquí depende más del dominio de la aplicación.
Cuando estos dos se hacen, el "descanso" es bastante sencillo. En esencia, debe saber qué se supone que debe hacer cada parte de la cosa y, por lo tanto, sabe con qué está tratando cuando comienza a trabajar en un archivo fuente. Sin embargo, es importante que cada vez que encuentre algo "sospechoso" en una fuente, el programa parezca hacer algo irrelevante, volver a su arquitectura y gráfico de llamadas, y hacer correcciones si es necesario.
Al resto los métodos mencionados anteriormente se aplican bien. Acabo de describir esto para dar una idea de lo que se puede hacer en casos realmente horribles. Desearía tener solo 10K líneas de código para tratar en ese entonces ...
Yo diría que la respuesta de IanW (solo imprímala y siga el rastreo) es probablemente la mejor. Dicho esto, tengo un poco de la idea de la pared:
Intente ejecutar el código (probablemente el binario) a través de un disemador que pueda reconstruir el código C (si puede encontrar uno para el 8051). Tal vez identificará algunas rutinas que no puedes (fácilmente).
Tal vez te ayude.