Ensimmäinen 6502-konekieliohjelma tehty

Olen ollut viime aikoina erityisen kiinnostunut 1980-luvun 8-bittisiä kotimikroja matkivista ohjelmistoista eli emulaattoreista. Suosituimmat CPU:t tuolloin olivat Zilog Z80 ja MOS 6502. Commodore 64 kotitietokoneessa oli MOS 6510, joka on käskykannaltaan 100% identtinen 6502:n kanssa. Toisin sanoen, sama konekieli kelpaa molemmille prosessoreille.

VICE (VersatIle Commodore Emulator) ohjelmassa riittää tutkimista. 6502 CPU emulaatiota silmälläpitäen ajattelin opetella hieman assembler-ohjelmointia 6502 prosessorille. En aio tehdä mitään monimutkaista, lähinnä tarkoitus on tutustua ohjelmien tekoon, jotka eivät ole aivan triviaaleja. Kokemukseni assembly-kielistä on hyvin rajallista, mutta ymmärrän miten niillä ohjelmoidaan. En siis ole aivan aloittelija niissä ja käytetyt konseptit ovat pääosin tuttuja. Tietenkin jokaisen assembly-kielen ymmärtäminen edellyttää sitä, että tuntee allaolevan CPU:n rakenteen ja käskykannan. Kyse on niin koneenläheisestä ohjelmoinnista kuin ylipäänsä tavalliselle ihmiselle on softatasolla mahdollista.

Olen joskus tutustunut aika pintapuolisesti Sun Microsystemsin (nykyään Oraclen omistuksessa) UltraSPARC-prosessorien käskykantaan, mutta en ole koskaan ohjelmoinut sillä. Kyseisessä RISC-teknologiaan perustuvassa CPU-arkkitehtuurissa on aika erikoinen piirre eli siinä on ns. delay slot konekielisille käskyille, joka suoritetaan muistaakseni "etukäteen". Tästä johtuen noissa SPARC-ympäristöissä assembler-ohjelmoijan tai assembleria tuottavan korkeamman tason kääntäjäohjelman pitää laittaa esimerkiksi ehdottomien hyppykäskyjen perään NOP-käsky. Se siis johtuu siitä, että CPU tietääkseni suorittaa käskyjä PC:n eli program counterin "edellä" ja siksi noita NOP eli "No Operation" käskyjä tarvitaan ikäänkuin "harmittomaksi täytteeksi" konekielikäskyjen virtaan. 64-bittinen UltraSPARC RISC-arkkitehtuuri on periaatteessa hyvin elegantti, mutta se tosiasia, että NOP-käskyjä tarvitaan delay slotin takia, ei ole esteettisesti erityisen kaunista. Tätä mieltä minä ainakin olen. Toisaalta Intelin 64-bittisessä arkkitehtuurissa on hirvittävä määrä historian painolastia eli nykyinen CPU-rauta käsittääkseni tukee yhä edelleen aivan muinaisia käskyjä ja tapoja käsitellä muistiavaruutta - se vasta onkin rumaa!

Kauan sitten ohjelmoin hieman virtuaalista CPU:ta assemblerilla eräällä tietokoneen toimintaan perehdyttävällä kurssilla. Tuosta kurssista on jo vuosia aikaa ja silloin käytettiin Microsoftin alkeellisen DOS-käyttöjärjestelmän päällä pyörivää simulaattoriohjelmaa. Konekielen kohteena olevaa CPU:ta siis ei ollut fyysisesti ollenkaan ollenkaan olemassa piimöhkäleenä eli "rautana" koneen sisällä. Se CPU oli vain tietokoneohjelman avulla mallinnettu, fiktiivinen CPU, jossa oli hyvin yksinkertainen käskykanta. Simulaattoriympäristö sisälsi assemblerin ja mahdollisuuden jonkin verran debugata käännettyjä konekieliohjelmia esimerkiksi tutkimalla virtuaalisen CPU:n rekistereitä.

Tänään ajattelin yrittää saada pystyyn 6502 ristiinkääntäjäympäristön, jolla saisi Linuxissa tuotettua Commodore 64 koneelle sopivia PRG-formaatissa olevia konekieliohjelmia. En tiedä PRG-formaatin tarkkaa muotoa, mutta tutkittuani mahdollisimman yksinkertaisen konekieliohjelman binääristä rakennetta, minusta näyttää siltä, että tiedoston alussa on kaksi tavua, jotka kertovat mihin osaan Commodore 64:n RAM-muistia ohjelma pitää ladata. Seuraavat tavut näyttäisivät olevan vain konekieliohjelma sellaisenaan eli formaatti olisi äärimmäisen pelkistetty.

