Cómo McSema maneja las excepciones de C ++ Pista de bits Blog de recuperación de lesiones cerebrales hipóxicas graves

Los programas de C ++ que usan excepciones son problemáticos para los levantadores binarios. Las operaciones de “lanzamiento” y “captura” de flujo de control no local que aparecen en el código fuente de C ++ no se asignan claramente a representaciones binarias sencillas. Se podría alegar que la compilación, el tiempo de ejecución y la biblioteca de desenrollado de pila colaboran para que las excepciones funcionen. Recientemente completamos nuestra investigación sobre excepciones y podemos afirmar, más allá de toda duda razonable, que mcsema es el único levantador binario que levanta correctamente los programas con flujo de control basado en excepciones.

Nuestro trabajo sobre mcsema tuvo que cerrar la brecha semántica entre la semántica del lenguaje de alto nivel de un programa y su representación binaria, lo que requería una comprensión completa de cómo funcionan las excepciones del mezclador de tanque anóxico bajo el capó.


Esta publicación está organizada en tres secciones: primero, explicaremos cómo se manejan las excepciones de C ++ en Linux para las arquitecturas x86-64 y explicaremos los conceptos básicos de manejo de excepciones. En segundo lugar, mostraremos cómo utilizamos este conocimiento para recuperar información de excepción a nivel binario. Y tercero, explicaremos cómo emitir información de excepción para el ecosistema LLVM. Un manual corto en el manejo de excepciones de C ++

Las excepciones son una construcción de lenguaje de programación que proporciona una manera estandarizada de manejar situaciones anormales o erróneas. Funcionan al redirigir automáticamente el flujo de ejecución a un controlador especial llamado el controlador de excepciones cuando se produce un evento de este tipo. Usando excepciones, es posible ser explícito acerca de las formas en que las operaciones pueden fallar y cómo deben manejarse esas fallas. Por ejemplo, algunas operaciones como la creación de instancias de objetos y el procesamiento de archivos pueden fallar de varias maneras. El manejo de excepciones le permite al programador manejar estas fallas de manera genérica para grandes bloques de código, en lugar de verificar manualmente cada operación individual.

Las excepciones son una parte fundamental de C ++, aunque su uso es opcional. El código que puede fallar está rodeado por un bloque try {…}, y las excepciones que pueden surgir se capturan a través de un bloque catch {…}. La señalización de condiciones excepcionales se activa a través de la palabra clave throw, que genera una excepción de un tipo específico. La Figura 1 muestra un programa simple que utiliza la semántica de excepción de C ++. Intente crear el programa usted mismo (clang ++ -o exception exception.Cpp) o mire el código en el explorador del compilador.

Este sencillo programa puede lanzar explícitamente excepciones std :: runtime_error y std :: out_of_range basadas en argumentos de entrada. También implícitamente lanza la excepción std :: bad_alloc cuando se queda sin memoria. El programa instala tres manejadores de excepciones: uno para std :: out_of_range, uno para std :: bad_alloc y un manejador general para excepciones genéricas desconocidas. Ejecute las siguientes entradas de muestra para activar el silencio profundo de nanoxia 4 y repase las tres condiciones excepcionales:

Veamos el mismo programa a nivel binario. El explorador del compilador muestra el código binario generado por el compilador para este programa. El compilador convierte los enunciados lanzados en un par de llamadas a las funciones libstdc ++ (__cxa_allocate_exception y __cxa_throw) que asignan la estructura de excepción e inician el proceso de limpieza de objetos locales en los ámbitos que conducen al desenlace de la pila de excepciones (vea las líneas 40-48 en compilador explorador).

• llama a la función de personalidad libstdc ++. Primero, el desenrollador de pila llama a una función especial provista por libstdc ++ llamada función de personalidad. La función de personalidad determinará si la excepción generada se maneja mediante una función en algún lugar de la pila de llamadas. En términos de alto nivel, la función de personalidad determina si hay un bloque catch que debería llamarse para esta excepción. Si no se puede ubicar ningún controlador (es decir, la excepción no se ha manejado), la función de personalidad finaliza el programa llamando a std :: terminate.

• limpia los objetos asignados. Para llamar limpiamente al bloque catch, el desenrollador debe limpiar primero (es decir, llamar a los destructores de recuperación de lesiones cerebrales anóxicas hipóxicas para cada objeto asignado) después de cada función llamada dentro del bloque try. El desenrollador recorrerá la pila de llamadas, utilizando la función de personalidad para identificar un método de limpieza para cada marco de pila. Si hay signos de acciones de limpieza de lesiones cerebrales hipóxicas, el desenrollador llama al código de limpieza asociado.

