Browser'da çalışan Win64 makine
@exoproc/simulate paketinin çalıştırdığı Win64 user-mode makinesinin tasarımı ve sınırları
@exoproc/simulate, tam bir Windows kurulumu boot etmeye çalışmaz. Makine doğrudan bir Win64 user-mode process context'iyle başlar. x86-64 koruma modelinde bu, Ring 3 yani CPL 3 seviyesidir. CPU instruction yürütür; yalnız syscall instruction'ına ulaştığında kontrol JavaScript tarafındaki kernel'e geçer.
Bu sınır bilinçlidir. Exoproc'un incelemek istediği alan process, thread, sanal bellek, Windows x64 çağrı düzeni ve native API davranışıdır. BIOS, bootloader, aygıt sürücüsü ve gerçek Ring 0 emülasyonu bu davranışları öğretmek için zorunlu değildir.
Makineyi canlı denemek için Exoproc IDE'ye bakın — orada gördüğünüz process listesi, thread'ler ve debugger aşağıda anlatılan aynı Win64Machine modeli üzerinde çalışır (IDE kendi Win64Machine örneğini ayrı bir Web Worker içinde tutar).
Bir instruction adımında ne olur?
Debugger'daki Step Into düğmesi bir animasyon karesini ilerletmez. Çekirdek aşağıdaki sırayı uygular:
RIPadresinden en fazla 15 byte executable memory olarak okunur.- REX prefix, opcode, ModRM ve displacement alanları çözülür.
- Instruction'ın kaynak operand'ı register veya sanal bellekten okunur.
- Sonuç register'a, stack'e veya process address space'ine yazılır.
RIP,RSP,RFLAGSve değişen diğer register'lar güncellenir.- Instruction
syscalliseRAXsyscall numarası kabul edilir ve JS kernel handler'ı çağrılır. - Kernel dönüş değeri
RAXiçine yazılır ve user-mode yürütme devam eder.
Örnek thread gerçekten şu byte dizisini yürütür:
push rbp
mov rbp, rsp
sub rsp, 20h
mov rax, [rcx]
add rax, 1
mov [rcx+8], rax
int3
add rsp, 20h
pop rbp
retMemory panelinde [rcx+8] alanının değişmesi, önceden tanımlanmış bir steps[].memoryWrites kaydından değil 48 89 41 08 instruction'ının yürütülmesinden kaynaklanır.
CPU backend'i neden yerel?
Simülatör unicorn.js, Unicorn Engine veya başka bir native/WASM CPU motoru kullanmaz. X64Cpu instruction byte'larını doğrudan process memory'sinden alır ve kendi TypeScript decoder/interpreter katmanında yürütür.
Decoder şu alanları kendisi çözer:
- legacy opcode ve iki byte'lı
0Fopcode'ları, - REX.W/R/X/B register genişletmeleri,
- ModRM register ve memory operand'ları,
- SIB base/index/scale adresleme,
disp8,disp32, RIP-relative adresler,- immediate ve relative branch değerleri.
Execution katmanı register dosyasını, RIP, RSP, RFLAGS, stack ve process memory'sini doğrudan değiştirir. add, sub, and, or, xor, cmp ve test gibi instruction'lar zero/sign/carry/overflow flag'lerini simülatör içinde üretir. Henüz desteklenmeyen bir opcode host motoruna devredilmez; adres ve opcode bilgisini taşıyan UnsupportedInstructionFault ile durur. Böylece debugger'da görülen decode, register değişimi ve memory write aynı backend'in sonucudur.
JS kernel ve DLL sınırı
JS kernel normal bir JavaScript fonksiyonunu doğrudan Win32 API diye çağırmaz. Her process'e ortak ABI paketinde tanımlı DLL'ler için executable export sayfaları map edilir. En alt seviyedeki bir kernel export'u şu thunk'ı kullanabilir:
mov eax, syscall_id
syscall
retBu sayede bir FFI çağrısında call stack ve Windows x64 ABI korunur:
package TypeScript
→ bun:ffi browser shim
→ kernel32.dll!OpenProcess thunk
→ mov eax, generated_syscall_id
→ syscall
→ JS kernel handle table
→ RAX dönüş değeriİlk dört integer/pointer argüman RCX, RDX, R8 ve R9 üzerinden taşınır. Beşinci ve sonraki argümanlar caller shadow space'inden sonra stack'e yazılır. Örneğin VirtualAllocEx içindeki flProtect beşinci argümandır; JS kernel bunu [RSP+28h] adresinden okur.
Global makine, ayrı process'ler
getGlobalWin64Machine() aynı JavaScript realm'i içinde tek bir makine döndürür. Her çağrıldığında makineyi yeniden boot etmez; aynı process tablosu içinde yeni bir process oluşturur.
import { getGlobalWin64Machine } from '@exoproc/simulate';
const machine = getGlobalWin64Machine();
const first = machine.createRandomDebugProcess();
const second = machine.createRandomDebugProcess();
first.machine === second.machine; // true
first.pid !== second.pid; // trueExoproc IDE bu makineyi bir Web Worker içinde çalıştırır: bun:ffi modülünün aktif process context'i worker'a özgüdür ve CPU run loop'u sayfanın ana thread'ini bloklamaz. Renderer tarafı @exoproc/simulate'i hiç import etmez, yalnızca window.exoproc üzerinden tipli bir mesajlaşma katmanıyla konuşur.
bun:ffi browser katmanı
Next.js yapılandırması bun:ffi import'unu packages/simulate/src/runtime/bun-ffi.ts dosyasına yönlendirir. Katmanın mevcut sorumlulukları:
ptrile process belleğine buffer map etmek,readvetoArrayBufferile aynı address space'i okumak,dlopenile process'e map edilmiş DLL export'unu çözmek,CFunctionile adresi Windows x64 ABI üzerinden çağırmak,cckullanıldığındacimporttarafından istenensymbol_ptrexport adreslerini çözmek.
bindWin64Process(process) çalıştırılacak package kodunu belirli bir process context'ine bağlar. Bu bağ gelecekte Worker bootstrap tarafından otomatik kurulacak.
Ortak Win32 tanımları
bun-xffi içindeki DLL imzaları @exoproc/win32-abi paketine taşınmıştır. Tanımların biçimi native binding tarafındakiyle aynıdır; farklı bir manifest dili veya signature parser kullanılmaz:
export const Kernel32Definitions = {
OpenProcess: {
args: [CType.DWORD, CType.BOOL, CType.DWORD],
returns: CType.HANDLE,
},
// ...
};Her DLL dosyası tanımlarla birlikte kendi descriptor'ını taşır:
export const Kernel32Dll = {
name: 'kernel32',
knownToLinker: true,
definitions: Kernel32Definitions,
};Tüm DLL'lerin ortak suffix'i ayrı bir sabittir:
export const Win32DllSuffix = '.dll';bun-xffi, aynı nesneyi doğrudan cimport(Kernel32Dll.definitions, ...) çağrısına verir; library ve knownToLinker descriptor'dan, suffix ise global Win32DllSuffix sabitinden gelir. Simülatör Object.entries(Kernel32Dll.definitions) üzerinden export adını, argüman sayısını ve dönüş tipini okuyup DLL thunk tablosunu üretir. Tam dosya adı getWin32DllFileName(Kernel32Dll) ile kernel32.dll olarak oluşturulur. imageBase ve simülasyona özgü syscall numarası ortak nesneye eklenmez; @exoproc/simulate içindeki adaptörün sorumluluğunda kalır.
CType, native ortamda değerleri hardcode etmez; doğrudan bun:ffi içindeki FFIType enum'unu kullanır. Browser build'inde Next.js'in bun:ffi alias'ı @exoproc/simulate içindeki bağımsız sayısal tabloya yönlenir. Bu tablo win32-abi import etmez ve bir Proxy trap'i ile tanımadığı FFIType alanlarında hemen hata verir. Böylece yeni kullanılan bir Bun FFI tipi sessizce undefined olup ABI imzasını bozmaz.
Otomatik syscall kataloğu
DefaultWin32ExportCatalog, src/bin/dll/index.ts içindeki DefaultWin32GuestDlls manifest'ini binary kaynağı olarak kabul eder. Her manifest girdisi ABI imzasını @exoproc/win32-abi içindeki descriptor'dan alır; simülasyona ait binding'i ise src/bin/dll/<ad>.ts dosyası belirler. Yeni bir fonksiyon tanımı eklendiğinde simülatörde ayrıca export adı, ordinal ya da syscall numarası yazılmaz.
Jeneratör her DLL için otomatik olarak:
- çakışmayan bir user-mode image base,
- DLL'e ayrılmış bir syscall ID bloğu,
- her fonksiyon için ordinal ve otomatik syscall ID,
- export adı ile signature bağlantısı,
- seçilen binding'e göre executable x64 export byte'ları
üretir. Win64Machine yeni process oluştururken kataloğun tamamını process address space'ine map eder. kernel32, ntdll, msvcrt, user32, gdi32, advapi32, psapi ve ws2_32 yine src/bin/dll/ altında ayrı dosyalardır; ancak bu dosyalarda export adlarını veya sayıları yeniden yazan ikinci bir definition listesi bulunmaz. Şimdilik özel guest kodu olmayan DLL dosyaları bu nedenle bilerek çok kısadır.
for (const dll of DefaultWin32ExportCatalog.dlls) {
for (const exported of Object.values(dll.functions)) {
const code = DefaultWin32ExportCatalog.createExportThunk(exported);
// DLL export slotuna map edilir.
}
}ABI tanımı bir fonksiyonun adını ve çağrı düzenini bildiği için export, adres ve varsayılan dispatch kaydı tamamen üretilebilir. Fonksiyonun gerçek semantiği ise tanımdan çıkarılamaz: örneğin WriteFile konsola mı, VFS dosyasına mı yoksa pipe'a mı yazacağını JS kernel handler'ından öğrenir. Handler'ı henüz bulunmayan otomatik export kaybolmaz; çağrıldığında ERROR_CALL_NOT_IMPLEMENTED döndürür.
Her export'un syscall olması da gerekmez. Win32ExportBindingRegistry seçilen fonksiyonu:
- otomatik syscall thunk'ı,
- başka bir DLL export'una doğrudan forward,
- sabit dönüş değeri,
- caller'ın derlediği gerçek x64 guest wrapper
olarak bağlayabilir. Örneğin varsayılan katalog msvcrt.dll!fclose export'unu kernel32.dll!CloseHandle adresine forward eder. msvcrt.dll!fopen ise guest tarafında çalışan x64 wrapper ile CreateFileA çağırır. İki durumda da fopen veya fclose için elle syscall numarası verilmez.
const bindings = new Win32ExportBindingRegistry()
.forwardFunction(Win32Api.msvcrt.fclose, Win32Api.kernel32.CloseHandle)
.guestFunction(Win32Api.msvcrt.fopen, ({ resolveExport }) =>
compileFopen(resolveExport('kernel32.dll', 'CreateFileA')),
);
const catalog = new Win32ExportCatalog(
DefaultWin32GuestDlls.map((dll) => dll.source),
{ bindings },
);
const machine = new Win64Machine({
win32Catalog: catalog,
});Bu ayrım DLL katmanlarını korur: CRT fonksiyonu ister Win32 API'lerine bağlanan guest koduyla yazılabilir, ister gerçekten kernel hizmeti gerekiyorsa syscall olarak bırakılabilir.
CMD process'i
Win32CommandPrompt, global makinede VFS'deki derlenmiş cmd.exe image'ını ayrı bir process'e yükler. Process spawn aşaması inherited CONIN$ / CONOUT$ handle'larını, argc, argv ve tam environment vector'ünü hazırlar. Guest main thread başlangıçta RCX=argc, RDX=argv, R8=envp alır; banner ve prompt'u standart output handle'ına WriteFile ile çizer ve standart input handle'ında ReadFile bekler. Input geldiğinde satırı CreateProcessA ile yeni executable process'ine dönüştürür. TypeScript tarafında komutları ayıran bir built-in switch bulunmaz.
Backend kullanımı ve mevcut komut yüzeyi için Win64 CMD sayfasına bakın.
Şu anda gerçek olanlar
- x64 byte fetch, decode ve instruction yürütme,
- her thread için ayrı register context ve user stack,
- process başına ayrı sanal address space,
r,rw,rxverwxpage korumaları,- unmapped veya yanlış korumalı erişimde
STATUS_ACCESS_VIOLATION, - Windows x64 register ve stack argüman düzeni,
- main entry point için
RCX=argc,RDX=argv,R8=envpbaşlangıç sözleşmesi, - DLL export tablosu ve executable syscall thunk'ları,
kernel32,ntdll,msvcrt,user32,gdi32,advapi32,psapivews2_32tanımlarından üretilen DLL yüzeyi,- process/thread handle tablosu,
- spawn sırasında kurulan inherited stdin, stdout ve stderr handle'ları,
- global, case-insensitive Win32 VFS ile process başına current directory,
- process başına case-insensitive environment ve child process'e kopyalanan
PATH/PATHEXT, OpenProcess,VirtualAllocEx,WriteProcessMemory,ReadProcessMemoryveCreateRemoteThreadiçin ilk kernel handler'ları,- konsol ve normal VFS dosyaları için çalışan
CreateFileA,ReadFile,WriteFileveCloseHandle, - process başına cursor, wrap ve scroll davranışı olan sabit 80×25 ASCII video framebuffer'ı,
- host ağına bağlanmayan, yalnız
127.0.0.0/8üzerinde ICMP echo sağlayan Winsock transport'u, - relocation tabanlı küçük x64 program compiler'ı ve child process loader'ı,
CreateProcessA, environment API'leri ve executablePATHçözümleme,- gerçek CPU state'ini okuyan modüler debugger.
Henüz gerçek olmayanlar
Bu sürüm bir Wine alternatifi veya genel amaçlı Windows uygulama çalıştırıcısı değildir:
- tam x86-64 instruction set henüz yoktur,
- compiler'ın kendi import/data relocation'ları işlenir; genel PE32+ import, base relocation, TLS ve exception tabloları henüz yüklenmez,
- scheduler preemption ve gerçek paralel thread yürütmesi yoktur,
- callback thunk'ları ve arbitrary TinyCC derlemesi henüz yoktur,
node:fs,node:path,Bun.*ve tüm native bağımlılıklar henüz VFS'e yönlendirilmemiştir,- gerçek Windows DLL binary'leri çalıştırılmaz; export'lar üretilmiş syscall thunk'ları, forwarder'lar veya küçük guest wrapper'lardır.
Bu ayrım önemlidir: debugger artık sahte değildir, fakat makinenin desteklediği ISA ve Win32 yüzeyi bilinçli olarak küçüktür.
Geliştirme sırası
- Decoder'a Exoproc'un ürettiği bütün integer, branch ve memory instruction'larını eklemek.
- PE32+ loader ile section, import, relocation ve entry point yüklemek.
- Her docs örneğini ayrı Web Worker process context'inde çalıştırmak.
node:fsve geçici dosya işlemlerini ortak Win64 VFS'e bağlamak.- Ortak ABI tanımlarından sonra
packages/xffiçalışma kodunu browser alias'larıyla doğrudan çalıştırmak. - Ardından
winapi,accessorsventhreadpaketlerini aynı kernel yüzeyine taşımak.
Full-system emülasyon gerekirse bu çekirdeğin üzerine ayrıca Ring 0/interrupt/boot katmanı eklenebilir. Mevcut tasarımda ise kernel bilerek JS host tarafında kalır ve guest yürütme doğrudan Ring 3/CPL 3 seviyesinde başlar.
Neden hazır bir browser VM kullanılmadı?
v86 browser'da başarılı bir full-system x86 emülatörüdür; ancak kendi uyumluluk tablosunda 64-bit extension'ların ve debugger için gerekli bazı single-step özelliklerinin bulunmadığını belirtir. Exoproc doğrudan Windows x64 user-mode davranışını hedeflediği için bu temel uygun değildir.
BoxedWine Wine ile browser'da Windows uygulaması çalıştırılabileceğini kanıtlar; bunu 32-bit Wine ile birlikte Linux kernel ve CPU emüle ederek yapar. Bu yaklaşım gerçek bir Wine benzeri sistem için doğru referanstır, fakat Exoproc'un dar Win64 ABI laboratuvarından çok daha büyük bir runtime ve dağıtım yüzeyi oluşturur.
Bu nedenle mevcut yol full-system makineyi küçültmeye çalışmak değil, doğrudan gerekli user-mode x64 ISA ve Win64 syscall yüzeyini büyütmektir.