TIES448 Kääntäjätekniikka
Luento 2 (16.3.2017)
Esimerkkikoodit
Huomioita akateemisesta rehellisyydestä
- Rehellisyys on välttämätöntä mutta ei riittävää suoritukseen.
- Aina pitää osoittaa myös itse asian osaamista.
- Pelkkä copypaste ei ole yleensä riittävä vastaus mihinkään, vaikka se olisi rehellisesti tehty.
- Yleensä vähintäänkin se copypaste pitää ymmärtää hyvin ja kyetä sen sisältö selittämään.
- Kokoelma copypasteja saattaa olla riittävä vastaus, jos kokoelman kokoaminen osoittaa asian ymmärrystä.
- Tosin harvoin johtaa hyvään arvosanaan.
- Näihin sääntöihin on poikkeuksia.
Luettavaa
Viralliset dokumentaatiot (laajoja, pitkälti TMI):
- AMD64 Architecture Programmer’s Manual
- ARM Architecture Reference Manuals
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 puolestammegcc
linkittää mukaan C-standardikirjaston- mukaan lukien C:n alustuskirjaston, joka kutsuu
main
-funktiota
- mukaan lukien C:n alustuskirjaston, joka kutsuu
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ä nimimain
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
- ARM:n
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
- hyppykäskyn nimi alkaa ARMissa
- hypyn kohteena jokin lähellä oleva käsky
1f
viittaa käskyä seuraavaan1:
-labeliin1b
viittaa käskyä edeltävään1:
-labeliin2f
/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 muotoone
- jos epätosi, hyppää ulos
S
- hyppää silmukan alkuun
- ehdoton hyppy ARM:ssa
b
ja AMD64:ssäjmp
Application Binary Interface
(ABI)
- ISA-kohtainen
- yleensä myös käyttöjärjestelmäkohtainen
- voi olla myös kääntäjäkohtainen
- määrittelee mm.
- tietotyyppien esitystavat
- aliohjelmien kutsurajapinnan
- jos käännetty ohjelma ja käännetty kirjasto noudattavat samaa ABIa, ne voi linkittää yhteen
ABI-vaihtoehtoja
- System V Application Binary Interface, AMD64 Architecture Processor Supplement, Draft Version 0.99.8
- jatkossa AMD64 SysV ABI
- käytössä mm. Linuxissa
- ei käytössä Windowsissa!
- Visual C++ x64 Software Conventions
- 64-bittinen Windows
- Application Binary Interface (ABI) for the ARM Architecture
- kutsutaan yleensä nimellä ARM EABI
- jatkossa tarkastellaan vain ARM EABIa ja AMD64 SysV ABIa
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
elir14
- kutsujan oma paluuosoite tuhoutuu samalla
- paluuosoite tallentuu automaattisesti rekisteriin
- 4 ensimmäistä kokonaislukuparametria välitetään rekistereissä
r0
,r1
,r2
jar3
- 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}
- yksinkertaisinta laittaa pinoon:
- 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ä (SysV ABI)
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äskyret
- paluuosoite kulkee automaattisesti pinossa
- paluuosoite on 8 tavua pitkä, mutta pinon osoitteen tulee olla 16:lla jaollinen kutsun tapahduttua, joten laitetaan jotain muuta myös pinoon
- 6 ensimmäistä kokonaislukuparametria välitetään rekistereissä
rdi
,rsi
,rdx
,rcx
,r8
jar9
- 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
- mutta
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ä (SysV ABI)
rbx
,rbp
,r12
,r13
,r14
,r15
- ARM:ssa
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 numeroltaanr12
,lr
onr14
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
- vain kaksoistarkkuus (
- 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äksid16
,...,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)
- poikkeuksena printf ym (
- 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
- Muuttujalle annetaan nimi
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 muististar0
:n sisältämän osoitteen kohdalta
- huomaa, että
tmp1
jatmp2
-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äksidouble
-tyyppinen parametriprintf
:n kaltaisille aliohjelmille (stdargs
)double
-parameterit jaetaan kahdeksi 32-bittiseksi parametriksi, jotka välitetään kahdella jaollisesta rekisteristä alkaen kuten kokonaisluvutmov 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 yhteencvtsi2sd
muuttaa kokonaisluvun liukuluvuksidivsd
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
- hakasulkeisiin kirjoitetaan muistiosoitteen lähde
scanf
-funktion tyyppisille aliohjelmille (stdarg
) on kerrottavaal
-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
C-kirjastofunktioiden yhteenveto
Vapaaehtoisia harjoitustehtäviä
Kirjoita assembly-ohjelma:
- joka lukee käyttäjältä rivin ja tulostaa saman rivin niin, että pienet kirjaimet on muutettu isoiksi kirjaimiksi (vain ASCII, eli ei ääkkösiä ym).
- joka laskee syötteessä olevien sanojen lukumäärän.
- joka tulostaa Fibonaccin lukujonon (siihen asti kunnes 64-bittisestä kokonaisluvusta loppuu esitystarkkuus tai kunnes käyttäjä ohjelman keskeyttää Ctrl-C:llä)
Näitä voidaan käsitellä ohjauksissa.
These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.