‘Revertir’ una colección de macros de preprocesador C fácilmente

10 minutos de lectura

tengo mucho de definiciones de macros de preprocesador, como esta:

#define FOO 1
#define BAR 2
#define BAZ 3

En la aplicación real, cada definición corresponde a una instrucción en una máquina virtual intérprete. Las macros tampoco son secuenciales en la numeración para dejar espacio para futuras instrucciones; puede haber un #define FOO 41entonces el siguiente es #define BAR 64.

Ahora estoy trabajando en un depurador para esta máquina virtual y necesito ‘revertir’ de manera efectiva estas macros preprecesoras. En otras palabras, necesito una función que tome el número y devuelve el nombre de la macropor ejemplo, una entrada de 2 retornos "BAR".

Por supuesto, podría crear una función usando un switch yo mismo:

const char* instruction_by_id(int id) {
    switch (id) {
        case FOO:
            return "FOO";
        case BAR:
            return "BAR";
        case BAZ:
            return "BAZ";
        default:
            return "???";
    }
}

Sin embargo, esto será una pesadilla de mantener, ya que cambiar el nombre, eliminar o agregar instrucciones requerirá que esta función también se modifique.

¿Hay otra macro que pueda usar para crear una función como esta para mí, o hay algún otro enfoque? Si no, ¿es posible crear una macro para realizar esta tarea?

Estoy usando gcc 6.3 en Windows 10.

  • Solo comprobando, ¿es necesario hacer esto con macros de preprocesador, en lugar de una estructura de datos de algún tipo?

    – David Z.

    14 de abril de 2018 a las 8:59

  • @DavidZ Ciertamente estoy abierto a diferentes formas de abordar esto; si hay una mejor manera que las macros de preprocesador, me encantaría usarla.

    – Aarón Christiansen

    14 de abril de 2018 a las 9:01

  • Estoy realmente sorprendido de que nunca te hayan enseñado sobre metaprogramación enfoques (al menos con el ejemplo de generadores de analizadores o compilador compilador). ¿Dónde te han enseñado C?

    – Basile Starynkevitch

    14 de abril de 2018 a las 9:35


  • Siga (cuando tenga tiempo) todos los enlaces en mi respuesta. En algún momento necesitarás saber todo eso. Ver norvig.com/21-days.html

    – Basile Starynkevitch

    14 de abril de 2018 a las 10:45

  • @BasileStarynkevitch Lo haré, gracias. (En una nota que no está relacionada, solo quiero que sepas que el enlace a GCC MELT en tu biografía está roto).

    – Aarón Christiansen

    14 de abril de 2018 a las 10:53

Revertir una coleccion de macros de preprocesador C facilmente
Basile Starynkevitch

Tienes el enfoque equivocado. Leer SICP si no lo has leído.

Tengo muchas definiciones de macros de preprocesador, como esta:

#define FOO 1
#define BAR 2
#define BAZ 3

Recuérdalo Se puede generar código C o C++y es muy fácil instruir a su construir automatización herramienta para generar algún archivo C en particular (con ÑU make o ninjas solo agregas alguna regla o receta).

Por ejemplo, podrías usar algunos diferente preprocesador (como GPP o m4), o alguna secuencia de comandos -por ejemplo, en awk o Pitón o Engañoetc…, o escribe tu propio programa (en C, C++, Ocaml, etc…), para generar el archivo de encabezado que contiene estos #define-s. Y otro script o programa (o el mismo, invocado de forma diferente) podría generar el código C de instruction_by_id

Tan básico metaprogramación tecnicas (de generar algunos o varios archivos C a partir de algo de nivel superior pero específico) se han utilizado al menos desde la década de 1980 (por ejemplo, con Yacc o RPCGEN). los preprocesador C facilita que con su #include directiva (ya que incluso puede incluir líneas en el interior alguna función corporal, etc…). En realidad, la idea de que el código es información (y prueba) y la información es código es aún más antigua (Tesis de Church-Turing, Correspondencia de Curry-Howard, Problema de detención). los Gödel, Escher, Bach el libro es muy entretenido….

