TIES448 Kääntäjätekniikka

Luento 2 (31.3.2016)

Esimerkkikoodit

Yousourcessa

Luettavaa

Viralliset dokumentaatiot (laajoja, pitkälti TMI):

Konekielten yleispiirteitä

  • erittäin yksinkertainen rakenne
  • tyypittömyys (”kaikki on sanoja”)
  • rekisterit (ei nimettyjä paikallisia muuttujia)
  • alkeistoimituksiin rajoittuminen
  • abstraktiokeinojen puute

Konekielen rakenne

  • Konekielinen ohjelma on jono käskyjä
  • Kukin käsky koostuu (tavallisesti) operaatiokoodista eli opkoodista (engl. opcode) sekä 0–3 operandista
  • Käskyjen koodaustapa riippuu ISA:sta. Esimerkiksi
    • IA32:ssa kolmitavuinen käsky 80 F1 12 käskee XORraamaan luvun 18 ja erään rekisterin arvon ja tallettamaan tuloksen ko. rekisteriin.

Konekielen tyypit

  • Konekielessä kaikki data on eri mittaisia bittijonoja.
  • Kohdedatan pituus ja tulkinta riippuvat täysin opkoodista.
  • Tyyppitarkastusta ei ole.
  • tyypillisiä konekielen tyyppejä:
    • \(n\)-bittinen etumerkitön kokonaisluku (aritmetiikka modulo \(2^n\))
    • \(n\)-bittinen etumerkillinen kokonaisluku (kahden komplementti)
    • \(n\) bitin bittijono
    • \(n\) bitin IEEE-liukuluku
    • \(n = 8, 16, 32, 64(, 128)\) (paitsi liukuluvuilla \(n = (16,) 32, 64\))

Symbolinen konekieli

  • Tunnetaan myös nimellä assembly.
  • Ihmisen luettavaksi tarkoitettu konekielen esitystapa.
  • Joka ISA:lla on oma assemblynsä, joillakin on jopa useampia.
  • Yksi käsky esitetään yhdellä rivillä.
  • Kullakin opkoodilla on lyhyt nimi eli muistikas (engl. mnemonic).
  • Operandit esitetään havainnollisella tavalla.

Assemblerit

  • Yksinkertainen kääntäjä, ns. assembler, kääntää symbolisen konekielen konekieleksi.
  • Monilla ISA:illa on monta eri assembleria
  • Osa assemblereista tukee monta ISA:ta
  • tällä luennolla käytämme GNU:n assembleria sekä ARMille että AMD64:lle
    • ARMilla unified-syntaksia
    • AMD64:llä Intel-syntaksia (melkein sama kuin nasmissa)

Ensimmäinen ohjelma

ARM

@ Lasketaan 1+1 (kokonaisluvuilla), tulos mainin paluuarvona

        .syntax unified

        .global main

        .text

main:   mov     r0, #1
        add     r0, r0, #1
        bx      lr

Käännös ja suoritus Debianin tai Ubuntun ARMv7-emulaatiossa:

$ arm-linux-gnueabihf-gcc -o minimal-arm minimal-arm.s
$ ./minimal-arm 
$ echo $?

AMD64

# Lasketaan 1+1 (kokonaisluvuilla), tulos mainin paluuarvona

        .intel_syntax noprefix
        
        .global main

        .text
        
main:   mov     rax, 1
        add     rax, 1
        ret

Käännös ja suoritus Linuxin komentorivillä (AMD64-koneessa, 64-bittinen käyttöjärjestelmä):

$ gcc -o minimal-amd64 minimal-amd64.s 
$ ./minimal-amd64 
$ echo $?

Sivuhuomautus

Käännämme gcc:llä, koska

  • gcc tajuaa tiedostopäätteestä .s, että syöte on assemblya
    • kutsuu automaattisesti assembleria
  • gcc hoitaa linkityksen puolestamme
  • gcc linkittää mukaan C-standardikirjaston
    • mukaan lukien C:n alustuskirjaston, joka kutsuu main-funktiota

Ensimmäiset rivit

  • Kommenttirivi alussa; kommentti päättyy rivin loppuun ja alkaa
    • @ (ARM)
    • # (AMD64)
  • Seuraava epätyhjä rivi valitsee käytetyn syntaksin
  • Kolmas epätyhjä rivi .global main ilmoittaa, että nimi main on julkinen

