Come si passa dall'assembly al codice macchina (generazione del codice)


16

C'è un modo semplice per visualizzare il passaggio tra l'assemblaggio del codice e il codice macchina?

Ad esempio, se si apre un file binario nel blocco note, viene visualizzata una rappresentazione in formato testo del codice macchina. Suppongo che ogni byte (simbolo) che vedi sia il corrispondente carattere ASCII per il suo valore binario?

Ma come possiamo passare dall'assemblaggio al binario, cosa succede dietro le quinte ??

Risposte:


28

Guarda la documentazione del set di istruzioni e troverai voci come questa da un microcontrollore per ogni istruzione:

esempio di istruzioni addlw

La riga "codifica" indica l'aspetto di tale istruzione in binario. In questo caso, inizia sempre con 5, quindi un bit non importa (che può essere uno o zero), quindi la "k" sta per il valore letterale che stai aggiungendo.

I primi bit sono chiamati "opcode", sono univoci per ciascuna istruzione. Fondamentalmente la CPU guarda il codice operativo per vedere di che istruzione si tratta, quindi sa decodificare le "k" come numero da aggiungere.

È noioso, ma non è così difficile da codificare e decodificare. Avevo una lezione di laurea in cui dovevamo farlo a mano negli esami.

Per creare effettivamente un file eseguibile completo, devi anche fare cose come allocare memoria, calcolare gli offset dei rami e metterlo in un formato come ELF , a seconda del tuo sistema operativo.


10

I codici operativi di assemblaggio hanno, per la maggior parte, una corrispondenza uno a uno con le istruzioni della macchina sottostante. Quindi tutto ciò che devi fare è identificare ogni codice operativo nel linguaggio assembly, mapparlo sull'istruzione macchina corrispondente e scrivere l'istruzione macchina su un file, insieme ai suoi parametri corrispondenti (se presenti). Quindi ripetere la procedura per ciascun codice operativo aggiuntivo nel file di origine.

Ovviamente, ci vuole molto di più per creare un file eseguibile che verrà caricato ed eseguito correttamente su un sistema operativo e la maggior parte degli assemblatori decenti hanno alcune funzionalità aggiuntive oltre alla semplice mappatura dei codici operativi alle istruzioni della macchina (come le macro, ad esempio).


7

La prima cosa di cui hai bisogno è qualcosa come questo file . Questo è il database di istruzioni per processori x86 usato dall'assemblatore NASM (che ho aiutato a scrivere, sebbene non le parti che traducono effettivamente le istruzioni). Consente di selezionare una linea arbitraria dal database:

ADD   rm32,imm8    [mi:    hle o32 83 /0 ib,s]      386,LOCK

