Spoznali bomo, kaj je pravzaprav zbirni jezik. Glede na to, da se zbirni jeziki med seboj razlikujejo, bomo na konceptualnem nivoju spoznali, kako z njim deklariramo ukaze in podatke ter katere korake moramo narediti, da pridemo do izvršljivega programa. Spoznali bomo tudi, kaj potrebujemo za odkrivanje in odpravljanje napak v programih, pisanih v zbirnem jeziku.
To je najnižji nivo programiranja, ki poenostavi izredno nepregledno in zamudno programiranje na nivoju strojnega jezika. Kot bomo spoznali kasneje, uporabljamo primitivne ukaze s kraticami (mnemoniki), ki nas v angleškem jeziku spominjajo na to, kar želimo od računalnika. Med ukazi v zbirnem jeziku (assembly language) in strojnem jeziku velja običajno kar razmerje 1:1
Primer:
mov ax,bx |
pomeni, da vsebino regitra AX prepišemo v register BX. In za to zadošča en ukaz na strojnem nivoju.
Dobri programerji lahko na ta način napišejo zelo kompaktne in hitre programe. Kar počnemo v nekem višjem programskem jeziku, lahko zanesljivo zakodiramo tudi v zbirnem jeziku. Kljub temu se danes programiranje v zbirnem jeziku redko uporablja. Celo operacijski sistemi, ki morajo biti sprogramirani res učinkovito, so danes programirani v jezikih, kot so C, C++ in podobni. Lahko pa v take programe (na primer v jeziku C) vključimo dele optimizirane kode, zapisane v zbirnem jeziku (»inline« kodiranje). Žal pa programiranje v zbirnem jeziku pomeni tudi to, da tak program ni prenosljiv na računalnike z drugo zgradbo CPE.
Pripraviti želimo program (v binarni kodi), ki naj bi tekel na našem računalniku. Takemu računalniku recimo ciljni računalnik.
Najprej s primernim urejevalnikom napisemo program v zbirnem jeziku. To seveda lahko naredimo na katerikoli računalniku (To ni nujno isti računalnik, na katerem želimo poganjati naš program).
Podobno, kot smo si pomagali z urejevalnikom za pisanje samega programa, si sedaj pomagajmo z zbirnikom (assembler). Zbirnik je program, ki bere program v zbirnem jeziku in tvori program v binarni kodi.
Ta program mora v resnici še preiti nekaj faz (povezovanje s sistemskimi in drugimi podprogrami, nalaganje v pomnilnik), vendar to ni pomembno za razumevanje samega zbiranja.
Med samim potekom dobimo kar nekaj uporabnih izpisov, ki nam pomagajo pri ugotavljanju napak v našem programu. Po prehodu čez zbirnik je to datoteka z izpisom programa (listing file), ki tipično vsebuje naslednje:
•
Izvorno kodo programa
•
Koristne naslove (kje je kaj)
•
Prevedeno (objektno) kodo (strojni jezik)
•
Imena segmentov (to je posebnost Intelove
tehnologije)
•
Simbole (imena spremenljivk, constant in procedur)
Primer take datoteke z listanjem bomo spoznali kasneje.
Po prehodu čez povezovalnik, ki doda »zunanje« rutine, dobimo še datoteko z izpisi naslovov (Map file). V primeru Intelove tehnologije tako dobimo izpisane podatke o posameznih segmentih:
•
Začetni naslov segmenta
•
Končni naslov segmenta
•
Velikost segmenta
•
Tip segmenta
Če binarni program pogledamo bolj podrobno, zasledimo (vsaj po eno) področje s podatki, programsko kodo ter sklad. Področij s kodo in s podatki je lahko tudi več in se med seboj lahko prepletajo (kar pa je, vsaj za programerje-začetnike,nevarno).
S kakšnimi mnemoniki lahko povemo zbirniku, da želimo rezervirati prostor za podatke?
S kakšnimi instrukcijami povemo zbirniku, kakšna naj bo programska koda?
Zbirnik razporeja (alocira prostor za) podatke in kodo sekvenčno, tako kot bere vrstice v našem programu. Ne pozabimo, da je zbirnik navaden program in ima kot tak svoje spremenljivke. Ena od teh je lokacijski števec. Ta se med zbiranjem povečuje za toliko besed, kolikor jih je potrebnih za posamezen podatek ali kodiran ukaz.
Delovanje računalnika lahko ponazorimo s pisanjem programa za izračun A+B-C. Ta izraz razstavimo na naslednje operacije: A prepišemo v akumulatorski register, vsebini akumulatorja prištejemo B, nato odštejemo C in končno ustavimo računalnik ter preverimo vsebino akumulatorja. Kako bi izgledal tak program v zbirnem jeziku za Intel x86?
Primer programa v zbirnem jeziku:
TITLE Add and Subtract ; This program adds and subtracts 32-bit integers. INCLUDE unility.inc .code Main PROC moveax,10000h ; EAX = 10000h add eax,40000h ; EAX = 50000h sub eax,20000h ; EAX = 30000h call DumpRegs ; display registers exit main ENDP END main |
Opazimo več vrstic. Prej smo sicer rekli, da ena vrstica v zbirnem jeziku ustreza enemu strojnemu ukazu, pa to očitno ni čisto res. Najprej bi opozorili, da program vsebuje tudi komentarje. Ti so v našem primeru zaradi nazornosti pobarvani zeleno. Opazimo, da se vsak komentar začne s podpičjem in nadaljuje do konca vrstice. Zbirnik seveda komentarje ignorira.
Sicer pa posamezne vrstice pomenijo neko navodilo (direktivo ) zbirniku ali pa res zapis želenega programskega ukaza (ekvivalent strojnemu ukazu). Zaradi večje preglednosti so navodila v našem primeru pobarvana modro, ukazi pa rdeče.
Navodila (directive)
•
Z njimi povemo zbirniku, kaj naj naredi
•
Njim ne ustreza noben strojni ukaz samega
računalnika
•
Z njimi deklariramo procedure, podatke in model
pomnenja (*).
• Različni zbirniki uporabljajo različne, čeprav podobne direktive
Ukazi (instrukcije)
•
Jih zbirnik preslika v strojno kodo
•
Med izvajanjem programa jih CPE spoznava in izvaja
•
Stavek, ki opisuje ukaz, tipično vsebuje oznako
(labelo), mnemonic, operande in komentar
* Pripomba: Model pomnenja je posebnost Intelove tehnologije. Zbirniku lahko napovemo, da bo naš program kratek ali dolg in bo temu primerno izbiral tip strojnih ukazov.
Če pogledamo ukaze našega primera, zlahka ugotovimo, da naj bi program nekaj prenesel (mov), pa temu nekaj prištel (add) in odštel (sub). Malo manj nam je razumljiva vrstica call DumpRegs, pa o tem več kasneje.
Zaradi večje preglednosti lahko te vrstice pišemo tudi s tabulatorjem. Prva kolona je oznaka (label ) vrstice. Druga kolona vsebuje operacijo. Tretja kolona vsebuje morebitni operand, četrta kolona vsebuje komentar.
Vsaka vrstica (ki ni le komentar) vsebuje besedico, ki pove zbirniku, kaj naj na danem mestu naredi. Tem besedam, ki so v bistvu okrajšave angleških napotkov, pravimo mnemoniki.
Opazimo, da imajo nekateri stavki oznake (v načem primeru »main«). Zbirnik loči oznake od mnemonikov po tem, da se (vsaj v našem primeru) oznake obvezno začnejo v prvi koloni, mnemoniki pa morajo imeti pred seboj vsaj en presledek.
Za mnemoniki je lahko eden ali več argumentov, odvisno od vrste mnemonika. Če jih je več, so (običajno) ločeni z vejicami.
Zbirnik bi naš program v zbirnem jeziku prebral. Navodila (direktive) povedo zbirniku, kakšno ime ima naš program (vrstica TITLE), Povedo mu, da bo moral vključiti in uporabljati nekatere, vnaprej pripravljene podprograme (vrstica include unility.inc), ter da program vsebuje neko proceduro oziroma kodo (vrstici .code in PROC Main).
Zbirnik tvori strojno (binarno) sliko programa. Omenili smo že, da pripravi tudi izpis (takoimenovano listanje) programa, ki nam pride prav pri iskanju morebitnih napak. Poglejmo si listanje za naš primer:
Primer izpisa programa („listanje” programa)
00000000 .code 00000000 main PROC 00000000 B8 00010000 mov eax,10000h 00000005 05 00040000 add eax,40000h 0000000A 2D 00020000 sub eax,20000h 0000000F E8 00000000E call DumpRegs exit 00000014 6A 00 * push +000000000h 00000016 E8 00000000E * call ExitProcess 0000001B main ENDP END main |
V bistvu je ta izpis sestavljen iz treh delov:
Čas je, da razložimo pomen vrstice »call DumpRegs». To namreč ni preprost strojni ukaz pač pa klic podprograma s tem imenom.
Če se morda komu zdi samo programiranje v zbirnem jeziku preprosto, bi se zagotovo ustavil, ko bi moral preko računalnika narediti kakšen lep izpis, kot smo to navajeni v višjih programskih jezikih. Zasilno si lahko pomagamo tako, da nam nekdo naredi podprogram za izpis vsebine registrov računalnika in ugotavljamo, ali je po določenih računalniških korakih njihova vsebina takšna, kot jo pričakujemo. Da pa lahko tak, vnaprej pripravljen podprogram kličemo, moramo zbirniku povedati, da naj ga vključi (zato je v izvornem programu tudi navodilo »include utility.inc«
In kakšen izpis bi dobili v našem primeru:
Primer izhoda (izpisa registrov)
V prejšnjem primeru smo imeli podatke »integrirane« v sam program. Če pogledamo prejšnji primer listanja programa, bomo v strojni kodi kaj hitro odkrili podatke 1000, 4000 in 2000 (v šestnajstiškem formatu).
V splošnem pa so podatki v drugem delu pomnilnika kot sama programska koda. Spomnimo se naslednje slike:
Za vsak podatek moramo povedati, kje je, kakšnega tipa je in kakšno vrednost ima. To naredimo s primernimi navodili zbirniku.
Za primer zbirnika za Intel, bi to lahko zgledalo tako:
Podatki1 DB 10,4,10H
Podatki2 DW 100,?,-5
Podatki3 DD 3*20,0FFFDH
Vidimo, da imajo te vrstice podobno obliko, kot ostale vrstice zbirnega jezika. Zaradi večje preglednosti smo mnemonike pobarvali rdeče. Operande pa modro. Pomen mnemonikov je naslednji:
• DB (define byte) Vsak operand zaseda en bajt
• DW (define word) Vsak operand zasede eno besedo (dva bajta)
• DD (define double word) Vsak operand zasede 2 besedi (4 bajte)
Vsaki vrstici lahko po potrebi dodamo oznako (labelo), ki omogoča naslavljanje tako rezerviranih pomnilniških lokacij.
Operandi so lahko številske konstante, podane v desetiškem ali šestnajstiškem sistemu (slednje imajo na koncu črko H). Lahko pa tudi kar rezerviramo pomnilniško lokacijo in ne podamo njene vsebine (za to uporabimo namesto številske vrednosti znak ?). Poleg tega lahko povemo, da potrebujemo večkratno dolžino (uporabno za rezervacijo polj).
In kako izgleda rezervirani pomnilnik za naš primer:
Opazimo tri področja, ki so velika 3 bajte, 6 bajtov oziroma 8 bajtov.
Program v zbirnem jeziku bo imel pravilno zgradbo, če uporabimo predlogo oziroma šablono, kot jo kaže spodnja slika:
TITLE Program Template ; Program Description: ; Author: ; Creation Date: ; Revisions: ; Date: Modified by: INCLUDE utility.inc .data ; (tu vstavimo spremenljivke in konstante) .code main PROC ; (tu vstavimo programsko kodo, torej ukaze) exit main ENDP ; (tu podamo kodo za morebitne podprograme) END main |
Pravilno napisan program mora vsebovati tudi podatke o avtorju, o verziji programa ter seveda sam opis programa.
Niso tako enostavne, kot to velja za programiranje v višjih jezikih. Programiranje v zbirnem jeziku pač ni temu namenjeno. Če že moramo predvideti tudi vpis podatkov in izpis rezultatov, mora nekdo sprogramirati primerne podprograme, ki jih po potrebi kličemo. Nekaj podobnega smo zasledili tudi v našem prejšnjem primeru.
Če nam program ne da pravega rezultata, ga moramo razhroščiti (debug). Pomagamo si tako, da njegovo izvajanje prožimo korak za korakom. V tem načinu lahko po vsakem koraku preverimo stanje računalnikovih registrov. V ta namen oporabjamo takoimenovane razhroščevalnike (debuggers).
Normalno med izvajanjem programa ne vidimo njegove izvorne kode. Razhroščevalnik je programsko orodje, ki omogoča, da naš program teče v »razhroščevalnem režimu« (debugging mode). To pomeni, da lahko v vsakem trenutku izvajanja programa vidimo njegovo izvorno kodo, ga prekinemo, pogledamo vrednosti spremenljivk in te tudi spreminjamo ter nadaljujemo izvajanje programa.
Poznamo dve vrsti razhroščevalnikov: konzolne in vizualne (grafične) razhroščevalnike. Bolj prijazni so grafični (vizualni), kjer vidimo izvorno kodo v enem oknu, v drugem pa vrednosti spremenljivk, oziroma v primeru zbirnega jezika vrednosti pomnilniških lokacij in delovnih registrov.
Vsak razhroščevalnik mora omogočati:
• Prikaz vrednosti v registrih in pomnilniku
• Vnašanje vrednosti v registre in pomnilnik
• Izvajanje zaporedja programskih ukazov
• Prekinitev programa, kjerkoli to želimo.
Ena od pomembnih lastnosti razhroščevalnikov je sledenje programa. To pomeni:
• Koračno izvajanje programa ukaz za ukazom in sprotno sledenje vsebin registrov in pomnilniških lokacij.
• Nastavljanje prekinitvenih točk (breakpoints). Razhroščevalniku povemo, pri katerem programskem ukazu naj se zaustavi. To pride v poštev predvsem pri programih, ki deloma potekajo v zankah in bi bilo koračno sledenje prezamudno.
• Nastavljanje pogojev za zaustavljanje (watchpoint). Zahtevamo, da se program zaustavi, ko pride na primer do spremembe vsebine nekega registra ali kakšne druge opazovane vrednosti.
Primer bolj preprostega razhroščevalnika je OllyDbg, ki je tudi prosto dobavljiv. Spodnja slika predstavlja tipično zaslonsko sliko tega orodja:
Primer profesionalnega, grafično usmerjenega razhroščevalnika je PEBrowse Professional Interactive Windows Debugger, ki ga uporabljamo za sistemsko programiranje. Primer ekranske slike tega orodja kaže spodnja slika
Poleg prikaza programske kode v simbolični in šestnajstiški obliki vidimo tudi trenutno stanje registrov računalnika.