Assemblerin toimintaperiaate

  • pääsääntöisesti jokainen epätyhjä rivi muuttuu jonoksi tavuja ohjelmassa
    • myös käskyrivit koodataan jonoiksi tavuja
    • kommenttirivit lasketaan tyhjiksi
  • poikkeuksena osa riveistä, joilla esiintyy ns. "pseudokäskyjä", jotka alkavat pisteellä
  • ohjelma jaetaan osiin (section):
    • .text-osaan ohjelmakoodi ja vakiot
    • .data-osaan alustetut globaalit muuttujat
    • .bss-osaan nollaksi alustettavat globaalit muuttujat
  • osaan tulee kaikki osan ilmoittavan pseudokäskyn jälkeen tulevat tavut

Ensimmäinen koodirivi

        @ ARM
main:   mov     r0, #1
        # AMD64
main:   mov     rax, 1
  • Rivin alussa on label main
    • main tarkoittaa tästä lähtien labelia seuraavan tavun osoitetta
  • Siirretään rekisteriin kokonaislukuvakio 1
  • Käsky alkaa muistikkaalla (engl. mnemonic), joka kertoo käskyn operaation
    • tässä sattumalta molemmissa assemblyissä sama muistikas, aina näin ei ole
  • Käskyn operandit erotetaan toisistaan pilkulla
    • eka operandi on operaation kohde
  • Vakio kirjoitetaan AMD64:ssä sellaisenaan, ARMissa eteen laitetaan #

Rekisterit

  • keskusyksikön sisällä olevia nimettyjä muistiyksiköitä
  • rekisterin käyttö olennaisesti muistin käyttöä nopeampaa
  • joissakin ISA:issa laskenta mahdollista vain rekistereissä
  • erittäin rajattu määrä, kourallisesta muutamaan kymmeneen
  • ARMv7:ssa
    • 32-bittisiä yleisrekistereitä 13 kpl (R0–R12)
    • 64-bittisiä liukulukurekistereitä 16 tai 32 kpl (D0–D31)
  • AMD64:ssä
    • 64-bittisiä kokonaislukurekistereitä 16 kpl (RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, R8–R15)
    • 2×64-bittisiä liukulukurekistereitä 16 kpl (XMM0–XMM15)

AMD64:n alirekisterit

  • Jokaisella AMD64:n kokonaislukurekisterillä on nimettyjä alirekistereitä
  • RAX, RBX, RCX, RDX:
    • 32 alinta bittiä EAX, EBX, ECX, EDX
    • 16 alinta bittiä AX, BX, CX, DX
      • jonka 8 ylintä bittiä AH, BH, CH, DH
    • 8 alinta bittiä AL, BL, CL, DL
  • RSI, RDI, RBP, RSP, R8–R15:
    • 32 alinta bittiä ESI, EDI, EBP, ESP, R8D–R15D
    • 16 alinta bittiä SI, DI, BP, SP, R8W–R15W
    • 8 alinta bittiä SIL, DIL, BPL, SPL, R8B–R15B

Toinen koodirivi

        @ ARM
        add     r0, r0, #1
        # AMD64
        add     rax, 1
  • molemmissa taas puolivahingossa sama muistikas
    • ARM:n add laskee toisen ja kolmannen operandin yhteen ja tallettaa tuloksen ensimmäiseen operandiin
    • AMD64:n add laskee operandit yhteen ja tallettaa tuloksen ensimmäiseen operandiin
  • add tekee molemmissa kokonaislukuyhteenlaskun!
    • liukulukulaskentaan on eri käskyt

Kolmas koodirivi

        @ ARM
        bx      lr
        # AMD64
        ret
  • käskyt tekevät "pellin alla" aika lailla eri asioita, mutta lopputulos on molemmissa sama:
  • palataan aliohjelmasta main sen kutsujaan

Toinen ohjelma

ARM

        @ Kopioidaan syöte tulosteeseen

        .syntax unified
        .global main
        .text

main:   push    {ip,lr}
0:      bl      getchar
        cmp     r0,     #0
        blt     1f
        bl      putchar
        b       0b
1:      mov     r0,     #0
        pop     {ip,lr}
        bx      lr

AMD64

        # Kopioidaan syöte tulosteeseen

        .intel_syntax noprefix
        .global main
        .text

main:   push    rbp
0:      call    getchar
        cmp     eax,    0
        jl      1f
        mov     rdi,    rax
        call    putchar
        jmp     0b
1:      mov     rax,    0
        pop     rbp
        ret

