Matalan tason ohjelmointi

Beta

10. Konekieliohjelmointi

Konekielellä ohjelmointi on varsin erilaista kuin korkeamman tason ohjelmointikielen käyttäminen. Haasteena on, että konekielinen ohjelma tulee toteuttaa käyttäen yksinkertaisia komentoja eikä käytettävissä ole korkean tason ohjausrakenteita, kuten ehtolauseita ja silmukoita.

Tämän luvun tavoitteena on oppia konekielisen ohjelmoinnin perustekniikat, joiden avulla pystyy periaatteessa toteuttamaan minkä tahansa ohjelman konekielellä. Seuraavassa luvussa perehdymme vielä tarkemmin siihen, miten aliohjelmat toteutetaan konekielessä.

Tiedon käsittely

Konekielisessä ohjelmoinnissa tietoa voi säilyttää prosessorin rekistereissä sekä tietokoneen muistissa. Rekistereissä olevaa tietoa on huomattavasti nopeampaa käsitellä kuin muistissa olevaa tietoa, mutta rekistereissä voi säilyttää kerrallaan vain vähän tietoa.

Tässä luvussa käsittelemme muistia staattisen muistinvarauksen kautta, jolloin koodissa määritellään tarvittavat muistialueet, jotka varataan kiinteästi ohjelman suorituksen ajaksi. Seuraavassa luvussa tutustumme pinomuistin käyttämiseen konekielen näkökulmasta.

Rekisterit

Tavalliset tiedon käsittelemiseen käytettävät 64-bittiset rekisterit ovat:

Rekisterien nimissä näkyy x86-konekielen historia, mutta nykypäivän näkökulmasta rekistereitä voi käyttää useimmissa tilanteissa vapaasti tiedon käsittelemiseen. Usein tapana on käyttää ensisijaisesti perusrekistereitä ja ottaa tarvittaessa käyttöön lisärekistereitä, jos tarvitaan enemmän rekistereitä.

Perusrekisterit jakautuvat osiin seuraavasti:

64-bittinen rekisteri 32-bittinen alaosa 16-bittinen alaosa 8-bittinen yläosa 8-bittinen alaosa
rax eax ax ah al
rbx ebx bx bh bl
rcx ecx cx ch cl
rdx edx dx dh dl
rsi esi si – sil
rdi edi di – dil

Rekistereissä rax, rbx, rcx ja rdx 16-bittinen alaosa jakautuu 8-bittiseen yläosaan ja alaosaan. Rekistereissä rsi ja rdi ei ole tapaa viitata 8-bittiseen yläosaan.

Lisärekisterit jakautuvat osiin seuraavasti:

64-bittinen rekisteri 32-bittinen alaosa 16-bittinen alaosa 8-bittinen alaosa
r8 r8d r8w r8b
r9 r9d r9w r9b
r10 r10d r10w r10b
r11 r11d r11w r11b
r12 r12d r12w r12b
r13 r13d r13w r13b
r14 r14d r14w r14b
r15 r15d r15w r15b

Muistinvaraus

Tavalliset konekielisen ohjelmoinnin tietoyksiköt ja muistinvaraukseen liittyvät avainsanat ovat:

Tietoyksikkö Koko Muistinvaraus
byte 1 tavu (8 bittiä) db / resb
word 2 tavua (16 bittiä) dw / resw
dword 4 tavua (32 bittiä) dd / resd
qword 8 tavua (64 bittiä) dq / resq

Koodin osio .data varaa staattisesti muistia tiedolle, jolla on tietty alkusisältö. Esimerkiksi seuraava koodi varaa kolme muistialuetta:

section .data
    name db "Uolevi"
    size dq 100
    table dd 3, 1, 4, 1, 5, 9, 2, 6

Muistialueet sisältävät seuraavaa tietoa:

Koodissa muistialueisiin voidaan viitata niiden nimien kautta. Esimerkiksi name vastaa muistiosoitetta, josta alkaa merkkijonon "Uolevi" tilanvaraus.

C-koodissa vastaavat muistialueet voisi varata suunnilleen seuraavasti:

char name[] = "Uolevi";
long size = 100;
int table[] = {3, 1, 4, 1, 5, 9, 2, 6};

Erona on kuitenkin, että konekielessä name, size ja table ovat vain osoitteita muistialueiden alkuun eikä niihin liity tietoa tyypeistä tai taulukoiden koosta. Tietoyksiköiden koot (kuten db ja dd) määrittävät varattavan muistialueen koon, mutta ne eivät vaikuta muuten muistin käyttämiseen.

Huomaa, että C:n merkkijonon "Uolevi" lopussa on nollatavu, jota ei ole vastaavan merkkijonon lopussa konekielen tilanvarauksessa.

Koodin osio .bss varaa vastaavalla tavalla muistia tiedolle, jonka alkusisältönä on nollaa. Esimerkiksi seuraavasti:

section .bss
    buffer resb 128

Tässä buffer on nollaa sisältävä muistialue, jonka koko on 128 tavua.

Osioiden .data ja .bss erona on, että osion .data alkusisältö tallennetaan ohjelman binääritiedostoon, kun taas osion .bss alkusisältöä ei tallenneta. Tämän takia osion .bss käyttäminen pienentää binääritiedoston kokoa. Esimerkiksi jos 128 tavun muistialue buffer määriteltäisiin osiossa .data, binääritiedostossa olisi 128 tavua nollia tämän muistialueen alkusisältönä.

Tiedon kopiointi

Komento mov kopioi tietoa rekisterien ja muistin välillä. Komentoa voidaan käyttää useilla eri tavoilla:

  1. Kopiointi kahden yhtä suuren rekisterin välillä: Esimerkiksi mov rax, rbx kopioi rekisterin rbx sisällön rekisteriin rax (64-bittiset rekisterit) ja mov ecx, eax kopioi rekisterin eax sisällön rekisteriin ecx (32-bittiset rekisterit).

  2. Vakioarvon sijoitus rekisteriin: Esimerkiksi mov rax, 42 sijoittaa rekisteriin rax luvun 42. Koska kohteena oleva rekisteri on 64-bittinen, luku 42 sijoitetaan siihen 64-bittisenä lukuna.

  3. Kopiointi rekisterin ja muistin välillä: Esimerkiksi mov [num], rax kopioi tietoa rekisteristä rax muistiin osoitteeseen num ja komento mov rax, [num] kopioi tietoa muistista osoitteesta num rekisteriin rax. Kopioitavan tiedon määrä on 64 bittiä rekisterin rax koon perusteella.

  4. Vakioarvon sijoitus muistiin: Esimerkiksi mov qword [num], 42 sijoittaa 64-bittisen luvun 42 muistiin osoitteeseen num. Tässä tulee ilmoittaa erikseen muistiin sijoitettavan luvun koko (qword = 64-bittinen luku), koska komennossa ei ole rekisteriä, josta voisi päätellä kopioitavan tiedon määrän.