La recuperación del flujo de control basado en excepciones es una propuesta desafiante para las herramientas de análisis binario como mcsema. Los datos fundamentales son difíciles de recopilar, porque la información de excepción se distribuye en todo el binario y se vincula mediante varias tablas. Utilizar datos de excepción para recuperar el flujo de control es difícil, porque las operaciones que afectan el flujo, como el desenrollado de la pila, las llamadas a las funciones de personalidad y la decodificación de la tabla de excepciones ocurren fuera del ámbito del programa compilado.

Aquí hay un breve resumen del objetivo final. McSema debe identificar cada bloque básico que pueda generar una excepción (i.E. El contenido de un bloque try) y asociarlo con el controlador de excepciones y el código de limpieza apropiados (i.E. El bloque catch o el pad de aterrizaje). Esta asociación se utilizará para volver a generar controladores de excepciones en el nivel LLVM. Para asociar bloques con plataformas de aterrizaje, mcsema analiza la tabla de excepciones para proporcionar estas asignaciones.

La tabla de excepciones proporciona a los tiempos de ejecución del idioma la información para admitir excepciones. Tiene dos niveles: el nivel independiente del idioma y el nivel específico del idioma. Ubicar los cuadros de la pila y restaurarlos es independiente del lenguaje, y por lo tanto se almacena en el nivel independiente. La identificación del marco que maneja las excepciones y la transferencia del control depende del idioma, por lo que se almacena en el nivel específico del idioma. Nivel de idioma independiente de ansiedad significado en síntomas hindi

La tabla se almacena en secciones especiales en el binario llamado .Eh_frame y .Eh_framehdr. La sección .Eh_frame contiene uno o más registros de información de marco de llamada codificados en el formato de información de depuración DWARF. Cada registro de información de trama contiene un registro de entrada de información común (CIE), seguido de uno o más registros de entrada de descriptor de trama (FDE). Juntos, describen cómo desenrollar a la persona que llama según el indicador de instrucción actual. Más detalles se describen en la documentación de los estándares de Linux. Nivel específico del idioma

El área de datos específicos del idioma (LSDA) contiene punteros a datos relacionados, una lista de sitios de llamadas y una lista de registros de acción. Cada función tiene su propio LSDA, que se proporciona como los datos de aumento de la entrada del descriptor de trama (FDE). La información del LSDA es esencial para recuperar la información de excepción de C ++ y para traducirla a la semántica de LLVM.

Siguiendo el encabezado de LSDA, la tabla de sitios de llamadas enumera todos los sitios de llamadas que pueden generar una excepción. Cada entrada en la tabla de sitios de llamada indica la posición del sitio de llamada, la posición de la plataforma de aterrizaje y el primer registro de acción para ese sitio de llamada. Una entrada que falta en la tabla del sitio de la llamada indica que una llamada no debe lanzar una excepción. La información de esta tabla será utilizada por mcsema durante la etapa de traducción para emitir el tratamiento de homeopatía semántica LLVM adecuado para la lesión cerebral hipóxica en los sitios de llamadas que pueden generar excepciones.

La tabla de acciones sigue la tabla de sitios de llamadas en el LSDA y especifica tanto las cláusulas de captura como las especificaciones de excepción. Por especificaciones de excepción aquí nos referimos a la característica de C ++ muy difamada llamada “especificaciones de excepción”, que enumera las excepciones que una función puede lanzar. Los dos tipos de registro tienen el mismo formato y se distinguen únicamente por el primer campo de cada entrada. Los valores positivos para este campo especifican los tipos utilizados en las cláusulas catch. Los valores negativos especifican especificaciones de excepción. La Figura 3 muestra la tabla de acciones con cláusulas catch (rojo), cláusula catch-all (naranja), una especificación de excepción (azul). (la función de especificación de excepción ha quedado en desuso en C ++ 17.) debido a que esta función está en desuso y rara vez se usa, actualmente mcsema no maneja especificaciones de excepción. .Gcc_except_table: 4022CF db 7fh; ar_filter [1]: -1 (índice de especificación de excepción = 4022EC)

La asociación inicial que es la encefalopatía isquémica anóxica entre los bloques que pueden generar excepciones y los controladores para esas excepciones se realiza durante la recuperación de CFG, a través de la información extraída de la tabla de excepciones. Esta asociación es necesaria porque el traductor debe garantizar que las funciones que pueden generar excepciones se llamen a través de la semántica de invocación de LLVM y no la instrucción de llamada típica. La instrucción de invocación tiene dos puntos de continuación: flujo normal cuando la llamada tiene éxito y flujo de excepción (i.E., el controlador de excepciones) si la función genera una excepción (figura 4). El reemplazo de la llamada con invocación debe cubrir cada invocación de esa función. Cualquier llamada a la función convence al optimizador de que la función no se ejecuta y no necesita una tabla de excepciones.

