Come sono codificati i valori letterali in bytecode?

4

Nota : questa domanda è in qualche modo correlata a In che modo esattamente il bytecode è "analizzato" ?, ma non è un duplicato di esso. In questa domanda, sto chiedendo una parte specifica di come viene generato bytecode, non di come "bytecode" viene "analizzato".

Come indicato nel titolo, in che modo i letterali (come stringhe, numeri interi, ecc.) sono codificati in file bytecode? La mia confusione deriva dal fatto che la rappresentazione in byte di ogni dato letterale è di lunghezza variabile. Ciò significa che una macchina virtuale non avrebbe idea di quanti byte ha bisogno di raccogliere per poter leggere l'intero letterale. Se la mia domanda non è ancora chiara, credo che un esempio visivo aiuterà a illustrare la mia confusione.

Considera questo esempio. Un parser ha appena costruito un albero di sintassi astratto. Ha convertito l'espressione 3 + 2 in:

   +
  / \
 3   2

Il tuo compilatore / interprete ora inizia a camminare sull'albero. Genera il seguente codice bytec:

 PUSH          3            PUSH        2         ADD
  |            |             |          |          |    
|-----| |--------------|  |-----|  |----------| |-----|
b'\x00' b'\x00\x00\x00\'  b'\x00'  b'\x00\x00\' b'\x05'

La tua macchina virtuale inizia quindi a leggere nel file bytecode. Legge il primo byte e vede che è l'opcode PUSH. Ora ha bisogno di leggere l'argomento nell'opcode PUSH.

Ma ecco il problema . La macchina virtuale non ha modo di sapere quanti byte ha bisogno di leggere per ottenere l'intero argomento su PUSH. gli argomenti a PUSH sono un numero variabile di byte, quindi la macchina virtuale non sa quanti byte ha bisogno di leggere per ogni argomento. Come visto nello pseudo bytecode di cui sopra, il numero di byte utilizzati per rappresentare valori diversi può variare e non è coerente.

E mentre l'esempio precedente utilizza solo numeri interi, questo può valere anche per altre cose. Ad esempio stringhe o rappresentazioni di stringhe di nomi di identificatori.

Ho provato a cercare su vari blog e persino sulla documentazione ufficiale di qualche codice bytecode, ma non ho ancora trovato una spiegazione su come i letterali siano codificati.

Le informazioni sull'armadio che ho trovato sono state una frase di questa risposta dato da Ratchet Freak alla domanda a cui mi sono collegato nell'intestazione. Si legge:

To give an example that makes the bytes-per-operation very explicit there is SPIR-V. The first 4-byte word of each instruction is constructed as 2-byte length + 2-byte opcode.

Sembra che quello che sta dicendo è che SPIR-V force opcode tutti i loro argomenti da comprimere o espandere per riempire due byte. Mentre suppongo che potrebbe fare questo, sono abbastanza sicuro che questo non è ciò che intendeva.

Qual è la pratica comune quando si codificano valori letterali, le cui rappresentazioni di byte sono di lunghezza variabile, in file bytecode? Naturalmente, presumo che la loro sia una pratica comune, ma forse ogni lingua lo fa in modo diverso?

    
posta Christian Dean 22.12.2016 - 20:03
fonte

3 risposte

6

La tua domanda si applica più in generale dei sistemi di byte code, all'architettura generale delle istruzioni, all'hardware o al codice byte.

What is the common practice when encoding literal values, whose byte representations are of variable length?

Ci sono circa una mezza dozzina di tecniche ragionevoli.

  • L'opcode indica il numero di byte del letterale che sta seguendo l'opcode. Ciò significa che di solito ci sono molti opcode altrimenti identici. Si noti che l'opcode deve (in qualche modo) codificare la dimensione o il tipo della manipolazione dell'operando (ad es. Push int-32-bit), che può essere eseguita insieme o separatamente dalla codifica della dimensione / conteggio dei byte di dati letterali (spesso chiamata immediata) seguendo l'opcode. Nel caso in cui questi differiscano (spesso il letterale immediato descritto dall'istruzione è più breve del tipo per l'operando), quindi, i byte che seguono l'opcode sono espansi come per la definizione dell'opcode (es. Usando l'estensione del segno), da la dimensione del letterale immediato fornito alla dimensione del tipo di operando.

  • Ci sono altri bit dopo l'opcode, ma sono considerati separati dall'opcode, che indicano la dimensione del letterale (e / o il formato di tutti gli operandi). Quando un set di istruzioni ha sotto-codici raggruppati insieme, a volte i bit oltre l'opcode principale indicano cose sui vari operandi.

  • Una variante dell'ultimo è che ogni operando ha il proprio descrittore separato (eventualmente raggruppato prima del letterale). Questi sono tipici nelle macchine di registro in stile CISC (come il VAX) che hanno più istruzioni di operando, come addl3 (tre operandi add lunghi).

  • Ci sono bit nel letterale stesso che indicano se più dati del letterale seguono; ad esempio, un bit di ciascun byte può essere dedicato a indicare più byte, vale a dire che ogni byte letterale restituisce 7 bit e indica se il byte successivo è un valore letterale o il letterale è completato. Ciò è alquanto ostile alle prestazioni di interpretazione (del software), ma l'hardware può decodificarlo meglio di quanto sembrerebbe indicare l'approccio ingenuo. Se stai facendo una JIT invece di un interprete, questo potrebbe funzionare.

  • Viene utilizzato un riferimento indiretto di qualche tipo e il letterale viene memorizzato altrove. Questo è il caso, ad esempio, con stringhe in codice byte Java / C #. In Java, l'opcode della stringa push utilizza un indice nella tabella costante. Le interfacce binarie delle applicazioni spesso specificano un registro macchina o una posizione globale accessibile per costanti più grandi come le stringhe o altre costanti blob a 32-bit, 64-bit o più grandi.

  • A volte i valori letterali possono essere abbastanza grandi o abbastanza complessi che vengono utilizzate più istruzioni per assemblare il letterale. Alcune architetture forniscono un carico immediato che prende il suo operando letterale e lo inserisce nei byte alti del registro (o stack). Quindi un normale add immediato è usato per introdurre i bit bassi del letterale. Questo a volte si trova in architetture che usano istruzioni di dimensioni fisse.

