Membuat mesin arcade emulator. Bagian 2

gambar

Bagian pertama ada di sini .

Disassembler prosesor 8080


Kenalan


Kami akan membutuhkan informasi tentang opcodes dan perintahnya masing-masing. Ketika Anda mencari informasi di Internet, Anda akan melihat bahwa ada banyak informasi campuran tentang 8080 dan Z80. Z80 adalah pengikut 8080 - ia menjalankan semua instruksi 8080 dengan kode hex yang sama, tetapi juga memiliki instruksi tambahan. Saya pikir, sementara Anda harus menghindari informasi tentang Z80, agar tidak bingung. Saya membuat tabel opcode untuk pekerjaan kami, ada di sini .

Setiap prosesor memiliki panduan referensi pabrikan. Biasanya ini disebut "Programmer's Environment Manual". Manual 8080 disebut Panduan Pengguna Sistem Komputer Mikro 8080 Intel. Itu selalu disebut "buku data", jadi saya juga akan menyebutnya begitu. Saya dapat mengunduh referensi 8080 dari http://www.datasheetarchive.com/ . PDF ini adalah pemindaian berkualitas rendah, jadi jika Anda menemukan versi yang lebih baik, maka gunakanlah.

Mari kita mulai dan melihat Space Invaders ROM. (File ROM dapat ditemukan di Internet.) Saya bekerja di Mac OS X, jadi saya hanya menggunakan perintah hexdump untuk melihat isinya. Untuk pekerjaan lebih lanjut, cari hex editor untuk platform Anda. Berikut adalah 128 byte pertama dari file invaders.h:

$ hexdump -v invaders.h 0000000 00 00 00 c3 d4 18 00 00 f5 c5 d5 e5 c3 8c 00 00 0000010 f5 c5 d5 e5 3e 80 32 72 20 21 c0 20 35 cd cd 17 0000020 db 01 0f da 67 00 3a ea 20 a7 ca 42 00 3a eb 20 0000030 fe 99 ca 3e 00 c6 01 27 32 eb 20 cd 47 19 af 32 0000040 ea 20 3a e9 20 a7 ca 82 00 3a ef 20 a7 c2 6f 00 0000050 3a eb 20 a7 c2 5d 00 cd bf 0a c3 82 00 3a 93 20 0000060 a7 c2 82 00 c3 65 07 3e 01 32 ea 20 c3 3f 00 cd 0000070 40 17 3a 32 20 32 80 20 cd 00 01 cd 48 02 cd 13 ... 

Ini adalah awal dari program Space Invaders. Setiap angka heksadesimal adalah perintah atau data untuk program. Kami dapat menggunakan referensi atau informasi referensi lainnya untuk memahami apa arti kode hex ini. Mari kita telusuri lebih jauh kode gambar ROM.

Byte pertama dari program ini adalah $ 00. Melihat tabel, kita melihat bahwa itu adalah NOP, serta dua perintah berikut. (Tapi jangan berkecil hati, Space Invaders mungkin menggunakan perintah ini sebagai penundaan untuk membiarkan sistem sedikit tenang setelah power-up.)

Perintah keempat adalah $ C3, yaitu, dilihat dari tabel, ini adalah JMP. Definisi perintah JMP menyatakan bahwa ia menerima alamat dua-byte, yaitu, dua byte berikutnya adalah alamat hop JMP. Lalu dua NOP lagi datang ... jadi, Anda tahu apa? Biarkan saya menandatangani beberapa instruksi pertama sendiri ...

  0000 00 NOP 0001 00 NOP 0002 00 NOP 0003 c3 d4 18 JMP $18d4 0006 00 NOP 0007 00 NOP 0008 f5 PUSH PSW 0009 c5 PUSH B 000a d5 PUSH D 000b e5 PUSH H 000c c3 8c 00 JMP $008c 000f 00 NOP 0010 f5 PUSH PSW 0011 c5 PUSH B 0012 d5 PUSH D 0013 e5 PUSH H 0014 3e 80 MVI A,#0x80 0016 32 72 20 STA $2072 

Tampaknya ada beberapa cara untuk mengotomatisasi proses ini ...

Disassembler, Bagian 1


