Sanal Makine Nasıl Yazılır (2022)
(jmeiners.com)- Bilgisayarların iç işleyişini ve programlama dillerinin nasıl yürütüldüğünü anlamak için, LC-3 eğitim mimarisi üzerinde assembly programları çalıştıran yaklaşık 250 satırlık C tabanlı bir VM doğrudan uygulanıyor
- Uygulama hedefi; 65.536 adet 16 bit bellek konumu, 10 register, 16 opcode, koşul bayrakları, trap routine’leri ve bellek eşlemeli register’ları olan küçük bir bilgisayar modeli
- Yürütme döngüsü,
PC’nin işaret ettiği komutu okuyup artırdıktan sonra opcode’u yorumlayarakADD,LDI,BR,JMP,TRAPgibi komutları çalıştıran fetch-decode-execute yapısıyla işliyor - Program yükleme, nesne dosyasındaki ilk 16 bit origin’i okuyup belleğe yerleştiriyor ve LC-3’ün big-endian biçimini, çoğu modern bilgisayarda kullanılan little-endian biçimine uydurmak için byte swap uyguluyor
- Klavye girişi ve konsol çıktısı trap routine’leri ile
KBSR/KBDRbellek eşlemeli register’ları üzerinden işleniyor; Unix/macOS ve Windows için farklı terminal giriş tamponlama kodları gerekiyor
Eğitimin hedefi ve varsayımlar
- LC-3 sanal makinesini doğrudan uygulayarak assembly dili programlarını çalıştırma sürecini takip eder
- Nihai kod C için yaklaşık 250 satırdır; Unix için
lc3.cve Windows içinlc3-win.csağlanır - Gereken ön bilgi temel C veya C++ okuma becerisi ve ikili aritmetiktir
- Tüm kod GitHub repo’da bulunur; eğitimin kendisi literate program biçimindedir, yani kod blokları birleştirilerek nihai kaynak oluşturulur
Sanal makine ne yapar?
- VM, CPU ve bazı donanım bileşenleri gibi davranan bir programdır
- Aritmetik işlemler gerçekleştirir
- Belleği okur ve yazar
- I/O aygıtlarıyla etkileşime girer
- Kendi makine dilini anlayarak program çalıştırır
- VM’in amacına bağlı olarak gerçek donanımı sadakatle yeniden üretebilir ya da yazılım geliştirme kolaylığı için yeni bir sanal mimari sunabilir
- JVM, standart bir yürütme platformu sağlayan VM’lerin başlıca örneklerinden biridir; JVM’in uygulandığı cihazlarda Java, Kotlin ve Clojure programları değişiklik yapılmadan çalıştırılabilir
- İzole yürütme de VM’lerin önemli bir kullanım alanıdır
- Garbage collection’da VM, çalışan programın dışından stack’i ve bellek referanslarını gözlemleyebilir
- Ethereum smart contract’ları dosya sistemine, ağa, diske vb. erişemeyen bir VM içinde çalıştırılır
LC-3 mimari bileşenleri
- Uygulama hedefi, üniversitelerde bilgisayar mimarisi ve assembly eğitiminde kullanılan LC-3’tür
- LC-3 belleği 65.536 konuma sahiptir ve her konum 16 bitlik bir değer saklar
- Toplam depolama kapasitesi 128KB’dir
- C uygulamasında
uint16_t memory[MEMORY_MAX]dizisiyle temsil edilir
- Toplam 10 register vardır
R0~R7: 8 genel amaçlı registerPC: bir sonraki çalıştırılacak komutun bellek adresiCOND: önceki hesaplama sonucunun koşul bayrağı
- LC-3 komutlarının tamamı 16 bittir ve soldaki 4 bit opcode’dur
- 16 opcode tanımlıdır
OP_BR,OP_ADD,OP_LD,OP_ST,OP_JSR,OP_AND,OP_LDR,OP_STR,OP_RTI,OP_NOT,OP_LDI,OP_STI,OP_JMP,OP_RES,OP_LEA,OP_TRAPbunlara dahildir
- Koşul bayrakları, önceki hesaplama sonucunun işaretini gösterir
FL_POS: pozitifFL_ZRO: 0FL_NEG: negatif
Assembly ve makine dili
- LC-3 VM’in gerçekte çalıştırdığı şey, insan tarafından okunan assembly değil, 16 bit makine dili komut dizisidir
- Assembler, metin olarak yazılmış LC-3 assembly’sini 16 bit ikili komutlara dönüştürür
Hello Worldörneği şu akışa sahiptir.ORIG x3000: programın yükleneceği bellek adresini belirtirLEA R0, HELLO_STR: string adresiniR0’a yüklerPUTS:R0’ın işaret ettiği string’i yazdırırHALT: programı durdurur.STRINGZ "Hello World!": string verisini programın içinde saklar
.ORIG,.STRINGZCPU komutları değil, assembler yönergeleridir- Koşullar ve döngüler
BRn LOOPgibigotoya yakın dallanma komutlarıyla uygulanır
Yürütme döngüsünün temel adımları
- VM yürütmesi aynı adımları tekrarlar
PCregister adresinden komutu okurPC’yi artırır- Komutun üst 4 bitinden opcode’u alır
- Opcode’a karşılık gelen uygulama kodunu çalıştırır
- Sonraki komutu yeniden okur
- Varsayılan başlangıç adresi
0x3000’dir - Bazı komutlar yürütme akışını atlatmak için
PC’yi doğrudan değiştirir- Dallanma ve jump komutları sayesinde, sadece
PC’yi artıran bir yapıda bile döngüler ve koşullu yürütme mümkün olur
- Dallanma ve jump komutları sayesinde, sadece
maindöngüsüswitch (op)ile opcode’a göre işleme kodunu çağırırOP_ADD,OP_AND,OP_NOT,OP_BR,OP_JMP,OP_JSR,OP_LD,OP_LDI,OP_LDR,OP_LEA,OP_ST,OP_STI,OP_STR,OP_TRAPişlenirOP_RES,OP_RTIkullanılmayan opcode’lardır veabort()ile ele alınabilir
Komutların uygulanma biçimi
ADDiki değeri toplar, hedef register’a yazar ve koşul bayraklarını güncellerADDiçin iki mod vardır- Register modu: ikinci operand başka bir register’dan okunur
- Immediate modu: ikinci operand komutun alt 5 biti olan
imm5’ten okunur
imm5gibi 16 bitten kısa değerler, sign extension ile 16 bitlik değere genişletilmelidir- Pozitifler 0 ile doldurulur
- Negatifler özgün değeri korumak için 1 ile doldurulur
- Register’a değer yazan komutlar
update_flagsileR_COND’u günceller- Değer 0 ise
FL_ZRO - En yüksek bit 1 ise
FL_NEG - Diğer durumlarda
FL_POS
- Değer 0 ise
LDI, “load indirect” komutudur- Komutun
PCoffset9değerine sign extension uygulanır - Geçerli
PC’ye eklenerek bellek adresi elde edilir - O konumda saklanan değer yeniden adres olarak kullanılarak nihai veri okunur
- Okunan değer hedef register’a yazılır ve koşul bayrakları güncellenir
- Komutun
Başlıca komut kümesi
- Aritmetik ve bit işlemleri
ADD: toplamaAND: bit düzeyinde ANDNOT: bit düzeyinde NOT
- Kontrol akışı
BR: koşul bayraklarıyla komutun koşul bitlerini karşılaştırarakPC’yi taşırJMP: belirtilen register değeriniPColarak ayarlarRET: belirtimde ayrı bir anahtar sözcüktür, ancakJMP’in özel bir durumudurJSR,JSRR: geçerliPC’yiR7’ye kaydeder ve subroutine konumuna atlar
- Bellek okuma
LD:PCtabanlı offset adresinden okurLDI: dolaylı adresi bir kez daha izleyerek okurLDR: base register ve offset ile hesaplanan adresten okurLEA: effective address’in kendisini register’a kaydeder
- Bellek yazma
ST:PCtabanlı offset adresine yazarSTI: dolaylı adresi izleyerek yazarSTR: base register ve offset ile hesaplanan adrese yazar
Trap routine ve I/O
- LC-3, ortak işler ve I/O aygıtlarına erişim için trap routine sağlar
- Trap routine, LC-3’ün işletim sistemi veya API’si gibi düşünülebilir
- Trap code’lar şöyle tanımlanır
TRAP_GETC = 0x20: klavyeden karakter girişi alır, terminale echo etmezTRAP_OUT = 0x21: karakter çıktısı verirTRAP_PUTS = 0x22: word string çıktısı verirTRAP_IN = 0x23: karakter girişi alır ve terminale echo ederTRAP_PUTSP = 0x24: byte string çıktısı verirTRAP_HALT = 0x25: programı durdurur
- Resmi LC-3 simülatöründe trap routine’ler assembly ile yazılmıştır; bu VM’de ise C fonksiyonları olarak uygulanır
PUTS,R0’da saklanan adresten başlayarakx0000ile karşılaşana kadar karakterleri yazdırır- LC-3 string’leri C string’leri gibi 1 bayt biriminde değil, her bellek konumunda bir karakter saklar
- Her bellek konumu 16 bit olduğundan C çıktısında
char’a dönüştürülerek yazdırılır
HALTtrap’i"HALT"çıktısını verir ve yürütme bayrağını 0 yaparak VM döngüsünü sonlandırır
Program imajı yükleme
- LC-3 assembly programı makine diline dönüştürüldüğünde, komut ve veri dizilerini içeren bir dosya oluşturulur
- Nesne dosyasının ilk 16 biti, programın bellekte nereye yerleştirileceğini gösteren origin’dir
- Loader önce origin’i okur, kalan veriyi origin adresinden itibaren belleğe kopyalar
- LC-3 programları big-endian biçimdedir
- Çoğu modern bilgisayar little-endian olduğundan, yüklenen her
uint16_tiçinswap16uygulanır - Eski PPC Mac gibi big-endian bilgisayarlarda swap yapılmamalıdır
- Çoğu modern bilgisayar little-endian olduğundan, yüklenen her
read_image, dosyayı binary modda açar,read_image_file’ı çağırır ve ardından dosyayı kapatır
Bellek eşlemeli register’lar
- Genel register tablosuyla erişilmeyen özel register’lar belirli bellek adreslerine eşlenir
- LC-3’te uygulanması gereken iki bellek eşlemeli register vardır
MR_KBSR = 0xFE00: keyboard status registerMR_KBDR = 0xFE02: keyboard data register
KBSRbir tuşa basılıp basılmadığını gösterir;KBDRise hangi tuşa basıldığını saklarGETC, giriş gelene kadar yürütmeyi bloke eder; ancakKBSRveKBDR, aygıt durumunu polling ederek giriş beklerken bile programın tepki vermeye devam etmesini sağlar- Bellek okuma, doğrudan dizi okumak yerine
mem_readüzerinden yapılır- Adres
MR_KBSRisecheck_key()ile klavye durumu kontrol edilir - Tuş varsa
KBSR’nin en yüksek biti set edilir veKBDR’yegetchar()değeri yazılır - Tuş yoksa
KBSR0 olarak ayarlanır
- Adres
Platforma özgü terminal işlemleri
- Klavye girişi ve terminal davranışını doğru işlemek için platforma özgü giriş tamponlama ayarları gerekir
- Linux/macOS/UNIX uygulaması
termios,selectvb. kullanır- canonical mode ve echo devre dışı bırakılır
- Girişin mümkün olup olmadığı
selectile kontrol edilir
- Windows uygulaması
GetStdHandle,GetConsoleMode,SetConsoleMode,_kbhitvb. kullanır- echo ve line input ayarlanır
- Tuş girişi
WaitForSingleObjectve_kbhitile kontrol edilir
- Program başlarken
disable_input_buffering()çağrılır, çıkarkenrestore_input_buffering()çağrılır SIGINTalınırsa terminal ayarları geri yüklenir, satır sonu yazdırılır ve program sonlandırılır
VM çalıştırma ve hata ayıklama
- VM build örneği şöyledir
gcc lc3.c -o lc3-vm
- Çalıştırmak için derlenmiş LC-3 nesne dosyasını argüman olarak verin
lc3-vm path/to/2048.obj
- Örnek olarak sağlanan nesne dosyaları
2048.objverogue.obj’dir - 2048 örneği WASD tuşlarıyla kontrol edilir
- Program doğru çalışmıyorsa sorun büyük olasılıkla komut uygulamasındaki bir hatadır
- LC-3 assembly kaynağını okurken debugger ile VM komutlarını tek tek yürütme yöntemi önerilir
- Beklenen komuta gidilmeyen bir nokta varsa ilgili komutun belirtimini ve uygulamasını yeniden kontrol edin
İsteğe bağlı: C++ generic tabanlı uygulama
- Daha kısa bir C++ uygulama tekniği de isteğe bağlı olarak ele alınır
- Birçok komut sign extension,
PCtabanlı offset, dolaylı adres hesaplama gibi tekrarlanan işleri paylaştığından, komut yürütme küçük işlem adımlarından oluşan bir pipeline olarak görülebilir - C++ template’leri ve bit flag’leri kullanılarak her opcode için yalnızca gereken işlem adımları derleme zamanında dahil edilir
- Bu yöntem kod tekrarını azaltır ve her işlem adımının çip üzerinde fiziksel alan kapladığı gerçek donanım kablolama biçimine daha yakındır
- Fikrin kaynağı olarak Bisqwit’s NES emulator belirtilir
Kaynaklar ve katkılar
- atul-g, tüm sistem davranışını özetleyen reference card’ı katkı olarak sağladı
- Farklı dillerdeki uygulamalar GitHub topic’i
lc3altında derlenmiştir- C, C++, Go, Haskell, Java, JavaScript, Kotlin, Lua, OCaml, Python, Ruby, Rust, Swift, TypeScript, Zig vb. dahildir
- Kendi uygulamanızın listede görünmesini istiyorsanız GitHub topic’i olarak
lc3eklemeniz yeterlidir - Windows platform desteği inkydragon tarafından katkı olarak sağlandı
- Projede entegrasyon testleriyle ilgili bir good first issue bulunur
1 yorum
Hacker News yorumları
Gençliğimde community college'daki bilgisayar bilimine giriş dersinde basit bir CPU komut kümesi tasarlamış, kendi sanal makinemizi ve assembler'ımızı yapıp assembly programları yazıp çalıştırmıştık.
Şaşırtıcı derecede kolaydı ve bilgisayarlar çok daha az gizemli gelmeye başlamıştı.
FPGA için gerçek bir CPU tasarımından basit bir işletim sistemi ve onun üzerinde çalışan programlar yazmaya kadar, bilişimin tüm katmanları bu yöntemle öğrenilebilir gibi geliyor.
Modern bilişimin gerektirdiği performans ve güvenliği bir kenara bırakıp hedefi “çalışsın yeter” olarak koyarsanız, bu alan beklenmedik ölçüde basit.
Hafızam beni yanıltmıyorsa en azından bellek segmentasyonu, korumalı kip ve MMU işin içine giriyor.
PDP üzerinde BASIC ile yazılmış basit bir bilgisayar/assembler'dı; ödevlerden biri de döngüyle toplama yaparak basit çarpmayı gerçekleştirmekti.
Bir arkadaşım bunun yerine programı değiştirip yeni bir MUL komutu oluşturdu; öğretmen bundan hiç hoşlanmadı.
Meraklı ve öğrenmek isteyen biri bu temel katmanları kolayca öğrenebilir; ama “hızlı para kazanıp olabildiğince çabuk işe girebilir hâle gelmek” isteyen biri için aynı şey geçerli değil.
Önerilen kitaplar:
Bu kitapları okumuş biri yorum eklerse herkes için faydalı olur.
Nintendo emülatörü, VT-x kullanan bir hipervizör, geleneksel çok görevli işletim sistemi, yeni bir betik dilinin yorumlayıcısı, SQL sorgu en iyileyicisi, düzenli ifade eşleştiricisi, oyun sunucusunda güvenilmeyen oyuncu kodunu çalıştıran bir güvenlik gözetleyicisi vb. neredeyse hiç ortak değerlendirme noktası yokmuş gibi görünse de hepsi sanal makine.
Karakter hücreli terminal kaçış dizilerini belirleyen terminfo biçiminin içinde bile yığın tabanlı bir sanal makine var.
Derine inince, bugünkü anlamıyla bilgisayarı bilgisayar yapan şey sanal makinedir; Turing'in 1936 tarihli Entscheidungsproblem makalesi de sanal makinelerin birbirini taklit edebilmesine dayanıyordu.
Ben Eater'ın breadboard CPU serisini izledikten sonra tek istediğim kendi CPU'mu tasarlayıp emüle etmek.
Oturup bunu tasarlayacak zamanı bulabilsem keşke.
Brookshear Machine veya Little Computer gibi eğitim amaçlı mimarilerin gerçek mimarilere hiç benzemediği için faydasız olmanın ötesinde zararlı olduğunu düşünüyorum.
Bu tür şeylerin kullanıldığı dersleri alan öğrencilerin, hiç ders almamış kişilere göre bilgisayarları daha çarpık anladığını gördüm.
Kendi bilgisayarının nasıl çalıştığını biraz öğrenmek isteyen çoğu kişi için işletim sistemleri dersi daha iyi; burada da yalnızca kısa bir eğitime zaman varsa “Writing my own bootloader”ı öneririm.
https://dev.to/frosnerd/writing-my-own-boot-loader-3mld
Bu, “Write your own VM” eğitimlerinin kötü olduğu anlamına gelmiyor; benim deneyimime göre onu yapacak kişilerin çoğu için başka konuların daha yararlı olduğu anlamına geliyor.
Bilgisayar mimarisini öğrenmek için LC-3'ün neden kötü olduğunu biraz daha açıklayabilir misin?
Gerçek donanımdan tamamen farklı ve aşırı basit olduğunu anlıyorum, ama CPU emülatörü yazma açısından da kötü olup olmadığını merak ediyorum.
1960'larda yapılmış olabilecek türden onluk bir makineydi, ama 1970'lerden sonra kimse böyle bir şey üretmedi.
Bu tür sistemler pek çok temel bilgiyi öğretebilir, ancak https://en.wikipedia.org/wiki/Hacker%27s_Delight içindeki teknikler çoğunlukla yaygın sayı temsil biçimlerine dayandığı için öğrenmesi zor olur.
Pek bilmediğim için Wikipedia'ya kısaca baktım; karikatürü görünce tuhaf bir şey beklemiştim ama ilk bakışta o kadar da şok edici görünmedi.
s/360, biraz x86, çok az ARM ya da başka RISC ailesi mimarilerinin karışımı gibi; atlanan ve tuhaf kısımlar çok, ama hedef hızlıca çalışan bir uygulamaya ulaşmak gibi görünüyor.
Eğitim amaçlı olarak neden “faydasız olmanın ötesinde zararlı” gördüğünü bilmek isterim.
Hindistan'daki birçok bilgisayar bilimi dersinde hâlâ 8086/8088 kullanılıyor gibi.
Özellikle ortadaki PC'ye göreli sözcük üzerinden çift dolaylı yükleme yapabiliyorsunuz.
Buna rağmen çıkarma işlemini olumsuzlamadan, olumsuzlamayı da NOT ve ADD ,,#-1 ile oluşturmanız gerekiyor.
Sınırlı komut kodlama alanı düşünüldüğünde NOT d,s = XOR d,s,#-1 daha iyi bir kullanım olurdu gibi geliyor.
Katı bir ayrım yapacaksak bu bir sanal makine değil, emülatör
Betimleyici anlamda bu terim uygulanabilir ve donanım sanallaştırmasından önceki dönemde belli bir belirsizlik de vardı; ancak günümüzde “Virtual Machine” ifadesinin ezici çoğunlukla yaygın kullanımı, VT-x gibi donanım sanallaştırma özelliklerini kullanan ortamları ifade eder
JVM yaygın biçimde dağıtılıyor, Ethereum VM’e EVM deniyor, https://www.linuxfoundation.org/hubfs/LF%20Research/The_Stat... da BPF ve eBPF’yi tekrar tekrar “virtual machines” olarak tanımlıyor, https://webassembly.org/ ise “WebAssembly (kısaca Wasm), yığın tabanlı bir sanal makine için ikili komut biçimidir” diye başlıyor
“Sanal makine”, sanal bir makineyi adlandırmak için hâlâ en yaygın ifade
Şahsen “fictive machine”, “fictious machine”, “imaginary computer”, “fantastic automaton” gibi ifadeleri daha çok seviyorum ama benimsenmeleri pek olası görünmüyor
“Sanal makine” yerine her zaman “emülatör” kullanılamaz
wasmtime’a emülatör denebilir belki ama WebAssembly’nin kendisine emülatör demek doğru olmaz; WebAssembly, wasmtime’ın emüle ettiği sanal makinedir
Emülatörlere sanal makine demek de yaygındır; çalışmakta olan bir emülatör örneği de başka bir anlamda sanal makinedir
Donanım sanallaştırma ortamına “sanal makine” demek de geçerlidir ve bu son anlamla bir ölçüde örtüşür
Mevcut ortamda bu kullanım ezici çoğunlukla yaygın olabilir, ama başka yerlerde bunun mutlaka böyle olması gerekmez
En saf anlamıyla sanal makine, uydurulmuş/yaratılmış bir bilgisayardan ibarettir; ne için kullanılacağını ya da nasıl çalıştığını ima etmez
Yazıda klasik konsol emülasyonu örnek veriliyor, ancak sunulan tanıma göre mümkün olan sanal makinelerin çok daha fazla olduğu açık
Asıl nokta, sanal makinenin soyut bir kavram olması ve çok sayıda türünün bulunması
Simülatörler, emülatörler, hipervizörler vb. hepsi sanal makinedir; henüz adı konmamış tuhaf sanal makine biçimleri de vardır
Kaba konuşmaya çalışmıyorum; tam tersine saygılı olmaya çalışıyorum ve öğrenmek isteyenler için bu terimi netleştirmek istiyorum
“Sanal makine”, nedeni ne olursa olsun makine kodu ya da bayt kodu çalıştıran her türlü yazılım için yaygın biçimde kullanılır
Sanallaştırmayı içerebilir, ancak Java’nın JVM’i veya Ruby’nin YARV’ı (Yet Another Ruby VM) gibi dil çalışma zamanları için de sıkça kullanılır
Bu terimi o kadar sık duymadığınız alan daha ziyade emülasyondur; bunun bir nedeni de modern emülatörlerin çoğunun tüm sistemi emüle etmektense, emüle edilen yazılımı dinamik yeniden derleme tekniklerine yönelmiş olmasıdır
Java’nın “ezici çoğunlukla yaygın kullanım” kapsamına girdiği söylenebilir