McSema usa dos pilas diferentes: una para el código elevado y otra para el código nativo (i.E. Funciones externas). La pila dividida pone limitaciones en el desenrollado de la pila, ya que la ejecución gratuita de la prueba de ansiedad social nativa (i.E. Libstdc ++ API) no tiene una vista completa de la pila. Para admitir el desenrollado de la pila, agregamos una nueva bandera, –abi-library, que permite el uso de la misma pila para la ejecución de código elevado y nativo.

La –abi -raries flag anoxia vs hipoxia permite el uso de la misma pila para código nativo y elevado eliminando la necesidad de código elevado para transiciones nativas. McSema necesita realizar la transición de las pilas para que una función externa que no conozca mcsema pueda ver el estado de la CPU como estaba en el programa original. Las bibliotecas de interfaz binaria de aplicaciones (ABI), que proporcionan firmas de funciones externas, incluidos el valor de retorno, el tipo de argumento y el conteo de argumentos, permiten que el código elevado llame directamente a las funciones nativas en la misma pila. La Figura 5 muestra una instantánea de las firmas de funciones definidas a través de las bibliotecas ABI.

Los controladores de excepciones y los métodos de limpieza son llamados por el tiempo de ejecución del lenguaje y se espera que sigan una convención de llamada estricta. El código elevado no sigue la semántica de la convención de llamada estándar, ya que expresa las instrucciones originales como operaciones en el estado de la CPU. Para admitir estas devoluciones de llamada, implementamos un adaptador especial que convierte un estado nativo en un contexto de máquina que se puede usar al levantarse el código de la mini torre de nanoxia deep silence 4. Se ha tenido especial cuidado en conservar el registro RDX, que almacena el índice de tipo de la excepción.

Hay un truco más para emitir controladores de excepciones funcionales: el orden correcto de los índices de tipo. Recuerde que nuestro ejemplo motivador (figura 1) tiene tres controladores de excepción: std :: out_of_range, std :: bad_alloc y el controlador catch-all. A cada uno de estos controladores se les asigna un índice de tipo, digamos 1, 2, 3 respectivamente (figura 6a), lo que significa que el programa original espera que el índice de tipo 1 se corresponda con std :: out_of_range. .Gcc_except_table: 402254 db 3; ar_filter [1]: 3 (catch typeinfo = 000000)

Durante el proceso de elevación, mcsema recrea los controladores de excepciones utilizados en el programa. El índice de tipo asignado a cada controlador se genera en tiempo de compilación. Cuando el código de bits elevado se compila en un nuevo binario, los índices de tipo podrían ser, y con frecuencia se reasignan. Por ejemplo, std :: out_of_range podría obtener el índice de tipo 3 en un nuevo binario (figura 6b). Esto causaría que el binario elevado ejecute el controlador catch-all cuando se lanza std :: out_of_range!

Para garantizar que se llame al controlador de excepciones correcto, mcsema genera un mapa estático (consulte gvar_landingpad_401133 en la figura 7) de los índices de tipo original para los índices de tipo nuevo, y corrige el índice de tipo durante el pasaje de ladningpad. La pasarela de landingpad es una función que mcsema genera automáticamente. No solo garantiza que el índice de tipo sea correcto, sino que también realiza una transición entre el estado elevado y el nativo.

Al ser llamado, la transferencia guarda el estado de ejecución nativo, carga el estado levantado y llama a los controladores de excepción (que se han levantado y espera el estado levantado). Cuando el paso de ansiedad a las náuseas regresa (en caso de que no se haya manejado la excepción), se debe hacer lo contrario, y pasar del estado elevado al estado nativo para volver al código de la biblioteca en tiempo de ejecución. La Figura 7 muestra el paso de la plataforma de aterrizaje generado para nuestro ejemplo motivador. El código de paso generado obtiene el índice de tipo del registro RDX utilizando la función __mcsema_get_type_index. Corrige y restaura el contexto de la máquina de la ejecución levantada usando la función __mcsema_exception_ret. La instrucción de envoltura a través de la instrucción de invocación guarda la pila y el puntero de marco en el contexto de la función.

Por lo que sabemos, mcsema es el único elevador binario para manejar las excepciones de C ++, que son comunes en todo el software de C ++ de cualquier complejidad. Como hemos demostrado, la recuperación de flujo de control basada en excepciones y la traducción precisa es un tema extremadamente complejo y difícil de implementar correctamente. La implementación del manejo de excepciones afectó a todas las partes del mcsema, incluido el nuevo daño cerebral hipóxico después de los desafíos de un paro cardíaco para la recuperación del flujo de control y la traducción. Los detalles finos, como el reordenamiento del índice de tipo y la garantía de que cada llamada se reemplaza con una invocación, todo se debía descubrir de la manera más difícil mediante la depuración de fallas sutiles y frustrantes.