risposta data 22.12.2016 - 21:02
fonte
4

the arguments to PUSH are a variable number of bytes, so the virtual machine does not know how many bytes it needs to read for each argument.

Di solito, l'architettura stabilisce che tutti gli argomenti sono un numero fisso di byte.

Si noti che potrebbero esserci più varianti di PUSH, ognuna delle quali richiede un numero diverso di byte. Quindi potresti avere un PUSHWORD, un PUSHBYTE, un PUSHHORT e ognuno avrà un codice operativo univoco . Potrebbero essere tutti chiamati solo PUSH in assembly, ma poi ci deve essere abbastanza contesto negli argomenti (es. Specificando un registro a 16 bit invece di un registro a 32 bit) per determinare quale opcode unico opcode in realtà è PUSH.

Le tue istruzioni generate sarebbero un po 'più simili a questa:

 PUSH3         3           PUSH2        2         ADD
  |            |             |          |          |    
|-----| |--------------|  |-----|  |----------| |-----|
b'\x03' b'\x00\x00\x00\'  b'\x02'  b'\x00\x00\' b'\x05'

Si noti che le istruzioni push sono diverse e hanno opcode diversi. Ciò non è limitato a PUSH, potresti avere più codici operativi per ogni operazione aritmetica e logica, quindi puoi specificare se stai aggiungendo byte o parole, o XORing solo un byte o una parola intera.

Le stringhe (o qualsiasi struttura di dati non atomici come matrici, strutture o elenchi) non vengono di solito fornite come immediate (cioè parte dell'istruzione). Invece sono memorizzati in una posizione separata in memoria e indicati tramite un indirizzo di memoria (che sarebbe di dimensioni fisse e quindi potrebbe essere fornito come parte dell'istruzione).

Pertanto (supponendo che tu abbia un'istruzione di stampa delle stringhe nel tuo bytecode) "PRNT" Hello World "non apparirebbe così:

PRNT               "Hello World"
 |                      |
|--| |------------------------------------------|
\x45 \x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64

Sembrerebbe invece questo:

// data section
// This example assumes the string is loaded at address /xcafebeef.
// HWString is a label referring to that. The label is useful in
// assembly, but probably not needed in the actual bytecode.

HWString:     "Hello World"
                   |
|----------------------------------------------|
\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64\x00  // null terminator, if you're a fan of C-style strings.

// later in the file

// text section
PRNT  HWString
 |        |
|--| |--------|
\x45 \xcafebeef

Potresti dare un'occhiata all'architettura MIPS (32 bit), in cui tutte le istruzioni sono esattamente a 32 bit e tutte le istruzioni si inseriscono in uno di tre formati .

Java è un altro esempio. In particolare, ha bipush ( b yte i mmediate push ) e sipush ( s hort i mmediate push ). Il primo prende un singolo operando da un byte, quest'ultimo prende un singolo operando a due byte, sempre.

    
risposta data 22.12.2016 - 20:11
fonte
0

Gli oggetti letterali sono memorizzati in una matrice al di fuori del bytecode. E poi il bytecode put solo indicizza in quell'array.

Un esempio di Ruby,

$ ruby --dump insns -e '[nil,0,1,2,"str",/regexp/]'
== disasm: <RubyVM::InstructionSequence:<main>@-e>======================
0000 trace            1                                               (   1)
0002 putnil           
0003 putobject_OP_INT2FIX_O_0_C_ 
0004 putobject_OP_INT2FIX_O_1_C_ 
0005 putobject        2
0007 putstring        "str"
0009 putobject        /regexp/
0011 newarray         6
0013 leave            

Come puoi vedere ci sono diversi bytecode put .

  • Alcuni sono 1 byte di longe e specializzati per nil e numeri comuni come 0 e 1
  • Gli altri sono lunghi 2 byte e includono un indice nell'array dell'oggetto letterale
risposta data 22.12.2016 - 22:04
fonte