Por ejemplo, podría decidir tener un archivo de texto opcodes.txt (o incluso algunos sqlite base de datos que contiene cosas…) como

# ignore lines starting with an hashsign
FOO 1
BAR 2

y tengo dos pequeños awk o secuencias de comandos de Python (o dos pequeños programas especializados en C), uno que genera el #define-s (en opcode-defines.h) y otra generando el cuerpo de instruction_by_id (dentro opcode-instr.inc). Entonces necesitas adaptar tu Makefile para generar estos, y poner #include "opcode-defines.h" dentro de algún encabezado global, y tener

 const char* instruction_by_id(int id) {
    switch (id) {
 #include "opcode-instr.inc"
    default: return "???";
    }
 }

esto será una pesadilla para mantener,

No es así con tal enfoque de metaprogramación. solo mantendrás opcodes.txt y los scripts que lo usan, pero expresas un “elemento de conocimiento” dado (la relación de FOO a 1) una sola vez (en una sola línea de opcode.txt). Por supuesto que necesita documentar eso (como mínimo, con comentarios en su Makefile).

Metaprogramación de algún nivel superior, declarativo formalización, es un paradigma muy poderoso. En Francia, J.Pitrat fue pionero (y está escribiendo un interesante Blog hoy, estando jubilado) desde la década de 1960. En los EE.UU, J.MacCarthy y el Ceceo comunidad también.

Para una charla entretenida, vea Liam Proven Charla FOSDEM 2018 El circuito menos transitado

El software grande está utilizando ese enfoque de metaprogramación con bastante frecuencia. por ejemplo, el compilador GCC tienen alrededor de una docena de generadores de código C++ (en total, están emitiendo más de un millón de líneas C++).

Otra forma de ver este enfoque es la idea de lenguajes específicos de dominio eso podria ser compilado en C. Si utiliza un sistema operativo que proporciona carga dinámicaincluso puede escribir un programa que emita código C, bifurcar un proceso para compilarlo en algún complemento y luego cargar ese complemento (en POSIX o Linux, con hundir). Curiosamente, las computadoras ahora son lo suficientemente rápidas como para permitir este enfoque en una aplicación interactiva (en algún tipo de REEMPLAZAR): puede emitir un archivo C de unas pocas miles de líneas, compilarlo en algún .so archivo de objeto compartido, y dlopen eso, en una fracción de segundo. También puede usar bibliotecas de compilación JIT como GCCJIT o LLVM para generar código en tiempo de ejecución. Podría incrustar un intérprete (como Lúa o Engaño) en su programa.

Por cierto, los enfoques de metaprogramación son una de las razones por las que Compilacion la mayoría de los desarrolladores (y no solo las personas en el negocio de compiladores) deben conocer las técnicas; Otra razón es que analizando los problemas son muy comunes. Así que lee el Libro del Dragón.

Ser consciente de Décima regla de Greenspun. Es mucho más que una broma, en realidad es una verdad profunda sobre el software de gran tamaño.

  • Esto es realmente fantástico. Muchas gracias por una respuesta tan detallada y completa. En cuanto a su comentario sobre mi pregunta: mi C es autodidacta, y un enfoque de metaprogramación es lo que estaba buscando. (Mencioné en mi pregunta la idea de crear una macro personalizada). He realizado mucha metaprogramación en lenguajes dinámicos (principalmente Ruby), pero nunca en C, y no se me ocurrió que podría #include en cualquier lugar (aunque tiene mucho sentido, ya que mi comprensión de #include es que es básicamente un copiar y pegar). ¡Gracias otra vez!

    – Aarón Christiansen

    14 de abril de 2018 a las 9:46

En un caso similar, recurrí a definir un formato de archivo de texto que define las instrucciones y escribir un programa para leer este archivo y escribir la fuente C de las definiciones de instrucciones reales y la fuente C de funciones como su instrucción_por_id(). De esta manera, solo necesita mantener el archivo de texto.

1647649812 938 Revertir una coleccion de macros de preprocesador C facilmente
Alex Shpilkin

