- Untuk memahami cara kerja internal komputer dan bagaimana bahasa pemrograman dijalankan, dibuat sendiri VM berbasis C sekitar 250 baris yang menjalankan program assembly di atas arsitektur edukasi LC-3
- Target implementasinya adalah model komputer kecil dengan 65.536 lokasi memori 16-bit, 10 register, 16 opcode, flag kondisi, trap routine, dan register memory-mapped
- Loop eksekusi bekerja dengan struktur fetch-decode-execute: membaca instruksi yang ditunjuk
PC, menaikkan nilainya, lalu menafsirkan opcode untuk menjalankan instruksi sepertiADD,LDI,BR,JMP, danTRAP - Pemuatan program membaca origin 16-bit pertama dari file objek lalu menaruhnya ke memori, serta melakukan byte swap agar format big-endian LC-3 cocok dengan format little-endian yang dipakai kebanyakan komputer modern
- Input keyboard dan output konsol ditangani lewat trap routine dan register memory-mapped
KBSR/KBDR, dan membutuhkan kode buffering input terminal yang berbeda untuk Unix/macOS dan Windows
Tujuan dan prasyarat tutorial
- Mengikuti proses menjalankan program bahasa assembly dengan mengimplementasikan sendiri mesin virtual LC-3
- Kode akhirnya sekitar 250 baris dalam C, dan tersedia
lc3.cuntuk Unix sertalc3-win.cuntuk Windows - Pengetahuan prasyarat yang dibutuhkan adalah kemampuan dasar membaca C atau C++ serta aritmetika biner
- Seluruh kode ada di repo GitHub, dan tutorialnya sendiri berbentuk literate program yang merangkai blok-blok kode menjadi source akhir
Apa yang dilakukan mesin virtual
- VM adalah program yang bertindak seperti CPU dan sebagian komponen hardware
- Melakukan operasi aritmetika
- Membaca dan menulis memori
- Berinteraksi dengan perangkat I/O
- Memahami bahasa mesin-nya sendiri untuk menjalankan program
- Bergantung pada tujuannya, VM bisa mereproduksi hardware nyata dengan setia atau menyediakan arsitektur virtual baru demi kemudahan pengembangan software
- JVM adalah contoh representatif VM yang menyediakan platform eksekusi standar; pada perangkat yang mengimplementasikan JVM, program Java, Kotlin, dan Clojure dapat dijalankan tanpa modifikasi
- Eksekusi terisolasi juga merupakan kegunaan penting VM
- Dalam garbage collection, VM dapat mengamati stack dan referensi memori dari luar program yang sedang berjalan
- Ethereum smart contract berjalan di dalam VM yang tidak dapat mengakses file system, jaringan, disk, dan sebagainya
Struktur arsitektur LC-3
- Target implementasi adalah LC-3, yang digunakan untuk pendidikan arsitektur komputer dan assembly di universitas
- Memori LC-3 memiliki 65.536 lokasi, dan setiap lokasi menyimpan nilai 16-bit
- Total kapasitas penyimpanannya adalah 128KB
- Dalam implementasi C, ini direpresentasikan sebagai array
uint16_t memory[MEMORY_MAX]
- Total register ada 10 buah
R0~R7: 8 register serbagunaPC: alamat memori instruksi berikutnya yang akan dieksekusiCOND: flag kondisi dari hasil komputasi terakhir
- Semua instruksi LC-3 berukuran 16-bit, dan 4 bit paling kiri adalah opcode
- Ada 16 opcode yang didefinisikan
- Termasuk
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_TRAP
- Flag kondisi menunjukkan tanda dari hasil komputasi terakhir
FL_POS: positifFL_ZRO: nolFL_NEG: negatif
Assembly dan bahasa mesin
- Yang benar-benar dijalankan VM LC-3 bukan assembly yang bisa dibaca manusia, melainkan array instruksi bahasa mesin 16-bit
- Assembler mengubah assembly LC-3 yang ditulis sebagai teks menjadi instruksi biner 16-bit
- Contoh
Hello Worldmemiliki alur berikut.ORIG x3000: menentukan alamat memori tempat program dimuatLEA R0, HELLO_STR: memuat alamat string keR0PUTS: mencetak string yang ditunjukR0HALT: menghentikan program.STRINGZ "Hello World!": menyimpan data string di dalam program
.ORIG,.STRINGZbukan instruksi CPU, melainkan directive assembler- Kondisi dan perulangan diimplementasikan dengan instruksi branch yang lebih mirip
goto, sepertiBRn LOOP
Prosedur inti loop eksekusi
- Eksekusi VM mengulangi prosedur yang sama
- Membaca instruksi dari alamat register
PC - Menaikkan
PC - Mengambil opcode dari 4 bit teratas instruksi
- Menjalankan kode implementasi yang sesuai dengan opcode
- Kembali membaca instruksi berikutnya
- Membaca instruksi dari alamat register
- Alamat awal default adalah
0x3000 - Sebagian instruksi mengubah
PCsecara langsung untuk melompat dalam alur eksekusi- Berkat instruksi branch dan jump, struktur yang sekadar menaikkan
PCtetap dapat menjalankan loop dan eksekusi kondisional
- Berkat instruksi branch dan jump, struktur yang sekadar menaikkan
- Loop
mainmemanggil kode penanganan per opcode lewatswitch (op)- Menangani
OP_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_TRAP OP_RES,OP_RTIadalah opcode yang tidak digunakan dan bisa ditangani denganabort()
- Menangani
Cara implementasi instruksi
ADDmenjumlahkan dua nilai, menyimpan hasilnya ke register tujuan, lalu memperbarui flag kondisiADDmemiliki dua mode- Mode register: operand kedua dibaca dari register lain
- Mode immediate: operand kedua dibaca dari
imm5, yaitu 5 bit terbawah instruksi
- Nilai yang lebih pendek dari 16-bit seperti
imm5harus diperluas menjadi nilai 16-bit lewat sign extension- Nilai positif diisi dengan 0
- Nilai negatif diisi dengan 1 agar nilainya tetap terjaga
- Instruksi yang menulis nilai ke register memperbarui
R_CONDlewatupdate_flags- Jika nilainya 0 maka
FL_ZRO - Jika bit paling atas bernilai 1 maka
FL_NEG - Selain itu maka
FL_POS
- Jika nilainya 0 maka
LDIadalah instruksi “load indirect”PCoffset9pada instruksi di-sign-extend- Ditambahkan ke
PCsaat ini untuk mendapatkan alamat memori - Nilai yang tersimpan di lokasi itu dipakai lagi sebagai alamat untuk membaca data akhir
- Nilai yang dibaca disimpan ke register tujuan dan flag kondisi diperbarui
Set instruksi utama
- Operasi aritmetika dan bit
ADD: penjumlahanAND: bit ANDNOT: bit NOT
- Alur kontrol
BR: memindahkanPCdengan membandingkan flag kondisi dan bit kondisi pada instruksiJMP: menetapkan nilai register tertentu kePCRET: secara spesifikasi adalah keyword terpisah, tetapi merupakan kasus khusus dariJMPJSR,JSRR: menyimpanPCsaat ini keR7lalu melompat ke lokasi subrutin
- Membaca memori
LD: membaca dari alamat offset berbasisPCLDI: membaca dengan mengikuti alamat tidak langsung sekali lagiLDR: membaca dari alamat hasil perhitungan base register dan offsetLEA: menyimpan alamat efektif itu sendiri ke register
- Menulis memori
ST: menyimpan ke alamat offset berbasisPCSTI: menyimpan dengan mengikuti alamat tidak langsungSTR: menyimpan ke alamat hasil perhitungan base register dan offset
Trap routine dan I/O
- LC-3 menyediakan trap routine untuk pekerjaan umum dan akses perangkat I/O
- Trap routine dapat dipandang sebagai sistem operasi atau API untuk LC-3
- Trap code didefinisikan sebagai berikut
TRAP_GETC = 0x20: input satu karakter dari keyboard, tanpa echo ke terminalTRAP_OUT = 0x21: output satu karakterTRAP_PUTS = 0x22: output word stringTRAP_IN = 0x23: input satu karakter lalu echo ke terminalTRAP_PUTSP = 0x24: output byte stringTRAP_HALT = 0x25: menghentikan program
- Pada simulator resmi LC-3, trap routine ditulis dalam assembly, tetapi pada VM ini diimplementasikan sebagai fungsi C
PUTSmencetak karakter mulai dari alamat yang tersimpan diR0sampai menemukanx0000- String LC-3 tidak menyimpan satu karakter per 1 byte seperti string C, melainkan satu karakter per lokasi memori
- Karena setiap lokasi memori berukuran 16-bit, saat mencetak di C nilainya dikonversi ke
char
- Trap
HALTmencetak"HALT", lalu mengubah flag eksekusi menjadi 0 untuk menghentikan loop VM
Memuat image program
- Saat program assembly LC-3 diubah menjadi bahasa mesin, akan dihasilkan file berisi array instruksi dan data
- 16-bit pertama pada file objek adalah origin yang menunjukkan di mana program harus diletakkan di memori
- Loader membaca origin terlebih dahulu, lalu menyalin sisa data ke memori mulai dari alamat origin
- Program LC-3 menggunakan format big-endian
- Karena kebanyakan komputer modern menggunakan little-endian, setiap
uint16_tyang dimuat perlu diterapkanswap16 - Pada komputer big-endian seperti PPC Mac lama, swap tidak boleh dilakukan
- Karena kebanyakan komputer modern menggunakan little-endian, setiap
read_imagemembuka file dalam mode biner, memanggilread_image_file, lalu menutup file
Register memory-mapped
- Register khusus yang tidak diakses lewat tabel register biasa dipetakan ke alamat memori tertentu
- Dalam LC-3, ada dua register memory-mapped yang perlu diimplementasikan
MR_KBSR = 0xFE00: keyboard status registerMR_KBDR = 0xFE02: keyboard data register
KBSRmenunjukkan apakah ada tombol yang ditekan, danKBDRmenyimpan tombol apa yang ditekanGETCmenghentikan eksekusi sampai ada input, tetapiKBSRdanKBDRmemungkinkan program terus responsif saat menunggu input dengan melakukan polling status perangkat- Pembacaan memori tidak dilakukan dengan membaca array secara langsung, melainkan melalui
mem_read- Jika alamatnya
MR_KBSR, status keyboard diperiksa dengancheck_key() - Jika ada tombol, bit paling atas
KBSRdiaktifkan dan nilaigetchar()disimpan keKBDR - Jika tidak ada tombol,
KBSRdiatur menjadi 0
- Jika alamatnya
Penanganan terminal per platform
- Untuk menangani input keyboard dan perilaku terminal dengan benar, diperlukan pengaturan buffering input yang berbeda per platform
- Implementasi Linux/macOS/UNIX menggunakan
termios,select, dan sebagainya- Menonaktifkan canonical mode dan echo
- Memeriksa ketersediaan input dengan
select
- Implementasi Windows menggunakan
GetStdHandle,GetConsoleMode,SetConsoleMode,_kbhit, dan sebagainya- Mengatur echo dan line input
- Memeriksa input keyboard dengan
WaitForSingleObjectdan_kbhit
- Saat program dimulai,
disable_input_buffering()dipanggil, dan saat selesairestore_input_buffering()dipanggil - Saat menerima
SIGINT, pengaturan terminal dipulihkan, karakter baris baru dicetak, lalu program keluar
Menjalankan dan men-debug VM
- Contoh build VM adalah sebagai berikut
gcc lc3.c -o lc3-vm
- Untuk menjalankannya, berikan file objek LC-3 yang sudah dirakit sebagai argumen
lc3-vm path/to/2048.obj
- File objek contoh yang disediakan adalah
2048.objdanrogue.obj - Contoh 2048 dikendalikan dengan tombol WASD
- Jika program tidak berjalan dengan benar, kemungkinan besar ada kesalahan pada implementasi instruksi
- Disarankan membaca source assembly LC-3 sambil menjalankan instruksi VM satu per satu dengan debugger
- Jika ada titik yang tidak berpindah ke instruksi yang diharapkan, periksa kembali spesifikasi dan implementasi instruksi tersebut
Opsi: implementasi berbasis generik C++
- Teknik implementasi C++ yang lebih ringkas juga dibahas sebagai opsi
- Karena banyak instruksi berbagi pekerjaan berulang seperti sign extension, offset berbasis
PC, dan perhitungan alamat tidak langsung, eksekusi instruksi dapat dipandang sebagai pipeline dari langkah-langkah pemrosesan kecil - Dengan template C++ dan bit flag, hanya langkah pemrosesan yang dibutuhkan untuk tiap opcode yang disertakan saat kompilasi
- Pendekatan ini mengurangi duplikasi kode dan lebih mirip dengan cara wiring hardware nyata yang menempati ruang fisik pada chip
- Sebagai sumber ide disebutkan Bisqwit’s NES emulator
Materi dan kontribusi
- atul-g menyumbangkan reference card yang merangkum cara kerja seluruh sistem
- Implementasi dalam berbagai bahasa dirangkum lewat topik GitHub
lc3- Termasuk C, C++, Go, Haskell, Java, JavaScript, Kotlin, Lua, OCaml, Python, Ruby, Rust, Swift, TypeScript, Zig, dan lainnya
- Agar implementasi Anda muncul dalam daftar, cukup tambahkan topik GitHub
lc3 - Dukungan platform Windows dikontribusikan oleh inkydragon
- Proyek ini memiliki good first issue terkait pengujian terintegrasi
1 komentar
Pendapat di Hacker News
Saat remaja, di kelas pengantar ilmu komputer di community college, saya merancang set instruksi CPU sederhana, lalu membuat sendiri virtual machine dan assembler untuk menulis serta menjalankan program assembly
Ternyata sangat mudah, dan komputer terasa jauh tidak terlalu misterius
Rasanya semua lapisan komputasi bisa dipelajari dengan cara seperti ini, mulai dari merancang CPU sungguhan untuk FPGA hingga menulis sistem operasi sederhana dan program yang berjalan di atasnya
Kalau mengesampingkan performa dan keamanan yang dituntut komputasi modern, dan targetnya hanya “asal jalan”, bidang ini ternyata sederhana di luar dugaan
Kalau ingatan saya benar, setidaknya ada segmentasi memori, protected mode, dan MMU
Itu komputer/assembler sederhana yang ditulis dengan BASIC di PDP, dan salah satu tugasnya adalah mengimplementasikan perkalian sederhana dengan melakukan penjumlahan berulang dalam loop
Teman saya malah memodifikasi programnya untuk membuat instruksi MUL baru, dan gurunya sama sekali tidak senang
Orang yang punya rasa ingin tahu dan ingin belajar bisa dengan mudah memahami lapisan-lapisan dasar seperti ini, tetapi tidak demikian bagi orang yang ingin “cepat menghasilkan uang dan secepat mungkin siap kerja”
Buku-buku yang direkomendasikan:
Kalau ada yang sudah membaca buku-buku di atas dan bisa menambahkan komentar, itu akan bermanfaat bagi semua orang
Emulator Nintendo, hypervisor yang memakai VT-x, sistem operasi multitasking tradisional, interpreter untuk bahasa scripting baru, pengoptimal kueri SQL, regex matcher, sandbox keamanan yang menjalankan kode pemain tak tepercaya di server game, dan sebagainya tampaknya hampir tidak punya pertimbangan yang sama, tetapi semuanya adalah virtual machine
Bahkan di dalam format terminfo yang menentukan escape sequence terminal sel karakter, ada virtual machine berbasis stack
Kalau dilihat lebih dalam, virtual machine-lah yang membuat komputer menjadi “komputer” dalam pengertian masa kini, dan makalah Entscheidungsproblem Turing tahun 1936 pun bergantung pada fakta bahwa virtual machine dapat meniru satu sama lain
Setelah menonton seri CPU breadboard Ben Eater, saya hanya jadi ingin merancang dan mengemulasikan CPU sendiri
Semoga saya bisa menemukan waktu untuk duduk dan merancangnya
Menurut saya arsitektur edukasional seperti Brookshear Machine atau Little Computer sama sekali tidak mirip arsitektur nyata, sehingga bukan hanya tidak berguna tetapi bahkan berbahaya
Saya pernah melihat mahasiswa yang mengikuti kelas memakai hal semacam itu justru memahami komputer dengan lebih keliru daripada orang yang tidak pernah mengikuti kelas apa pun
Bagi kebanyakan orang yang ingin sedikit belajar tentang cara kerja komputer mereka, kelas sistem operasi lebih baik, dan kalau di sana pun hanya ada waktu untuk satu tutorial singkat, saya merekomendasikan “Writing my own bootloader”
https://dev.to/frosnerd/writing-my-own-boot-loader-3mld
Ini bukan berarti tutorial “Write your own VM” itu buruk, melainkan menurut pengalaman saya, bagi sebagian besar orang yang akan mengikutinya, topik lain akan lebih membantu
Bisa jelaskan lebih lanjut mengapa LC-3 buruk untuk mempelajari arsitektur komputer?
Saya paham bahwa itu sangat berbeda dari hardware nyata dan terlalu sederhana, tetapi saya penasaran apakah itu juga buruk dari sudut pandang menulis emulator CPU
Itu mesin desimal yang mungkin saja dibuat pada 1960-an, tetapi setelah 1970-an tidak ada lagi yang membuat bentuk seperti itu
Sistem seperti itu bisa mengajarkan banyak dasar, tetapi teknik-teknik di https://en.wikipedia.org/wiki/Hacker%27s_Delight sebagian besar bergantung pada cara representasi angka yang umum, sehingga sulit dipelajari dengannya
Karena tidak terlalu tahu, saya sempat melihat Wikipedia, dan setelah melihat kartunnya saya mengira akan menemukan sesuatu yang aneh, tetapi sekilas tidak terlihat terlalu mengejutkan
Terasa seperti campuran s/360, sedikit x86, dan sedikit sekali ARM atau arsitektur RISC lain; memang banyak bagian yang dihilangkan dan ada bagian yang aneh, tetapi tujuannya tampaknya agar cepat sampai pada implementasi yang berjalan
Saya ingin tahu apa yang membuat Anda menganggapnya “bukan hanya tidak berguna tetapi berbahaya” untuk pendidikan
Di banyak kelas ilmu komputer di India, tampaknya mereka masih memakai 8086/8088
Khususnya, ia bisa melakukan load indirect ganda melalui word relatif PC yang berada di tengah
Namun demikian, pengurangan harus dibangun dari negasi, dan negasi harus dibuat dari NOT dan ADD ,,#-1
Mengingat ruang encoding instruksi yang terbatas, rasanya NOT d,s = XOR d,s,#-1 akan menjadi pemanfaatan yang lebih baik
Kalau mau benar-benar dibedakan, ini bukan virtual machine, melainkan emulator
Dalam arti deskriptif, istilah itu bisa saja diterapkan, dan pada era sebelum virtualisasi perangkat keras memang ada sedikit ambiguitas, tetapi dalam penggunaan modern, makna “Virtual Machine” yang sangat dominan merujuk pada lingkungan yang memakai fitur virtualisasi perangkat keras seperti VT-x
JVM tersebar luas, Ethereum VM disebut EVM, https://www.linuxfoundation.org/hubfs/LF%20Research/The_Stat... juga berulang kali menjelaskan BPF dan eBPF sebagai “virtual machines”, dan https://webassembly.org/ dibuka dengan kalimat “WebAssembly (disingkat Wasm) adalah format instruksi biner untuk virtual machine berbasis stack”
“Virtual machine” masih merupakan istilah paling umum untuk menyebut mesin virtual
Secara pribadi saya lebih menyukai ungkapan seperti “fictive machine”, “fictious machine”, “imaginary computer”, atau “fantastic automaton”, tetapi sepertinya tidak akan diadopsi
Kita tidak bisa selalu memakai “emulator” sebagai pengganti “virtual machine”
wasmtime mungkin bisa disebut emulator, tetapi menyebut WebAssembly itu sendiri sebagai emulator tidaklah akurat; WebAssembly adalah virtual machine yang diemulasi oleh wasmtime
Menyebut emulator sebagai virtual machine juga umum, dan instance emulator yang sedang berjalan juga merupakan virtual machine dalam arti lain
Menyebut lingkungan virtualisasi perangkat keras sebagai “virtual machine” juga valid, dan sampai batas tertentu tumpang tindih dengan makna terakhir ini
Dalam lingkungan saat ini, penggunaan itu mungkin sangat dominan, tetapi belum tentu demikian di tempat lain
Dalam makna yang paling murni, virtual machine hanyalah komputer yang dibuat-buat, dan tidak mengimplikasikan untuk apa ia digunakan atau bagaimana cara kerjanya
Artikel ini juga memakai emulasi konsol klasik sebagai contoh, tetapi jelas bahwa menurut definisi yang diberikan, ada jauh lebih banyak virtual machine yang mungkin
Intinya, virtual machine adalah konsep abstrak dan jenisnya sangat banyak
Simulator, emulator, hypervisor, dan sebagainya semuanya adalah virtual machine, dan ada juga bentuk-bentuk virtual machine aneh yang belum diberi nama
Saya tidak bermaksud bersikap kasar, justru ingin menghormati, dan ingin memperjelas istilah ini bagi orang-orang yang sedang belajar
“Virtual machine” lazim dipakai untuk perangkat lunak apa pun yang menjalankan kode mesin atau bytecode, terlepas dari alasannya
Itu bisa mencakup virtualisasi, tetapi juga sering dipakai untuk runtime bahasa, seperti JVM milik Java atau YARV (Yet Another Ruby VM) milik Ruby
Justru bidang tempat istilah ini tidak terlalu sering terdengar adalah emulasi, sebagian juga karena kebanyakan emulator modern cenderung memakai teknik recompilation dinamis terhadap perangkat lunak target yang diemulasi, alih-alih mengemulasi seluruh sistem
Java sendiri rasanya bisa dianggap termasuk “penggunaan yang sangat dominan”