Come si passa dallassembly al codice macchina (generazione del codice)

Esiste un modo semplice per visualizzare il passaggio tra lassemblaggio del codice al codice macchina?

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

Ma come si passa dallassembly al binario, cosa succede dietro le quinte ??

Risposta

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

istruzione addlw di esempio

La riga “encoding” dice come appare quellistruzione in binario. In questo caso, inizia sempre con 5 unità, quindi un bit non importa (che può essere uno o zero), quindi la “k” sta per il letterale che stai aggiungendo.

i primi bit sono chiamati “codice operativo”, sono univoci per ciascuna istruzione. La CPU fondamentalmente guarda il codice operativo per vedere di che istruzione si tratta, quindi sa decodificare la “k” come numero da aggiungere.

È noioso, ma non così difficile da codificare e decodificare. Avevo un corso di laurea in cui dovevamo farlo a mano durante gli esami.

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

Risposta

Gli opcode dellassieme 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 allistruzione della macchina corrispondente e scrivere listruzione della macchina in un file, insieme ai suoi parametri corrispondenti (se presenti). Quindi ripeti il processo per ogni codice operativo aggiuntivo nel file sorgente.

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

Answer

Il primo ciò di cui hai bisogno è qualcosa come questo file . Questo è il database delle istruzioni per i processori x86 utilizzato dallassembler NASM (che ho aiutato a scrivere, sebbene non le parti che traducono effettivamente le istruzioni). Scegli una riga arbitraria dal database:

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

Ciò significa che descrive listruzione ADD. Esistono 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 inclusa direttamente nellistruzione). Unistruzione di assembly di esempio che utilizzerebbe questa versione è questa:

 add eax, 42  

Ora, è necessario prendere il testo immesso e analizzarlo in singole istruzioni e operandi. Per listruzione precedente, probabilmente si otterrebbe una struttura che contiene listruzione, ADD e un array di operandi (un riferimento al registro EAX e il valore 42). Una volta ottenuta questa struttura, si esegue il database delle istruzioni e si trova la riga che corrisponde sia al nome dellistruzione che ai tipi di operandi. Se non trovi una corrispondenza, è un errore che deve essere presentato allutente (“combinazione illegale di codice operativo e operandi” o simile è il testo usuale).

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

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

Questo è un insieme di istruzioni che descrivono come generare listruzione del codice macchina richiesta:

  • Il mi è una descrizione degli operandi: uno un modr/m (registro o memoria) operando (il che significa che “dovremo aggiungere un modr/m byte a la fine dellistruzione, alla quale verremo più avanti) e una unistruzione immediata (che verrà utilizzata nella descrizione dellistruzione).
  • 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 16- formato di output a bit, listruzione necessita di un prefisso di sostituzione della dimensione delloperando.Se stessimo producendo un output a 16 bit, dovremmo produrre il prefisso ora (0x66), ma presumo che non lo siamo e andiamo avanti.
  • Il prossimo è 83. Questo è un byte letterale in esadecimale. Lo restituiamo.
  • Il prossimo è /0. Questo specifica alcuni bit extra di cui avremo bisogno nel modr / m bytem, e ci induce a generarlo. Il byte modr/m è usato per codificare registri o riferimenti indiretti alla memoria. 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 sia daccordo con la dimensione richiesta dellistruzione dal database originale (lo fa). Il 0 è il numero del registro. Un modr/m byte è una struttura dati specificata dal processore, che ha questo aspetto:

      (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 cè un solo registro, dobbiamo riempire il campo rm con qualcosa. Questo è lo scopo dei dati aggiuntivi specificati in /0, quindi lo inseriamo nel campo rm, 0b000.
  • Il modr/m byte è quindi 0b11000000 o 0xC0. Lo produciamo.
  • Il prossimo è ib,s. Questo specifica un byte immediato con segno. Guardiamo gli operandi e notiamo che abbiamo un immediato valore disponibile. Lo convertiamo in un byte con segno e lo restituiamo (42 => 0x2A).

Listruzione assemblata completa è quindi: 0x83 0xC0 0x2A. Inviala al tuo modulo di output, insieme a una nota che nessuno dei byte costituisce riferimenti di memoria (il modulo di output potrebbe aver bisogno di sapere se lo fanno).

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

Commenti

  • Grazie. Ottima spiegazione ma non dovrebbe ‘ essere ” 0x83 0xC0 0x2A ” piuttosto che ” 0x83 0xB0 0x2A ” perché 0b11000000 = 0xC0
  • @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 ragione. 🙂

Risposta

In pratica, un assemblatore di solito non “producono direttamente alcuni eseguibili , ma alcuni file oggetto (da inviare in seguito al linker ). Tuttavia, ci sono delle eccezioni (puoi usare alcuni assemblatori per produrre direttamente alcuni eseguibili binari ; sono rari).

Innanzitutto, nota che molti assemblatori oggi sono software gratuito . Quindi scarica e compila sul tuo computer i sorgenti codice di GNU as (una parte di binutils ) e di nasm . Quindi studia il loro codice sorgente. A proposito, ti consiglio di utilizzare Linux a tale scopo (è un sistema operativo molto intuitivo per gli sviluppatori e per il software libero).

Il file oggetto prodotto da un assemblatore contiene in particolare un segmento di codice e trasferimento . È organizzato in un formato di file ben documentato, che dipende dal sistema operativo. Su Linux, quel formato (utilizzato per file oggetto, librerie condivise, core dump ed eseguibili) è ELF . Il 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 di Levine Linker e caricatori per ulteriori informazioni.

Il segmento di codice in tale file oggetto contiene codice macchina con buchi (da riempire, con laiuto delle informazioni di rilocazione, dal linker). Il codice macchina (rilocabile) generato da un assemblatore è ovviamente specifico di un set di istruzioni architettura .Gli ISA x86 o x86-64 (utilizzati nella maggior parte dei processori per laptop o desktop) sono terribilmente complesso nei dettagli. Ma un sottoinsieme semplificato, chiamato y86 o y86-64, è stato inventato per scopi didattici. Leggi le diapositive su di esse. Altre risposte a questa domanda spiegano anche un po di questo. Ti consigliamo di leggere un buon libro su Computer Architecture .

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

Come viene avviato un eseguibile dal kernel del sistema operativo (ad es. come funziona la chiamata di sistema execve su Linux ) è una domanda diversa (e complessa). Di solito imposta uno spazio degli indirizzi virtuale (nel processo che lo fa execve (2) …) quindi reinizializza lo stato interno del processo (inclusi i registri user-mode ). Un linker dinamico , come ld-linux.so (8) su Linux, potrebbe essere coinvolti in fase di esecuzione. 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 fornito alcuni riferimenti (molto incompleti). Dovresti trovarne di più.

Commenti

  • Per quanto riguarda i formati di file oggetto, per un principiante I ‘ d consiglia di guardare il formato RDOFF prodotto da NASM. Questo è stato intenzionalmente progettato per essere il più semplice realisticamente possibile e funzionare ancora in una varietà di situazioni. La sorgente NASM include un linker e un caricatore per il formato. (Divulgazione completa – Ho progettato e scritto tutto questo)

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *