La recompilación dinámica es muy habitual en emuladores. Para emulatar un procesador mediante interpretación hace falta un ordenador mucho mas potente que el original. Además, la emulación no se limita a emular el/los procesadores de la máquina original sino todos los dispositivos que intervienen en ella. Por eso, para conseguir mejores rendimientos y poder emular una máquina mas o menos recientes con ordenadores o consolas actuales hay que acudir a la recompilación dinámica.
Emular mediante interpretación consiste en decodificar cada instrucción y ejecutarla simulando su comportamiento original; actualizando una memoria virtual para la máquina, registros virtuales, banderas (flags) etc.
La recompilación dinámica consiste en convertir código máquina del procesador que se pretende emular en código máquina del procesador que ejecuta el emulador mejorando el rendimiento entre un 300% y un 1500%. Entre 3 y 15 veces mas rápido. Aunque el rendimiento afecta mucho sobre las implementaciones del intérprete y el recompilador dinámico. Pero si suponemso que ambos están bien hechos y son rápidos, el rendimiento obtenido oscila entre el rango citado anteriormente.
Para hacer una buena emulación y que sea rápida hay que conocer a la perfección las dos máquinas que entran en juego (la que se pretende emular y la que emula) y conocer sus limitaciones y cuellos de botella. Localizar las instrucciones mas frecuentes y que consumen casi todo el tiempo del procesador y optimizarlas es un punto crítico a tener en cuenta.
Creación de código máquina dinámicamente.
SIMD (Single Instruction, Multiple Data)
Apuntes:
The "mod-reg-r/m" byte (in Figure 5.14) specifies a basic addressing mode. This byte contains the following fields:
![]() |
The REG field specifies an 80x86 register. Depending on the instruction, this can be either the source or the destination operand. Many instructions have the "d" (direction) field in their opcode to choose whether this operand is the source (d=0) or the destination (d=1) operand. This field is encoded using the bit patterns found in the following table:
For certain (single operand) instructions, the REG field may contain an opcode extension rather than a register value (the R/M field will specify the operand in this case).
The MOD and R/M fields combine to specify the other operand in a two-operand instruction (or the only operand in a single-operand instruction like NOT or NEG). Remember, the "d" bit in the opcode determines which operand is the source and which is the destination. The MOD and R/M fields together specify the following addressing modes:
|
MOD
|
R/M
|
Addressing Mode
|
|---|---|---|
|
%00
|
%000
|
[eax]
|
|
%01
|
%000
|
[eax+disp8]
|
|
%10
|
%000
|
[eax+disp32]
|
|
%11
|
%000
|
register (al/ax/eax)1
|
|
%00
|
%001
|
[ecx]
|
|
%01
|
%001
|
[ecx+disp8]
|
|
%10
|
%001
|
[ecx+disp32]
|
|
%11
|
%001
|
register (cl/cx/ecx)
|
|
%00
|
%010
|
[edx]
|
|
%01
|
%010
|
[edx+disp8]
|
|
%10
|
%010
|
[edx+disp32]
|
|
%11
|
%010
|
register (dl/dx/edx)
|
|
%00
|
%011
|
[ebx]
|
|
%01
|
%011
|
[ebx+disp8]
|
|
%10
|
%011
|
[ebx+disp32]
|
|
%11
|
%011
|
register (bl/bx/ebx)
|
|
%00
|
%100
|
SIB Mode
|
|
%01
|
%100
|
SIB + disp8 Mode
|
|
%10
|
%100
|
SIB + disp32 Mode
|
|
%11
|
%100
|
register (ah/sp/esp)
|
|
%00
|
%101
|
Displacement Only Mode
(32-bit displacement)
|
|
%01
|
%101
|
[ebp+disp8]
|
|
%10
|
%101
|
[ebp+disp32]
|
|
%11
|
%101
|
register (ch/bp/ebp)
|
|
%00
|
%110
|
[esi]
|
|
%01
|
%110
|
[esi+disp8]
|
|
%10
|
%110
|
[esi+disp32]
|
|
%11
|
%110
|
register (dh/si/esi)
|
|
%00
|
%111
|
[edi]
|
|
%01
|
%111
|
[edi+disp8]
|
|
%10
|
%111
|
[edi+disp32]
|
|
%11
|
%111
|
register (bh/di/edi)
|
Programa de muestra:
import std.stdio, std.string, std.file, std.path, std.c.stdlib, std.c.stdio;
import std.c.windows.windows;
enum REG4 {
EAX = 0b000, ECX = 0b001, EDX = 0b010, EBX = 0b011,
ESP = 0b100, EBP = 0b101, ESI = 0b110, EDI = 0b111
}
extern(Windows) {
void Sleep(uint);
uint GetTickCount();
uint QueryPerformanceCounter(ulong *lpPerformanceCount);
uint QueryPerformanceFrequency(ulong *lpFrequency);
}
ulong qpfreqv; static this() { QueryPerformanceFrequency(&qpfreqv); }
ulong getMilli() {
ulong v;
QueryPerformanceCounter(&v);
return v * 1000 / qpfreqv;
}
// TST27
// Clase que se encarga de la emulación de un TST27
// procesador inventado para esta demostración
class TST27 {
// Banco de registros
// Usaremos EBX para almacenar la dirección de los registros
static uint regs[32];
static bool stopped = false;
// Memoria
// Usaremos ECX para almacenar la dirección de la memoria
static ubyte mem[0x1000];
static ubyte* pc;
static void write1(ubyte v) { *cast(ubyte *)pc = v; pc += 1; }
static void write2(ushort v) { *cast(short *)pc = v; pc += 2; }
static void write4(uint v) { *cast(int *)pc = v; pc += 4; }
void dump_regs() {
for (int n = 0; n < 32; n++) {
printf("$%02d = %08X\n", n, regs[n]);
}
}
extern(C) void break_execution() {
dump_regs();
//stopped = true; while (stopped) Sleep(1);
printf("Fin de ejecución");
exit(-1);
}
extern(C) static void print_reg(ubyte r) {
if (r > 23) {
printf(" \n");
} else {
printf(" r%02d = %08X\n", r, TST27.regs[r]);
}
}
// Escribe las instrucciones en código máquina de este procesador
void SET(ubyte r, uint value) { write1(0x00); write1(r); write4(value); }
void ADD(ubyte r1, ubyte r2) { write1(0x01); write1(r1); write1(r2); }
void JLT(ubyte r1, ubyte r2, uint addr) { write1(0x02); write1(r1); write1(r2); write4(addr); }
void PRINT(ubyte r) { write1(0x03); write1(r); }
void END() { write1(0x04); }
// Obtiene la dirección de memoria de la instrucción actual
uint label() { return pc - mem.ptr; }
void clear() {
for (int n = 0; n < mem.length; n++) mem[n] = 0x00;
pc = mem.ptr;
}
// Inicializa la memoria con las instrucciones
void init(int r = 0) {
clear();
void test1() {
uint l;
SET(0, 0x00000000);
SET(1, 0x01FFFFFF);
SET(2, 0x00000001);
PRINT(0);
PRINT(1);
PRINT(2);
PRINT(24);
l = label();
ADD(0, 2);
JLT(0, 1, l);
PRINT(0);
PRINT(1);
PRINT(2);
END();
}
void test2() {
uint l1, l2;
SET(1, 0x00FFFFFF);
SET(2, 0x00000001);
SET(10, 0x00000000);
SET(11, 0x00000005);
PRINT(0);
PRINT(1);
PRINT(2);
PRINT(10);
PRINT(11);
PRINT(24);
l1 = label();
ADD(10, 2);
SET(0, 0x00000000);
l2 = label();
ADD(0, 2);
JLT(0, 1, l2);
JLT(10, 11, l1);
PRINT(0);
PRINT(1);
PRINT(2);
PRINT(10);
PRINT(11);
END();
}
auto funcs = [&test1, &test2];
writefln("PTR: %08X\n", cast(uint)cast(void *)funcs[r]);
funcs[r]();
//test1();
//test2();
pc = mem.ptr;
}
// Ejecutamos el código interpretando
void run_int() {
while (true) {
ubyte op = *pc;
switch (op) {
case 0x00: // SET - Da un valor 32 bits
regs[*(pc + 1)] = *cast(uint *)(pc + 2);
pc += 6;
break;
case 0x01: // ADD - Añade a un registro otro
regs[*(pc + 1)] += regs[*(pc + 2)];
pc += 3;
break;
case 0x02: // JLT - Salta si el primer registro es menor que el segundo
if (regs[*(pc + 1)] < regs[*(pc + 2)]) {
pc = mem.ptr + *cast(uint *)(pc + 3);
} else {
pc += 7;
}
break;
case 0x03: // PRINT - Imprime un registro por pantalla
print_reg(*(pc + 1));
pc += 2;
break;
case 0x04: // END - Termina la ejecución del programa
return;
break;
}
}
}
// Ejecutamos el código con recompilación dinámica
void run_dyna() {
Dyna8086 dyna = new Dyna8086;
void recompile_dir(ubyte* pc) {
dyna.set_active();
bool end = false;
while (!end) {
ubyte op = *pc;
// Cargamos la dirección del banco de registros en EBX
Dyna.MOV4(REG4.EBX, cast(uint)regs.ptr);
// Cargamos la dirección del banco de la memoria
Dyna.MOV4(REG4.ECX, cast(uint)mem.ptr);
Dyna.label(pc - mem.ptr);
switch (op) {
case 0x00: // SET - Da un valor 32 bits
Dyna.MOV4(REG4.EAX, *cast(uint *)(pc + 2));
Dyna.VMR_SET(*(pc + 1));
pc += 6;
break;
case 0x01: // ADD - Añade a un registro otro
// TODO: damos por sentado que r2 vale 1
Dyna.VMR_INC(*(pc + 1));
pc += 3;
break;
case 0x02: // JLT - Salta si el primer registro es menor que el segundo
Dyna.VMR_GET(*(pc + 2));
Dyna.VMR_CMPL(*(pc + 1), REG4.EAX);
Dyna.JLL(*cast(uint *)(pc + 3));
pc += 7;
break;
case 0x03: // PRINT - Imprime un registro por pantalla
Dyna.INVOKE_STDCALL(cast(void *)&print_reg, [*(pc + 1)]);
pc += 2;
break;
case 0x04: // END - Termina la ejecución del programa
Dyna.RET();
end = true;
break;
}
}
//
}
recompile_dir(pc);
dyna.run();
}
}
alias Dyna8086 Dyna;
class Dyna8086 {
// LABELS //////////
static void*[uint] labels;
static void label(uint v) { labels[v] = pc; }
// BUFFER //////////
static ubyte *pc;
static void write1(ubyte v) { *cast(ubyte *)pc = v; pc += 1; }
static void write2(ushort v) { *cast(short *)pc = v; pc += 2; }
static void write4(uint v) { *cast(int *)pc = v; pc += 4; }
static int rel4(void* p) { return p - pc - 4; }
// OPS /////////////
static void INC (REG4 r) { write1(0b_01000_000 | r); }
static void DEC (REG4 r) { write1(0b_01001_000 | r); }
static void PUSHR4(REG4 r) { write1(0b_01010_000 | r); }
static void POPR4 (REG4 r) { write1(0b_01011_000 | r); }
static void PUSH1 (byte v) { assert(0 != 0); }
static void PUSH4 (int v) {
if (v > 0xFF) { write1(0b_01101000); write4(v); }
else { write1(0b_01101010); write1(v); }
}
static void POP4 (REG4 r) { write1(0b01011000 | r); }
static void MOV4 (REG4 r, int v) { write1(0b_10111_000 | r); write4(v); }
static void ADD1 (REG4 r, ubyte v) { write1(0b_10000011); write1(0b_11000_000 | r); write1(v); }
static void CALL (REG4 r) { write1(0b_11111111); write1(0b_11010_000 | r); }
// El puntero a la función es relativo al PC después de decodificar toda la instrucción
static void CALL (void* p) { write1(0b_11101000); write4(rel4(p)); }
static void RET (int dis = 0) { write1((dis == 0) ? 0b_11000011 : 0b_11000010); }
static void JMP (void* p) { write1(0b_11101001); write4(rel4(p)); }
static void CMP (REG4 r1, REG4 r2) {
// 3B DF
// 00111011 11011111
// CMP EBX, EDI
write1(0b_00111011);
write1(0b_11_000_000 | r2 | (r1 << 3));
}
static void JL (void* p) {
void *npc = pc + 2;
// 0x7F == JG
//if (abs(p - npc) <= 0x7f) {
if (true) {
write1(0x7C);
write1(p - npc);
} else {
assert(0 != 0);
}
}
// MACROS //////////
// La convencion de windows especifica que los parametros se pasan por pila
// y la funcion se encarga de quitar los elementos de la pila
static void INVOKE_WINDOWS(void *p, int[] vs = []) {
foreach (v; vs) PUSH4(v);
CALL(p);
}
// La convencion stdcall especifica que los parametros se pasan por pila
// y la funcion que llama se encarga de dejar la pila en su estado original
static void INVOKE_STDCALL(void *p, int[] vs = []) {
INVOKE_WINDOWS(p, vs);
ADD1(REG4.ESP, vs.length * 4);
}
// Salta a una etiqueta especificada
static void JMPL (uint l) { JMP(labels[l]); }
static void JLL(uint l) {
JL(labels[l]);
}
static void _VMR_WREG(ubyte r, REG4 rr = REG4.EAX) {
write1(0b_01_000_011 | (rr << 3)); // [EBX + r8]
write1(r * 4);
}
static void VMR_SET (ubyte r) {
write1(0x89); _VMR_WREG(r, REG4.EAX); // MOV ..., EAX
}
static void VMR_GET (ubyte r) {
write1(0x8B); _VMR_WREG(r, REG4.EAX); // MOV EAX, ...
}
static void VMR_INC (ubyte r) {
write1(0xFF); // INC/DEC ...
write1(0b_01_000_011); // [EBX + r8] (INC)
write1(r * 4);
}
static void VMR_DEC (ubyte r) {
write1(0xFF); // INC/DEC ...
write1(0b_01_001_011); // [EBX + r8] (DEC)
write1(r * 4);
}
static void VMR_CMPL (ubyte r1, REG4 r2) {
write1(0b_00111001);
write1(0b_01_000_011 | (r2 << 3));
write1(r1 * 4);
}
static void VMR_CMPR (REG4 r1, ubyte r2) {
write1(0b_00111011);
write1(0b_01_000_011 | (r1 << 3));
write1(r2 * 4);
}
static void BREAK() {//
//INVOKE_STDCALL(&break_execution);
exit(-1);
}
// LOCAL ///////////
static ubyte *local_pc;
ubyte[] data;
this() { data = new ubyte[0x1000]; local_pc = &data[0]; }
void set_active() { pc = local_pc; }
void dump() {
int p = 0;
while (p < data.length) {
printf("%06X | ", p);
for (int n = 0; n < 16; n++, p++) {
if (n == 8) printf(" ");
printf("%02X ", data[p]);
}
printf("\n");
if (&data[p] > pc) break;
}
}
void run() {
void *p = data.ptr;
asm { call p; }
}
}
void demo1() {
printf("DEMO1\n");
static uint regs[32];
static ubyte mem[0x1000];
Dyna dyna = new Dyna;
dyna.set_active();
extern(C) static int test(int a) {
printf("test:[%d]\n", a);
return a - 1;
}
/*
Dyna.PUSH4(0xFFFFFFFF);
Dyna.POPR4(REG4.EBX);
Dyna.PUSHR4(REG4.EAX);
Dyna.CALL(&test);
Dyna.POPR4(REG4.EBX);
Dyna.PUSHR4(REG4.EAX);
Dyna.CALL(&test);
Dyna.POPR4(REG4.EBX);
Dyna.PUSHR4(REG4.EAX);
Dyna.CALL(&test);
Dyna.POPR4(REG4.EBX);
Dyna.PUSHR4(REG4.EAX);
Dyna.CALL(&test);
Dyna.POPR4(REG4.EBX);
Dyna.PUSHR4(REG4.EAX);
Dyna.POPR4(REG4.EBX);
*/
/*
Dyna.PUSH4(0xFFFFFFFF);
Dyna.INC(REG4.EAX);
*/
// Cargamos la dirección del banco de registros en EBX
Dyna.MOV4(REG4.EBX, cast(uint)regs.ptr);
// Cargamos la dirección del banco de la memoria
Dyna.MOV4(REG4.ECX, cast(uint)mem.ptr);
/*
Dyna.MOV4(REG4.EAX, 10);
Dyna.WREG(0);
*/
Dyna.VMR_GET(0);
Dyna.PUSHR4(REG4.EAX);
Dyna.CALL(&test);
Dyna.ADD1(REG4.ESP, 4);
//Dyna.MOV4(REG4.EDX, 0x6FFFFFFF);
Dyna.MOV4(REG4.EDX, 0x00FFFFFF);
Dyna.label(1);
Dyna.VMR_INC(0);
Dyna.VMR_CMPL(0, REG4.EDX);
Dyna.JLL(1);
//
Dyna.VMR_GET(0);
Dyna.PUSHR4(REG4.EAX);
Dyna.CALL(&test);
Dyna.ADD1(REG4.ESP, 4);
//Dyna.BREAK();
//Dyna.JMPL(1);
/*
Dyna.label(1);
Dyna.INVOKE_STDCALL(&test, [1]);
Dyna.INVOKE_STDCALL(&test, [2]);
Dyna.JMPL(1);
*/
Dyna.RET();
dyna.run();
dyna.dump();
printf("\n\n");
}
void demo2(int r = 0) {
uint start;
int intt = 0, dynat = 0;
TST27 cpu = new TST27();
printf("DEMO2 (%d)\n", r);
if (true) {
//if (false) {
writefln("Ejecutando programa interpretado: {");
cpu.init();
start = getMilli();
cpu.run_int();
intt = getMilli() - start;
writefln("} TIEMPO: %d ms\n", intt);
}
if (true) {
writefln("Ejecutando programa con recompilación dinámica:");
cpu.init(r);
start = getMilli();
cpu.run_dyna();
dynat = getMilli() - start;
writefln("} TIEMPO: %d ms\n", dynat);
}
writefln("La mejora del rendimiento ha sido de un: %.2f%% (%.1f veces mas rápido)", cast(real)intt * 100 / cast(real)dynat, (cast(real)intt / cast(real)dynat));
}
int main(char[][] args) {
demo1();
demo2(0);
demo2(1);
return 0;
}