Disassembler adalah program yang hanya menerjemahkan aliran nomor hex kembali ke kode sumber dalam bahasa assembly. Ini persis tugas yang kami lakukan dengan tangan di bagian sebelumnya - kesempatan bagus untuk mengotomatisasi pekerjaan ini. Menulis kode ini, kami berkenalan dengan prosesor dan mendapatkan kode debug yang nyaman, yang berguna saat menulis emulator CPU.

Berikut adalah algoritma pembongkaran kode 8080:

  1. Baca kode ke buffer
  2. Kami mendapatkan pointer ke awal buffer
  3. Gunakan byte dalam pointer untuk menentukan opcode.
  4. Tampilkan nama opcode, jika perlu menggunakan byte setelah opcode sebagai data
  5. Pindahkan penunjuk ke jumlah byte yang digunakan oleh perintah ini (1, 2 atau 3 byte)
  6. Jika buffer tidak berakhir, lanjutkan ke langkah 3

Untuk meletakkan dasar prosedur, saya menambahkan beberapa instruksi di bawah ini. Saya akan menjelaskan prosedur lengkap untuk mengunduh, tetapi saya sarankan Anda mencoba menulisnya sendiri. Tidak akan memakan banyak waktu, dan secara paralel Anda akan belajar set instruksi dari prosesor 8080.

  /* *codebuffer -       8080 pc -          */ int Disassemble8080Op(unsigned char *codebuffer, int pc) { unsigned char *code = &codebuffer[pc]; int opbytes = 1; printf ("%04x ", pc); switch (*code) { case 0x00: printf("NOP"); break; case 0x01: printf("LXI B,#$%02x%02x", code[2], code[1]); opbytes=3; break; case 0x02: printf("STAX B"); break; case 0x03: printf("INX B"); break; case 0x04: printf("INR B"); break; case 0x05: printf("DCR B"); break; case 0x06: printf("MVI B,#$%02x", code[1]); opbytes=2; break; case 0x07: printf("RLC"); break; case 0x08: printf("NOP"); break; /* ........ */ case 0x3e: printf("MVI A,#0x%02x", code[1]); opbytes = 2; break; /* ........ */ case 0xc3: printf("JMP $%02x%02x",code[2],code[1]); opbytes = 3; break; /* ........ */ } printf("\n"); return opbytes; } 

Dalam proses penulisan prosedur ini dan mempelajari setiap opcode, saya belajar banyak tentang prosesor 8080.

  1. Saya menyadari bahwa sebagian besar tim mengambil satu byte, sisanya dua atau tiga. Kode di atas mengasumsikan bahwa perintah berukuran satu byte, tetapi instruksi dua dan tiga byte mengubah nilai variabel “opbytes” sehingga ukuran perintah yang benar dikembalikan.
  2. The 8080 memiliki register dengan nama A, B, C, D, E, H dan L. Ada juga program counter (program counter, PC) dan stack pointer yang terpisah (stack pointer, SP).
  3. Beberapa instruksi bekerja dengan register berpasangan: B dan C adalah pasangan, serta DE dan HL.
  4. A adalah register khusus, banyak instruksi bekerja dengannya.
  5. HL juga merupakan register khusus, digunakan sebagai alamat untuk setiap pembacaan dan penulisan data ke memori.
  6. Saya menjadi ingin tahu tentang tim "RST", jadi saya membaca sedikit panduan ini. Saya perhatikan bahwa itu mengeksekusi kode di tempat-tempat tetap dan referensi menyebutkan penanganan interupsi. Setelah membaca lebih lanjut, ternyata semua kode ini pada awal ROM adalah interrupt service rutin (ISRs). Interupsi dapat dihasilkan secara terprogram menggunakan perintah RST, atau dihasilkan oleh sumber pihak ketiga (bukan prosesor 8080).

Untuk mengubah semua ini menjadi program kerja, saya baru saja menyiapkan prosedur yang melakukan langkah-langkah berikut:

  1. Ini membuka file yang diisi dengan kode yang dikompilasi 8080
  2. Membacanya ke buffer memori
  3. Melewati buffer memori, menyebabkan Disassemble8080Op
  4. Meningkatkan PC yang dikembalikan oleh Disassemble8080Op
  5. Keluar di akhir buffer

