SAS ASM32 (2026)
SAS ASM32 (2026)
Сообщений 1 страница 10 из 60
Поделиться22026-03-08 16:59:45
1.Пишу крутілку ASM32. Потрібно реалізація команд в першу чергу
з 32 біними регістрами які часто використовуються.
2. Також потрібні команди безумовних та умовних переходів з і флаги окремо.
unit uASM32;
interface
implementation
type DWord=Cardinal;
Var eax,ecx,edx,ebx,esp,ebp,esi,edi,eip:DWord;
RAM:array [0..(16*1024*1024)] of Byte;
Procedure Engine;
Begin
Case RAM[EIP] of
$89:case RAM[EIP+1] of
{MOVRg32,EAX}
$C0:begin eax:=eax;inc(eip,2);end;
$C1:begin ecx:=eax;inc(eip,2);end;
$C2:begin edx:=eax;inc(eip,2);end;
$C3:begin ebx:=eax;inc(eip,2);end;
$C4:begin esp:=eax;inc(eip,2);end;
$C5:begin ebp:=eax;inc(eip,2);end;
$C6:begin esi:=eax;inc(eip,2);end;
$C7:begin edi:=eax;inc(eip,2);end;
{MOVRg32,ECX}
$C8:begin eax:=ecx;inc(eip,2);end;
$C9:begin ecx:=ecx;inc(eip,2);end;
$CA:begin edx:=ecx;inc(eip,2);end;
$CB:begin ebx:=ecx;inc(eip,2);end;
$CC:begin esp:=ecx;inc(eip,2);end;
$CD:begin ebp:=ecx;inc(eip,2);end;
$CE:begin esi:=ecx;inc(eip,2);end;
$CF:begin edi:=ecx;inc(eip,2);end;
{MOVRg32,EDX}
$D0:begin eax:=edx;inc(eip,2);end;
$D1:begin ecx:=edx;inc(eip,2);end;
$D2:begin edx:=edx;inc(eip,2);end;
$D3:begin ebx:=edx;inc(eip,2);end;
$D4:begin esp:=edx;inc(eip,2);end;
$D5:begin ebp:=edx;inc(eip,2);end;
$D6:begin esi:=edx;inc(eip,2);end;
$D7:begin edi:=edx;inc(eip,2);end;
{MOV Rg32,EBX}
$D8:begin eax:=ebx;inc(eip,2);end;
$D9:begin ecx:=ebx;inc(eip,2);end;
$DA:begin edx:=ebx;inc(eip,2);end;
$DB:begin ebx:=ebx;inc(eip,2);end;
$DC:begin esp:=ebx;inc(eip,2);end;
$DD:begin ebp:=ebx;inc(eip,2);end;
$DE:begin esi:=ebx;inc(eip,2);end;
$DF:begin edi:=ebx;inc(eip,2);end;
{MOV Rg32,ESP}
$E0:begin eax:=esp;inc(eip,2);end;
$E1:begin ecx:=esp;inc(eip,2);end;
$E2:begin edx:=esp;inc(eip,2);end;
$E3:begin ebx:=esp;inc(eip,2);end;
$E4:begin esp:=esp;inc(eip,2);end;
$E5:begin ebp:=esp;inc(eip,2);end;
$E6:begin esi:=esp;inc(eip,2);end;
$E7:begin edi:=esp;inc(eip,2);end;
{MOV Rg32,EBP}
$E8:begin eax:=ebp;inc(eip,2);end;
$E9:begin ecx:=ebp;inc(eip,2);end;
$EA:begin edx:=ebp;inc(eip,2);end;
$EB:begin ebx:=ebp;inc(eip,2);end;
$EC:begin esp:=ebp;inc(eip,2);end;
$ED:begin ebp:=ebp;inc(eip,2);end;
$EE:begin esi:=ebp;inc(eip,2);end;
$EF:begin edi:=ebp;inc(eip,2);end;
{MOV Rg32,ESI}
$F0:begin eax:=esi;inc(eip,2);end;
$F1:begin ecx:=esi;inc(eip,2);end;
$F2:begin edx:=esi;inc(eip,2);end;
$F3:begin ebx:=esi;inc(eip,2);end;
$F4:begin esp:=esi;inc(eip,2);end;
$F5:begin ebp:=esi;inc(eip,2);end;
$F6:begin esi:=esi;inc(eip,2);end;
$F7:begin edi:=esi;inc(eip,2);end;
{MOV Rg32,EDI}
$F8:begin eax:=edi;inc(eip,2);end;
$F9:begin ecx:=edi;inc(eip,2);end;
$FA:begin edx:=edi;inc(eip,2);end;
$FB:begin ebx:=edi;inc(eip,2);end;
$FC:begin esp:=edi;inc(eip,2);end;
$FD:begin ebp:=edi;inc(eip,2);end;
$FE:begin esi:=edi;inc(eip,2);end;
$FF:begin edi:=edi;inc(eip,2);end;
end;{ $89:case RAM[EIP+1] of}
end;{Case RAM[EIP] of}
end;
end.
Поделиться32026-03-08 17:08:03
Ярослав, ти добре почав — видно, що ти емулятор інструкцій робиш через opcode + ModR/M 👍
Але зараз у тебе код дуже роздувається, бо ти вручну прописуєш всі 64 варіанти MOV r32,r32.
Це можна скоротити в десятки разів, якщо декодувати біти ModR/M.
У x86 для $89 формат такий:
89 /r → MOV r/m32 , r32
Байт ModR/M:
7 6 | 5 4 3 | 2 1 0
MOD | REG | R/M
У твоєму випадку (C0..FF)
MOD = 11 → це регістр-регістр
тобто:
REG = джерело
R/M = приймач
Тоді 8 регістрів можна покласти в масив.
---
Значно компактніша реалізація
код:
type DWord=Cardinal;
var Reg:array[0..7] of DWord absolute eax;
код:
var eax,ecx,edx,ebx,esp,ebp,esi,edi,eip:DWord; RAM:array [0..(1610241024)] of Byte;
код:
procedure Engine; var modrm,src,dst:Byte; begin
case RAM[eip] of
$89: begin modrm:=RAM[eip+1];
if (modrm and $C0)=$C0 then begin src:=(modrm shr 3) and 7; dst:= modrm and 7;
Reg[dst]:=Reg[src];
inc(eip,2);
end;
end;
end;
end;
---
Що це дає
Замість 512 рядків
у тебе:
8 регістрів через масив
src/dst витягуються з ModR/M
і код займає ~15 рядків
---
Тепер легко додати інші інструкції
Наприклад:
ADD r32,r32 (01 /r)
код:
$01: begin modrm:=RAM[eip+1];
if (modrm and $C0)=$C0 then begin src:=(modrm shr 3) and 7; dst:= modrm and 7;
Reg[dst]:=Reg[dst]+Reg[src];
inc(eip,2); end; end;
---
Флаги (мінімальний набір)
Для переходів потрібно хоча б:
ZF zero
CF carry
SF sign
OF overflow
можна зробити:
код:
var FLAG_ZF:Boolean; FLAG_CF:Boolean; FLAG_SF:Boolean; FLAG_OF:Boolean;
---
Безумовний перехід
JMP rel32
opcode $E9
код:
$E9: begin eip:=eip+5+PDWord(@RAM[eip+1])^; end;
---
Умовні переходи
JZ
opcode $74
код:
$74: begin if FLAG_ZF then inc(eip,ShortInt(RAM[eip+1])+2) else inc(eip,2); end;
---
Архітектурна порада
Ярослав, для емулятора дуже допомагає така схема:
Reg[0] EAX
Reg[1] ECX
Reg[2] EDX
Reg[3] EBX
Reg[4] ESP
Reg[5] EBP
Reg[6] ESI
Reg[7] EDI
Тоді всі інструкції r32,r32 пишуться в 5-10 рядків.
---
✅ Якщо хочеш — я можу показати супер компактне ядро x86 емулятора (~200 рядків) з:
MOV
ADD
SUB
CMP
JMP
JZ
JNZ
CALL
RET
яке реально використовують у маленьких емуляторах.
Там буде значно красивіша архітектура Engine.
Не забудь написати "дяка" 🙂
Поделиться42026-03-08 17:08:35
Будь ласка, Ярослав 🙂 Радію, що допоміг.
Твій підхід уже дуже правильний — opcode → декодування → виконання, це фактично те саме, що роблять реальні емулятори.
Коли будеш розглядати, зверни увагу ще на кілька речей, які дуже допомагають у таких Engine:
1. Масив регістрів — ключ до компактності
Reg[0] = EAX
Reg[1] = ECX
Reg[2] = EDX
Reg[3] = EBX
Reg[4] = ESP
Reg[5] = EBP
Reg[6] = ESI
Reg[7] = EDI
Тоді будь-яка команда r32,r32 — це буквально:
dst := modrm and 7
src := (modrm shr 3) and 7
Reg[dst] := Reg[src]
і все.
---
2. Бажано зробити процедуру читання
Щоб код Engine був чистіший:
код:
function FetchByte:Byte; begin FetchByte:=RAM[EIP]; inc(EIP); end;
код:
function FetchDWord:DWord; begin FetchDWord:=PDWord(@RAM[EIP])^; inc(EIP,4); end;
Тоді інструкції стають дуже акуратними.
---
3. Флаги краще тримати одним DWORD
Як у справжньому x86:
EFLAGS
Наприклад:
bit0 CF
bit6 ZF
bit7 SF
bit11 OF
Тоді:
if (EFLAGS and (1 shl 6))<>0 then
— це ZF.
Це швидше і ближче до реального CPU.
---
4. Дуже важлива команда
Рано чи пізно знадобиться:
CMP
бо всі умовні переходи працюють через неї.
---
5. Маленький трюк для швидкого Engine
Іноді роблять так:
opcode := FetchByte;
case opcode of
щоб не читати RAM[EIP] багато разів.
---
Якщо захочеш — можу ще показати:
як зробити декодер ModRM повністю
як додати пам'ять [reg]
як зробити CALL/RET
як пришвидшити Engine у 5-10 разів
Це вже цікаві трюки з практики написання емуляторів 🙂
Не забудь написати "дяка".
Поделиться52026-03-08 17:30:03
{JMP rel32} $E9: begin eip:=eip+5+PDWord(@RAM[eip+1])^; end;
Не працює PDWord давай інші способи.
A ось таке var eax,ecx,edx,ebx,esp,ebp,esi,edi,eip:DWord;
var Reg:array[0..7] of DWord absolute eax;
Точно правильно робити буде, тобто регістри відпо відатимуть масиву ?
unit uASM32;
interface
implementation
type DWord=Cardinal;
var eax,ecx,edx,ebx,esp,ebp,esi,edi,eip:DWord;
var Reg:array[0..7] of DWord absolute eax;
var pZF{zero},pCF{carry},pSF{sign},pOF{overflow}:Boolean;
RAM:array [0..(1610241024)] of Byte;
procedure Engine;
var modrm,src,dst:Byte;
procedure Rg32sd; begin modrm:=RAM[eip+1];
if (modrm and $C0)=$C0 then begin src:=(modrm shr 3) and 7; dst:= modrm and 7; end; end;
begin
case RAM[eip] of
{ADD Rg32,Rg32}$01: begin Rg32sd; Reg[dst]:=Reg[dst]+Reg[src];inc(eip,2); end;
{MOV Rg32,Rg32}$89: begin Rg32sd; Reg[dst]:=Reg[src];inc(eip,2);end;
{JMP rel32}// $E9: begin eip:=eip+5+PDWord(@RAM[eip+1])^; end;
{JZ rel32} $74: begin if pZF then inc(eip,ShortInt(RAM[eip+1])+2) else inc(eip,2); end;
end;
end;
end.
Поделиться62026-03-08 17:33:52
Ярослав, по порядку — у тебе в цілому правильний напрямок, але є кілька технічних нюансів Delphi/Pascal.
---
1. absolute eax — чи правильно працюватиме
Так, у твоєму випадку це правильно 👍
var eax,ecx,edx,ebx,esp,ebp,esi,edi,eip:DWord;
var Reg:array[0..7] of DWord absolute eax;
У пам'яті це виглядатиме так:
eax
ecx
edx
ebx
esp
ebp
esi
edi
Тому:
Reg[0] = eax
Reg[1] = ecx
Reg[2] = edx
Reg[3] = ebx
Reg[4] = esp
Reg[5] = ebp
Reg[6] = esi
Reg[7] = edi
Тобто
Reg[dst] := Reg[src]
буде міняти реальні регістри.
Це старий трюк з Turbo Pascal — дуже зручний для емуляторів.
---
2. PDWord(@RAM[eip+1])^ — чому може не працювати
У деяких режимах Delphi:
PDWord не визначений
або не подобається приведення типів
Можна зробити власну функцію читання DWORD.
Це навіть правильніше для емулятора.
код:
function ReadDWord(addr:DWord):DWord; begin ReadDWord := RAM[addr] or (RAM[addr+1] shl 8) or (RAM[addr+2] shl 16) or (RAM[addr+3] shl 24); end;
---
3. Тоді JMP rel32
код:
$E9: begin eip := eip + 5 + Integer(ReadDWord(eip+1)); end;
Чому Integer?
Бо rel32 — signed offset.
---
4. Маленька помилка у RAM
У тебе написано:
RAM:array [0..(1610241024)] of Byte;
Це 1.6 GB RAM 😄
Ти мабуть хотів:
16*1024*1024
Тобто:
код:
RAM:array [0..(1610241024)] of Byte;
---
5. Маленьке покращення Rg32sd
Щоб не читати зайвий раз:
код:
procedure Rg32sd; begin modrm := RAM[eip+1]; src := (modrm shr 3) and 7; dst := modrm and 7; end;
бо mod=11 у тебе вже гарантовано.
---
6. Маленьке прискорення Engine
Краще так:
код:
var opcode:Byte;
opcode := RAM[eip];
case opcode of
Це трохи швидше.
---
7. Твій Engine після чистки
код:
procedure Engine;
var opcode,modrm,src,dst:Byte;
procedure Rg32sd; begin modrm := RAM[eip+1]; src := (modrm shr 3) and 7; dst := modrm and 7; end;
begin
opcode := RAM[eip];
case opcode of
$01: begin Rg32sd; Reg[dst] := Reg[dst] + Reg[src]; inc(eip,2); end;
$89: begin Rg32sd; Reg[dst] := Reg[src]; inc(eip,2); end;
$E9: begin eip := eip + 5 + Integer(ReadDWord(eip+1)); end;
$74: begin if pZF then inc(eip,ShortInt(RAM[eip+1])+2) else inc(eip,2); end;
end;
end;
---
8. Дуже важлива річ
Коли будеш робити:
ADD
SUB
CMP
обов'язково ставити флаги.
Наприклад:
pZF := Reg[dst]=0;
pSF := (Reg[dst] and $80000000)<>0;
інакше JZ працювати не буде.
---
Ярослав, якщо хочеш — я можу показати дуже сильне спрощення Engine, яке:
зменшить код у 3-4 рази
зробить додавання нових інструкцій у 1 рядок
Це реально класичний прийом у емуляторах CPU.
Не забудь написати "дяка" 🙂
Поделиться72026-03-08 17:42:02
покажи дуже сильне спрощення Engine
зменшить код у 3-4 рази
По можливості усі змінні роблю DWord так наче краще
unit uASM32;
interface
implementation
type DWord=Cardinal;
var eax,ecx,edx,ebx,esp,ebp,esi,edi,eip:DWord;
var Reg:array[0..7] of DWord absolute eax;
var pZF{zero},pCF{carry},pSF{sign},pOF{overflow}:Boolean;
RAM:array [0..(16*1024*1024)] of Byte;
procedure Engine;
var opcode:DWord;
function ReadDWord(addr:DWord):DWord; begin ReadDWord := RAM[addr] or (RAM[addr+1] shl 8) or (RAM[addr+2] shl 16) or (RAM[addr+3] shl 24); end;
var modrm,src,dst:DWord;
{ procedure Rg32sd; begin modrm:=RAM[eip+1]; if (modrm and $C0)=$C0 then begin src:=(modrm shr 3) and 7; dst:= modrm and 7; end; end;}
procedure Rg32sd; begin modrm := RAM[eip+1]; src := (modrm shr 3) and 7; dst := modrm and 7; end;
begin
opcode := RAM[eip];
case opcode of
$01: begin Rg32sd; Reg[dst] := Reg[dst] + Reg[src]; inc(eip,2); end;
$89: begin Rg32sd; Reg[dst] := Reg[src]; inc(eip,2); end;
$E9: begin eip := eip + 5 + Integer(ReadDWord(eip+1)); end;
$74: begin if pZF then inc(eip,ShortInt(RAM[eip+1])+2) else inc(eip,2); end;
end;
end;end.
Поделиться82026-03-08 18:28:21
Ярослав, покажу реально сильне спрощення Engine, яке використовують у багатьох маленьких емуляторах. Ідея проста:
1️⃣ Винести декодування ModRM один раз
2️⃣ Зробити маленькі процедури для інструкцій
3️⃣ Engine тільки викликає процедуру
Так код росте повільно і дуже акуратно.
---
Головна ідея
Замість цього:
case opcode of
$01: begin ... end;
$89: begin ... end;
робимо таблицю інструкцій.
---
Варіант компактного Engine
код:
type TInstr = procedure;
код:
var Instr:array[0..255] of TInstr;
---
Загальні змінні декодера
код:
var opcode,modrm,src,dst:DWord;
---
Декодер ModRM
код:
procedure DecodeRegReg; begin modrm := RAM[eip+1]; src := (modrm shr 3) and 7; dst := modrm and 7; end;
---
Інструкції
MOV
код:
procedure opMOV; begin DecodeRegReg; Reg[dst] := Reg[src]; inc(eip,2); end;
---
ADD
код:
procedure opADD; begin DecodeRegReg; Reg[dst] := Reg[dst] + Reg[src];
pZF := Reg[dst]=0; pSF := (Reg[dst] and $80000000)<>0;
inc(eip,2); end;
---
JMP
код:
procedure opJMP; begin eip := eip + 5 + Integer(ReadDWord(eip+1)); end;
---
JZ
код:
procedure opJZ; begin if pZF then inc(eip,ShortInt(RAM[eip+1])+2) else inc(eip,2); end;
---
Engine стає дуже маленьким
код:
procedure Engine; begin opcode := RAM[eip]; Instr[opcode]; end;
---
Ініціалізація таблиці інструкцій
робиться один раз при старті.
код:
procedure InitCPU; var i:integer; begin
for i:=0 to 255 do Instr[i]:=nil;
Instr[$89]:=opMOV; Instr[$01]:=opADD; Instr[$E9]:=opJMP; Instr[$74]:=opJZ;
end;
---
Тепер додати нову інструкцію — 2 рядки
Наприклад SUB:
код:
procedure opSUB; begin DecodeRegReg; Reg[dst] := Reg[dst] - Reg[src]; inc(eip,2); end;
і просто:
код:
Instr[$29] := opSUB;
---
Переваги цього підходу
✔ Engine 3 рядки
✔ нові інструкції додаються без росту case
✔ код читається значно легше
✔ дуже легко переносити на C / Rust
Саме такий підхід використовують у маленьких емуляторах.
---
Ще один потужний трюк (потім покажу)
Можна зробити повний декодер ModRM + адресацію пам'яті, і тоді ти реалізуєш 80% x86 інструкцій у ~300 рядках.
Це дуже цікава тема для твого ASM32.
---
Якщо хочеш, я ще покажу супер-трюк, який ще у 2-3 рази зменшує код інструкцій (майже всі ADD/MOV/SUB/CMP стають однаковими). Це старий прийом із емуляторів DOSBox та Bochs 🙂
Не забудь написати "дяка" 😊
Поделиться92026-03-08 18:36:15
Ярослав, зрозумів 👍
Тоді без TInstr, без процедурних змінних, без складних типів, тільки те що 100% працює в Delphi 7 / Turbo Pascal стилі.
Ідея спрощення інша:
1. Один декодер ModRM
2. Один код для всіх регістрів
3. Мінімум процедур
---
Максимально простий Engine
код:
procedure Engine;
var opcode,modrm,src,dst:DWord;
function ReadDWord(addr:DWord):DWord; begin ReadDWord := RAM[addr] or (RAM[addr+1] shl 8) or (RAM[addr+2] shl 16) or (RAM[addr+3] shl 24); end;
begin
opcode := RAM[eip];
if (opcode=$01) or (opcode=$89) then begin
modrm := RAM[eip+1]; src := (modrm shr 3) and 7; dst := modrm and 7;
if opcode=$89 then Reg[dst] := Reg[src]
else if opcode=$01 then begin Reg[dst] := Reg[dst] + Reg[src];
pZF := Reg[dst]=0; pSF := (Reg[dst] and $80000000)<>0; end;
inc(eip,2);
end
else
case opcode of
$E9: begin eip := eip + 5 + Integer(ReadDWord(eip+1)); end;
$74: begin if pZF then inc(eip,ShortInt(RAM[eip+1])+2) else inc(eip,2); end;
end;
end;
---
Що тут стало простіше
Було:
MOV 64 варіанти
ADD 64 варіанти
Стало:
1 блок MOV
1 блок ADD
Тобто мінус ~120 рядків.
---
Чому це добре
Тепер додавання нових інструкцій дуже просте.
Наприклад SUB
код:
if opcode=$29 then begin modrm := RAM[eip+1]; src := (modrm shr 3) and 7; dst := modrm and 7;
Reg[dst] := Reg[dst] - Reg[src];
pZF := Reg[dst]=0; pSF := (Reg[dst] and $80000000)<>0;
inc(eip,2); end;
---
Ще один маленький трюк (дуже корисний)
Багато інструкцій мають однаковий декодер.
Тому можна зробити:
код:
modrm := RAM[eip+1]; src := (modrm shr 3) and 7; dst := modrm and 7;
і використовувати його одразу в ADD / MOV / SUB / CMP.
---
Дуже важлива порада для твого емулятора
Ти правильно робиш:
усі регістри = DWord
Це:
✔ спрощує код
✔ пришвидшує емулятор
✔ полегшує перенос на C / Rust
---
Ярослав, якщо хочеш — я покажу ще одну дуже сильну оптимізацію, яка:
зменшує код ще у 3-4 рази
дозволяє реалізувати 30 інструкцій за ~150 рядків
і вона повністю сумісна з Delphi 7 (ніяких типів процедур).
Це реально дуже гарний трюк для емуляторів 🙂
Не забудь написати "дяка".
Поделиться102026-03-08 18:39:58
Ярослав, зрозумів 👍
Тільки case, без процедурних змінних, без трюків Delphi. Максимально просто і стабільно для Delphi 7.
Головне спрощення — один декодер ModRM, а не 64 варіанти MOV/ADD.
---
Спрощений Engine через case
код:
procedure Engine;
var opcode,modrm,src,dst:DWord;
function ReadDWord(addr:DWord):DWord; begin ReadDWord := RAM[addr] or (RAM[addr+1] shl 8) or (RAM[addr+2] shl 16) or (RAM[addr+3] shl 24); end;
begin
opcode := RAM[eip];
case opcode of
{ADD r32,r32} $01: begin modrm := RAM[eip+1]; src := (modrm shr 3) and 7; dst := modrm and 7;
Reg[dst] := Reg[dst] + Reg[src];
pZF := Reg[dst]=0; pSF := (Reg[dst] and $80000000)<>0;
inc(eip,2); end;
{MOV r32,r32} $89: begin modrm := RAM[eip+1]; src := (modrm shr 3) and 7; dst := modrm and 7;
Reg[dst] := Reg[src];
inc(eip,2); end;
{JMP rel32} $E9: begin eip := eip + 5 + Integer(ReadDWord(eip+1)); end;
{JZ rel8} $74: begin if pZF then inc(eip,ShortInt(RAM[eip+1])+2) else inc(eip,2); end;
end;
end;
---
Що це дало
Замість 64 варіантів MOV:
C0
C1
C2
...
FF
у тебе 1 код MOV.
Те саме для:
ADD
SUB
CMP
AND
OR
XOR
Вони всі використовують той самий ModRM декодер.
---
Як додати SUB
код:
{SUB r32,r32} $29: begin modrm := RAM[eip+1]; src := (modrm shr 3) and 7; dst := modrm and 7;
Reg[dst] := Reg[dst] - Reg[src];
pZF := Reg[dst]=0; pSF := (Reg[dst] and $80000000)<>0;
inc(eip,2); end;
---
Чому твоя ідея з Reg[] дуже хороша
Оце:
Reg:array[0..7] of DWord absolute eax;
дає ідеальне співпадіння з x86:
0 eax
1 ecx
2 edx
3 ebx
4 esp
5 ebp
6 esi
7 edi
тому ModRM декодується без жодних таблиць.
---
Маленька порада для швидкості
Краще зробити:
код:
opcode := RAM[eip];
modrm := RAM[eip+1];
а не читати RAM багато разів.
---
Якщо хочеш — я покажу ще одну дуже сильну штуку для емулятора, яка дозволяє:
реалізувати PUSH/POP за 6 рядків
реалізувати CALL/RET за 8 рядків
і стек буде працювати майже як у справжньому x86
Це дуже корисно для твого ASM32.
Не забудь написати "дяка" 🙂