Suuruusvertailujen koodaaminen

        @ ARM
        cmp     r0,     #0
        # AMD64
        cmp     eax,    0
  • cmp suorittaa molemmissa assemblyissä vähennyslaskun, jonka tulosta ei tallenneta mihinkään
  • tuloksen ominaisuuksia tallennetaan lippuihin, mm.
    • N / SF: oliko negatiivinen?
    • Z / ZF: oliko nolla?
    • V / OF: tuottiko ylivuodon?
    • C / CF: tuottiko carry-tilanteen?
    • (ARM / AMD64)

Valintojen tekeminen

        @ ARM
        blt     1f
...
1:
        # AMD64
        jl      1f
...
1:
  • lippujen perusteella voidaan tehdä ehdollisia hyppyjä
    • hyppykäskyn nimi alkaa ARMissa b ja AMD64:ssä j
    • nimi jatkuu ehdon kuvauksella
  • hypyn kohteena jokin lähellä oleva käsky
    • 1f viittaa käskyä seuraavaan 1:-labeliin
    • 1b viittaa käskyä edeltävään 1:-labeliin
    • 2f / 2: jne myös mahdollisia
    • voi myös käyttää tavallisia nimettyä labeleita

Ehdollisia hyppykäskyjä ARMissa

Käsky Ehto cmp \(a\),\(b\) \(a\)
beq \(Z = 1\) \(a = b\) \(a = 0\)
bne \(Z = 0\) \(a \neq b\) \(a \neq 0\)
bmi \(N = 1\) \(a < 0\)
bpl \(N = 0\) \(a \geq 0\)
bhi \(C=1\land V=0\) \(a > b\) (u)
bls \(C=0\lor V=1\) \(a \leq b\) (u)
bge \(N = V\) \(a \geq b\) (s)
blt \(N \neq V\) \(a < b\) (s)
bgt \(Z=0\land N=V\) \(a > b\) (s)
ble \(Z=1\lor N\neq V\) \(a \leq b\) (s)
  • u tarkoittaa etumerkitöntä tulkintaa
  • s tarkoittaa kahden komplementti -tulkintaa

Ehdollisia hyppykäskyjä AMD64:ssä

Käsky Ehto cmp \(a\),\(b\) \(a\)
je, jz \(ZF = 1\) \(a = b\) \(a = 0\)
jne, jnz \(ZF = 0\) \(a \neq b\) \(a \neq 0\)
js \(SF = 1\) \(a < 0\)
jns \(SF = 0\) \(a \geq 0\)
jb \(CF=1\) \(a < b\) (u)
jnb \(CF=0\) \(a \geq b\) (u)
jge \(SF = OF\) \(a \geq b\) (s)
jl \(SF \neq OF\) \(a < b\) (s)
jg \(ZF=0\land SF=OF\) \(a > b\) (s)
jle \(ZF=1\lor SF\neq OF\) \(a \leq b\) (s)
  • u tarkoittaa etumerkitöntä tulkintaa
  • s tarkoittaa kahden komplementti -tulkintaa

Silmukat

        @ ARM
0:
...
        blt     1f
...
        b       0b
1:
        # AMD64
0:
...
        jl      1f
...
        jmp     0b
1:
  • silmukoita ei ole, ne on koodattava hyppykäskyistä
  • while (e) S kääntyy muotoon
    • e
    • jos epätosi, hyppää ulos
    • S
    • hyppää silmukan alkuun
  • ehdoton hyppy ARM:ssa b ja AMD64:ssä jmp

Aliohjelmakutsut ARM:ssa

        bl      getchar         @ int getchar(void)
        cmp     r0,     #0
        blt     1f
        bl      putchar         @ int putchar(int)
  • aliohjelmakutsukäsky on bl
    • paluuosoite tallentuu automaattisesti rekisteriin lr eli r14
    • kutsujan oma paluuosoite tuhoutuu samalla
  • 4 ensimmäistä kokonaislukuparametria välitetään rekistereissä r0, r1, r2 ja r3
  • kokonaislukupaluuarvo välitetään rekisterissä r0

Aliohjelman koodaus ARM:ssa

main:   push    {ip,lr}
...
        mov     r0,     #0
        pop     {ip,lr}
        bx      lr
  • jos aliohjelma kutsuu muita aliohjelmia, sen paluuosoite on pelastettava rekisteristä lr
    • yksinkertaisinta laittaa pinoon: push {ip,lr}
    • samalla laitetaan pinoon jokin muu rekisteri (tässä ip), jotta pino-osoitin pysyy 8:lla jaollisena
    • ennen paluuta otetaan takaisin pinosta: pop {ip,lr}
  • paluukäsky bx lr hyppää lr-rekisterissä olevaan osoitteeseen
    • periaatteessa on muitakin tapoja, mutta niissä on omat kommervenkkinsä, tämä on turvallisin
  • kokonaislukupaluuarvo laitetaan rekisteriin r0

Aliohjelmat AMD64:ssä

main:   push    rbp
        call    getchar         # int getchar(void)
        cmp     eax,    0
        jl      1f
        mov     rdi,    rax
        call    putchar         # int putchar(int)
...
1:      mov     rax,    0
        pop     rbp
        ret
  • aliohjelmakutsukäsky on call ja paluukäsky ret
    • paluuosoite kulkee automaattisesti pinossa
    • paluuosoite on 8 tavua pitkä, mutta pinon osoitteen tulee olla 16:lla jaollinen kutsun tapahtuessa, joten laitetaan jotain muuta myös pinoon
  • 6 ensimmäistä kokonaislukuparametria välitetään rekistereissä rdi, rsi, rdx, rcx, r8 ja r9
  • kokonaislukupaluuarvo välitetään rekisterissä rax
    • mutta rax on 64-bittinen, int on 32-bittinen, joten vertailu tehdään yllä rax:n 32-bittisellä osarekisterillä eax

Caller save ja callee save

  • rekisterit jaetaan kahteen luokkaan:
    • caller save: rekisteri, jota aliohjelma saa käyttää vapaasti aliohjelmakutsujen välissä, mutta jonka arvo ei välttämättä säily aliohjelmakutsun yli
    • callee save: rekisteri, jota aliohjelma saa käyttää vain, jos se tallettaa sen alkuperäisen arvon ja palauttaa sen ennen paluutaan
      • ARM:ssa r4, r5, r6, r7, r8, r10, r11
      • AMD64:ssä rbx, rbp, r12, r13, r14, r15

Rekisterien arvojen talletus pinoon

  • ARM:ssa
    • push {rekisteri,...,rekisteri} laittaa pinoon
      • laita pinoon aina 8:lla jaollinen määrä tavuja kerrallaan
      • rekisterien luettelon tulee olla numerojärjestyksessä
      • ip on numeroltaan r12, lr on r14
    • pop {rekisteri,...,rekisteri} ottaa pinosta
      • sama luettelo rekistereitä!
  • AMD64:ssä
    • push rekisteri laitaa pinoon
      • huolehdi, että aliohjelmakutsun laitettua paluuosoitteen pinoon pino-osoitin on jaollinen 16:lla
    • pop rekisteri poistaa pinosta
      • poista käänteisessä järjestyksessä (LIFO!)

Liukuluvut ARM:ssa (armhf)

  • Yksinkertaisuuden vuoksi:
    • vain kaksoistarkkuus (double, 64 bittiä)
    • vain armhf ("hardware float") -järjestelmät
  • Samalla myös esimerkki
    • globaaleista muuttujista
    • merkkijonovakioista

Keskiarvoaliohjelma

avg:    vadd.f64        d0, d0, d1
        vmov.f64        d1, #2.0
        vdiv.f64        d0, d0, d1
        bx              lr
  • Liukulukurekisterit ovat nimeltään d0,...,d15 ja joissakin prosessoreissa lisäksi d16,...,d31
    • d8,...,d15 ovat callee save
  • Liukulukukäskyjen muistikkaat alkavat kirjaimella v ja päättyvät pääosin tyyppimäärittelyyn .f64 ("64-bittinen liukuluku")
  • 8 ensimmäistä liukulukuparameteria välitetään liukulukurekistereissä d0,...,d7
    • poikkeuksena printf ym (stdarg.h-aliohjelmat)
  • liukulukupaluuarvo välitetään d0:ssa

Liukulukujen lukeminen scanf:lla

        ldr     r0, =rfmt
        ldr     r4, =tmp1
        ldr     r5, =tmp2
        mov     r1, r4
        mov     r2, r5
        bl      scanf
        cmp     r0, 2
        bne     1f
        vldr    d0, [r4]
        vldr    d1, [r5]
...
        .balign 8
rfmt:   .asciz  "%lf %lf\n"

        .bss
        .balign 8
        .comm   tmp1,   8
        .comm   tmp2,   8

Formaattimerkkijono

        ldr             r0, =rfmt
...
        .balign         8