Huomaa, että tietoyksikön koko voidaan antaa selkeyden vuoksi myös silloin, kun sen voisi päätellä muutenkin. Esimerkiksi komento mov qword [num], rax tarkoittaa samaa kuin lyhyempi komento mov [num], rax.

Rajoituksena mov-komennossa on, että tietoa ei voida kopioida kahden muistiosoitteen välillä, vaan korkeintaan yksi operandi voi olla muistiosoite. Esimerkiksi komento mov qword [num1], qword [num2] ei ole mahdollinen, vaan kopiointi tulee tehdä rekisterin kautta esimerkiksi seuraavasti:

    mov rax, [num1]
    mov [num2], rax

Useissa x86-konekielen komennoissa on rajoituksia liittyen siihen, miten komennoilla voi käsitellä muistia tai mitä rekistereitä komentojen yhteydessä voi käyttää. Esimerkiksi ei ole sallittua, että komennon molemmat operandit ovat muistiosoitteita, vaan tulee käyttää apuna rekistereitä.

Tällaiset rajoitukset konekielissä johtuvat siitä, että mitä monipuolisempia komentoja konekielessä on, sitä monimutkaisemmaksi tulee prosessorin toteutus sekä komentojen tavutason esitystapa. Konekielen suunnittelussa tavoitteena on toisaalta tarjota ohjelmoinnissa hyödyllisiä komentoja mutta toisaalta pitää konekieli riittävän yksinkertaisena.

Komentoa mov ei voi käyttää tiedon kopiointin kahden eri kokoisen rekisterin välillä. Esimerkiksi komento mov eax, dx ei ole mahdollinen, koska rekisteri eax on 32-bittinen ja rekisteri dx on 16-bittinen. Kuitenkin tässä voidaan käyttää komentoja movzx ja movsx, jotka kopioivat pienemmän rekisterin sisällön suurempaan.

Komento movzx on tarkoitettu etumerkittömän luvun kopiointiin, ja se nollaa kohderekisterin ylemmän puoliskon. Komento movsx on puolestaan tarkoitettu etumerkillisen luvun kopiointiin, ja se asettaa kohderekisterin ylemmän puoliskon luvun etumerkin mukaisesti.

Esimerkiksi kun rekisterin dx sisältönä on 10011001 10101111, komennot toimivat seuraavasti:

Komentoja movzx ja movsx voi käyttää, kun pienempi rekisteri on 8- tai 16-bittinen ja suurempi rekisteri on 16-, 32- tai 64-bittinen. Siis kaikki muut yhdistelmät ovat sallittuja paitsi kopiointi 32-bittisestä rekisteristä 64-bittiseen rekisteriin.

Komento movsxd kopioi etumerkillisen luvun 32-bittisestä rekisteristä 64-bittiseen rekisteriin. Tälle komennolle ei ole etumerkitöntä vastinetta, mutta kopiointi 32-bittiseen rekisteriin tavallisella mov-komennolla nollaa vastaavan 64-bittisen rekisterin ylemmän puoliskon.

Laskuoperaatiot

Konekielen tavalliset laskukomennot ovat:

Komento Kuvaus Esimerkki Selitys
inc Kasvatus yhdellä inc rax Kasvattaa rekisterin rax arvoa yhdellä.
dec Vähennys yhdellä dec rax Vähentää rekisterin rax arvoa yhdellä.
add Yhteenlasku add rax, rbx Lisää rekisterin rax arvoon rekisterin rbx arvon.
sub Vähennyslasku sub rax, rbx Vähentää rekisterin rax arvosta rekisterin rbx arvon.
mul / imul Kertolasku mul rbx / imul rbx Kertoo rekisterin rax arvon rekisterin rbx arvolla ja sijoittaa tuloksen rekistereihin rdx:rax.
div / idiv Jakolasku div rbx / idiv rbx Jakaa rekisterien rdx:rax arvon rekisterin rbx arvolla ja sijoittaa tuloksen rekisteriin rax ja jakojäännöksen rekisteriin rdx.

Konekielessä ei voi käyttää laskulausekkeita kuten korkeamman tason ohjelmoinnissa, vaan laskut tulee esittää sarjana yksittäisiä operaatioita. Esimerkiksi laskutoimitus (42 + 1337) * 5 voidaan toteuttaa seuraavasti:

    mov rax, 42
    mov rbx, 1337
    add rax, rbx
    mov rbx, 5
    mul rbx

Tämä koodi muodostuu seuraavista vaiheista:

Kahden n-bittisen luvun kertolaskun tulos vie tilaa 2n bittiä, minkä takia tulos esitetään kahdessa rekisterissä. Esimerkiksi tässä kerrotaan 64-bittiset luvut, jolloin tuloksena oleva 128-bittinen luku esitetään kahden 64-bittisen rekisterin yhdistelmänä. Merkintä rdx:rax tarkoittaa, että rekisteri rdx sisältää tuloksen ylemmän puoliskon ja rekisteri rax sisältää tuloksen alemman puoliskon.

Tässä kerrottavat luvut ovat pieniä ja kertolaskun tulos mahtuu kokonaan alempaan 64-bittiseen rekisteriin. Rekisterin rax sisällöksi tulee kertolaskun tulos 6895 ja rekisterin rdx sisällöksi tulee 0. Komento mul varautuu siis tallentamaan tuloksen kahteen rekisteriin, mutta tulos voi mahtua myös kokonaan alempaan rekisteriin, jolloin ylempi rekisteri nollautuu.

Komennoissa mul ja div käytettävät kiinteät rekisterit määräytyvät seuraavasti komennon operandin koon mukaan:

Komennoissa mul ja div näkyy x86-konekielen historia: komennot käyttävät kiinteitä rekisterejä, koska prosessorit oli aikoinaan helpompaa toteuttaa tällaisilla rajoituksilla laskutoimituksissa.

Kerto- ja jakolasku ovat bittitasolla melko monimutkaisia, eikä niitä ole monissa vanhemmissa konekielissä. Esimerkiksi 1980-luvun kotitietokone Commodore 64 sekä ensimmäinen Nintendo-pelikonsoli (NES) perustuvat 6502-konekieleen, jossa ei ole kerto- ja jakolaskua.