Tiesin, että CC65 on C-ristiinkääntäjä 6502 prosessorille ja että sen saa Linuxille avoimena lähdekoodina. Kyseiseen ohjelmistopakettiin kuuluu tähän tutustumisprojektiini turhia osia kuten se C-kääntäjä ja tuki erilaisille ajonaikaisille 6502-ympäristöille, mutta sen mukana tulee myös toimiva cross assembler. Toisin sanoen, voin naputella 6502 assembly-kieltä Linuxissa ja kyseinen cross assembler -ohjelma generoi konekieltä, jota voi ajaa 6502 CPU:lla.

Aloitin tekemällä git-versionhallintaohjelmalla checkoutin eli gitin kielellä kloonauksen CC65:n github-lähdekoodivarastosta:

mkdir src
cd src
git clone https://github.com/cc65/cc65.git

Olisin toki asentanut paketin mieluiten Fedora Linux 27:n paketinhallinnan kautta suoraan Fedoran repoista, mutta tätä ohjelmistoa ei ole saatavilla RPM-paketoituna niistä. Siksi jouduin hakemaan suoraan lähdekoodipaketin.

Tämä ohjelmisto ei myöskään ole GNU Autotoolsia hyödyntävä, joten siinä ei ole mitään configure-skriptiä, jolla generoitaisiin C-käännöstä ohjaavat Makefile-tiedostot. Makefilet ovat valmiina, joten kääntämiseen riittivät suoraan komennot:

cd cc65
make

Tai itse asiassa minulle riitti lyhennetty komento m, sillä olen tehnyt make-komennolle bash-komentotulkkiin aliaksen:

alias m='make'

Kyseinen asetus on tiedostossa /home/kalevi/.bashrc ja komentotulkki lukee sen käynnistymisensä yhteydessä.

Valitettavasti Fedora Linux 27:ssa on SGML-dokumenttien prosessointiin liittyvä bugi, joka estää doc-hakemistossa make-komennon onnistumisen. Virheitä tulee liikaa ja HTML-tiedostojen generointi epäonnistuu. Googlen avulla sain selville, että joku on raportoinut tämän ohjelmistovirheen syyskuussa 2017 Red Hatille, mutta raporttiin ei ole reagoitu mitenkään. Siellä on ilmeisesti DocBook-DTD määrittelyissä jokin Redh Hatin ja Fedoran oma patchi eli ohjelmistomuutos, joka rikkoo linuxdoc-ohjelman toimivuuden. Tästä syystä en saanut generoitua HTML-tiedostoja, vaan jouduin lukemaan suoraan SGML-tiedostoja saadakseni selville miten CC65:n avulla luodaan Commodore 64:ssä toimiva PRG-tiedosto, jossa on konekieltä.

Kun käännös oli valmis, oli aika asentaa ohjelmisto. Tein sille oman hakemistopuun ja käskin make-komennolla asentaa kaiken sinne:

mkdir /opt/cc65
make PREFIX=/opt/cc65 install

Valitusta tuli siitä, että HTML-dokumentit puuttuivat, koska niitä ei edellä mainitsemani ohjelmistovirheen takia pystytty luomaan, mutta muuten homma näytti toimivan.

Tein alihakemiston ensimmäiselle kokeelliselle 6502 assembler-ohjelmalleni ja loin sinne seuraavanlaisen Makefile-tiedoston:

CL65=/opt/cc65/bin/cl65
PROGRAM=first
SOURCE=$(PROGRAM).s
EXE=$(PROGRAM).prg
START_ADDR=$$C000
C64CONFIG=/opt/cc65/share/cc65/cfg/c64-asm.cfg

$(EXE): $(SOURCE)
$(CL65) -o $(EXE) --start-addr \$(START_ADDR) -t c64 -C $(C64CONFIG) $(SOURCE)

clean:
rm -f $(EXE) *.o

Jos aiot ottaa siitä mallia, niin ÄLÄ KOPIOI sitä tuosta ylhäältä, sillä make-ohjelmassa on suunnitteluvirhe, joka vaatii horisontaalisia tabulaattorimerkkejä Makefile-tiedostoon. Ohjelman luoja on itse myöntänyt, että hän oli aivottomasti kopioinut tuon tabulaattorivaatimuksen jostain toisesta ohjelmasta ja vasta myöhemmin ymmärtänyt, että tuon mallina käytetyn ohjelmiston tekijä oli pitänyt sitä isona mokana. Eli jos haluat toimivan Makefile-tiedoston, downloadaa se gzip-pakattuna tästä linkistä, sillä copy-paste ei toimi puuttuvien tabulaattorien vuoksi.

