Scrittura assemblatore / disassemblatore basato su tavolo Z80

0

Ho un progetto a lungo termine: computer fai-da-te con vari processori. Uno dei miei desideri non è solo l'hardware, ma anche il software.
Così ho iniziato da assembler / disassembler per Linux, anche se ci sono molti assemblatori di Z. Voglio implementare tutte le istruzioni conosciute, come LD D, RES 0, (IX + n) anche se non verrà mai utilizzato.
Non ho mai hackerato i codici sorgente degli assemblatori degli altri come voglio farlo nel modo in cui lo immagino. Voglio sapere se la mia idea è strana, sbagliata, cattiva di progettazione o non così male.
Ho creato una tabella che descrive ciascuna istruzione che ho trovato sulla rete:

typedef struct
{
    uint32_t opcode;
    uint8_t data_size;
    bool reljmp;
    char *mnemo;
    char *hash;
} opcode_table;

Sembra:

{.opcode=0x10, .mnemo="DJNZ %#.2x", .data_size=1, .reljmp=true   },

Il tavolo stesso è lo stesso per assemblatore e disassemblatore.

Assembler

Il codice sorgente viene analizzato per mezzo di GNU Bison / Flex due volte. Il primo è a secco per calcolare gli offset delle etichette, espandere macrose, ecc. Indirizzi e variabili utente sono contenute nelle tabelle hash (UThash è usato).
Ogni utente di istruzioni scritto è convertito nel modulo specifico, come DJNZ% #. 2x in questo esempio (se Bison è stato in grado di formare la stringa ed è corretto). E poi passa alla funzione handle:

int handle_instruction ( char* instruction, intmax_t data, size_t size )

Segue una semplice ricerca lineare per stringa (la prima ottimizzazione che vedo - usa gli hash delle stringhe e la ricerca binaria)

const opcode_table* new_opc = find_opcode ( instruction );

Al primo passaggio se c'è un'etichetta è sostituita da INTMAX_MIN sul lato Bison, quindi se c'è INTMAX_MIN come argomento nella seconda esecuzione suppongo che dovrei cercare in hashtable per l'indirizzo precedentemente calcolato

Siccome uso il mnemonico preformattato nella tabella, è molto semplice stamparlo:

if ( PASS2 == run_pass && verbose )
    {
        printf ( "%#.4x: ", PC );
        printf ( new_opc->mnemo, ( uint16_t ) data );
        puts ( "" );
    }

Non ho bisogno di formattarlo manualmente - è tutto automatico.

Disassemblatore

È molto semplice - basta leggere byte per byte e vedere se è prefisso o meno - quindi basta guardare nella tabella per opcode e ottenere mnemonico corrispondente e stamparlo

char* compile_string ( const char* format, ... )
{
    char* string;
    va_list args;
    va_start ( args, format );
    if ( 0 > vasprintf ( &string, format, args ) ) string = NULL;
    va_end ( args );
    return string;
}

Il codice risultante è abbastanza grande (~ 300kb) a causa della tabella, ma non importa. Il vantaggio principale di una tabella è la facilità di aggiungere un nuovo supporto per la CPU. Voglio implementare un Forth sulla stessa base. Il problema principale che ho incontrato è il rilevamento dei bug. Al momento ci sono errori nella logica.
Come test principale per l'assemblatore, ho impostato la compilazione di Monitor 48 scritto per TASM. È male farlo sul 30-40% a causa di problemi relativi al calcolo del salto.
Qual è il modo migliore di testare questo tipo di programmi? Posso compilare tutte le istruzioni, ma nel codice reale potrebbero esserci dei problemi. Posso confrontare il risultato con altri assemblatori, ma in modo molto limitato, perché non supportano macro TASM-like, per esempio, o alcune istruzioni.

    
posta pugnator 11.03.2015 - 18:51
fonte

0 risposte

Leggi altre domande sui tag