Jos konekielessä ei ole kerto- ja jakolaskua, ne pystyy kuitenkin toteuttamaan tarvittaessa yhteen- ja vähennyslaskun sekä bittioperaatioiden avulla. Useissa tapauksissa kerto- ja jakolaskut on melko helppoa korvata muilla komennoilla, kuten tulemme huomaamaan myös tällä kurssilla.

Laskuissa tulee tietyissä tilanteissa ottaa huomioon lukujen etumerkillisyys. Yhteen- ja vähennyslaskussa tämä ei vaikuta asiaan, koska nämä operaatiot suoritetaan bittitasolla samalla tavalla riippumatta etumerkillisyydestä. Niinpä komentoja add ja sub voi käyttää sekä etumerkittömien että etumerkillisten lukujen kanssa.

Sen sijaan kerto- ja jakolaskussa tuloksen bittiesitykseen vaikuttaa etumerkillisyys, minkä takia saatavilla on erilliset komennot eri tilanteita varten. Komennot mul ja div käsittelevät etumerkittömiä lukuja, kun taas komennot imul ja idiv käsittelevät etumerkillisiä lukuja.

Konekielen tasolla muistin sisältöön ei liity tietoa, minkä tyyppistä tietoa muistissa on tietyssä kohdassa. Tämän takia esimerkiksi komento mul ei voi ”tunnistaa” luvun tyyppiä ja suorittaa kertolaskua eri tavalla etumerkittömälle ja etumerkilliselle luvulle, vaan tarvitaan erilliset komennot mul ja imul.

Komentoa imul voidaan itse asiassa käyttää usealla eri tavalla:

Huomaa, että useamman operandin syntaksi on mahdollinen vain komennossa imul, eikä sitä voi käyttää komennoissa mul, div tai idiv.

Komennon imul useamman operandin syntaksissa kertolaskun tulosta ei esitetä kahden rekisterin yhdistelmänä vaan vain yhdessä rekisterissä. Jos tulos ei mahdu kokonaan yhteen rekisteriin, tuloksesta jää talteen vain alempi puolisko ja tuloksen ylempi puolisko katoaa.

Useamman operandin imul vastaa sitä, miten kertolasku on yleensä toteutettu korkeamman tason ohjelmointikielissä. Esimerkiksi seuraavat koodit toimivat samalla tavalla:

int a, b;

a = 123;
b = 456;
a = a * b; // 56088

a = 123456789;
b = 987654321;
a = a * b; // -67153019
mov eax, 123
mov ebx, 456
imul eax, ebx ; 56088

mov eax, 123456789
mov ebx, 987654321
imul eax, ebx ; -67153019

Molemmissa koodeissa sekä kerrottavat luvut että kertolaskun tulos ovat 32-bittisiä. Ensimmäisen kertolaskun tulos 56088 mahtuu 32-bittiseen muuttujaan ja rekisteriin, kun taas toisen kertolaskun tulos näkyy muodossa –67153019, koska 32 bittiä ei riitä sen esittämiseen.

Vaikka komento imul on varsinaisesti tarkoitettu etumerkillisten lukujen kertolaskuun, komennon useamman operandin syntaksia voi käyttää myös etumerkittömien lukujen kertolaskussa. Syynä tähän on, että lukujen etumerkillisyys ei bittitasolla vaikuta kertolaskun tuloksen jälkipuoliskoon. Tämän takia ei ole merkittävä puute, ettei komennolla mul ole vastaavaa useamman operandin syntaksia.

Jakolaskussa tulee ottaa huomioon, että jaettava luku esitetään aina kahden rekisterin yhdistelmänä. Tämän takia ylempi rekisteri tulee alustaa oikealla tavalla silloinkin, kun jaettava luku mahtuu kokonaan alempaan rekisteriin. Esimerkiksi kun rax sisältää jaettavan luvun, myös rdx tulee alustaa ennen jakolaskua.

Etumerkittömässä jakolaskussa riittää nollata ylempi rekisteri. Esimerkiksi seuraava koodi jakaa rekisterin rax arvon rekisterin rbx arvolla:

mov rdx, 0
div rbx

Etumerkillisessä jakolaskussa etumerkki vaikuttaa ylemmän rekisterin alustukseen. Tässä tapauksessa voidaan käyttää komentoa cqo, joka laajentaa rekisterissä rax olevan etumerkillisen luvun oikealla tavalla rekistereihin rdx:rax:

cqo
idiv rbx

Komento cqo on osa laajempaa komentoperhettä, jotka laajentavat etumerkillisen luvun suurempaan rekisteriin:

  • Komento cbw tekee laajennuksen al → ax
  • Komento cwd tekee laajennuksen ax → dx:ax
  • Komento cwde tekee laajennuksen ax → eax
  • Komento cdq tekee laajennuksen eax → edx:eax
  • Komento cdqe tekee laajennuksen eax → rax
  • Komento cqo tekee laajennuksen rax → rdx:rax

Huomaa, että komennot käsittelevät aina kiinteitä rekistereitä eikä niissä esiintyviä rekistereitä ole mahdollista muuttaa.

Bittioperaatiot

Komennot and, or, xor ja not suorittavat vastaavan bittioperaation. Esimerkiksi komento and rax, rbx sijoittaa rekisteriin rax and-operaation tuloksen rekisterien rax ja rbx arvoille.

Klassinen trikki konekielisessä ohjelmoinnissa on nollata rekisterin arvo xor-operaatiolla. Esimerkiksi komento xor rax, rax nollaa rekisterin rax arvon, koska kahden saman luvun xor-operaation tuloksena on aina nolla.

Trikin hyötynä on, että xor-komento voi olla tehokkaampi ja viedä vähemmän tilaa tavutasolla kuin vastaava mov-komento.

Esimerkiksi komennon mov eax, 0 (vastaa mov rax, 0) tavuesitys on b8 00 00 00 00, kun taas komennon xor rax, rax tavuesitys on 48 31 c0, eli tässä tapauksessa xor-komento säästää tilaa kaksi tavua.

Etumerkillinen luku voidaan muuttaa käänteiseksi komennoilla not ja inc, mutta tätä varten on myös erillinen komento neg, joka on käytännössä parempi tapa tehdä muutos. Seuraavat koodit toimivat samalla tavalla:

    not rax
    inc rax
    neg rax

Komennot shl ja shr suorittavat bittisiirron vasemmalle ja oikealle. Näitä komentoja voidaan hyödyntää toteuttamaan helpommin tietynlaiset kerto- ja jakolaskut. Esimerkiksi komento shl rax, 1 kertoo rekisterin rax arvon kahdella ja komento shr rbx, 2 jakaa rekisterin rbx arvon neljällä.