Commodore 64:n näyttömuisti merkkien suhteen alkaa muistiosoitteesta 1024 (desimaalina) eli 0400 heksadesimaalina. BASIC-kielessä on mahdollista kirjoittaa iso A-kirjain ruudun vasempaan ylänurkkaan komennolla POKE 1024,1 .

Tuota on helppo testata vaikkapa VICE-emulaattorissa.

Ajattelin, että ensimmäinen 6502 assembler-ohjelmani olkoon ekvivalentti kyseisen POKE-komennon kanssa, jotta nähdään, että CC65-kääntäjäympäristö tuottaa toimivia binääreitä. Tämä on siis suurinpiirtein yksinkertaisin kuviteltavissa oleva konekieliohjelma, joka tekee jotain silmillä havaittavaa Commodore 64 koneessa. Kirjoitin vim-tekstieditorilla seuraavan ohjelman ja tallensin sen tiedostoon first.s.

LDA #$01
STA $0400
RTS

Sen jälkeen annoin komennon make ja totesin, että first.prg tiedosto syntyi kuten pitikin. Se on kooltaan vaivaiset 8 tavua, joten siinä on melkoinen ihmettelyn aihe nykyihmisille, jotka ovat tottuneet 64-bittisiin koneisiin, joissa on isoja ajettavia binääreitä!

Kuten aiemmin sanoin, PRG-formaatissa kaksi ensimmäistä tavua sisältävät sen muistiosoitteen, johon niitä tavuja seuraava konekielinen 6502-ohjelma pitää ladata. Kolmas tavu on LDA-komento käännettynä konekieleksi, neljäs tavu on LDA-komennon välitön parametri (eli numeerinen arvo 1). Viides tavu on STA-komento konekielisenä, kuudes ja seitsemäs tavu ovat STA-komennon parametri.

Niistä on huomioitava sen verran, että konekielisessä muodossa nuo tavut ovat käänteisessä järjestyksessä verrattuna assembler-representaatioon. Eli 0x00 tavu tulee RAM-muistiin ensin alemmalle muistipaikalle ja sen perässä on tavu 0x40. Tuo siis on POKE-komennolle annettu desimaalinen muistiosoite 1024 muunnettuna heksadesimaaliksi. Käänteinen järjestys konekielessä johtuu siitä, että 6502 on ns. little endian prosessori, jossa tavujärjestyksessä vähiten merkitsevä tavu tulee ensin. Intelin modernit CPU:t ovat myös little endian koneita, mutta esimerkiksi UltraSPARC on big endian. Joissain CPU-arkkitehtuureissa tavujärjestyksen voi valita dynaamisesti.

Kuten varmaan jo arvasitkin, se kahdeksas eli viimeinen tavu PRG-tiedostossa on RTS-komento konekielisenä. Se tarkoittaa paluuta aliohjelmasta kutsuvaan ohjelmaan. Seuraavaksi oli aika käynnistää VICE-emulaattori ja testata toimiiko konekieliohjelmamme. Annoin komennon:

x64 first.prg &

jolloin VICE käynnistyi terminaalista tausta-ajoon ja sille annettu first.prg parametri kertoi emulaattorille, että sen tulee ladata automaattisesti kyseinen PRG-tiedosto kun Commodore 64 emulaatio on saatu pystyyn.

Unohdinkin muuten selittää miksi valitsin konekieliohjelmalle aloitusosoitteeksi 0xc000. Sain tuon vinkin jostain netistä eli siitä alkaen Commodore 64:ssä on vapaata muistia neljä kilotavua eli 4KB eli 4096 tavua. Siis enemmän kuin tarpeeksi meidän aloitusohjelmallemme, joka on muistissa kooltaan vain 6 tavua kun ensimmäiset kaksi tavua kertoivat vain aloitusosoitteen paikan Commodore 64:lle.

Miten voimme kutsua ohjelmaamme Commodore 64 emulaation sisältä? Helposti. Katsotaan ensin mitä kyseinen osoite on desimaalina. Komensin ennen emulaattorin käynnistystä Linux bash-tulkissa:

printf "%d\n" 0xc000
49152

Ohjelmamme on siis ladattu osoitteeseen 49152 desimaalina. Alla on kuvakaappaus ohjelman latauksesta ja annetusta SYS49152 BASIC-komennosta. En muista mistä lyhenne SYS tulee, mutta se siis tarkoittaa: "Aloita konekielisen ohjelman suorittaminen muistiosoitteesta se-ja-se".

Huomaa, että ruudun vasempaan yläkulmaan on tulostunut iso A-kirjain. CC65 teki ehjän ajettavan binäärin ja ohjelmamme teki mitä pitikin!

first 6502 assembler program on commodore 64

Jatkan tästä myöhemmin monimutkaisempien 6502-ohjelmien parissa.