Risposte:
Altri motivi per cui i compilatori producono l'assemblaggio anziché il codice macchina adeguato sono:
add eax,2
può essere tradotto in 83 c0 02
o in 66 83 c0 02
, a seconda dell'ultima direttiva avvenuta come use16
.
Un compilatore di solito converte il codice di alto livello direttamente nel linguaggio macchina, ma può essere costruito in modo modulare in modo che un back-end emetta il codice macchina e l'altro codice assembly (come GCC). La fase di generazione del codice produce "codice" che è una rappresentazione interna del codice macchina, che deve quindi essere convertito in un formato utilizzabile come linguaggio macchina o codice assembly.
Storicamente un numero notevole di compilatori ha prodotto direttamente il codice macchina. Tuttavia, ci sono alcune difficoltà a farlo. Generalmente qualcuno che sta cercando di confermare che un compilatore funziona correttamente troverà più semplice esaminare l'output del codice assembly rispetto al codice macchina. Inoltre, è possibile (ed era storicamente comune) utilizzare un compilatore C o Pascal a un passaggio per produrre un file di linguaggio assembly che può quindi essere elaborato utilizzando un assemblatore a due passaggi. La generazione diretta del codice richiederebbe l'utilizzo di un compilatore C o Pascal a due passaggi oppure l'utilizzo di un compilatore a passaggio singolo seguito da un mezzo di back-patch per gli indirizzi di salto in avanti [se un ambiente di runtime rende disponibili le dimensioni di un programma avviato in un punto fisso, un compilatore potrebbe scrivere un elenco di patch alla fine del codice e fare in modo che il codice di avvio applichi tali patch in fase di esecuzione; tale approccio aumenterebbe la dimensione dell'eseguibile di circa quattro byte per punto patch, ma migliorerebbe la velocità di generazione del programma].
Se l'obiettivo è avere un compilatore che funzioni rapidamente, la generazione diretta del codice può funzionare bene. Per la maggior parte dei progetti, tuttavia, i costi di generazione del codice in linguaggio assembly e di assemblaggio non rappresentano attualmente un grosso problema. Avere compilatori produce codice in una forma che può interagire bene con il codice prodotto da altri compilatori è generalmente un vantaggio abbastanza grande da giustificare l'aumento dei tempi di compilazione.
Anche le piattaforme che utilizzano lo stesso set di istruzioni possono avere diversi formati di file oggetto trasferibili. Mi viene in mente "a.out" (primi UNIX), OMF, MZ (EXE MS-DOS), NE (Windows a 16 bit), COFF (UNIX System V), Mach-O (OS X e iOS) e ELF (Linux e altri), nonché varianti di questi, come XCOFF (AIX), ECOFF (SGI) e Portable Executable (PE) basato su COFF su Windows a 32 bit. Un compilatore che produce un linguaggio assembly non deve conoscere molto i formati di file oggetto, consentendo all'assemblatore e al linker di incapsulare tale conoscenza in un processo separato.
Vedi anche Differenza tra OMF e COFF su Stack Overflow.
Di solito i compilatori lavorano internamente con sequenze di istruzioni. Ogni istruzione sarà rappresentata da una struttura di dati che rappresenta il nome dell'operazione, gli operandi e così via. Quando gli operandi sono indirizzi, tali indirizzi saranno generalmente riferimenti simbolici, non valori concreti.
L'output dell'assemblatore è relativamente semplice. Praticamente si tratta di prendere la struttura dei dati interna dei compilatori e scaricarli in un file di testo in un formato specifico. L'output dell'assemblatore è anche relativamente facile da leggere, il che è utile quando è necessario verificare cosa sta facendo il compilatore.
L'output di file di oggetti binari richiede molto più lavoro. Lo scrittore del compilatore deve sapere come sono codificate tutte le istruzioni (che possono essere tutt'altro che banali su alcuni CPUS), devono convertire alcuni riferimenti simbolici per programmare gli indirizzi relativi al contatore e altri in una qualche forma di metadati nel file oggetto binario . Devono scrivere tutto in un formato altamente specifico per il sistema.
Sì, puoi assolutamente creare un compilatore in grado di generare direttamente oggetti binari senza scrivere assemblatore come passaggio intermedio. La domanda, come tante altre cose nello sviluppo del software, è se la riduzione dei tempi di compilazione valga lo sforzo extra di sviluppo e manutenzione.
Il compilatore con cui ho più familiarità (freepascal) può emettere assemblatore su tutte le piattaforme ma può solo emettere oggetti binari direttamente su un sottoinsieme di piattaforme.
Un compilatore dovrebbe essere in grado di produrre un output dell'assemblatore oltre al normale codice trasferibile a vantaggio del programmatore.
Una volta non trovo il bug in un programma C in esecuzione su Unix System V su una macchina LSI-11. Niente sembrava funzionare. Alla fine, disperato, il compilatore C protable espelleva una versione assembler della sua traduzione. Avevo finalmente trovato il bug! Il compilatore stava allocando più registri di quelli esistenti nella macchina! (Il compilatore ha assegnato i registri da R0 a R8 su una macchina con solo registri da R0 a R7.) Sono riuscito a aggirare il bug nel compilatore e il mio programma ha funzionato.
Un altro vantaggio dell'output dell'assemblatore è il tentativo di utilizzare librerie "standard" che utilizzano protocolli di passaggio di parametri diversi. I compilatori C successivi mi permettono di impostare il protocollo con un parametro ("pascal" farebbe in modo che il compilatore aggiungesse i parametri nell'ordine dato rispetto allo standard C di invertire l'ordine).
Ancora un altro vantaggio è consentire al programmatore di vedere che lavoro spaventoso sta facendo il suo compilatore. Una semplice istruzione C richiede circa 44 istruzioni macchina. I valori vengono caricati dalla memoria e quindi eliminati rapidamente. ecc, ecc, ecc ...
Personalmente credo che avere un compilatore anziché un modulo oggetto trasferibile sia davvero stupido. Durante la compilazione del programma, il compilatore raccoglie molte informazioni sul programma. Di solito memorizza tutte queste informazioni in qualcosa chiamato una tabella dei simboli. Dopo aver eliminato il codice assembler, lancia tutta questa tabella di informazioni. L'assemblatore esamina quindi il codice escreto e ritrae alcune delle informazioni già presenti nel compilatore. Tuttavia, l'assemblatore non sa nulla delle dichiarazioni If delle dichiarazioni For o While. Quindi tutte queste informazioni mancano. Quindi l'assemblatore produce il modulo oggetto trasferibile che il compilatore non ha fatto.
Perché???