Tarkemmin ottaen komento shr soveltuu jakolaskuun vain luvun ollessa etumerkitön, koska se lisää luvun alkuun nollabittejä. Saatavilla on kuitenkin toinen komento sar (aritmeettinen bittisiirto), joka lisää luvun alkuun etumerkin mukaista bittiä ja toimii oikein luvun ollessa etumerkillinen.

Kertolaskun tapauksessa luvun etumerkillisyydellä ei ole vaikutusta ja komento shl soveltuu sekä etumerkittömän että etumerkillisen luvun kertolaskuun. Tästäkin komennosta on olemassa aritmeettinen versio sal, mutta komennot shl ja sal toimivat aina samalla tavalla.

Komennot rol ja ror pyörittävät luvun bittejä vasemmalle ja oikealle. Komento rol (vasen pyöritys) siirtää bittejä luvun alusta luvun lopuun, ja vastaavasti komento ror (oikea pyöritys) siirää bittejä luvun lopusta luvun alkuun. Esimerkiksi komento rol rax, 1 pyörittää rekisterin rax bittejä askeleen vasemmalle.

Huomaa, että C-kielen standardissa ei ole operaatioita bittipyörityksiin, vaikka useissa konekielissä on komennot niitä varten.

Bittisiirroissa ja bittipyörityksissä on erityistä, että jos toinen operandi (askelten määrä) annetaan rekisterissä, tässä tilanteessa ainoa sallittu rekisteri on 8-bittinen rekisteri cl. Esimerkiksi komennot shl rax, cl ja ror rax, cl tekevät bittisiirron ja bittipyörityksen rekisterin cl askelten määrän perusteella.

Muistin osoittaminen

Katsotaan vielä tarkemmin erilaisia tapoja osoittaa muistia. Aloitetaan seuraavasta koodista:

section .data
    number dq 42

section .text
    mov rax, number
    mov rbx, [number]

Tämä koodi sijoittaa rekisteriin rax muistiosoitteen number ja rekisteriin rbx muistiosoitteessa number olevan 64-bittisen luvun. C-kielen näkökulmasta voitaisiin ajatella, että number on osoitin muistissa olevaan muuttujaan ja [number] puolestaan tarkoittaa samaa kuin C-kielessä syntaksi *number.

Tarkastellaan sitten seuraavaa koodia:

section .data
    table dd 3, 1, 4, 1, 5, 9, 2, 6

section .text
    mov rbx, table
    mov rcx, 2
    mov eax, [rbx + 4 * rcx]

Tämä koodi sijoittaa rekisteriin rbx taulukon table muistiosoitteen ja rekisteriin rcx haettavan alkion indeksin. Sitten koodi hakee rekisteriin eax taulukon table alkion osoitteesta rbx + 4 * rcx. Tässä taulukon alkiot ovat 32-bittisiä (tietoyksikkö dd) eli muistiosoituksen kerroin 4 vastaa alkion kokoa tavuina.

Yleisemmin x86-konekielen komennoissa 64-bittinen muistiosoite voidaan ilmaista kaavalla base + (scale * index) + offset, jonka osat ovat:

Esimerkiksi komennossa mov eax, [rbx + 4 * rcx] taulukon alkuosoite on rbx, alkion koko on 4 tavua, alkion indeksi on rcx ja siirto puuttuu (eli se on 0).

Myös komento mov rbx, [number] vastaa yllä olevaa kaavaa, kun muistiosoite number esitetään siirtona ja kaikki muut osat puuttuvat.

Komento lea (load effective address) sijoittaa rekisteriin tietyn kaavan tuottaman muistiosoitteen. Esimerkiksi koodi

    lea rax, [rbx + 4 * rcx]

sijoittaa rekisteriin rax muistiosoitteen, joka saadaan kaavalla rbx + 4 * rcx. Toisin kuin komento mov, komento lea ei käsittele muistissa olevaa tietoa vaan vain laskee, missä osoitteessa tieto sijaitsee.

Vaikka komento lea on tarkoitettu muistiosoitteen laskemiseen, mikään ei estä käyttämästä sitä myös muuhun laskemiseen. Esimerkiksi äskeinen koodi laskee rekisteriin rax lausekkeen rbx + 4 * rcx arvon, vaikka tämä ei olisi mikään todellinen ohjelman käytettävissä oleva muistiosoite. Tämän ansiosta seuraavat koodit tuottavat saman tuloksen:

    lea rax, [rbx + 4 * rcx]
    mov rax, rcx
    shl rax, 2
    add rax, rbx

Komentoa lea pystyy siis käyttämään joissakin tilanteissa lyhentämään ja tehostamaan koodia. Tässä on oleellista, että lea vain laskee muistiosoitteen eikä yritä käsitellä muistia kyseisessä osoitteessa.

Seuraavat komennot toimivat suunnilleen samalla tavalla:

  • mov rax, number
  • lea rax, [number]

Kumpikin komento sijoittaa rekisteriin rax muistiosoitteen number. Kuitenkin erona on, että mov-komento sallii 64-bittisen muistiosoitteen, kun taas lea-komennossa muistiosoite (eli vakiona annettu ”siirto” kaavassa, joka laskee muistiosoitteen) voi olla vain 32-bittinen.

Komento mov rax, [number] toimii myös muistiosoitteen ollessa 64-bittinen, mutta tämä on rekisteriin rax liittyvä poikkeus eikä komento toimi muilla rekistereillä (kuten rbx tai rsi). Tämä on muistutus siitä, että x86-konekielessä on melkein loputtomasti poikkeuksia ja yllätyksiä.

Hyppykomento

Konekielisessä ohjelmoinnissa haarautuminen ja silmukat toteutetaan hyppykomennon avulla, joka siirtää koodin suoritukseen toiseen kohtaan koodia. Konekielessä on useita erilaisia hyppykomentoja, joissa on erilaisia ehtoja sille, missä tilanteessa hyppy tapahtuu.

Yksinkertaisin hyppykomento on jmp, joka suorittaa hypyn ilman ehtoa. Komentoa voidaan käyttää esimerkiksi seuraavasti:

    mov rax, 0
    jmp skip
    inc rax
skip:
    mov rbx, rax

Yllä olevassa koodissa komento jmp skip hyppää nimiöön skip, minkä takia komentoa inc rax ei suoriteta.

Konekielen komento jmp vastaa siis C-kielen komentoa goto. C-kielessä komento goto on melko harvinainen, koska ohjausrakenteet voidaan yleensä toteuttaa paremmin muilla tavoilla. Konekielessä kuitenkin hyppykomentoja käytetään jatkuvasti, koska muita tapoja ei ole.

Ehdollinen hyppy

Ehdollinen hyppy voidaan toteuttaa kahden komennon yhdistelmänä. Ensin käytetään komentoa cmp, joka vertailee kahta arvoa toisiinsa. Tämän jälkeen käytetään ehdollista hyppykomentoa, joka päättää vertailun tuloksen perusteella, tapahtuuko hyppy vai jatkuuko koodin suoritus eteenpäin.

Ehdollisen hypyn voi toteuttaa esimerkiksi näin:

    cmp rax, 0
    je is_zero

Tässä komento cmp rax, 0 vertaa toisiinsa rekisterin rax arvoa ja lukua 0. Hyppykomento je (jump if equal) suorittaa hypyn nimiöön is_zero, jos vertailun tuloksena on, että arvot ovat yhtä suuret.

Hyppykomentojen avulla voidaan suorittaa vastaavat vertailut kuin korkeamman tason ohjelmointikielissä. Kun on suoritettu ensin komento muotoa cmp X, Y, voidaan käyttää seuraavia hyppykomentoja:

Komento Hypyn ehto
je X == Y
jne X != Y
jg / ja X > Y
jge / jae X >= Y
jl / jb X < Y
jle / jbe X <= Y

Kun taulukon rivillä on kaksi komentoa, ensimmäistä komentoa käytetään etumerkillisten lukujen kanssa ja toista komentoa etumerkittömien lukujen kanssa. Esimerkiksi komentoa jg käytetään etumerkillisten lukujen kanssa ja komentoa ja käytetään etumerkittömien lukujen kanssa.

Kun verrataan lukujen järjestystä, vertailu tulee tehdä eri tavalla etumerkillisille ja etumerkittömille luvuille, minkä takia konekielessä käytetään eri komentoja riippuen lukujen bittiesitysten tulkinnasta.

Esimerkiksi jos verrataan tavuja X = 00000000 ja Y = 10101010, lukujen etumerkillisyys vaikuttaa näin:

  • Jos luvut ovat etumerkillisiä, X = 0 ja Y= –86, joten X > Y
  • Jos luvut ovat etumerkittömiä, X = 0 ja Y= 170, joten X < Y

Yllä olevat komennot voidaan antaa myös käänteisessä muodossa: esimerkiksi komento jg voidaan antaa myös muodossa jnle ja komento jge voidaan antaa myös muodossa jnl. Nämä ovat assemblerin tarjoamia vaihtoehtoisia nimiä, jotka viittaavat tavutasolla samaan konekieliseen komentoon.

Silmukka

Silmukka voidaan toteuttaa yhdistämällä ehdollinen hyppykomento ja rekisteri, jonka arvoa muutetaan silmukassa. Esimerkiksi seuraavasti:

    mov rax, 0
    mov rcx, 1
loop_start:
    add rax, rcx
    inc rcx
    cmp rcx, 100
    jle loop_start

Tämä silmukka laskee summan 1 + 2 + … + 100 rekisteriin rax käyttäen apuna rekisteriä rcx. Silmukka toimii seuraavasti:

Käytännössä silmukan voi toteuttaa monella vähän erilaisella tavalla riippuen siitä, missä vaiheessa koodi muuttaa ja vertailee rekisteriä sekä millä komennolla tai komennoilla hypyt on toteutettu.

Seuraavassa on toisenlaista silmukkaa käyttävä esimerkkiohjelma, joka tulostaa komentoriville merkit a–z merkki kerrallaan.

letters.asm

global _start

section .data
    letter db 'a'

section .text
_start:
    mov rax, 1               ; write
    mov rdi, 1               ; stdout
    mov rdx, 1               ; length
    mov rsi, letter

loop_start:
    syscall                  ; print letter
    cmp byte [letter], 'z'   ; last letter reached?
    je finish
    inc byte [letter]        ; move to next letter
    jmp loop_start

finish:
    mov byte [letter], 10
    syscall                  ; print newline

    mov rax, 60              ; exit
    mov rdi, 0
    syscall

Ohjelman tulostus on seuraava:

$ ./letters
abcdefghijklmnopqrstuvwxyz

Ohjelma muodostuu seuraavista osista:

Rekisterien valmistelu:

    mov rax, 1               ; write
    mov rdi, 1               ; stdout
    mov rdx, 1               ; length
    mov rsi, letter

Tämä koodi valmistelee rekisterit tekstiä tulostavaa systeemikutsua varten. Rekisteri rsi sisältää tekstin osoitteen muistissa ja rekisteri rdx kertoo tulostettavien merkkien määrän. Näitä rekisterien arvoja käytetään ohjelmassa useissa systeemikutsuissa, jotka tulostavat yhden merkin kerrallaan.

Tulostussilmukka:

loop_start:
    syscall                  ; print letter
    cmp byte [letter], 'z'   ; last letter reached?
    je finish
    inc byte [letter]        ; move to next letter
    jmp loop_start

Tämä koodi toteuttaa silmukan seuraavasti:

Rivinvaihto ja ohjelman lopetus:

finish:
    mov byte [letter], 10
    syscall                  ; print newline

    mov rax, 60              ; exit
    mov rdi, 0
    syscall

Tämä koodi tulostaa rivinvaihdon systeemikutsulla ja suorittaa sitten toisen systeemikutsun, joka päättää ohjelman.

Hyppykomennon toiminta

Miten hyppykomento tietää, minkä tuloksen komennon cmp tekemä vertailu on tuottanut? Tämä tieto välittyy prosessorin lippurekisterissä (rflags), jossa olevat bitit antavat tietoa ohjelman komentojen tuloksista.

Kun koodissa on komento cmp X, Y, prosessori suorittaa laskun X - Y ja päivittää lippurekisteriä tämän laskun tuloksen perusteella. Sitten hyppykomento päättelee lippurekisterin sisällöstä, tuleeko ehdollista hyppyä tehdä.

Esimerkiksi hyppykomento je (jump if equal) suorittaa hypyn siinä tapauksessa, että lippurekisterin nollalippu (zero flag) on asetettu. Nollalippu on asetettu, jos viimeksi tehdyn laskun tuloksena oli 0. Komennon cmp jälkeen tämä tarkoittaa sitä, että vertailtavat arvot X ja Y ovat yhtä suuria.

Seuraavassa koodissa komennon cmp tulos ei välity komennolle je, koska komentojen välissä on toinen komento inc:

    cmp rax, 0
    inc rax
    je is_zero

Komento inc rax päivittää lippurekisteriä, minkä takia komennon cmp rax, 0 tuottama tulos ei ole enää saatavilla. Yllä oleva koodi suorittaa hypyn, jos komennon inc rax jälkeen rekisterin rax arvona on 0.

Koska komennon cmp lisäksi muutkin komennot päivittävät lippurekisteriä, silmukan voi joissain tilanteissa toteuttaa lyhyemmin ilman komentoa cmp. Esimerkiksi seuraava silmukka käy läpi luvut 10:stä 1:een:

    mov rcx, 10
loop_start:
    dec rcx
    jnz loop_start

Tässä komento dec rcx vähentää rekisterin arvoa ja samalla päivittää lippurekisteriä. Komento jnz suorittaa hypyn, jos nollalippua ei ole asetettu eli komennon dec rcx tuloksena rcx ei ole 0. Toisin sanoen tässä silmukassa hyppy tapahtuu, jos rekisterin rcx sisältönä on suurempi luku kuin 0.

Komento jnz (jump if not zero) on tavutasolla sama kuin komento jne (jump if not equal). Nämä komennot toimivat samalla tavalla, koska ne suorittavat hypyn, jos nollalippua ei ole asetettu.

Yllä olevassa koodissa on luontevampaa käyttää komentoa jnz, koska ajatuksena on tehdä hyppy, kun edellisen komennon tuloksena ei ole 0. Komento jne olisi tässä hankalammin ajateltava, koska koodi ei vertaa yhtäsuuruutta.

Yllä olevaa silmukkaa voi tiivistää entisestään komennolla loop:

    mov rcx, 10
loop_start:
    loop loop_start

Komento loop vastaa komentojen dec ja jnz yhdistelmää ja olettaa, että silmukassa on käytössä rekisteri rcx. Komentoa loop ei kuitenkaan käytetä yleensä, koska se on erillisiä komentoja hitaampi. Komento on myös rajoittunut alaspäin menevään silmukkaan, jossa on käytössä rekisteri rcx.

Nollalipun lisäksi usein käytettyjä lippurekisterin lippuja ovat:

  • Merkkilippu (sign flag) ilmaisee, onko laskun tulos negatiivinen.
  • Muistilippu (carry flag) ilmaisee ylivuodon laskun tuloksessa, kun käsitellään etumerkittömiä lukuja.
  • Ylivuotolippu (overflow flag) ilmaisee ylivuodon laskun tuloksessa, kun käsitellään etumerkillisiä lukuja.

Esimerkiksi seuraavassa koodissa komento jc (jump if carry) suorittaa hypyn, jos laskussa tapahtuu etumerkitön ylivuoto:

    add rax, rcx
    jc overflow

Itse asiassa komento jc on toinen nimi komennolle jb, joka suorittaa hypyn tapauksessa X < Y etumerkittömillä luvuilla. Tässä muistilipun arvo kertoo, tuleeko hyppy tehdä, koska komennon cmp X, Y suorittamassa laskussa X - Y tapahtuu ylivuoto tarkalleen ehdon X < Y pätiessä.

Systeemikutsut

Linuxissa ja muissa moderneissa käyttöjärjestelmissä ohjelmat eivät saa käsitellä tietokoneen laitteistoa vapaasti vaan ne toimivat käyttöjärjestelmän alaisuudessa. Esimerkiksi kun ohjelma haluaa lukea tietoa näppäimistöltä tai tulostaa tietoa näytölle, sen täytyy pyytää tähän lupa käyttöjärjestelmältä.

Linuxissa 64-bittinen ohjelma viestii käyttöjärjestelmän kanssa systeemikutsujen kautta. Kun ohjelma haluaa tehdä jotain käyttöjärjestelmän valvomaa, se suorittaa systeemikutsun komennolla syscall. Systeemikutsun numero annetaan rekisterissä rax ja parametrit annetaan rekistereissä järjestyksessä rdi, rsi, rdx, r10, r8, r9. Systeemikutsun jälkeen kutsun paluuarvo tulee rekisteriin rax.

Kaikki systeemikutsut on listattu Linuxin lähdekoodissa ja verkossa on useita sivustoja, jotka kertovat tarkemmin systeemikutsujen käyttämisestä.

Esimerkkejä systeemikutsuista

Systeemikutsu 0 (read):

Systeemikutsu 0 lukee tietoa annetusta tiedostosta. Kutsun parametrit ovat tiedostotunnus (rdi), osoitin muistialueeseen (rsi) sekä luettavan tiedon enimmäismäärä tavuina (rdx). Kutsun paluuarvo on luettujen tavujen määrä tai negatiivinen arvo virhetilanteessa.

Esimerkiksi seuraava koodi lukee käyttäjältä enintään 16 tavua tietoa puskuriin buffer. Tiedostotunnus 0 tarkoittaa standardisyötettä (stdin).

    mov rax, 0               ; read
    mov rdi, 0               ; stdin
    mov rsi, buffer
    mov rdx, 16
    syscall

Systeemikutsu 1 (write):

Systeemikutsu 1 kirjoittaa tietoa annettuun tiedostoon. Kutsun parametrit ovat tiedostotunnus (rdi), osoitin muistialueeseen (rsi) sekä kirjoitettavan tiedon määrä tavuina (rdx). Kutsun paluuarvo on kirjoitettujen tavujen määrä tai negatiivinen arvo virhetilanteessa.

Esimerkiksi seuraava koodi tulostaa käyttäjälle 16 tavua tietoa puskurista buffer. Tiedostotunnus 1 viittaa standarditulostukseen (stdout).

    mov rax, 1               ; write
    mov rdi, 1               ; stdout
    mov rsi, buffer
    mov rdx, 16
    syscall

Systeemikutsu 60 (exit):

Systeemikutsu 60 sulkee ohjelman. Kutsun ainoa parametri on ohjelman paluuarvo ( rdi). Esimerkiksi seuraava koodi sulkee ohjelman paluuarvolla 0.

    mov rax, 60              ; exit
    mov rdi, 0
    syscall

Systeemikutsujen seuraaminen

Linuxissa komento strace näyttää ohjelman tekemät systeemikutsut. Esimerkiksi luvun 9 esimerkkiohjelman suoritus tuottaa seuraavan raportin:

$ strace ./test
execve("./test", ["./test"], 0x7ffeac6f79e0 /* 19 vars */) = 0
write(1, "All your base are belong to us\n", 31All your base are belong to us
) = 31
exit(0)                                 = ?
+++ exited with 0 +++

Tässä systeemikutsu execve käynnistää ohjelman ja systeemikutsut write ja exit ovat varsinaiset ohjelman tekemät systeemikutsut.

Systeemikutsu vs. keskeytys

