Membuat assembler "z80asm" menempatkan instruksi pada alamat memori yang diketahui

Dec 30 2020

Saya sedang menulis OS yang sangat dasar untuk komputer homebrew Z80 saya. Sebagai seorang pemula bahasa assembly absolut, saya berhasil mendapatkan "os plus memory monitor" yang berfungsi yang dapat menampilkan konten memori dan memuat byte ke RAM. Dalam melakukannya, saya menulis beberapa "rutinitas sistem" untuk menghubungkan beberapa perangkat I / O. Misalnya, saya memiliki rutinitas "Printc" yang membaca satu byte dan menggambar karakter ASCII yang sesuai di layar.

Ini bekerja dengan kode yang dibangun oleh assembler karena assembler memutuskan di mana harus meletakkan byte pertama rutin dan menggunakan alamat itu ketika menemukan perintah jp dengan label yang sama.

Sekarang, saya ingin memanggil rutinitas Printc dari program yang dimuat secara dinamis. Saya dapat mengetahui di mana assembler menempatkan byte pertama rutin di ROM berkat -lbendera, yang menghasilkan output yang berisi:

...
Print:    equ $043a Printc: equ $043e
Readc:    equ $0442 Readline: equ $0446
...

Sekarang saya dapat menulis program seperti ini:

ld a, 0x50     ; ASCII code for P
call 0x043e    ; Calls Printc

Program ini berhasil mencetak huruf P: Saya memanggil rutinitas Printc saya menggunakan alamat memorinya.

Ini baik-baik saja selama saya tidak mengubah kode assembly apa pun sebelum deklarasi Printc di "os" saya. Jika saya melakukannya, label Printc akan diberikan ke alamat lain dan program saya yang ada akan berhenti bekerja.

Apa solusi kanonik untuk jenis masalah ini? Satu-satunya yang terlintas di benak saya adalah membuat "tabel lompat" di awal kode assembly saya, sebelum mengimpor, dengan daftar panggilan sistem, dengan harapan mereka akan mendapatkan alamat yang sama setiap kali. Sesuatu seperti:

...
; System routines
Sys_Print:
call Print
ret
Sys_Printc:
call Printc
ret
.... and so on

Tapi ini tampaknya cukup hackish ... Apakah mungkin untuk menginstruksikan z80asmassembler untuk menempatkan instruksi pertama rutinitas pada alamat memori yang saya putuskan?

Jawaban

10 Raffzahn Dec 30 2020 at 04:31

Apa solusi kanonik untuk jenis masalah ini?

Tidak ada solusi kanonik, tetapi banyak varian, semuanya dapat ditemukan dapat digunakan.

Satu-satunya yang terlintas di benak saya adalah membuat "meja lompat" di awal

Yang mana yang sangat bagus. Kecuali, biasanya seseorang akan menggunakan lompatan alih-alih panggilan untuk mengurangi panjang kode, mempercepat eksekusi, dan mengurangi beban tumpukan.


JUMP_TABLE:
PRINT    JP  _I_PRINT    ; First Function
READC    JP  _I_READC    ; Second Function
...

Tapi ini sepertinya cukup hackish ...

Tidak, banyak sistem 8080 dan Z80 bekerja seperti itu.

Langkah utama ke depan adalah bahwa semua titik masuk berada di satu lokasi dan urutan yang ditentukan.

Apakah mungkin untuk menginstruksikan perakit z80asm untuk menempatkan instruksi pertama rutinitas pada alamat memori yang saya putuskan?

Tentu, gunakan ORG untuk meletakkannya di alamat mana pun yang Anda inginkan (* 1). Tapi itu akan menjadi hackish atau setidaknya tidak terlalu melihat ke depan. Memiliki tabel lompat seperti itu di alamat yang ditentukan adalah awal yang baik. Tentu saja itu memakan ruang. Tiga byte per entri, tetapi hanya dua yang menjadi alamatnya. Bukankah lebih baik membuat tabel alamat saja? Suka:

SYS_TABLE:
         DW    _I_PRINT    ; First Function
         DW    _I_READC    ; Second Function

Memanggil suatu fungsi akan seperti itu

         LD    HL, (SYS_TABLE+0)   ; Load Address of First Function - PRINT
         JP    (HL)                ; Do it

Ini dapat dengan mudah digabungkan dengan jenis selektor fungsi:

SYS_ENTRY:
         PUSH  HL
         LD    H,0
         LD    L,A
         ADD   HL,HL
         ADD   HL,SYS_TABLE
         JP    (HL)

Sekarang bahkan tabel lompat dapat dipindahkan dalam ROM (atau RAM) sesuai kebutuhan.

Memanggilnya akan menggunakan nomor fungsi - seperti yang dimiliki banyak OS - cukup letakkan nomor fungsi di A dan panggil titik masuk sistem default (SYS_ENTRY).

         LD    A,0   ; Print
         CALL  SYS_ENTRY