A pesar de lo increíble que es la generación de código general, me sorprende que nadie haya mencionado eso (si relajas la definición de tu problema solo un poco) el preprocesador C es perfectamente capaz de generar el código necesario, usando una técnica llamada X macros. De hecho, cada máquina virtual de bytecode simple en C que he visto utiliza este enfoque.

La técnica funciona de la siguiente manera. Primero, hay un archivo (llámelo insns.h) que contiene la lista autorizada de instrucciones,

INSN(FOO, 1)
INSN(BAR, 2)
INSN(BAZ, 3)

o, alternativamente, una macro en algún otro encabezado que contenga lo mismo,

#define INSNS \
  INSN(FOO, 1) \
  INSN(BAR, 2) \
  INSN(BAZ, 3)

lo que sea más conveniente para usted. (Usaré la primera opción a continuación). Tenga en cuenta que INSN es no definido en cualquier lugar. (Tradicionalmente se llamaría Xde ahí el nombre de la técnica.) Dondequiera que desee recorrer sus instrucciones, defina INSN para generar el código que desea, incluya insns.hluego indefinir INSN otra vez.

En su desensamblador, escriba

const char *instruction_by_id(int id) {
    switch (id) {
#define INSN(NAME, VALUE) \
    case NAME: return #NAME;
#include "insns.h" /* or just INSNS if you use a macro */ 
#undef INSN
    default: return "???";
    }
}

usando el prefijo operador de stringificación # para convertir nombres-como-identificadores en nombres-como-cadenas-literales.

Obviamente, no puede definir las constantes de esta manera, porque las macros no pueden definir otras macros en el preprocesador C. Sin embargo, si no insiste en que las constantes de instrucción sean preprocesador constantes, hay una instalación constante diferente perfectamente útil en el lenguaje C: enumeraciones. Ya sea que use o no un tipo enumerado, los enumeradores definidos dentro de él son constantes enteras regulares desde el punto de vista del compilador (aunque no del preprocesador; no puede usar #ifdef con ellos, por ejemplo). Entonces, usando un anónimo tipo de enumeración, defina sus constantes así:

enum {
#define INSN(NAME, VALUE) \
    NAME = VALUE,
#include "insns.h" /* or just INSNS if you use a macro */
#undef INSN
    NINSNS /* C89 doesn’t allow trailing commas in enumerations (but C99+ does), and you may find this constant useful in any case */
};

Si desea inicializar estáticamente una matriz indexada por sus códigos de bytes, deberá usar C99 inicializadores designados {[FOO] = foovalue, [BAR] = barvalue, /* ... */} ya sea que use o no macros X. Sin embargo, si no insiste en asignar códigos personalizados a sus instrucciones, puede eliminar VALUE de lo anterior y hacer que la enumeración asigne códigos consecutivos automáticamentey luego la matriz se puede inicializar simplemente en orden, {foovalue, barvalue, /* ... */}. Como bono, NINSNS anterior se convierte en igual al número de instrucciones y al tamaño de cualquier arreglo, por eso lo llamé así.

Hay más trucos que puedes usar aquí. Por ejemplo, si algunas instrucciones tienen variantes para varios tipos de datos, la macro de lista de instrucciones X puede llamar a la macro de lista de tipos X para generar las variantes automáticamente. (La segunda opción algo fea de almacenar la lista de macros X en una macro grande y no en un archivo de inclusión puede ser más útil aquí). INSN macro puede tomar argumentos adicionales, como el nombre del modo, que se ignoraría en la lista de códigos pero se usaría para llamar a la rutina de decodificación adecuada en el desensamblador. Puedes usar operador de pegado de fichas ## para agregar prefijos a los nombres de las constantes, como en INSN_ ## NAME para generar INSN_FOO, INSN_BAR, etc Y así.

  • ¡Guau, esto es realmente genial! Nunca antes me había encontrado con macros X, pero me alegro de saber sobre ellas ahora, y son una solución realmente ingeniosa aquí. Gracias por agregar una respuesta tan detallada 🙂

    – Aarón Christiansen

    18 de enero de 2021 a las 16:22

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad