Stručný úvod do programovania v GNU assembleri pre MSP430. A nielen o tom. Takisto tu nájdete stručné pojednanie o práci kompilátora a linkera, prípadne ako pracovať s rôznymi časťami pamäte.
V tomto článku sa nebudeme venovať iba assembleru. Vzhľadom k tomu, že assembler viac zviditeľňuje niektoré veci, ktoré u vyšších jazykov obvykle nie sú viditeľné – mám na mysli hlavne prácu kompilátora a linkera s objektovými súbormi, sekcie a práca s nimi – venujeme na úvod trochu času aj týmto témam.
Tento článok nie je a ani nechce byť vyčerpávajúcou referenčnou príručkou. Snažím sa v ňom ukázať iba isté základy, a aby sa veci nekomplikovali nekonečnými variantami, vo viacerých prípadoch som zaviedol zjednodušenia. Aj preto odporúčam pozrieť si aj originálne referenčné príručky.
A teraz k veci. Na úvod trochu teórie:
Sekcie
Sekciám budeme venovať trochu viac času – správne pochopenie čo sú sekcie,ako so sekciami zaobchádzať a čo sa s nimi deje počas kompilácie a linkovania je dôležitým predpokladom k úspešnému programovaniu v assembleri – a nielen v ňom: všetko, čo tu bude povedané, rovnako platí aj pri programovani vo väčšine vyšších jazykov. Správna práca so sekciami nám zaručí, že programový kód alebo dáta sa dostanú do správnej oblasti pamäti.
Sekcia je časť binárnych dát, ktorá sa umiestňuje na špecifické miesto v pamäti. Sekcia je spojitá a môže mať špecifické vlastnosti, napríklad môže byť len na čítanie, obsahuje kód alebo dáta, prípadne predstavuje len vyhradené miesto bez skutočných dát. Sekcie sa nachádzajú v každom nezávisle kompilovanom zdrojovom kóde (takýto nezávisle kompilovaný kód budeme ďalej nazývať modul) a takisto vo výslednom programe.
Každý program sa môže skladať z viacerých modulov. Každý modul obsahuje dáta (či už inštrukcie programu alebo údaje) rozdelené do sekcií. Hlavnou úlohou linkera je pospájať sekcie z jedotlivých modulov do spojitých sekcií, poupravovať odvolávky do týchto sekcií a vytvoriť program.
V assembleri môžeme vytvoriť ľubovolne pomenovanú sekciu, avšak to neznamená, že linker ich dokáže spracovať. Linker musí o každej sekcii vedieť, do akej oblasti pamäti ju má umieststniť. Preto má význam používať iba tie sekcie, ktoré linker pozná.
GNU linker pre MSP430 pozná nasledujúce sekcie:
Názov sekcie | Definícia v assembleri |
---|---|
.text | .text |
.data | .data |
.bss | .section „.bss“,“aw“,@nobits |
.noinit | .section „.noinit“,“aw“,@nobits |
.vectors | .section „.vectors“, „ax“ |
.infomem | .section „.infomem“,“aw“ |
.infomemnobits | .section „.infomemnobits“,“aw“,@nobits |
.bootloader | .section „.bootloader“,“ax“ |
Sekcia .text
Táto sekcia obsahuje inštrukcie programu. Sekcia .text sa umiestňuje do Flash ROM.
Sekcia .data
Táto sekcia obsahuje inicializované dáta. Sekcia je umiestnená v RAM. Zdôrazňujem, že sa jedná o dáta inicializované, to znamená, že dáta majú priradenú hodnotu počas kompilácie. Tu nám vzniká zaujímavý problém: odkiaľ sa tieto inicializačné dáta dostanú do RAM po resetovaní MCU. Riešenie je jednoduché: kópia inicializačných dát je uložená vo Flash ROM, bezprostredne za sekciou .text. Umiestnenie týchto inicializačných dát je definované premennými __data_start_rom a __data_end_rom – jedná sa o premenné, ktorých hodnotu dosadí linker. Našou úlohou je po resete skopírovať tieto dáta do RAM, na adresu definovanú premennou __data_start. O premenných dosádzaných linkerom si viac povieme neskôr.
Sekcia .bss
Táto sekcia obsahuje neinicializované dáta a je umiestnená v RAM, bezprostredne za sekciou .data. Keďže sa jedná o neinicializované dáta, táto sekcia nepotrebuje žiadne miesto v ROM na kópiu dát. Dáta do sekcie .bss môžme umiestniť aj pomocou direktívy .lcomm alebo .comm.
Sekcia .bss, podľa definície, by mala byť po resete vynulovaná. Vyššie jazyky to tak aj robia. V assembleri je to na našej dobrej vôli
Sekcia .noinit
V podstate ide o sekciu .bss, s tým rozdielom, že táto sekcia sa nenuluje. V assembleri nemá veľký význam, pretože inicializáciu sekcií máme plne pod kontrolou. Pri programovaní vo vyšších jazykoch môže pomôcť skrátiť čas štartu programu po resete.
Sekcia .vectors
Ako názov naznačuje, táto sekcia obsahuje prerušovacie vektory. Keďže prerušovacie vektory sú závislé od presného umiestnenia v pamäti, nemá význam, aby bola táto sekcia definovaná vo viacerých objektových súboroch.
Sekcie .infomem a .infomemnobits
Tieto sekcie sa umiestňujú do informačnej pamäti. Sekcia .infomem obsahuje skutočné údaje, ktoré sa aj nahrajú do informačnej pamäte. Toto môže byť problém, ak informačná pamäť obsahuje konfiguračné údaje, ktoré nechceme premazať – napríklad pri upgrade firmware. Tu prichádza ku slovu sekcia .infomemnobits. Táto sekcia je rovnaká ako sekcia .infomem, ale neobsahuje dáta, takže informačnú pamäť neprepíše. Keďže obidve sekcie sa ukladajú na rovnaké miesto v pamäti, v programe môžme použiť len jednu z nich.
Sekcia .bootloader
Dáta v tejto sekcii sa budú nachádzať v pamäti vyhradenej pre bootloader. Keďže táto pamäť nie je prepisovateľná, táto sekcia nie je veľmi užitočná. Snáď len na určenie adresy bootloadera.
Premenné definované linkerom
Existuje niekoľko premenných, ktorých hodnotu dosadzuje linker. To znamená že ich nemusíme nikde definovať. Vďaka tomu, že GNU assembler považuje každý neznámy symbol za externý, nemusíme ich nikde deklarovať. Tieto premenné definujú najmä topológiu pamäti (čo významne napomáha prenositeľnosti programu) a rozloženie nášho programu v pamäti.
Máme k dispozícii tieto premenné:
__stack | adresa vrcholu zásobníka |
__data_start_rom | začiatok kópie inicializačných údajov pre sekciu .data v ROM |
__data_end_rom | koniec kópie inicializačných údajov pre sekciu .data v ROM; ukazuje na prvú adresu za údajmi |
__data_start | začiatok sekcie .data v RAM |
__bss_start | začiatok sekcie .bss v RAM |
__bss_end | koniec sekcie .bss v RAM |
__noinit_start | začiatok sekcie .noinit v RAM |
__noinit_end | koniec sekcie .noinit v RAM |
__vectors_start | začiatok tabuľky prerušovacích vektorov |
__boot_start | začiatok bootloadera |
Ak to nie je dobre vidno: každá uvedená premenná začína dvomi podtrhovníkmi.
Kompilácia, objektové súbory a linkovanie
Každý trochu väčší projekt sa skladá z niekoľkých zdrojových súborov, prípadne knižníc. Každý z týchto zdrojových súborov (modulov) sa kompiluje nezávisle a vytvára sa tzv. objektový súbor. Niekedy sa kombinujú moduly napísané v rôznych jazykoch. V ďalšom kroku linker vezme tieto objektové súbory a spojí ich do jedného programu.
A tu vyvstáva otázka, ako sa tieto nezávisle skompilované objektové súbory spoja dohromady.
Najprv sa pozrime, čo kompilátor vie alebo nevie o module v čase, keď keď náš zdrojový text kompiluje.
Kompilátor vie:
- aké sekcie sú v module a ako sú veľké
- aké dáta sú v nich obsiadnuté
- alé symboly sú viditeľné navonok (globálne symboly)
Kompilátor nevie:
- na akom mieste v pamäti budú sekcie umiestnené
- aké sú hodnoty výrazov v rámci modulu, ak sa odvolávajú na absolútnu adresu
- aké sú hodnoty globálnych symbolov, ak sa odvolávajú na absolútnu adresu
- a už vôbec nevie, aké sú hodnoty externých symbolov
Túto nevedomosť kompilátor rieši tak, že uloží do objektového súboru symbolické odvolávky a zvyšok práce prenechá linkeru.
Linker urobí nasledujúce:
- prehliadne všetky objektové súbory a ku každému nedefinovanému externému symbolu hľadá príslušný globálny symbol
- ak stále existujú nedefinované externé symboly, prehliadne knižnice a vyberie z nich moduly, ktoré tieto symboly definujú
- pospája sekcie z jednotlivých modulov a určí ich umiestnenie v pamäti – tým získame absolútne adresy pre jednotlivé sekcie
- modifikujú sa všetky výrazy, odvolávajúce sa na absolútne adresy
- modifikujú sa všetky výrazy, odvolávajúce sa na externé symboly
Takže toľko teória – prejdime konečne k tomu, ako má vyzerať program v assembleri.
Štruktúra programu v assembleri
Program v assembleri pozostáva zo strojových inštrukcií, direktív a poznámok.
Poznámka začína znakom bodkočiarka a končí na konci riadku. Taktiež je povolený C-čkový formát poznámky: /* … */
Každá instrukcia alebo direktíva sa musí nachádzať na samostatnom riadku.
Pokiaľ využívame preprocesor jazyka C (čo asi budeme), môžme do assemblerovského zdrojového kódu pridať aj direktívy tohoto preprocesora.
Konštanty
123 | dekadické číslo |
0b0101 | binárne číslo |
0123 | oktalové číslo – každé číslo, začínajúce nulou je oktalové číslo |
0x12ab | hexadecimálnme číslo |
‚X | znaková konštanta – nadobúda 8-bitovu ascii hodnotu znaku |
„str“ | reťazcová konštanta |
0e123.25E15 | konštanta s pohyblivou rádovou čiarkou |
Znaková a reťazcová konštanta môžu obsahovať tzv. escape sekvencie – detaily nájdete v dokumentácii.
Symboly, výrazy a návestia
Symbol je symbolický názov, ktorý môže nadobúdať nejakú hodnotu. Symbol má absolútnu hodnotu, ak jej finálna hodnota je známa už počas kompilácie, v opačnom prípade je symbol relokovateľný. Symbolu môžme priradiť hodnotu pomocou niektorej z direktív.
Názov symbolu začína podtrhovníkom alebo písmenom a pokračuje ľubovoľnou kombináciou písmen, číslic a podtrhovníkov.
Výraz sa skladá z konštánt a symbolov, pospájaných operátormi. Detaily nájdete v dokumentácii. O výraze hovoríme, že je absolútny v prípade, že jeho finálna hodnota je známa v čase kompilácie, v opačnom prípade je výraz relokovateľný
Návestie je špeciálny prípad symbolu. Nachádza sa na začiatku riadku a je oddelené od zvyšku riadku dvojbodkou, Návestie nadobúda akruálnu hodnotu pogramového čítača v sekcii, v ktorej sa nachádza. Ak sa jedná o bežnú relokovateľnú sekciu (napríklad .text alebo .data), jeho hodnota je relokovateľná.
Návestie môžeme používať ako akýkoľvek iný symbol.
Treba dávať pozor na jednu vec: ak uvedeme návestie pred deklaráciou sekcie, tak nadobudne hodnotu programového čítača predošlej sekcie:
NAV1: .data ; nesprávny zápis .data ; správny zápis NAV2:
Strojové inštrukcie
Samotným inštrukciám sa nebudem venovať – tieto sú popísané v dokumentácii k MCU a ich zápis je rovnaký aj v GNU assembleri. Jediný rozdiel je, že GNU assembler nepozná symbolické názvy špeciálnych registrov: PC, SP a SR.
Direktívy
Direktívy ovplyvňujú správanie kompilátora, umožňujú definovať premenné a podobne. Stručný prehľad základných direktív nasleduje. Kompletný zoznam direktív nájdete v dokumentácii ku GNU asssembleru.
.include súbor
Vloží obsah špecifikovaného súboru do zdrojového textu.
.text
.data
.section meno,flagy
Tieto direktívy definujú sekcie, v ktorej sa nachádza program alebo dáta, ktoré nasledujú za touto direktívou. Každý byte, vygenerovaný assemblerom musí byť umiestnený v niektorej sekcii.
.end
Ukončuje zdrojový text v assembleri, čokoľvek za touto direktívou sa ignoruje
.global symbol [, symbol …]
.globl symbol [, symbol …]
Definuje symboly ako globálne, to znamená že sú viditeľné pre iné objektové moduly. Nestačí symbol vymenovať v tejto direktíve. Symboly musia byť definované v tele modulu.
.extern symbol [, symbol …]
Definuje symbol ajko externý. V GNU assembleri je každý nedefinovaný symbol považovaný za externý, takže túto direktívu nie je potrebné použlívať.
.byte výraz [, výraz …]
.word výraz [, výraz …]
.short výraz [, výraz …]
.hword výraz [, výraz …]
.int výraz [, výraz …]
.long výraz [, výraz …]
.quad výraz [, výraz …]
.octa výraz [, výraz …]
Za každou z týchto direktív môže nasledovať zoznam celočíselných výrazov. Výsledkom týchto direktív je zoznam hodnôt uložených v pamäti. Veľkosť jednotlivých hodnôt je uvedená v tabuľke:
.byte | 1 byte |
.word | 2 byte |
.short | 2 byte |
.hword | 2 byte |
.int | 4 byte |
.long | 4 byte |
.quad | 8 byte |
.octa | 16 byte |
.single výraz [, výraz …]
.float výraz [, výraz …]
.double výraz [, výraz …]
Za každou z týchto direktív môže nasledovať zoznam výrazov v pohyblivej rádovej čiarke. Výsledkom týchto direktív je zoznam hodnôt, uložených v pamäti. Veľkosť jednotlivých hodnôt je uvedená v tabuľke:
.single | 4 byte |
.float | 4 byte |
.double | 8 byte |
.ascii reťazec [, reťazec …]
.asciiz reťazec [, reťazec …]
Uloží reťazecm ktorý je argumentom týchto direktív, do pamäte. Direktíva .asciiz reťazec ukončí nulou (C-čkový formát).
.lcomm symbol, veľkosť
Vyhradí miesto pre symbol v sekcii .bss. jedná sa o alternatívny spôsob vytvárania sekcie .bss. Nasledujúce príklady majú rovnaký efekt:
.lcomm sym,2 .section ".bss","aw",@nobits sym: .space 2
.comm symbol, veľkosť
Vyhradí miesto pre tzv. common symbol. Common symbol sa vyznačuje tým, že všetky common symboly s rovnakým menom zdieľajú to isté miesto v pamäti. Common symboly sa ukladajú do sekcie .bss.
.space počet [, hodnota]
.skip počet [, hodnota
Zaplní špecifikovaný počet byte špecifikovanou hodnotou. Ak táto hodnota nie je špecifikovamá, vyplní ho nulami.
.set symbol, výraz
.equ symbol, výraz
.equiv symbol, výraz
Priradí symbolu hodnotu výrazu, .equiv vyhlási chybu, ak symbol existuje.
.if abs_výraz
.elseif abs_výraz
.else
.endif
Podmienený preklad, výraz musí byť absolútny výraz
.if sym == 1 .word 128 .elseif sym == 2 .word 512 .else .word 1024 .endif
.rept n
.irp symbol, hodnota [, hodnota …]
.irpc symbol,reťazec
.endr
Opakovacie bloky – opakuje sa obsah medzi direktívou a koncom opakovania .endr. .rept zopakuje blok n-krát, .irp opakuje blok pre každú hodnotu v zozname hodnôt a .irpc opakuje blok pre každý znak v reťazci. V každom opakovaní sa symbol nahradí aktuálnou hodnotou, resp. znakom. Symbol v tele opakovania môžme použiť vo formáte \symbol
.rept 2 .word 1 .endr ; ; vysledok je: ; .word 1 ; .word 1 ; .irp sym,10,20,30 .word sym .endr ; ; vysledok je: ; .word 10 ; .word 20 ; .word 30 ; .irpc sym,abc lsym: .word 20 .endr ; ; vysledok je: ;la: .word 20 ;lb: .word 20 ;lc: .word 20
.macro meno, parametre
.exitm
.endm
Definícia makra, .endm ukončuje makro, .exitm vyskočí z makra ešte pred koncom. Hodnotu parametrov získame ako \p1, \p2 a pod.
; makro, ktore ulozi sucet a+b do r .macro sum,r,a,b mov #a,r add #b,r .endm ; sum r1,10,20 ; ; vysledok je: ; mov #10,r1 ; add #20,r1
.struct [výraz]
Táto direktíva umožňuje definovať štruktúry. V podstate zahajuje absolútnu sekcie, ktorej počiatočný čítač sa nastaví na hodnotu zadaného absolútneho výrazu (alebo na nulu). Každé návestie definované po direktíve .struct bude mať absolútnu hodnoty a môžeme ho používať ako offset jednotlivých členov štruktúry. Štruktúru môžme definovať viacerými spôsobmi – nasledujúce tri príklady sú identické:
.struct ; asi najprehladnejsi sposob deklaracie struktury str_a: .word 0 str_b: .word 0 str_size: .struct ; to iste, ale pouzili sme .space na alokovanie miesta str_a: .space 2 sts_b: .space 2 str_size:.struct ; neprehladna varianta podla dokumentacie str_a: .struct str_a+2 str_b: .struct str_b+2 str_size: . . . .data str: .space str_size ; alokovanie miesta pre strukturu str . . . .text mov #s,r13 ; priklad pouzitia struktury mov sa(r13),r15 mov sb(r13),r15
Uvedomme si, že direktíva nás prepne do vlastnej absolútnej sekcie. Takže po ukončení deklarácie štruktúr musíme zahájiť novú sekciu alebo pokračovať v prerušenej sekcii.
.list
.nolist
.eject
.psize riadky, stĺpce
Direktívy na riadenie formátu listingu
Listingy, mapy a dumpy
V súčasnosti, keď svet ovládajú rôzne IDE, nebýva zvykom pracovať s výstupmi z kompilátora, prípadne linkera. Podľa mňa je to chyba – listing programu alebo mapa obsahuje mnoho cenných informácií o hodnotách symbolov, veľkosti premenných a umiestnení sekcií vo fyzickej pamäte. Niekedy pohľad do listingu alebo mapy môže ušetriť hodiny hľadania tajomných chýb.
A ešte by som rád upozornil na zaujímavú utilitu – msp430-objdump – ktorá nám umožní nahliadnuť do vnútra skompilovaného modulu alebo zlinkovaného programu.
Napríklad msp430-objdump -h program nám vypíše základné atribúty sekcií alebo msp430-objdump -D program nám disassembluje program a my sa môžme pozrieť, či linkerom dosadené referencie zodpovedajú našim predstavám.
Kde nájsť ďalšie informácie
Referenčnú literatúru ku GNU assembleru, linkeru a ďalším utilitám nájdete na stránkach Redhatu (príslušné manuály sa nachádzajú až na konci tejto stránky):