Tentu saja akan lebih mudah dibaca jika OS menyediakan satu set persamaan untuk nomor fungsi :)

Sejauh ini program yang dimuat masih perlu mengetahui alamat tabel (SYS_TABLE) atau titik masuk untuk selektor (SYS_ENTRY). Tingkat abstraksi berikutnya akan memindahkan alamat mereka ke lokasi yang ditentukan, seperti 0100h, paling baik mungkin dalam bentuk JP, jadi setiap program pengguna selalu memanggil alamat tetap itu (0100h) tidak peduli apakah OS Anda ada di ROM atau RAM atau di mana pun.

Dan ya, jika ini tampak familier, karena ini cara yang sama CP / M menangani panggilan sistem, atau MS-DOS.

Berbicara tentang MS-DOS, ini menyediakan cara tambahan (dan lebih umum dikenal) untuk memanggil fungsi OS, yang disebut interupsi perangkat lunak, seperti INT 21h yang terkenal. Dan ada sesuatu yang sangat mirip yang ditawarkan Z80 (dan 8080 sebelumnya): Seperangkat delapan vektor ReSTart yang berbeda (0/8/16 / ...). Restart 0 dicadangkan untuk reset, semua yang lain dapat digunakan. Jadi mengapa tidak menggunakan yang kedua (RST 8h) untuk OS Anda? Panggilan fungsi kemudian akan terlihat seperti ini:

         LD    A,0   ; Print
         RST   8h

Sekarang kode program pengguna dipisahkan sebanyak mungkin dari struktur OS dan tata letak memori - tanpa perlu relokasi atau apa pun. Bagian terbaiknya adalah, dengan sedikit mengutak-atik, seluruh pemilih cocok dengan 8 byte yang tersedia, menjadikannya pengkodean yang optimal.


Sedikit saran:

Jika Anda menggunakan salah satu model ini, pastikan bahwa fungsi pertama (0) OS Anda adalah panggilan yang menyediakan informasi tentang OS, sehingga program dapat memeriksa kompatibilitasnya. Setidaknya dua nilai dasar harus dikembalikan:

  • Nomor rilis ABI
  • Nomor fungsi maksimum yang didukung.

Nomor rilis ABI mungkin atau mungkin tidak sama dengan nomor versi, tetapi tidak harus. Itu harus ditingkatkan dengan setiap perubahan API. Bersama dengan nomor fungsi maksimum yang didukung, informasi ini dapat digunakan oleh program pengguna untuk berhenti dengan anggun jika terjadi ketidaksesuaian - alih-alih mogok di tengah jalan. Untuk kemewahan, fungsinya mungkin juga mengembalikan pointer ke a

  • Struktur yang berisi informasi lebih lanjut tentang OS seperti
    • nama / versi yang dapat dibaca
    • Alamat dari berbagai sumber
    • titik masuk 'khusus'
    • Informasi mesin seperti ukuran RAM
    • antarmuka yang tersedia, dll.

Hanya mengatakan ...


* 1 - Dan tidak, selain beberapa anggapan, ORG tidak boleh menambahkan padding atau sejenisnya sendiri. Assembler yang melakukannya adalah pilihan yang buruk. Org seharusnya hanya mengubah tingkat alamat, tidak menentukan apa yang ada di area mana pun yang 'dilompati'. Melakukan hal itu dapat menambah tingkat kesalahan potensial - setidaknya segera setelah beberapa penggunaan ORG lanjutan selesai - percayalah, ORG adalah alat yang sangat serbaguna saat melakukan struktur yang kompleks.

Selain itu, mengisi area 'void' dengan beberapa padding akan menghasilkan padding ini menjadi bagian dari program dan bukan memori yang tidak tersentuh, menghilangkan alat utama untuk patch selanjutnya: ruang EPROM yang tidak diinisialisasi. Dengan hanya tidak mendefinisikan dan tidak memuat area ini, mereka akan tetap dalam keadaan apa pun yang dibersihkan (semua dalam kasus EPROM) dan dapat diprogram nanti - misalnya untuk menahan beberapa kode selama debugging, atau untuk menerapkan hot fix tanpa kebutuhan untuk memprogram perangkat baru.

Jadi memori yang tidak terdefinisi seharusnya hanya itu, tidak terdefinisi. Dan itulah mengapa bahkan format keluaran / pemuat assembler yang paling awal (pikirkan Motorola SREC atau Intel HEX ) digunakan untuk pengiriman program ke apa pun mulai dari fabrikasi ROM hingga program pengguna yang mendukung cara untuk meninggalkan area.

Singkat cerita: Jika seseorang ingin diisi, itu harus dilakukan secara cepat. z80asm melakukannya dengan benar.

12 WillHartung Dec 30 2020 at 04:55

Masalah dengan Z80ASM secara khusus adalah bahwa ia mengambil input rakitan dan mengeluarkan file biner statis. Hal ini baik dan buruk.