Mungkin terlihat seperti ini:

  int main (int argc, char**argv) { FILE *f= fopen(argv[1], "rb"); if (f==NULL) { printf("error: Couldn't open %s\n", argv[1]); exit(1); } //         fseek(f, 0L, SEEK_END); int fsize = ftell(f); fseek(f, 0L, SEEK_SET); unsigned char *buffer=malloc(fsize); fread(buffer, fsize, 1, f); fclose(f); int pc = 0; while (pc < fsize) { pc += Disassemble8080Op(buffer, pc); } return 0; } 

Pada bagian kedua, kita akan memeriksa output yang diperoleh dengan membongkar ROM Space Invaders.

Alokasi memori


Sebelum kita mulai menulis emulator prosesor, kita perlu mempelajari aspek lain. Semua CPU memiliki kemampuan untuk berkomunikasi dengan sejumlah alamat tertentu. Prosesor yang lebih lama memiliki alamat 16-, 24-, atau 32-bit. 8080 memiliki 16 kontak alamat, sehingga alamatnya berada dalam kisaran 0 - $ FFFF.

Untuk memahami alokasi memori permainan, kita perlu melakukan penyelidikan kecil. Setelah mengumpulkan informasi di sini dan di sini , saya menemukan bahwa ROM terletak di alamat 0, dan permainan memiliki 8 KB RAM mulai dari $ 2000.

Penulis salah satu halaman menemukan bahwa buffer video dimulai pada RAM dengan alamat $ 2.400, dan juga memberi tahu kami bagaimana 8080 port input-output digunakan untuk berkomunikasi dengan kontrol dan peralatan audio. Hebat!

Di dalam file ROM invaders.zip, yang dapat ditemukan di Internet, ada empat file: invaders.e, .f, .g dan .h. Setelah googling, saya menemukan artikel informatif yang memberi tahu cara memasukkan file-file ini ke dalam memori:

Space Invaders, (C) Taito 1978, Midway 1979

: Intel 8080, 2 ( Zilog Z80)

: $cf (RST 8) vblank, $d7 (RST $10) vblank.

: 256(x)*224(y), 60 , .
.
: 7168 , 1 (32 ).

: SN76477 .

:
ROM
$0000-$07ff: invaders.h
$0800-$0fff: invaders.g
$1000-$17ff: invaders.f
$1800-$1fff: invaders.e

RAM
$2000-$23ff:
$2400-$3fff:

$4000-:


Masih ada beberapa informasi yang bermanfaat, tetapi kami belum siap untuk menggunakannya.

Detail berdarah


Jika Anda ingin mengetahui ukuran ruang alamat yang dimiliki prosesor, maka Anda dapat memahaminya dengan melihat karakteristiknya. Spesifikasi 8080 memberi tahu kita bahwa prosesor memiliki 16 alamat kontak, yaitu menggunakan pengalamatan 16-bit. (Alih-alih spesifikasi, cukup untuk membaca manual, Wikipedia, google, dan sebagainya ...)

Di Internet ada cukup banyak informasi tentang perangkat keras Space Invaders. Jika Anda tidak dapat menemukan informasi ini, Anda bisa mendapatkannya dalam beberapa cara:

  • Tonton kode yang berjalan di emulator dan cari tahu apa fungsinya. Buat catatan dan perhatikan dengan seksama. Seharusnya cukup sederhana untuk dipahami, misalnya, di mana, menurut pendapat permainan, RAM harus ditempatkan. Juga mudah untuk menentukan tempat dia mencari memori video (kami akan meluangkan waktu untuk mempelajari ini).
  • Temukan diagram sirkuit mesin arcade dan lacak sinyal dari kontak alamat CPU. Lihat ke mana mereka pergi. Misalnya, A15 (alamat terlama) hanya dapat pergi ke ROM. Dari sini kita dapat menyimpulkan bahwa alamat ROM mulai dari $ 8000.

Ini bisa sangat menarik dan informatif untuk mencari tahu sendiri dengan mengamati eksekusi kode. Seseorang harus berurusan dengan semua ini untuk pertama kalinya.

Pengembangan baris perintah


Tujuan dari tutorial ini bukan untuk mengajari Anda cara menulis kode untuk platform tertentu, meskipun kami tidak akan dapat menghindari kode spesifik platform. Saya harap sebelum memulai proyek, Anda sudah tahu bagaimana cara mengkompilasi untuk platform target Anda.

Ketika Anda bekerja dengan kode yang berdiri sendiri, yang hanya membaca file dan menampilkan teks di konsol, tidak perlu menggunakan beberapa sistem pengembangan yang terlalu rumit. Bahkan, itu hanya mempersulit. Yang Anda butuhkan hanyalah editor teks dan terminal.

Saya pikir siapa pun yang ingin memprogram pada level rendah harus tahu cara membuat program sederhana dari baris perintah. Anda dapat mempertimbangkan bahwa saya menggoda Anda, tetapi keterampilan hacker elit Anda tidak banyak berarti jika Anda tidak dapat berfungsi di luar Visual Studio.

Di Mac, Anda dapat menggunakan TextEdit dan Terminal untuk dikompilasi. Di Linux, Anda dapat menggunakan gedit dan Konsole. Di Windows, Anda dapat menginstal cygwin dan alat-alat, dan kemudian menggunakan N ++ atau editor teks lainnya. Jika Anda ingin menjadi sangat keren, maka semua platform ini mendukung vi dan emacs untuk mengedit teks.

Mengkompilasi program dari satu file menggunakan baris perintah adalah tugas yang sepele. Misalkan Anda menyimpan program Anda dalam file bernama 8080dis.c . Pergi ke folder dengan file teks ini dan kompilasi seperti ini: cc 8080dis.c . Jika Anda tidak menentukan nama file output, itu akan disebut a.out , dan Anda dapat menjalankannya dengan mengetikkan ./a.out .

Faktanya, itu saja.

Menggunakan debugger


Jika Anda bekerja pada salah satu sistem berbasis Unix, berikut ini adalah pengantar singkat untuk debugging program baris perintah menggunakan GDB. Anda perlu mengkompilasi program seperti ini: cc -g -O0 8080dis.c . -g menghasilkan informasi debugging (yaitu, Anda dapat melakukan debugging berdasarkan teks sumber), dan -O0 menonaktifkan optimisasi sehingga ketika Anda melangkah melalui program, debugger dapat secara akurat melacak kode sesuai sepenuhnya dengan teks sumber.

Berikut adalah log beranotasi dari awal sesi debugging. Komentar saya dalam garis yang ditandai dengan tanda pound (#).

  $ gdb a.out GNU gdb 6.3.50-20050815 (Apple version gdb-1708) (Mon Aug 8 20:32:45 UTC 2011) Copyright 2004 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "x86_64-apple-darwin"...Reading symbols for shared libraries .. done #  ,       (gdb) b Disassemble8080Op Breakpoint 1 at 0x1000012ef: file 8080dis.c, line 7. #   "invaders.h"    (gdb) run invaders.h Starting program: /Users/bob/Desktop/invaders/a.out invaders.h Reading symbols for shared libraries +........................ done Breakpoint 1, Disassemble8080Op (codebuffer=0x100801000 "", pc=0) at 8080dis.c:7 7 unsigned char *code = &codebuffer[pc]; #gdb  n  "next".    "next" (gdb) n 8 int opbytes = 1; #p -    "print",     *code (gdb) p *code $1 = 0 '\0' (gdb) n 9 printf("%04x ", pc); #    "", gdb     ,    "next" (gdb) 10 switch (*code) (gdb) n #   ,    "NOP" 12 case 0x00: printf("NOP"); break; (gdb) n 285 printf("\n"); #c -  "continue",        (gdb) c Continuing. 0000 NOP #     Disassemble8080Op.   *opcode, # ,      NOP,    . Breakpoint 1, Disassemble8080Op (codebuffer=0x100801000 "", pc=1) at 8080dis.c:7 7 unsigned char *code = &codebuffer[pc]; (gdb) c Continuing. 0001 NOP Breakpoint 1, Disassemble8080Op (codebuffer=0x100801000 "", pc=2) at 8080dis.c:7 7 unsigned char *code = &codebuffer[pc]; (gdb) n 8 int opbytes = 1; (gdb) p *code $2 = 0 '\0' #  NOP,   (gdb) c Continuing. 0002 NOP Breakpoint 1, Disassemble8080Op (codebuffer=0x100801000 "", pc=3) at 8080dis.c:7 7 unsigned char *code = &codebuffer[pc]; (gdb) n 8 int opbytes = 1; #   ! (gdb) p *code $3 = 195 '?' # print     ,    /x    (gdb) p /x *code $4 = 0xc3 (gdb) n 9 printf("%04x ", pc); (gdb) 10 switch (*code) (gdb) # C3 -  JMP. . 219 case 0xc3: printf("JMP $%02x%02x",code[2],code[1]); opbytes = 3; break; (gdb) 285 printf("\n"); 

Disassembler, Bagian 2


Jalankan disassembler untuk file ROM invaders.h dan lihat informasi yang ditampilkan.

  0000 NOP 0001 NOP 0002 NOP 0003 JMP $18d4 0006 NOP 0007 NOP 0008 PUSH PSW 0009 PUSH B 000a PUSH D 000b PUSH H 000c JMP $008c 000f NOP 0010 PUSH PSW 0011 PUSH B 0012 PUSH D 0013 PUSH H 0014 MVI A,#$80 0016 STA $2072 0019 LXI H,#$20c0 001c DCR M 001d CALL $17cd 0020 IN #$01 0022 RRC 0023 JC $0067 0026 LDA $20ea 0029 ANA A 002a JZ $0042 002d LDA $20eb 0030 CPI #$99 0032 JZ $003e 0035 ADI #$01 0037 DAA 0038 STA $20eb 003b CALL $1947 003e SRA A 003f STA $20ea /* 0000000 00 00 00 c3 d4 18 00 00 f5 c5 d5 e5 c3 8c 00 00 0000010 f5 c5 d5 e5 3e 80 32 72 20 21 c0 20 35 cd cd 17 0000020 db 01 0f da 67 00 3a ea 20 a7 ca 42 00 3a eb 20 0000030 fe 99 ca 3e 00 c6 01 27 32 eb 20 cd 47 19 af 32 */ 

Instruksi pertama sesuai dengan instruksi yang kami tulis sebelumnya. Setelah mereka ada beberapa instruksi baru. Di bawah ini saya telah memasukkan data hex untuk referensi. Perhatikan bahwa jika Anda membandingkan memori dengan perintah, maka alamatnya seolah disimpan dalam memori dalam urutan terbalik. Begitulah. Ini disebut little endian - mesin dengan sedikit endian, seperti 8080, menyimpan byte paling signifikan pertama. (Lebih lanjut tentang endian dijelaskan di bawah ini.)

Saya sebutkan di atas bahwa kode ini adalah kode ISR untuk game Space Invaders. Kode untuk interupsi 0, 1, 2, ... 7 dimulai dengan alamat $ 0, $ 8, $ 20, ... $ 38. Tampaknya 8080 hanya memberikan 8 byte untuk setiap ISR. Terkadang program Space Invaders melewati sistem ini hanya dengan pindah ke alamat lain dengan lebih banyak ruang. (Ini terjadi pada $ 000c).

Selain itu, ISR 2 tampaknya lebih panjang dari memori yang dialokasikan untuk itu. Kode-nya menjadi $ 0018 (ini adalah tempat untuk ISR 3). Saya pikir Space Invaders tidak berharap melihat apa pun yang menggunakan interrupt 3.

File Space Invaders ROM dari Internet terdiri dari empat bagian. Saya akan menjelaskan ini di bawah, tetapi untuk sekarang, untuk beralih ke bagian selanjutnya, kita perlu menggabungkan keempat file ini menjadi satu. Di Unix:

  cat invaders.h > invaders cat invaders.g >> invaders cat invaders.f >> invaders cat invaders.e >> invaders 

Sekarang jalankan disassembler dengan file “invaders” yang dihasilkan. Ketika sebuah program dimulai pada $ 0000, hal pertama yang dilakukannya adalah beralih ke $ 18d4. Saya akan menganggap ini sebagai awal dari program. Mari kita cepat melihat kode ini.

  18d4 LXI SP,#$2400 18d7 MVI B,#$00 18d9 CALL $01e6 

Jadi, ia melakukan dua operasi dan memanggil $ 01e6. Saya akan memasukkan bagian dari kode dengan transisi ke dalam kode ini:

  01e6 LXI D,#$1b00 01e9 LXI H,#$2000 01ec JMP $1a32 ..... 1a32 LDAX D 1a33 MOV M,A 1a34 INX H 1a35 INX D 1a36 DCR B 1a37 JNZ $1a32 1a3a RET 

Seperti yang kita lihat dari alokasi memori Space Invaders, beberapa alamat ini menarik. $ 2000 adalah awal dari program "RAM yang bekerja". $ 2.400 adalah awal dari memori video.

Mari tambahkan komentar ke kode untuk menjelaskan apa yang dilakukannya secara langsung pada saat startup:

  18d4 LXI SP,#$2400 ; SP=$2400 -      18d7 MVI B,#$00 ; B=0 18d9 CALL $01e6 ..... 01e6 LXI D,#$1b00 ; DE=$1B00 01e9 LXI H,#$2000 ; HL=$2000 01ec JMP $1a32 ..... 1a32 LDAX D ; A = (DE),   ,       $1B00 1a33 MOV M,A ;  A  (HL),     $2000 1a34 INX H ; HL = HL + 1 ( $2001) 1a35 INX D ; DE = DE + 1 ( $1B01) 1a36 DCR B ; B = B - 1 ( 0xff,      0) 1a37 JNZ $1a32 ; ,   ,     b=0 1a3a RET 

Sepertinya kode ini akan menyalin 256 byte dari $ 1b00 hingga $ 2000. Mengapa Saya tidak tahu. Anda dapat mempelajari program lebih terinci dan merenungkan apa yang dilakukannya.

Ada masalah di sini. Jika kita memiliki sepotong memori yang mengandung kode, maka data mungkin akan bergantian dengannya.

Misalnya, sprite untuk karakter game dapat dicampur dengan kode. Ketika disassembler jatuh ke fragmen memori seperti itu, ia akan berpikir bahwa ini adalah kode dan terus "mengunyah" itu. Jika Anda kurang beruntung, maka kode apa pun yang dibongkar setelah data ini mungkin salah.

Sementara kita hampir tidak bisa berbuat apa-apa. Perlu diingat bahwa masalah seperti itu ada. Jika Anda melihat sesuatu seperti ini:

  • transisi dari kode yang benar-benar baik ke tim yang tidak ada dalam daftar disassembler
  • aliran kode tidak berarti (mis. POP B POP B POP B POP C XTHL XTHL XTHL)

di sini, mungkin, ada data yang merusak beberapa kode yang dibongkar. Jika ini terjadi, maka Anda perlu memulai lagi dari offset.

Ternyata Space Invaders secara berkala menemukan angka nol. Jika pembongkaran kami berhenti, nol akan memaksanya melakukan reset.

Analisis terperinci kode Space Invaders dapat ditemukan di sini .

Endian


Byte disimpan secara berbeda dalam model prosesor yang berbeda, dan penyimpanan tergantung pada ukuran data. Mesin big-endian menyimpan data dari yang lebih tua ke yang lebih muda. Little-endian menjaga mereka dari yang termuda ke yang tertua. Jika integer 32-bit 0xAABBCCDD ditulis ke memori setiap mesin, itu akan terlihat seperti ini:

Dalam little-endian: $ DD $ CC $ BB $ AA

Big-endian: $ AA $ BB $ CC $ DD

Saya mulai pemrograman pada prosesor Motorola yang menggunakan big-endian, jadi menurut saya lebih "alami", tetapi kemudian saya terbiasa dengan little-endian juga.

Disassembler dan emulator saya sepenuhnya menghindari masalah endian karena mereka hanya membaca satu byte setiap kali. Jika Anda ingin, misalnya, menggunakan pembaca 16-bit untuk membaca alamat dari ROM, maka perhatikan bahwa kode ini tidak portabel antara arsitektur CPU.

Source: https://habr.com/ru/post/id418683/


All Articles