Come passare dall'assemblaggio al codice macchina (generazione del codice)

14

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

Ad esempio, se apri un file binario nel blocco note, vedi una rappresentazione testuale del codice macchina. Suppongo che ogni byte (simbolo) che vedi sia il corrispondente carattere ascii per il suo valore binario?

Ma come passiamo dal montaggio al binario, cosa succede dietro le quinte ??

    
posta user12979 06.02.2014 - 21:53
fonte

4 risposte

26

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

Lariga"encoding" indica come appare l'istruzione in binario. In questo caso, inizia sempre con 5, quindi un bit non importa (che può essere uno o zero), quindi i "k" stanno per il letterale che stai aggiungendo.

I primi bit sono chiamati "opcode", sono unici per ogni istruzione. La CPU guarda fondamentalmente l'opcode per vedere quale istruzione è, quindi sa decodificare i "k" come numero da aggiungere.

È noioso, ma non così difficile da codificare e decodificare. Ho avuto una classe di studenti universitari dove dovevamo farlo a mano negli esami.

Per fare in realtà un file eseguibile completo, devi anche fare cose come allocare memoria, calcolare gli offset di ramo e inserirli in un formato come ELF , a seconda del tuo sistema operativo.

    
risposta data 06.02.2014 - 22:15
fonte
10

Gli opcode di assembly hanno, per la maggior parte, una corrispondenza uno a uno con le istruzioni della macchina sottostanti. Quindi tutto ciò che dovete fare è identificare ciascun codice operativo nella lingua dell'assembly, mapparlo all'istruzione macchina corrispondente e scrivere le istruzioni macchina su un file, insieme ai parametri corrispondenti (se presenti). Quindi ripeti il processo per ogni opcode aggiuntivo nel file sorgente.

Naturalmente, ci vuole molto di più per creare un file eseguibile che verrà caricato ed eseguito correttamente su un sistema operativo, e molti assemblatori decenti hanno alcune funzionalità aggiuntive oltre alla semplice mappatura degli opcode alle istruzioni della macchina (come i macro, per esempio).

    
risposta data 06.02.2014 - 22:05
fonte
6

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

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

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

add eax, 42

Ora, devi inserire il tuo testo e analizzarlo in singole istruzioni e operandi. Per l'istruzione di cui sopra, probabilmente si otterrebbe una struttura che contiene l'istruzione ADD e una matrice di operandi (un riferimento al registro EAX e il valore 42 ). Una volta che si dispone di questa struttura, si esegue il database delle istruzioni e si trova la riga che corrisponde sia al nome dell'istruzione che ai tipi di operandi. Se non trovi una corrispondenza, è un errore che deve essere presentato all'utente ("la combinazione illegale di opcode e operandi" o simile è il solito testo).

Una volta che abbiamo preso la linea dal database, guardiamo la terza colonna, che per questa istruzione è:

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

Questa è una serie di istruzioni che descrivono come generare le istruzioni del codice macchina richieste:

  • Il mi è una descrizione degli operandi: uno a modr/m (registro o memoria) operando (il che significa che dovremo aggiungere un modr/m byte alla fine dell'istruzione, che sarà venire a più tardi) e uno un'istruzione immediata (che sarà usata nella descrizione dell'istruzione).
  • Il prossimo è hle . Questo identifica come gestiamo il prefisso "lock". Non abbiamo usato "lock", 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 override della dimensione dell'operando. Se producessimo un output a 16 bit, produrremmo il prefisso ora ( 0x66 ), ma assumerò che non lo siamo e proseguiamo.
  • Il prossimo è 83 . Questo è un byte letterale in esadecimale. Lo pubblichiamo.
  • Il prossimo è /0 . Questo specifica alcuni bit aggiuntivi che ci serviranno nel file modr / m, e ci induce a generarlo. Il byte modr/m viene utilizzato per codificare registri o riferimenti di memoria indiretti. Abbiamo un unico operando di questo tipo, un registro. Il registro ha un numero, che è specificato in un altro file di dati :

    eax     REG_EAX         reg32           0
    
  • Controlliamo che reg32 concorda con la dimensione richiesta dell'istruzione dal database originale (lo fa). Il 0 è il numero del registro. Un modr/m byte è una struttura 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)
    
  • Poiché stiamo lavorando con un registro, il campo mod è 0b11 .

  • Il campo reg è il numero del registro che stiamo utilizzando, 0b000
  • Poiché in questa istruzione esiste un solo registro, è necessario compilare il campo rm con qualcosa. Questo è quanto previsto per i dati extra in /0 , quindi lo abbiamo inserito nel campo rm , 0b000 .
  • Il modr/m byte è quindi 0b11000000 o 0xC0 . Emettiamo questo.
  • Il prossimo è ib,s . Questo specifica un byte immediato firmato. Guardiamo gli operandi e notiamo che abbiamo un valore immediato disponibile. Lo convertiamo in un byte firmato e lo emettiamo ( 42 = > 0x2A ).

L'istruzione assemblata completa è quindi: 0x83 0xC0 0x2A . Mandalo al tuo modulo di output, insieme a una nota che nessuno dei byte costituisce un riferimento alla memoria (il modulo di output potrebbe aver bisogno di sapere se lo fa).

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

    
risposta data 04.06.2016 - 20:16
fonte
1

In pratica, un assemblatore di solito non produce direttamente qualche binario < a href="https://en.wikipedia.org/wiki/Eseguibile"> eseguibile , ma alcuni file oggetto (da alimentare in seguito al linker ). Tuttavia, ci sono delle eccezioni (puoi usare alcuni assemblatori per produrre direttamente alcuni eseguibili binari, sono rari).

Per prima cosa, nota che molti assemblatori sono oggi programmi software libero . Quindi scarica e compila sul tuo computer il codice sorgente di GNU come (una parte di binutils ) e di nasmo . Quindi studia il loro codice sorgente. A proposito, raccomando di usare Linux per questo scopo (è un sistema operativo molto amichevole per gli sviluppatori e libero da software).

Il file oggetto prodotto da un assemblatore contiene in particolare un segmento di codice e rilocazione istruzioni. È organizzato in un formato di file ben documentato, che dipende dal sistema operativo. Su Linux, quel formato (usato per file oggetto, librerie condivise, core dump ed eseguibili) è ELF . Tale file oggetto viene successivamente inserito nel linker (che alla fine produce un eseguibile). Le ricollocazioni sono specificate dalla ABI (ad es. x86-64 ABI ). Leggi il libro di Levine Linker e caricatori 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 una architettura del set di istruzioni . x86 o x86 -64 (utilizzato nella maggior parte dei processori laptop o desktop) Gli ISA sono estremamente complessi nei loro dettagli. Ma un sottoinsieme semplificato, chiamato y86 o y86-64, è stato inventato per scopi didattici. Leggi diapositive su di essi. Altre risposte a questa domanda spiegano anche un po 'di questo. Puoi leggere un buon libro sull'architettura del computer .

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

In che modo un eseguibile viene avviato dal sistema operativo kernel (ad esempio, come funziona la chiamata di sistema execve su Linux) è una domanda diversa (e complessa). Solitamente imposta alcuni spazio di indirizzi virtuali (nel process facendo ciò execve (2) ...) quindi reinizializza lo stato interno del processo (inclusi i registri in modalità utente ). Un linker dinamico -come ld-linux.so(8) su Linux- potrebbe essere coinvolto in fase di runtime. Leggi un buon libro, come Sistema operativo: tre pezzi facili . Anche il wiki OSDEV fornisce informazioni utili.

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

    
risposta data 01.09.2018 - 15:10
fonte

Leggi altre domande sui tag