Komento syscall on moderni tapa suorittaa systeemikutsu 64-bittisessä Linuxissa. Perinteinen 32-bittisessä Linuxissa käytettävä tapa on suorittaa sen sijaan komento int 0x80, joka aiheuttaa keskeytyksen (interrupt). Tämä keskeyttää ohjelman ja siirtää suorituksen käyttöjärjestelmälle systeemikutsua varten.

Esimerkiksi seuraava 32-bittinen koodi suorittaa systeemikutsun write:

    mov eax, 4               ; write
    mov ebx, 1               ; stdout
    mov ecx, buffer
    mov edx, 128
    int 0x80

Koodissa on seuraavat erot verrattuna 64-bittiseen koodiin:

Tässä 0x80 on keskeytyksen numero, joka tarkoittaa systeemikutsun suorittamista. Yleisemmin keskeytykset liittyvät tietokoneen matalan tason toimintaan (esimerkiksi muistin ja laitteiston käsittely), eikä käyttöjärjestelmän alaisuudessa toimiva ohjelma saa kutsua tällaisia keskeytyksiä.

Konekielinen ohjelmointi oli ennen varsin erilaista, koska ohjelma pystyi käsittelemään suoraan tietokoneen muistia ja laitteistoa ilman käyttöjärjestelmän valvontaa.

Seuraava MS-DOS-ohjelma antaa näytteen vanhan ajan ohjelmoinnista. Ohjelma piirtää punaisen pikselin näytön keskelle kutsumalla keskeytyksiä ja käsittelemällä suoraan näyttömuistin sisältöä.

org 100h

section .text
start:
    mov ax, 0x0013           ; 320x200 resolution, 256-color palette
    int 0x10                 ; set graphics mode

    mov ax, 0xa000           ; video memory segment
    mov es, ax               ; configure segment register

    mov di, 32160            ; pixel location: screen center
    mov al, 40               ; pixel color: red
    mov [es:di], al          ; draw pixel

    mov ah, 0x00
    int 0x16                 ; wait for keyboard input

    mov ax, 0x0003
    int 0x10                 ; restore text mode

    mov ax, 0x4c00
    int 0x21                 ; quit program

Koodi toimii seuraavasti:

  • Koodi asettaa grafiikkatilan kutsumalla keskeytystä 0x10 (näytön resoluutio 320x200 pikseliä, käytettävissä 256 väriä).
  • Koodi piirtää punaisen pikselin muuttamalla näyttömuistin sisältöä. Segmentissä 0xa000 oleva muistialue (320 · 200 = 64000 tavua) vastaa näytön sisältöä: jokainen tavu ilmaisee tietyn pikselin värin.
  • Koodi odottaa näppäimen painallusta (keskeytys 0x16), asettaa sitten takaisin tekstitilan (keskeytys 0x10) ja sulkee ohjelman (keskeytys 0x21).

Kertoman laskeminen

Tehdään seuraavaksi hieman monimutkaisempi konekielinen ohjelma, joka kysyy käyttäjältä luvun ja ilmoittaa sitten tämän luvun kertoman. Tavoitteena on, että ohjelmaa voi käyttää seuraavasti:

$ ./factorial
5
120
$ ./factorial
20
2432902008176640000

Ohjelman toteuttamisessa konekielellä on yllättävä vaikeus: Linuxin systeemikutsut käsittelevät lukuja merkkimuodossa, mutta laskutoimituksissa lukujen tulee olla binäärimuodossa. Esimerkiksi luku 120 muodostuu merkkimuodossa tavuista 31 32 30 (merkit '1', '2' ja '0') mutta 64-bittisenä binäärilukuna se muodostuu tavuista 78 00 00 00 00 00 00 00.

Itse asiassa kertoman laskemisen voi toteuttaa konekielellä varsin lyhyesti, mutta lukujen esitystavan muunnokset vaativat paljon enemmän koodia. Seuraavassa koodissa on kaksi aliohjelmaa muunnoksia varten:

Koodi kutsuu aliohjelmia komennolla call ja käyttää komentoa ret aliohjelmista takaisin palaamiseen. Tutustumme materiaalin seuraavassa luvussa tarkemmin siihen, miten nämä komennot toimivat.

factorial.asm

global _start

section .bss
    buffer resb 20

section .text
_start:
    call read_number

    mov rcx, rax             ; set loop counter
    mov rax, 1               ; initialize result
loop_start:
    cmp rcx, 1               ; check if result ready
    jle ready
    mul rcx                  ; multiply current result by counter
    dec rcx                  ; decrease counter
    jmp loop_start

ready:
    call print_number

    mov rax, 60              ; exit
    mov rdi, 0
    syscall

read_number:
    mov rax, 0               ; read
    mov rdi, 0               ; stdin
    mov rsi, buffer          ; input buffer
    mov rdx, 19              ; max input length
    syscall
    mov byte [buffer+19], 10 ; add newline at end

    mov rax, 0               ; initialize result
    mov rbx, 10              ; decimal base
    mov rcx, buffer          ; pointer to first digit

read_digit:
    cmp byte [rcx], 10       ; check for end of input
    je read_ready
    mul rbx                  ; multiply result by 10
    mov dl, [rcx]            ; load current digit
    sub dl, '0'              ; convert ASCII to numeric value
    add rax, rdx             ; add digit to result
    inc rcx                  ; move to next digit
    jmp read_digit

read_ready:
    ret

print_number:
    lea rcx, [buffer+19]     ; pointer to end of buffer
    mov byte [rcx], 10       ; add newline at end
    mov rbx, 10              ; decimal base

print_digit:
    mov rdx, 0
    div rbx                  ; extract last digit
    dec rcx                  ; move buffer pointer
    add dl, '0'              ; convert numeric value to ASCII
    mov [rcx], dl            ; store digit in buffer
    cmp rax, 0               ; check if conversion ready
    jne print_digit

    mov rax, 1               ; write
    mov rdi, 1               ; stdout
    mov rsi, rcx             ; buffer position
    lea rdx, [buffer+20]
    sub rdx, rcx             ; number length
    syscall

    ret

Pääohjelma

Seuraava koodi kutsuu aliohjelmia ja laskee kertoman:

_start:
    call read_number

    mov rcx, rax             ; set loop counter
    mov rax, 1               ; initialize result
loop_start:
    cmp rcx, 1               ; check if result ready
    jle ready
    mul rcx                  ; multiply current result by counter
    dec rcx                  ; decrease counter
    jmp loop_start

ready:
    call print_number

Koodi kutsuu ensin aliohjelmaa read_number, joka lukee luvun käyttäjältä. Aliohjelman päätteeksi käyttäjältä luettu luku on rekisterissä rax.

Tämän jälkeen koodi laskee luvun kertoman silmukan avulla. Koodissa on silmukka, jossa rekisteri rcx käy läpi kerrottavat luvut.

