Kirjautuminen

Haku

Tehtävät

Koodit: C: Kääntäjä yksinkertaiselle kielelle

Kirjoittaja: ville-v

Kirjoitettu: 09.09.2008 – 17.03.2017

Tagit: koodi näytille, vinkki

Jokaistahan on joskus askarruttanut oman ohjelmointikielen tekeminen, mutta se on jäänyt tulkin tasolle tai kokonaan kesken kykyjen loputtua. Ilkeät Mikrobitin toimittajatkaan eivät ole vastanneet tämänkaltaisiin kirjeisiin:

ville-v 12 vuotta kirjoitti:

Voiko kääntäjän tehdä esim. html:llä vai pitääkö käyttää kone kieltä?

Tämän koodivinkin tarkoitus on tarjota jonkinlainen tietopohja jolle oman ohjelmointikielen rakentaminen on mahdollista. Käännettävä kieli ei varsinaisesta täytä kaikkia ohjelmointikielen määritelmiä (sillä ei voi esimerkiksi ohjelmoida Turingin konetta), sopii silti hyvin esimerkiksi kääntäjän toiminnasta, ja on laajennettavissa tarpeiden mukaan.

Esimerkkikielessä on ainoastaan kolme komentoa (+, - ja #) jotka vievät kukin yhden tavun tilaa. Monimutkaisempien komentojen ja parsereiden rakentamiseen löytyy sopiva koodivinkki Yksinkertainen tulkki.

Komento # tulostaa käytettävän 8-bittisen muuttujan arvon, + lisää muuttujan arvoa yhdellä ja - vähentää yhdellä. Nämä komennot muunnetaan tietokoneen prosessorin ymmärtämään muotoon, esimerkiksi komennosta "+" tulee kahden tavun sarja, heksana "FE C0". Nämä luvut tarvitsee vain kirjoittaa yhteen pötköön suorittettavaan tiedostoon.

Numeroarvoja ei suinkaan ole vedetty hatusta, vaan ne ovat konekieliset arvot Assembly-kieliselle komennolle "inc al", joka kasvattaa al-rekisterin arvoa yhdellä, ja vastaa esimerkiksi C-kielen komentoa "muuttuja++". Assemblyllä ohjelmoitaessa kääntäjä muuntaa symboliset komennot numeroarvoiksi, mutta kääntäjää laadittaessa tämä työ on tehtävä käsin. Intelin käskykannalle, jota käytännössä kaikki kotitietokoneet käyttävät, hyödyllinen lista on Intel Pentium Instruction Set Reference, joka luettelee kaikki assembly-kieliset komennot käyttötapoineen ja niitä vastaavat numerot.

Assembly-kieltä on luonnollisesti osattava, jotta voit muodostaa käännetyt funktiot oman ohjelmointikielesi funktioille. Mahdollinen tapa olisi tietysti muodostaa ainoastaan perusrakenteet ja rakentaa niiden pohjalta monimutkaisempia funktioita, kuten esimerkiksi C-kielen standardikirjastossa.

Ensimmäinen koodi on kääntäjä. Toinen koodi on "Hello World!" -ohjelma, joka on rivitetty kirjainten mukaan; kääntäjä ohittaa kaikki merkit jotka eivät ole komentoja.

#include <stdio.h>

/* Käytettävät tiedostonimet */
char _output[] = "a.com", _input[] = "a.txt";

/* Testauksen selkeyden vuoksi onnistuu myös tulkkaus. */
/* Kun arvo on 0, ei tulkata vaan käännetään. */
unsigned char tulkkaa = 0;

/* Tulkattaessa tämä vastaa rekisterin al arvoa. */
unsigned char byte;

/*
Selväkielisten komentojen vastineet.
Heksakoodien vierellä vastaavat Intel-x86 Assembly -komennot.
*/

/*
Komentosarja tulostaa ruudulle halutun merkin.
Merkki tulostetaan rekisteristä al (8-bittinen).
Tulostus tapahtuu DOSin keskeytyksellä 10h.
Tällöin rekisterissä ah on tulostuksen koodi 0Eh.

Komentosarjaa ei ole toteutettu aliohjelmana, vaan se kirjoitetaan
komennon kohdalle. Kotitehtävä: muokkaa aliohjelmaksi.
        Lisää loppuun ret-komento (paluukohde on tallennettu pinoon).
        Kutsutaan call-käskyllä, parametriksi muistipaikka josta aliohjelma alkaa.
*/
char print_char[] = {
/* push bx */ 0x53,
/* push ax */ 0x50,
/* mov ah, 0Eh */ 0xB4, 0x0E,
/* xor bx, bx */ 0x33, 0xDB,
/* int 10h */ 0xCD, 0x10,
/* pop ax */ 0x58,
/* pop bx */ 0x5B
};

/*
Komento lisää rekisterin al arvoa.
*/
char increase[] = {
/* inc al */ 0xFE, 0xC0
};
/*
Komento vähentää rekisterin al arvoa.
*/
char decrease[] = {
/* dec al */ 0xFE, 0xC8
};
/*
Komento alustaa ohjelman:
Rekisteri al asetetaan nollaksi.
*/
char init[] = {
/* xor al, al */ 0x32, 0xC0
};

/*
Komentosarja poistuu ohjelmasta DOSin keskeytyksellä.
*/
char quit[] = {
/* mov ah, 4Ch */ 0xB4, 0x4C,
/* int 21h*/ 0xCD, 0x21
};

/*
Ottaa parametriksi merkin ja kirjoittaa tiedostoon sitä vastaavat komennot.
*/
void write_equivalent(unsigned char command)
{
        FILE *tiedosto = 0;
        /* Avataan tiedosto jonne ohjelma kirjoitetaan */
        if(!tulkkaa) tiedosto = fopen(_output, "ab");

        switch(command){
                /* Seuraavat komennot luetaan tiedostosta */
                case '+':
                        /* Lisätään merkin arvoa */
                        tulkkaa?++byte:
                        /* Kirjoitetaan vastaava komento tiedostoon */
                        fwrite(increase, sizeof(increase), 1, tiedosto);
                        break;
                case '-':
                        /* Vähennetään merkin arvoa */
                        tulkkaa?--byte:
                        /* Kirjoitetaan vastaava komento tiedostoon */
                        fwrite(decrease, sizeof(decrease), 1, tiedosto);
                        break;
                case '#':
                        /* Tulostetaan merkki */
                        tulkkaa?putchar(byte):
                        /* Kirjoitetaan vastaava komento tiedostoon */
                        fwrite(print_char, sizeof(print_char), 1, tiedosto);
                        break;

                /* Seuraavat komennot ovat sisäisessä käytössä */
                case '{':
                        /* Ohjelma alustetaan */
                        tulkkaa?byte = 0:
                        /* Kirjoitetaan vastaava komento tiedostoon */
                        fwrite(init, sizeof(init), 1, tiedosto);
                        break;
                case '}':
                        /* Ohjelma lopetetaan lopetuskäskyillä */
                        if(!tulkkaa) fwrite(quit, sizeof(quit), 1, tiedosto);
                        break;

                /* Muut merkit ohitetaan. */
                default:
                        break;
        }

        /* Suljetaan tiedosto jos se on avattu */
        if(tiedosto){ fclose(tiedosto);
        /* Jos tiedosto ei ole auki eikä ole tulkattu, virheilmoitus */
        if(!tulkkaa) puts("Virhe: Tiedostoa ei saatu auki.");}
}

int main(void)
{
        /* Tiedostosta luettu merkki */
        int a;

        /* Avataan lähdetiedosto */
        FILE *tiedosto;
        tiedosto = fopen(_input, "r");

        /* Tarkistetaan avauksen onnistuminen */
        if(!tiedosto){
                puts("Tiedostoa ei saatu auki.");
                return 1;
        }

        /* Tyhjennetään kohdetiedosto. */
        FILE *kohde = fopen(_output, "w");
        fclose(kohde);

        /* Kirjoitetaan ohjelman alku tiedostoon */
        write_equivalent('{');

        /* Käydään tiedostoa silmukassa läpi, kunnes se loppuu */
        while((a = fgetc(tiedosto)) != EOF){
                write_equivalent(a);
        }

        /* Kirjoitetaan ohjelman loppu tiedostoon */
        write_equivalent('}');

        /* Suljetaan lähdetiedosto */
        fclose(tiedosto);
        return 0;
}
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++#
+++++++++++++++++++++++++++++#
+++++++#
#
+++#
-------------------------------------------------------------------------------#
+++++++++++++++++++++++++++++++++++++++++++++++++++++++#
++++++++++++++++++++++++#
+++#
------#
--------#
-------------------------------------------------------------------#

Kommentit

ByteMan [14.09.2008 18:48:41]

#

ihan kiva. muistuttaa brain fuckia vähäsen :D

Hakoulinen [14.09.2008 21:16:52]

#

Erittäin mielenkiintoinen koodi. Tuohon on myös helppo lisätä omia ominaisuuksiaan.

EDIT: Koneeni ei vain anna suorittaa ohjelmaa. Liekö eri käskykanta AMD Athlon 64 X2:selle? Jos se on siitä kiinni, niin mistä löytyisi samaiset käskyjen numeeriset arvot? AMD:n sivuilta en onnistunut löytämään.

ville-v [15.09.2008 16:07:02]

#

Hakoulinen kirjoitti:

Koneeni ei vain anna suorittaa ohjelmaa. Liekö eri käskykanta AMD Athlon 64 X2:selle? Jos se on siitä kiinni, niin mistä löytyisi samaiset käskyjen numeeriset arvot? AMD:n sivuilta en onnistunut löytämään.

http://www.google.fi/search?q="amd athlon 64" assembly programming

Luulisi tosin, että 64-bittinen käyttöjärjestelmä suorittaa myös 32-bittisiä ohjelmia. Voi olla, että ongelma onkin käyttöjärjestelmässä. MS-DOSin keskeytykset (int-komento) eivät välttämättä toimi noin vain Vistassa (XP:llä kokeilin, toimivat ilman yhteensopivuustilaa) tai varsinkaan Linuxissa. Kannattaa kokeilla Dos-emulaattorilla (esim. Dosbox) tai etsiä oman käyttöjärjestelmäsi natiivikeskeytykset.

Puusilmä Ado [10.10.2008 10:47:27]

#

Tämän tuottamat EXEt toimivat yhtä hyvin kuin "copy sorsa.c sorsa.exe":llä tehdyt. MULTIFAIL.

thefox [04.11.2008 02:18:41]

#

Tämä koodivinkki ei tuota toimivia binääreitä. Ihan mielenkiintoista kyllä kuulla, että XP:ssä tämän ulostukset toimivat siitäkin huolimatta. Todennäköisesti XP tulkitsee .EXE-tiedoston samoin kuin .COM-päätteiset jos validia headeria ei löydy. (COM-tiedostot ladataan osoitteeseen 0x100 ja ajetaan sellaisenaan.)

Muuta koodista "a.exe" -> "a.com" niin vinkissä on jotain jotain järkeä. Tai lisää validin DOS-headerin tulostus (monimutkaistaa koodia turhaan).

Ja joo, taisivat tosiaan Windows Vistasta poistaa DOS-yhteensopivuuden kokonaan.

EDIT: on tää muutenkin aika "hauskasti" kirjoitettu, tuosta ternäärioperaattorin käytöstä joka käänteessä voi olla montaa mieltä...

ville-v [13.11.2008 18:14:52]

#

Totta töriset. Kokeilin rakentaa tuohon headeria, mutta päättelin, että se pitäisi rakentaa kääntämisen aikana. Vaihdoin siis ulostuksen .comiksi.

ErroR++ [14.11.2011 14:33:15]

#

Jos kone on 64 -bittinen, niin eipä sillä tuon tuottamat 16 -bittiset ohjelmat pyöri.

Grez [07.08.2015 13:54:35]

#

ErroR++ kirjoitti:

Jos kone on 64 -bittinen, niin eipä sillä tuon tuottamat 16 -bittiset ohjelmat pyöri.

Jos ajaa 32-bittistä käyttistä 64-bit koneessa niin 16-bittiset ohjelmatkin toimii. (Jos 16-bit softat ylipäätään toimii ko. 32-bit käyttiksessä, kuten Windowseissa)

Koodi123 [18.02.2019 16:31:25]

#

ville-v kirjoitti:

/* Käydään tiedostoa silmukassa läpi, kunnes se loppuu */
while((a = fgetc(tiedosto)) != EOF){
    write_equivalent(a);
}

Onko tässä virhe?
Pitäisikö EOF:in tilalle vaihtaa FEOF?

/* Käydään tiedostoa silmukassa läpi, kunnes se loppuu */
while((a = fgetc(tiedosto)) != FEOF) {
    write_equivalent(a);
}

Metabolix [19.02.2019 01:21:35]

#

Koodi123, olet erehtynyt. Kokeilitko tai luitko dokumentaatiota edes? EOF on aivan oikein.

Kirjoita kommentti

Muista lukea kirjoitusohjeet.
Tietoa sivustosta