Ciò significa che descrive le istruzioni ADD. Esistono più varianti di questa istruzione e quella specifica qui descritta è la variante che accetta un registro a 32 bit o un indirizzo di memoria e aggiunge un valore immediato a 8 bit (ovvero una costante inclusa direttamente nell'istruzione). Un'istruzione di assemblaggio di esempio che userebbe questa versione è questa:

add eax, 42

Ora, devi prendere il tuo input di testo e analizzarlo in singole istruzioni e operandi. Per l'istruzione sopra, ciò comporterebbe probabilmente una struttura che contiene l'istruzione ADDe una matrice di operandi (un riferimento al registro EAXe al valore 42). Una volta che hai questa struttura, corri attraverso il database delle istruzioni e trovi la riga che corrisponde sia al nome dell'istruzione che ai tipi degli operandi. Se non trovi una corrispondenza, è un errore che deve essere presentato all'utente ("combinazione illegale di opcode e operandi" o simile è il solito testo).

Una volta ottenuta la linea dal database, esaminiamo la terza colonna, che per questa istruzione è:

[mi:    hle o32 83 /0 ib,s] 

Questa è una serie di istruzioni che descrivono come generare l'istruzione del codice macchina richiesta:

  • La miè una descriptiuon degli operandi: una al modr/m(register o memoria) operando (i quali mezzi dovremo aggiungere un modr/mbyte alla fine della corrispondente istruzione, che arriveremo in seguito) e un'un'istruzione immediata (che sarà essere usato nella descrizione dell'istruzione).
  • Il prossimo è hle. Questo identifica come gestiamo il prefisso "lock". Non abbiamo usato "blocco", quindi lo ignoriamo.
  • Il prossimo è o32. Questo ci dice che se stiamo assemblando il codice per un formato di output a 16 bit, l'istruzione ha bisogno di un prefisso di sostituzione della dimensione dell'operando. Se producessimo un output a 16 bit, produrremmo il prefisso ora ( 0x66), ma suppongo che non lo siamo e andiamo avanti.
  • Il prossimo è 83. Questo è un byte letterale in esadecimale. Lo abbiamo prodotto.
  • Il prossimo è /0. Questo specifica alcuni bit extra di cui avremo bisogno nel modr / m bytem e ci fa generare. Il modr/mbyte viene utilizzato per codificare registri o riferimenti di memoria indiretta. Abbiamo un solo di questi operandi, un registro. Il registro ha un numero, che è specificato in un altro file di dati :

    eax     REG_EAX         reg32           0
  • Controlliamo che sia d' reg32accordo con la dimensione richiesta dell'istruzione dal database originale (lo fa). Il 0è il numero del registro. Un modr/mbyte è una struttura di dati specificata dal processore, che assomiglia a questo:

     (most significant bit)
     2 bits       mod    - 00 => indirect, e.g. [eax]
                           01 => indirect plus byte offset
                           10 => indirect plus word offset
                           11 => register
     3 bits       reg    - identifies register
     3 bits       rm     - identifies second register or additional data
     (least significant bit)
  • Perché stiamo lavorando con un registro, il modcampo è 0b11.

  • Il regcampo è il numero del registro che stiamo usando,0b000
  • Poiché in questa istruzione esiste un solo registro, è necessario compilare il rmcampo con qualcosa. Ecco a cosa servivano i dati extra specificati in /0, quindi li inseriamo nel rmcampo 0b000.
  • Il modr/mbyte è quindi 0b11000000o 0xC0. Abbiamo prodotto questo.
  • Il prossimo è ib,s. Questo specifica un byte immediato con segno. Osserviamo gli operandi e notiamo che abbiamo un valore immediato disponibile. Lo convertiamo in un byte con segno e lo emettiamo ( 42=> 0x2A).

L'istruzione assemblato completo è pertanto: 0x83 0xC0 0x2A. Invialo al tuo modulo di output, insieme a una nota che nessuno dei byte costituisce un riferimento di memoria (potrebbe essere necessario che il modulo di output lo sappia).

Ripetere l'operazione per ogni istruzione. Tieni traccia delle etichette in modo da sapere cosa inserire quando sono referenziate. Aggiungi funzionalità per macro e direttive che vengono passate ai moduli di output del file oggetto. E questo è fondamentalmente come funziona un assemblatore.


1
Grazie. Grande spiegazione ma non dovrebbe essere "0x83 0xC0 0x2A" anziché "0x83 0xB0 0x2A" perché 0b11000000 = 0xC0
Kamran

@Kamran - $ cat > test.asm bits 32 add eax,42 $ nasm -f bin test.asm -o test.bin $ od -t x1 test.bin 0000000 83 c0 2a 0000003... sì, hai perfettamente ragione. :)
Jules il

2

In pratica, un assemblatore di solito non produce direttamente alcuni eseguibili binari , ma alcuni file di oggetti (da inviare successivamente al linker ). Tuttavia, ci sono eccezioni (è possibile utilizzare alcuni assemblatori per produrre direttamente alcuni eseguibili binari; non sono comuni).

Innanzitutto, nota che molti assemblatori lo sono oggi programmi software gratuiti . Quindi scarica e compila sul tuo computer il codice sorgente di GNU come (una parte di binutils ) e di nasm . Quindi studia il loro codice sorgente. A proposito, ti consiglio di usare Linux a tale scopo (è un sistema operativo molto intuitivo e facile da usare per gli sviluppatori).

Il file oggetto prodotto da un assemblatore contiene in particolare un segmento di codice e istruzioni di trasferimento . È organizzato in un formato file ben documentato, che dipende dal sistema operativo. Su Linux, quel formato (usato per file oggetto, librerie condivise, core dump ed eseguibili) è ELF . Quel file oggetto viene successivamente immesso nel linker (che alla fine produce un eseguibile). I trasferimenti sono specificati dall'ABI (ad es. X86-64 ABI ). Leggi il libro Levers Linkers and Loaders per ulteriori informazioni.

Il segmento di codice in tale file oggetto contiene codice macchina con buchi (da compilare, con l'aiuto delle informazioni di trasferimento, dal linker). Il codice macchina (trasferibile) generato da un assemblatore è ovviamente specifico per un'architettura di set di istruzioni . Gli ISA x86 o x86-64 (utilizzati nella maggior parte dei processori per laptop o desktop) sono terribilmente complessi nei loro dettagli. Ma un sottoinsieme semplificato, chiamato y86 o y86-64, è stato inventato a scopo didattico. Leggi le diapositive su di essi. Altre risposte a questa domanda spiegano anche questo. Potresti voler leggere un buon libro su Computer Architecture .

La maggior parte degli assemblatori lavora in due passaggi , il secondo emette il trasferimento o corregge parte dell'output del primo passaggio. Ora usano le solite tecniche di analisi (quindi leggi forse The Dragon Book ).

Come un eseguibile viene avviato dal kernel del sistema operativo (ad esempio come funziona la execvechiamata di sistema su Linux) è una domanda diversa (e complessa). Di solito imposta un po 'di spazio di indirizzi virtuali (nel processo che esegue execve (2) ...) quindi reinizializza lo stato interno del processo (compresi i registri in modalità utente ). Un linker dinamico, come ld-linux.so (8) su Linux, potrebbe essere coinvolto in fase di esecuzione. Leggi un buon libro, come Sistema operativo: tre pezzi facili . Il wiki OSDEV fornisce anche informazioni utili.

PS. La tua domanda è così ampia che devi leggere diversi libri a riguardo. Ho fornito alcuni riferimenti (molto incompleti). Dovresti trovarne di più.


1
Per quanto riguarda i formati di file oggetto, per un principiante consiglierei di guardare il formato RDOFF prodotto dalla NASM. Questo è stato progettato intenzionalmente per essere il più semplice possibile e realisticamente funzionante in una varietà di situazioni. La sorgente NASM include un linker e un caricatore per il formato. (Informativa completa - Ho progettato e scritto tutto questo)
Jules,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.