La prueba es en Linux 32-bit x86 .
Básicamente, estoy tratando de registrar la información de los bloques básicos ejecutados insertando instrucciones de instrumentación en el código ensamblador.
Mi estrategia es así: escriba el índice de un bloque básico ejecutado en una matriz global y vacíe la matriz de la memoria al disco cuando la matriz esté llena (16M).
Aquí está mi problema. Necesito vaciar la matriz en el disco cuando termine la ejecución del binario instrumentado, incluso si no alcanza el límite de 16M. Sin embargo, simplemente no sé dónde encontrar la salida de un programa de assembly .
Intenté esto:
grep exit del programa ensamblador de destino y vacíe la memoria justo antes de la instrucción de call exit . Pero de acuerdo con alguna experiencia de depuración, el programa C de destino, por ejemplo, un binario md5sum , no llama a exit cuando finaliza la ejecución.
Vacíe la memoria al final de la función main . Sin embargo, en el código ensamblador, simplemente no sé dónde está el final exacto de la función main . Puedo hacer un enfoque conservador, por ejemplo, buscando todas las instrucciones ret , pero me parece que no todas las funciones main terminan con una instrucción ret .
Entonces, aquí está mi pregunta, ¿cómo identificar el final de ejecución exacto de un assembly code e insertar algunas instrucciones de instrumentación allí? Enganchar algún código de biblioteca está bien para mí. Entiendo que con una entrada diferente, el binario podría salir en una posición diferente, así que supongo que necesito una estimación conservadora. ¿Estoy claro? ¡Gracias!
Creo que no se puede hacer eso en el caso general. Primero, si main devuelve algún código, es un código de salida (si main no tiene un return explícito, los estándares C recientes requieren que el compilador agregue un return 0; implícito 0;). Luego, una función podría almacenar la dirección de exit en algunos datos (por ejemplo, una función global, un campo en una struct , ...), y alguna otra función podría llamarla indirectamente a través de un puntero de función. Prácticamente, un programa puede cargar algunos complementos usando dlopen y usar dlsym para el nombre de "exit" , o simplemente llamar a exit dentro del complemento, etc. AFAIU resuelve ese problema (de encontrar llamadas de exit reales, en el sentido dinámico) en general puede demostrarse equivalente al problema de la detención . Véase también el teorema de Rice .
Sin pretender un enfoque exhaustivo, sugeriría algo más (suponiendo que esté interesado en instrumentar programas codificados en C o C++, etc... cuyo código fuente esté disponible para usted). Puede personalizar el compilador GCC con MELT para cambiar los bloques básicos procesados dentro de GCC para llamar a algunas de sus funciones de instrumentación. No es trivial, pero es factible... Por supuesto, necesitará volver a compilar algún código C con un GCC personalizado para instrumentarlo.
(Descargo de responsabilidad, soy el autor principal de MELT ; no dude en ponerse en contacto conmigo para obtener más información...)
Por cierto, ¿sabes sobre atexit(3) ? Podría ser útil para su problema de descarga... Y también podría usar los trucos LD_PRELOAD (lea sobre los enlazadores dinámicos , consulte ld-linux(8) ).
atexit() manejará correctamente más del 95% de los programas. Puede modificar su cadena de controladores registrados o instrumentarlo como si fuera otros bloques. Sin embargo, algunos programas pueden terminar mediante el uso de _exit() que no invoca a los controladores atexit . Probablemente instrumentar _exit para invocar el vaciado de datos e instalar un controlador atexit (o on_exit() en programas similares a BSD) debería cubrir casi el 100% de los programas.
Anexo: tenga en cuenta que la especificación base de Linux dice que el inicio de la biblioteca C debe:
llamar a la función inicializadora (*init)().
llama a main() con los argumentos apropiados.
llame a exit() con el valor de retorno de main().
Pero de acuerdo con alguna experiencia de depuración, el programa C de destino, por ejemplo, un binario md5sum, no llama a exit cuando finaliza la ejecución.
Echemos un vistazo a un binario md5sum en un sistema i686 GNU/Linux:
En el desensamblado ( objdump -d /usr/bin/md5sum ) tenemos esto:
Disassembly of section .text: 08048f50 <.text>: 8048f50: 55 push %ebp 8048f51: 89 e5 mov %esp,%ebp 8048f53: 57 push %edi 8048f54: 56 push %esi 8048f55: 53 push %ebx 8048f56: 83 e4 f0 and $0xfffffff0,%esp 8048f59: 81 ec c0 00 00 00 sub $0xc0,%esp 8048f5f: 8b 7d 0c mov 0xc(%ebp),%edi [ ... ] 8049e8f: 68 b0 d6 04 08 push $0x804d6b0 8049e94: 68 40 d6 04 08 push $0x804d640 8049e99: 51 push %ecx 8049e9a: 56 push %esi 8049e9b: 68 50 8f 04 08 push $0x8048f50 8049ea0: e8 4b ef ff ff call 8048df0 <__libc_start_main@plt> 8049ea5: f4 hlt Todo esto es código repetitivo de inicio. La llamada main del programa real se invoca dentro de la llamada __libc_start_main . Si el programa regresa de eso, entonces, mira, hay una instrucción hlt . Ese es tu objetivo. Busque esa instrucción hlt e instrumente eso como el final del programa.