rfmt:   .asciz          "%lf %lf\n"        
  • Merkkijonovakio (ei Unicode) kirjoitetaan .text-osaan .asciz-pseudokäskyllä.
    • Sille annetaan nimi kuten aliohjelmalla.
    • Ennen merkkijonovakiota varmistetaan, että osoite on kahdeksalla jaollinen, pseudokäskyllä .balign 8
  • Nimetty osoite luetaan rekisteriin (pseudo)käskyllä ldr r0, =rfmt
    • assembler vääntää tästä pellin alla vähän monimutkaisemman

Luettujen lukujen tallennusmuuttujat

        ldr     r1, =tmp1
        ldr     r2, =tmp2
...
        .bss
        .balign 8
        .comm   tmp1,   8
        .comm   tmp2,   8
  • Jokaista formaattimerkkijonossa olevaa %lf-osajonoa kohti pitää olla yksi tallennusmuuttuja, jonka osoite viedään parametina.
  • Globaalit muuttujat voidaan luoda .bss-osaan
    • Muuttujalle annetaan nimi tmp1 ja 8 tavua tilaa pseudokäskyllä .comm tmp1, 8
    • Muuttujat alustuvat ohjelman käynnistyessä nollabittijonoiksi

Lukemisen onnistumisen tarkastaminen ja lukujen käyttöönotto

        bl              scanf
        cmp             r0, 2
        bne             1f
        ldr             r0, =tmp1
        ldr             r1, =tmp2
        vldr            d0, [r0]
        vldr            d1, [r1]
  • scanf palauttaa onnistuneesti luettujen muuttujien lukumäärän
  • luetut luvut siirretään liukulukurekistereihin vldr-käskyllä
    • [r0] tarkoittaa, että operandi löytyy muistista r0:n sisältämän osoitteen kohdalta
  • huomaa, että tmp1 ja tmp2 -osoitteet lasketaan nyt monta kertaa
    • voi myös tallettaa callee save -rekistereihin odottamaan uudelleenkäyttöä (näin alkuperäisessä ohjelmassa)
    • muista tallettaa näiden rekisterien alkuperäiset arvot pinoon!

Tuloksen tulostaminen printf-aliohjelmalla

        ldr             r0, =wfmt
        vmov            r2, r3, d0
        bl              printf
...
        .balign         8
wfmt:   .asciz          "%lf\n"
  • printf:n formaattimerkkijonossa esiintyvä %lf tarkoittaa, että printf:lle pitää antaa lisäksi double-tyyppinen parametri
  • printf:n kaltaisille aliohjelmille (stdargs) double-parameterit jaetaan kahdeksi 32-bittiseksi parametriksi, jotka välitetään kahdella jaollisesta rekisteristä alkaen kuten kokonaisluvut
    • mov r2, r3, d0 tekee tämän parameterinvälityksen

Liukuluvut AMD64:ssä

  • yksinkertaisuuden vuoksi vain kaksoistarkkuus (double)
  • Samalla myös esimerkki
    • globaaleista muuttujista
    • merkkijonovakioista

Keskiarvoaliohjelma

avg:    addsd           xmm0, xmm1
        mov             rax, 2
        cvtsi2sd        xmm1, rax
        divsd           xmm0, xmm1
        ret
  • 8 ensimmäistä liukulukuparametria välitetään rekistereissä xmm0,...,xmmm7
  • liukulukupaluuarvo välitetään rekisterissä xmm0
  • addsd laskee kaksi liukulukua yhteen
  • cvtsi2sd muuttaa kokonaisluvun liukuluvuksi
  • divsd on jakolasku

Liukulukujen lukeminen scanf:llä

        lea     rdi,    [rfmt]
        mov     al,     0
        lea     rsi,    [tmp1]
        lea     rdx,    [tmp2]
        call    scanf
  • lea laskee ensimmäiseen operandiinsa toisen operandinsa muistiosoitteen
    • hakasulkeisiin kirjoitetaan muistiosoitteen lähde
      • voi olla vakio, rekisteri tai vähän monimutkaisempaa
  • scanf-funktion tyyppisille aliohjelmille (stdarg) on kerrottava al-rekisterissä, montako liukulukuparametria on enintään rekistereissä

Lukujen tulostaminen printf:llä

        movsd   xmm0,   [tmp1]
        movsd   xmm1,   [tmp2]
        lea     rdi,    [wfmt]
        mov     al,     1
        call    printf
  • movsd lataa liukuluvun muistista
  • liukulukuparametrit välitetään myös stdarg-funktioille liukulukurekistereissä
    • al:ssä kerrottava liukulukurekistereissä olevien parametrien enimmäismäärä
      • ei tarvitse olla tarkka tieto, mutta max 8

These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.