Dalam sistem "normal", penetapan alamat, tak pelak lagi, adalah tanggung jawab penaut, bukan assembler. Tetapi assembler cukup sederhana sehingga banyak yang melewati aspek siklus build itu.

Karena Z80ASM menampilkan gambar biner literal, bukan file "objek", Z80ASM tidak memerlukan linker. Tapi itu juga tidak akan membiarkan Anda melakukan apa yang ingin Anda lakukan.

Pertimbangkan arahan ORG di mana-mana.

ORG memberi tahu assembler apa alamat awal (asal - jadi ORG) untuk kode assembly yang akan datang.

Artinya jika Anda melakukan ini:

    ORG 0x100
L1: jp L1

Assembler akan merakit instruksi JP ke JUMP ke alamat 0x100 (L1).

TAPI, ketika ia mengeluarkan file binernya, file tersebut hanya akan berukuran 3 byte. Instruksi lompat, diikuti oleh 0x100 dalam format biner. Tidak ada apa pun dalam file ini yang memberi tahu, yah, apa pun, bahwa file harus dimuat pada 0x100 agar "berfungsi". Informasi itu hilang.

Jika kamu melakukan:

    ORG 0x100
L1: jp L2

    ORG 0x200
L2: jp L1

Ini akan menghasilkan file yang panjangnya 6 byte. Ini akan menempatkan dua instruksi JP tepat setelah satu sama lain. Satu-satunya hal yang dilakukan pernyataan ORG adalah memberi tahu label apa yang seharusnya. Ini bukan yang Anda harapkan.

Jadi, hanya menambahkan ORG ke file Anda tidak akan melakukan apa yang ingin Anda lakukan, kecuali Anda memiliki metode alternatif untuk memuat kode di tempat tertentu yang Anda inginkan untuk kode Anda.

Satu-satunya cara untuk melakukannya dengan Z80ASM di luar kotak adalah dengan mengisi file output Anda dengan blok byte, ruang kosong, yang akan mengisi biner untuk meletakkan kode Anda di tempat yang tepat.

Biasanya, inilah yang dilakukan linker untuk Anda. Tugas penaut adalah mengambil potongan kode Anda yang berbeda, dan membuat gambar biner yang dihasilkan. Ia melakukan semua ini untuk Anda.

Pada assembler saya, yang tidak menggunakan linker, itu menghasilkan format file Intel HEX yang menyertakan alamat sebenarnya untuk setiap blok data.

Jadi, untuk contoh sebelumnya, ini akan membuat dua record. Satu ditujukan untuk 0x100, yang lain untuk 0x200, dan kemudian program pemuatan hex akan meletakkan semuanya di tempat yang benar. Ini adalah alternatif lain, tetapi Z80ASM tampaknya juga tidak mendukungnya.

Begitu.

Z80ASM sangat bagus jika Anda membuat gambar ROM mulai dari, katakanlah, secara sewenang-wenang, 0x1000. Anda akan ORG itu, mendapatkan biner yang dihasilkan, dan mengunduh seluruh file yang dibakar ke EPROM. Itu sempurna untuk itu.

Tetapi untuk apa yang ingin Anda lakukan, Anda harus memasukkan kode untuk memindahkan rutinitas Anda ke tempat yang tepat, atau membuat skema loader lain untuk mewujudkannya untuk Anda.

5 GeorgePhillips Dec 30 2020 at 03:16

The orgdirektif harus melakukan secara spesifik apa yang Anda minta. Namun, z80asm sedikit sederhana dalam format keluarannya. Sebagai gantinya Anda dapat menggunakan dsuntuk menempatkan rutinitas di alamat tertentu:

        ds     0x1000
printc:
        ...
        ret

        ds     0x1100-$
readc:
        ...
        ret

Ini akan selalu ditempatkan printcpada 0x1000 dan readcpada 0x1100. Ada banyak kekurangannya. Harus printctumbuh lebih besar dari 0x100 program tidak akan berkumpul dan Anda harus printcmembongkar dengan cara tertentu dan meletakkan kode ekstra di tempat lain. Untuk itu dan alasan lainnya, tabel lompat di lokasi tetap dalam memori lebih mudah dikelola dan lebih fleksibel:

           ds    0x100
v_printc:  jp    printc
v_readc:   jp    readc
           ...

Teknik lain adalah dengan menggunakan titik masuk tunggal dan memilih fungsi menggunakan nilai dalam Aregister. Ini setidaknya akan sedikit lebih lambat tetapi berarti bahwa hanya satu titik masuk yang perlu dipertahankan saat sistem operasi berubah.

Dan alih-alih melakukan a CALLke titik masuk, letakkan di salah satu RSTlokasi khusus (0, 8, 0x10, 0x18, 0x20, 0x28, 0x30, 0x38) di mana Anda dapat menggunakan RST 0x18sebagai panggilan byte tunggal ke lokasi memori 0x18. Biasanya RST 0dan RST 0x38dihindari karena salah satunya adalah titik masuk pwoer-on dan lokasi penangan model interupsi 1 masing-masing.