Lopuksi koodi kutsuu aliohjelmaa print_number, joka tulostaa luvun käyttäjälle. Aliohjelma olettaa, että tulostettava luku annetaan rekisterissä rax.

Luvun lukeminen

Aliohjelma read_number lukee luvun käyttäjältä:

read_number:
    mov rax, 0               ; read
    mov rdi, 0               ; stdin
    mov rsi, buffer          ; input buffer
    mov rdx, 19              ; max input length
    syscall
    mov byte [buffer+19], 10 ; add newline at end

    mov rax, 0               ; initialize result
    mov rbx, 10              ; decimal base
    mov rcx, buffer          ; pointer to first digit

read_digit:
    cmp byte [rcx], 10       ; check for end of input
    je read_ready
    mul rbx                  ; multiply result by 10
    mov dl, [rcx]            ; load current digit
    sub dl, '0'              ; convert ASCII to numeric value
    add rax, rdx             ; add digit to result
    inc rcx                  ; move to next digit
    jmp read_digit

read_ready:
    ret

Koodi käyttää puskuria buffer, johon luetaan aluksi käyttäjän syöte systeemikutsulla. Tämän jälkeen koodi käy läpi puskurissa olevat numerot vasemmalta oikealle rekisterin rcx avulla ja muodostaa luvun rekisteriin rax. Kun puskurissa tulee vastaan rivinvaihto, tämä päättää luvun lukemisen.

Koodin ideana on kertoa kunkin numeron kohdalla rekisterissä rax oleva luku 10:llä ja lisätä siihen käsiteltävä numero. Esimerkiksi kun käyttäjä antaa luvun 123, logiikka on seuraava:

Koska numerot tulevat käyttäjältä merkkeinä, koodissa on komento sub dl, '0' joka muuttaa merkkikoodin vastaavaksi lukuarvoksi.

Luvun tulostaminen

Aliohjelma print_number tulostaa luvun käyttäjälle:

print_number:
    lea rcx, [buffer+19]     ; pointer to end of buffer
    mov byte [rcx], 10       ; add newline at end
    mov rbx, 10              ; decimal base

print_digit:
    mov rdx, 0
    div rbx                  ; extract last digit
    dec rcx                  ; move buffer pointer
    add dl, '0'              ; convert numeric value to ASCII
    mov [rcx], dl            ; store digit in buffer
    cmp rax, 0               ; check if conversion ready
    jne print_digit

    mov rax, 1               ; write
    mov rdi, 1               ; stdout
    mov rsi, rcx             ; buffer position
    lea rdx, [buffer+20]
    sub rdx, rcx             ; number length
    syscall

    ret

Tämän koodin toiminta on käänteinen verrattuna luvun lukemiseen. Koodi muodostaa puskuriin buffer rekisterissä rax olevaa lukua vastaavan merkkijonon. Koodi käyttää silmukkaa, joka etenee oikealta vasemmalle rekisterin rcx avulla.

Koodi jakaa joka vaiheessa rekisterin rax arvon 10:llä ja siirtää jakojäännöksen tulostettavaan lukuun. Näin jatketaan, kunnes rekisterin rax arvoksi on tullut nolla. Esimerkiksi koodi tulostaa luvun 123 seuraavasti:

Koodi muuttaa luvun jakojäännöksen merkiksi komennolla add dl, '0', joka on myös käänteinen luvun lukemiseen verrattuna.

C-funktioiden kutsuminen

C-kielessä luvun lukeminen ja tulostaminen on helppoa käyttäen standardikirjaston funktioita scanf ja printf. Nämä funktiot hoitavat muunnokset luvun merkkimuodon ja binäärimuodon välillä, eikä ohjelmoijan tarvitse huolehtia asiaa.

Voimme käyttää funktioita scanf ja printf myös konekielisessä ohjelmassa, kunhan ohjelman käännöksessä linkitetään mukaan C:n standardikirjasto. Seuraava koodi toteuttaa kertoman laskemisen näiden funktioiden avulla:

factorial.asm

global main

extern scanf
extern printf
extern exit

section .data
    scanf_format db "%ld", 0
    printf_format db "%ld", 10, 0
    number dq 0

section .text
main:
    push rbp

    lea rdi, [scanf_format]  ; parameter 1: format string
    lea rsi, [number]        ; parameter 2: pointer to store result
    mov rax, 0               ; no vector parameters
    call scanf

    mov rcx, [number]        ; set loop counter
    mov rax, 1               ; initialize result
loop_start:
    cmp rcx, 1               ; check if result ready
    jle ready
    mul rcx                  ; multiply current result by counter
    dec rcx                  ; decrease counter
    jmp loop_start

ready:
    lea rdi, [printf_format] ; parameter 1: format string
    mov rsi, rax             ; parameter 2: number to print
    mov rax, 0               ; no vector parameters
    call printf

    mov rdi, 0
    call exit

    pop rbp
    ret

Ohjelma kutsuu ulkoisia funktioita scanf, printf ja exit, jotka kuuluvat C-kielen standardikirjastoon. Koodi on myös rakennettu C-ohjelman pääfunktion muotoon niin, että koodin suoritus alkaa nimiöstä main.

Seuraavat komennot kääntävät ja linkittävät ohjelman:

$ nasm -f elf64 factorial.asm -o factorial.o
$ gcc -no-pie factorial.o -o factorial

Ensimmäinen komento kääntää ohjelman NASM-kääntäjällä samalla tavalla kuin aiemminkin. Toinen komento linkittää koodin gcc:llä, jolloin siihen tulee mukaan C-kielen standardikirjasto.

Käännöslippu -no-pie tarkoittaa, että koodi on toteutettu perinteisellä tavalla eikä se ole PIE-yhteensopiva (Position Independent Executable). PIE on ohjelman turvallisuutta parantava tekniikka, jossa käyttöjärjestelmä lataa ohjelman muistiin satunnaiseen osoitteeseen kiinteän osoitteen sijasta.

PIE vaatii esimerkiksi, että osoitteet .data-osion muistialueille ilmoitetaan suhteessa ohjelmarekisterin rip arvoon. Tämän voi toteuttaa käytännössä rel-avainsanan avulla seuraavasti:

    lea rsi, [rel number]

Tämä koodi tuottaa lea-komennon, joka ei sisällä kiinteää osoitetta number vaan siirtymän rekisterin rip ja osoitteen number välillä.

Toteutamme tällä kurssilla konekieliset esimerkkiohjelmat perinteisellä tavalla, koska se on helpompaa konekieliohjelmoinnin opettelussa.