Studiul setului de instrucțiuni al procesorului ARM. Învățarea setului de instrucțiuni pentru procesorul ARM Exemple de instrucțiuni pentru asamblare ARM

GBA ASM - Ziua 2: Câteva informații despre asamblatorul ARM - Arhiva WASM.RU

ARM este compania care produce procesorul GBA. Procesoarele ARM sunt procesoare RISC (spre deosebire de procesoarele INTEL). RISC înseamnă Reduced Instruction Set Computers (CISC - Complex ...). În timp ce aceste procesoare nu au multe instrucțiuni (ceea ce este un lucru bun), instrucțiunile ARM (și poate și alte procesoare RISC, nu știu) au multe utilizări și combinații diferite, ceea ce face ca procesoarele RISC să fie la fel de puternice ca și ele.

Registrele

Nu știu despre alte procesoare ARM, dar cel folosit în GBA are 16 registre și spre deosebire de procesoarele Intel (și altele), toate registrele pot fi folosite în siguranță (de obicei). Registrele sunt următoarele:

r0,r1,r2,r3,r4,r5,r6,r7,r8,r9,r10,r11,r12,r13,r14,r15

Wow! Mult! explic în ordine.

ro: Fă ce vrei!

r2 la r12: la fel

r13: Pe unele sisteme ARM, r13 este un pointer de stivă (SP pe procesoarele INTEL). Nu sunt sigur dacă r13 joacă același rol în GBA, pot doar să vă avertizez să fiți atenți cu el atunci când lucrați cu stiva.

r14: Conține adresa de retur pentru procedurile apelate. Dacă nu le folosiți, atunci puteți face ce doriți cu ele.

r15: Contor de program și steaguri, la fel ca IP (Instruction Pointer pe Intel). Acesta diferă de registrul IP al Intel prin faptul că aveți acces liber la acesta, la fel ca orice alt registru, dar rețineți că modificarea acestuia va determina transferul controlului într-o altă secțiune de cod, iar steagurile se vor schimba .

Să facem un mic exercițiu de matematică... 16 minus 3 (de obicei) ne oferă 13 registre. Nu e misto? Ia-o ușurel.

Acum s-ar putea să vă întrebați ce sunt registrele cu adevărat. Registrele sunt zone speciale de memorie care fac parte din procesor și nu au o adresă validă, ci sunt cunoscute doar după numele lor. Registrele sunt de 32 de biți. Aproape totul în orice limbaj de asamblare folosește registre, așa că ar trebui să le cunoașteți la fel de bine ca și rudele dvs.

Instrucțiuni de asamblare ARM

În primul rând, vreau să încep prin a spune că, în opinia mea, cine a inventat asamblatorul ARM este un geniu.

În al doilea rând, vreau să-l prezint pe bunul meu prieten CMP. Salută-l și poate, doar poate, va deveni și el prietenul tău. CMP înseamnă CoMPare (compara). Această instrucțiune poate compara registrul și numărul, registrul și registrul sau registrul și locația de memorie. Apoi, după comparație, CMP setează steaguri de stare care vă spun rezultatul comparării. După cum vă amintiți, registrul r15 conține steaguri. Ei raportează rezultatul comparației. Instrucțiunea CMP a fost concepută special pentru a seta valoarea acestor steaguri și nimic altceva.

Steagurile pot conține următoarele stări:

    EQ Equal / Egal

    NE Nu este egal

    Set VS Overflow / Set Overflow

    VC overflow Clear

    HI Mai mare / Mai sus

    LS mai jos sau la fel

    PL Plus / Plus

    MI minus / minus

    Set de transport CS

    CC Carry Clear

    GE Mai mare sau egal

    GT Mai mare decât / Mai mult

    LE Mai mic sau egal

    LT Mai puțin decât

    Z este zero

    NZ nu este zero / nu este zero

Aceste stări joacă un rol foarte important în asamblatorul ARM.

NOTĂ: Indicatoarele stochează doar condițiile (Egal cu, Mai mică decât și așa mai departe). Nu mai contează.

Sufixe de condiție

Ați văzut deja instrucțiunea B (Filială). Instrucțiunea B face ceea ce se numește o ramură necondiționată (cum ar fi GoTo în Basic sau JMP în asamblarea INTEL). Dar poate avea un sufix (unul dintre cele enumerate mai sus), caz în care verifică dacă starea steagurilor se potrivește cu el. Dacă nu, atunci instrucțiunea de salt pur și simplu nu este executată. Deci, dacă doriți să verificați dacă registrul r0 este egal cu registrul r4 și apoi să săriți la o etichetă numită label34, atunci trebuie să scrieți următorul cod:

    CMP r0, r4; Comentariile în limbajul de asamblare vin după punct și virgulă (;)

    BEQ label34 ; B este o instrucțiune de săritură, iar EQ este un sens sufix

    ; „Dacă egal”

NOTĂ: În Goldroad Assembler, etichetele nu trebuie să fie urmate de (și nu ar trebui să existe nimic pe șir în afară de numele etichetei.

NOTĂ II: Nu este necesar să scrieți CMP și BEQ cu majuscule, aceasta este doar pentru a vă facilita înțelegerea.

Acum știi să sari în funcție de starea steagurilor, dar ceea ce nu știi este că poți face orice în funcție de starea steagurilor, doar adaugă sufixul necesar oricărei instrucțiuni!

De asemenea, nu trebuie să utilizați CMP pentru a seta starea steagurilor. Dacă doriți, de exemplu, ca instrucțiunea SUB (Scădere) să seteze steaguri, adăugați un sufix „S” la instrucțiune (sunt de la „Setați steaguri”). Acest lucru este util dacă nu doriți să setați starea steagurilor cu o instrucțiune CMP suplimentară, astfel încât să puteți face acest lucru și să săriți dacă rezultatul a fost zero, după cum urmează:

    SUBS r0,r1,0x0FF ; Setează steaguri în funcție de rezultatul execuției

    ; instrucțiuni

    ldrZ r0,=0x0FFFF ; Încărcați în registrul r0 0x0FFFF numai dacă starea

    steaguri este zero.

Revizuire

Astăzi am învățat (un pic mai mult) despre registre. Am aflat și despre flexibilitatea instrucțiunilor ARM, care pot fi executate (sau nu) în funcție de starea steagurilor. Am învățat multe astăzi.

Mâine vom folosi cunoștințele de asamblare ARM dobândite astăzi pentru a afișa imaginea pe ecranul GBA.

Ceva imposibil este imposibil doar până când devine posibil / Jean-Luc Picard, Capt. , USS Enterprise/. Mike H, trad. Acvila

Procesoarele CISC efectuează operații destul de complexe într-o singură instrucțiune, inclusiv operații aritmetice și logice asupra conținutului celulelor de memorie. Instrucțiunile CISC CPU pot fi de lungimi diferite.

În schimb, RISC are un set de instrucțiuni relativ simplu, cu o împărțire clară după tipul de operație:

  • lucrul cu memorie (citirea din memorie în registre sau scrierea din registre în memorie),
  • prelucrarea datelor în registre (aritmetică, logică, deplasări de date la stânga/dreapta sau rotație de biți în registru),
  • instrucțiuni pentru salturi condiționate sau necondiționate la alte adrese.

De regulă (dar nu întotdeauna și numai dacă codul programului intră în memoria cache a controlerului), o instrucțiune este executată într-un ciclu de procesor. Lungimea instrucțiunii procesorului ARM este fixă ​​- 4 octeți (un cuvânt computer). De fapt, un procesor ARM modern poate trece la alte moduri de operare, de exemplu, la modul THUMB, când lungimea instrucțiunii devine 2 octeți. Acest lucru vă permite să faceți codul mai compact. Cu toate acestea, nu acoperim acest mod în acest articol, deoarece nu este acceptat de procesorul Amber ARM v2a. Din același motiv, nu vom lua în considerare astfel de moduri precum Jazelle (optimizat pentru executarea codului Java) și nu vom lua în considerare comenzile NEON - comenzi pentru operații pe date multiple. Totuși, studiem un set de instrucțiuni ARM pur.

Registrele procesorului ARM.

Procesorul ARM are mai multe seturi de registre, dintre care doar 16 sunt disponibile în prezent pentru programator. Există mai multe moduri de funcționare ale procesorului, în funcție de modul de funcționare, se selectează banca corespunzătoare de registre. Aceste moduri de operare sunt:

  • modul aplicație (USR, modul utilizator),
  • modul supervizor sau modul sistemului de operare (SVC, modul supervizor),
  • modul de gestionare a întreruperilor (IRQ, mod de întrerupere) și
  • modul de procesare „întrerupere urgentă” (FIRQ, mod de întrerupere rapidă).

Adică, de exemplu, atunci când are loc o întrerupere, procesorul însuși merge la adresa programului de gestionare a întreruperilor și „comută” automat băncile de registre.

Procesoarele ARM mai vechi, pe lângă modurile de operare de mai sus, au moduri suplimentare:

  • Abort (utilizat pentru a gestiona excepțiile de acces la memorie),
  • Nedefinit (folosit pentru implementarea programatică a coprocesorului) și
  • modul de activitate privilegiat al sistemului de operare System.

Procesorul Amber ARM v2a nu are aceste trei moduri suplimentare.

Pentru Amber ARM v2a, setul de registre poate fi reprezentat după cum urmează:

Registrele r0-r7 sunt aceleași pentru toate modurile.
Registrele r8-r12 sunt comune numai pentru modurile USR, SVC, IRQ.
Registrul r13 este indicatorul stivei. El este în toate modurile.
Registrul r14 - registrul de întoarcere din subrutină este și el propriu în toate modurile.
Register r15 este un pointer către comenzi executabile. Este comun tuturor modurilor.

Se poate observa că modul FIRQ este cel mai izolat, are cele mai multe registre proprii. Acest lucru se face astfel încât unele întreruperi foarte critice să poată fi procesate fără a salva registrele în stivă, fără a pierde timp cu asta.

O atenție deosebită trebuie acordată registrului r15, cunoscut și sub denumirea de pc (Program Counter) - un pointer către comenzile executabile. Este posibil să se efectueze diverse operații aritmetice și logice asupra conținutului său, astfel execuția programului va trece la alte adrese. Cu toate acestea, pentru procesorul ARM v2a implementat în sistemul Amber există unele subtilități în interpretarea biților acestui registru.

Faptul este că în acest procesor, în registrul r15 ( pc), pe lângă indicatorul propriu-zis la comenzile executabile, conține următoarele informații:

Biții 31:28 - steaguri ale rezultatului unei operații aritmetice sau logice
Biții 27 - Masca IRQ de întrerupere, întreruperile sunt dezactivate când bitul este setat.
Biții 26 - Mască de întrerupere FIRQ, întreruperile rapide sunt dezactivate când bitul este setat.
Biți 25:2 - indicatorul către instrucțiunile programului în sine ocupă doar 26 de biți.
Biți 1:0 - modul de funcționare curent al procesorului.
3-Supervizor
2- Întreruperea
1-Întrerupere rapidă
0 - utilizator

În procesoarele ARM mai vechi, toate steagurile și biții de serviciu sunt localizați în registre separate. Registrul de stare curent al programului( cpsr ) și Registrul de stare program salvat ( spsr ), pentru care există comenzi speciale separate de accesat. Acest lucru se face pentru a extinde spațiul de adrese disponibil pentru programe.

Una dintre dificultățile stăpânirii asamblatorului ARM este numele alternative ale unor registre. Deci, așa cum am menționat mai sus, r15 este același computer. Există și r13 - acesta este același sp (Stack Pointer), r14 este lr (Link Register) - registrul adresei de retur din procedură. În afară de asta, r12 este același ip (Intra-Procedure -call scratch register), folosit de compilatorii C într-un mod special pentru a accesa parametrii din stivă. O astfel de denumire alternativă este uneori confuză atunci când te uiți în codul de program al altcuiva - există atât acestea, cât și aceste denumiri de registru.

Caracteristici ale executării codului.

În multe tipuri de procesoare (de exemplu, x86), numai un salt la o altă adresă de program poate fi efectuat în funcție de condiție. Nu este cazul cu ARM. Fiecare instrucțiune de procesor ARM poate fi executată sau nu în funcție de condiție. Acest lucru permite reducerea la minimum a numărului de salturi de program și, prin urmare, utilizarea mai eficientă a conductei (pipeline) a procesorului.

La urma urmei, ce este o conductă? O instrucțiune de procesor este acum preluată din codul programului, cea anterioară este deja decodificată și cea anterioară este deja executată. Acesta este cazul conductei de procesoare Amber A23 în 3 etape pe care o folosim în proiectul nostru pentru placa Mars Rover2Rover2. Modificarea procesorului Amber A25 are o conductă în 5 etape, este și mai eficientă. Dar, există un DAR mare. Instrucțiunile de salt forțează procesorul să elibereze conducta și să o reumple. Astfel, este selectată o nouă comandă, dar încă nu există nimic de decodat, cu atât mai puțin ceva de executat imediat. Eficiența execuției codului scade odată cu tranzițiile frecvente. Procesoarele moderne au tot felul de mecanisme de predicție a ramurilor care optimizează cumva umplerea conductei, dar nu este cazul procesorului nostru. În orice caz, ARM a fost înțelept să facă posibil ca fiecare instrucțiune să fie executată condiționat.

Într-un procesor ARM, în orice tip de instrucțiune, cei patru biți ai condiției de execuție a instrucțiunii sunt codificați în cei patru biți superiori ai codului de instrucțiune:

Există 4 indicatori de stare în procesor:
. Negativ - rezultatul operației este negativ,
. Zero - rezultatul este zero,
. Carry - la efectuarea unei operații cu numere nesemnate, a avut loc o transportare,
. oVerflow - la efectuarea unei operații cu numere semnate, a avut loc o depășire, rezultatul nu este plasat în registru)

Aceste 4 steaguri formează multe combinații posibile de condiții:

Cod Sufix Sens Steaguri
4"h0 echivalentul Egal Z set
4"h1 ne nu este egal Z clar
4"h2 cs/hs Carry set / nesemnat mai mare sau același Cset
4"h3 cc/lo Purtați clar/nesemnat mai jos C clar
4"h4 mi minus/negativ N set
4"h5 pl Plus / pozitiv sau zero N clar
4"h6 contra revărsare V set
4"h7 vc fara preaplin V clar
4"h8 Bună Nesemnat mai sus C set și Z clar
4" h9 ls Nesemnat mai jos sau același C clear sau Z set
4" ha GE Semnat mai mare sau egal N == V
4"hb lt Semnat mai puțin de N != V
4"hc gt Semnat mai mare decât Z == 0,N == V
4" hd le Semnat mai mic sau egal Z == 1 sau N != V
4 "el al mereu (neconditionat)
4" hf - stare invalidă

Acum, o altă dificultate în învățarea instrucțiunilor procesorului ARM decurge din aceasta - numeroasele sufixe care pot fi adăugate la codul de instrucțiuni. De exemplu, plus, cu condiția ca indicatorul Z să fie setat, este comanda addeq ca sufix add + eq. Salt la subrutină dacă flag N=0 este blpl ca bl + sufix pl .

Steaguri ( Negativ, Zero, Carry, Overflow ) nu același lucru este setat întotdeauna în timpul operațiilor aritmetice sau logice, așa cum se întâmplă, să zicem, într-un procesor x86, ci doar atunci când programatorul dorește. Pentru a face acest lucru, există un alt sufix la mnemonicul comenzii: „s” (codat în codul de comandă de bitul 20). Astfel, comanda add nu schimbă steagurile, dar comanda add schimbă steagurile. Și poate exista și o comandă de adăugare condiționată, dar care schimbă steaguri. De exemplu: addgts . Este clar că numărul de combinații posibile de nume de comandă cu sufixe diferite de execuție condiționată și steaguri de setare face ca codul de asamblare al procesorului ARM să fie foarte ciudat și dificil de citit. Cu toate acestea, cu timpul te obișnuiești și începi să înțelegi acest text.

Operatii aritmetice si logice (Procesarea datelor).

Procesorul ARM poate efectua diverse operații aritmetice și logice.

Codul real de operare pe patru biți (Opcode) este conținut în biții de instrucțiuni ale procesorului.

Orice operație este efectuată asupra conținutului registrului și a așa-numitului shifter_operand . Rezultatul operației este plasat într-un registru. Rn și Rd pe patru biți sunt indici de registru din banca activă a procesorului.

În funcție de bitul I 25 shifter_operand este tratat fie ca o constantă numerică, fie ca un index al celui de-al doilea registru al operandului și chiar o operație de deplasare a valorii celui de-al doilea operand.

Exemple simple de comenzi de asamblare ar arăta, de exemplu, astfel:

adăugați r0,r1,r2 @ puneți în registrul r0 suma valorilor registrelor r1 și r2
sub r5,r4,#7 @ puneți diferența (r4-7) în registrul r5

Operațiile efectuate sunt codificate după cum urmează:

4"h0 și Logic AND Rd:= Rn AND shifter_operand
4"h1 eor XOR Rd:= Rn XOR shifter_operand
4"h2 sub Scădere aritmetică Rd:= Rn - shifter_operand
4"h3 rsb Scădere inversă aritmetică Rd:= shifter_operand - Rn
4"h4 add Adunarea aritmetică Rd:= Rn + shifter_operand
4"h5 adc Adunare aritmetică plus flag de transport Rd:= Rn + shifter_operand + Carry Flag
4"h6 sbc Carry Arithmetic Scădere Rd:= Rn - shifter_operand - NOT(Carry Flag)
4"h7 rsc Carry Arithmetic Scădere inversă Rd:= shifter_operand - Rn - NOT(Carry Flag)
4"h8 tst ȘI logic, dar fără a salva rezultatul, sunt modificate numai steagurile Rn AND shifter_operand S bit întotdeauna setat
4"h9 teq SAU exclusiv logic, dar fără a stoca rezultatul, sunt modificate doar steagurile Rn EOR shifter_operand
S bit întotdeauna setat
4 "ha cmp Comparație, sau mai degrabă scădere aritmetică fără a stoca rezultatul, se schimbă doar steagurile Rn - shifter_operand S bit întotdeauna setat
4 "hb cmn Comparație a adunării inverse, sau mai degrabă aritmetice, fără a ne aminti rezultatul, doar steagurile Rn + shifter_operand S bit întotdeauna setat sunt modificate
4"hc orr logic SAU Rd:= Rn SAU shifter_operand
4"hd mov Copiați valoarea Rd:= shifter_operand (fără primul operand)
4"he bic Reset biți Rd:= Rn AND NOT(shifter_operand)
4"hf mvn Copiați valoarea inversă Rd:= NOT shifter_operand (fără primul operand)

schimbător de butoi.

Procesorul ARM are o schemă specială „barrel shifter” care permite ca unul dintre operanzi să fie deplasat sau rotit cu un anumit număr de biți înainte de orice operație aritmetică sau logică. Aceasta este o caracteristică destul de interesantă a procesorului, care vă permite să creați cod foarte eficient.

De exemplu:

@ înmulțirea cu 9 înseamnă înmulțirea unui număr cu 8
@ prin deplasarea la stânga cu 3 biți plus un alt număr
adăugați r0, r1, r1, lsl #3 @ r0= r1+(r1<<3) = r1*9

@ înmulțirea cu 15 este înmulțirea cu 16 minus numărul
rsb r0, r1, r1, lsl #4 @ r0= (r1<<4)-r1 = r1*15

@ acces la un tabel de cuvinte de 4 octeți, unde
@r1 este adresa de bază a tabelului
@r2 este indexul elementului din tabel
ldr r0,

În plus față de deplasarea logică la stânga lsl, există și o deplasare logică la dreapta lsr și o deplasare aritmetică la dreapta asr (deplasare care păstrează semnul, bitul cel mai semnificativ este înmulțit de la stânga simultan cu deplasarea).

Există, de asemenea, o rotație a biților ror - biții sunt împinși spre dreapta, iar cei care sunt împinși în afară sunt împinși spre stânga.
Există o schimbare de un bit prin indicatorul C - aceasta este comanda rrx. Valoarea registrului este deplasată la dreapta cu un bit. În stânga, steagul C este încărcat în bitul înalt al registrului

Deplasarea poate fi efectuată nu printr-o constantă numerică fixă, ci prin valoarea celui de-al treilea registru-operand. De exemplu:

se adaugă r0, r1, r1, lsr r3 @ este r0 = r1 + (r1>>r3);
se adaugă r0, r0, r1, lsr r3 @ este r0 = r0 + (r1>>r3);

Deci, shifter_operand este ceea ce descriem în instrucțiunile de asamblare precum „r1, lsr r3” sau „r2, lsl #5”.

Cel mai interesant lucru este că folosirea schimburilor în operațiuni nu costă nimic. Aceste schimbări nu necesită (de obicei) cicluri suplimentare de ceas și sunt foarte bune pentru performanța sistemului.

Utilizarea operanzilor numerici.

Operațiile aritmetice sau logice pot folosi ca al doilea operand nu numai conținutul registrului, ci și o constantă numerică.

Din păcate, există o limitare importantă aici. Deoarece toate comenzile au o lungime fixă ​​de 4 octeți (32 de biți), nu va funcționa să codifice „orice” număr în el. În codul de operare, 4 biți sunt deja ocupați de codul de condiție de execuție (Cond), 4 biți pentru codul de operare în sine (Opcode), apoi, 4 biți - registrul receptorului Rd și încă 4 biți - registrul primului operand Rn, plus alte steaguri diferite I 25 (doar indică o constantă numerică în codul operației) și S 20 (setarea steaguri după operație). În total, rămân doar 12 biți pentru o posibilă constantă, așa-numitul shifter_operand - am văzut asta mai sus. Deoarece 12 biți pot codifica numere doar într-un interval restrâns, dezvoltatorii procesorului ARM au decis să codifice constanta după cum urmează. Cei doisprezece biți ai shifter_operand sunt împărțiți în două părți: indicele de rotație de patru biți encode_imm și valoarea numerică reală de opt biți imm_8 .

Pe un procesor ARM, o constantă este definită ca un număr de 8 biți în interiorul unui număr de 32 de biți, rotit la dreapta cu un număr par de biți. Acesta este:

imm_32 = imm_8 ROR (encode_imm *2)

S-a dovedit destul de inteligent. Se pare că nu orice constantă numerică poate fi folosită în comenzile de asamblare.

Poti sa scrii

adăugați r0, r2, #255 @ constantă zecimală
adăugați r0, r3, #0xFF @ constantă în hex

deoarece 255 este în intervalul de 8 biți. Aceste comenzi vor fi compilate astfel:

0: e28200ff adăugați r0, r2, #255; 0xff
4: e28300ff adăugați r0, r3, #255; 0xff

Și poți chiar să scrii

adăugați r0, r4, #512
adăugați r0, r5, 0x650000

Codul compilat va arăta astfel:

0: e2840c02 adăugați r0, r4, #512; 0x200
4: e2850865 adăugați r0, r5, #6619136; 0x650000

În acest caz, numărul 512 în sine, desigur, nu se încadrează într-un octet. Dar, pe de altă parte, ne imaginăm în formă hexazecimală 32'h00000200 și vedem că acesta este 2 întors la dreapta cu 24 de biți (1 ror 24). Coeficientul de rotație este jumătate din cel al lui 24, adică 12. Deci, shifter_operand = ( 4'hc , 8'h02 ) - aceștia sunt cei doisprezece biți cei mai puțin importanți ai comenzii. Este la fel și cu numărul 0x650000. Pentru acesta shifter_operand = ( 4'h8, 8'h65 ).

Este clar că nu se poate scrie

adăugați r0, r1,#1234567

sau nu poti scrie

mov r0, #511

deoarece aici numărul nu poate fi reprezentat ca imm_8 și encode_imm - factorul de rotație. Compilatorul de asamblare va arunca o eroare.

Ce să faci când o constantă nu poate fi codificată direct în shifter_operand? Trebuie să faci tot felul de trucuri.
De exemplu, puteți încărca mai întâi numărul 512 într-un registru gratuit, apoi puteți scădea unul:

mov r0, #511
sub r0,r0,#1

A doua modalitate de a încărca un anumit număr într-un registru este să-l citiți dintr-o variabilă special rezervată în memorie:

ldr r7,my_var
.....
my_var: .word 0x123456

Cel mai simplu mod de a scrie este astfel:

ldr r2,=511

În acest caz (observați semnul „=") dacă constanta poate fi reprezentată ca imm_8 și encode_imm , dacă poate fi încadrată într-un shifter_operand de 12 biți, atunci compilatorul de asamblare va compila automat ldr într-o instrucțiune mov. Dar dacă numărul nu poate fi reprezentat în acest fel, atunci compilatorul însuși va rezerva o celulă de memorie pentru această constantă în program și va da el însuși un nume acestei celule de memorie și va compila comanda în ldr .

Iată ce am scris:

ldr r7,my_var
ldr r8,=511
ldr r8,=1024
ldr r9,=0x3456
........
My_var: .word 0x123456

După compilare am primit asta:

18: e59f7030 ldr r7, ; 50
1c: e59f8030 ldr r8, ; 54
20: e3a08b01 mov r8, #1024; 0x400
24: e59f902c ldr r9, ; 58
.............
00000050 :
50:00123456 .cuvânt 0x00123456
54:000001ff .cuvânt 0x000001ff
58:00003456 .cuvânt 0x00003456

Rețineți că compilatorul folosește adresarea memoriei în raport cu registrul computerului (aka r15 ).

Citirea unei celule de memorie și scrierea unui registru în memorie.

După cum am scris mai sus, procesorul ARM poate efectua numai operații aritmetice sau logice asupra conținutului registrelor. Datele pentru operații trebuie citite din memorie și rezultatul operațiunilor trebuie scris înapoi în memorie. Există comenzi speciale pentru aceasta: ldr (probabil din combinația „LoaD Register”) pentru citire și str (probabil „STOre Register”) pentru scriere.

S-ar părea că sunt doar două echipe, dar de fapt au multe variante. Priviți doar cum sunt codificate comenzile ldr /str ale procesorului Amber ARM pentru a vedea câți biți de semnalizare auxiliari L 20 , W 21 , B 22 , U 23 , P 24 , I 25 - și determină comportamentul specific al comenzii :

  • Bitul L 20 specifică dacă se scrie sau se citește. 1 - ldr , citit, 0 - str , scrie.
  • Bitul B 22 determină dacă se citește/scrie un cuvânt de 32 de biți sau un octet de 8 biți. 1 înseamnă operare pe octeți. Când un octet este citit într-un registru, biții superiori ai registrului sunt setați la zero.
  • Bitul I 25 specifică utilizarea câmpului Offset. Dacă I ​​25 ==0, atunci Offset este interpretat ca un offset numeric, care trebuie fie adăugat la adresa de bază din registru, fie scăzut. Dar a adăuga sau a scădea depinde de bitul U 23 .

(Cond) - condiție pentru efectuarea operației. Este interpretată în același mod ca și pentru comenzile logice/aritmetice - citirea sau scrierea pot fi condiționate.

Astfel, în textul de asamblare, puteți scrie ceva de genul acesta:

ldr r1, @ în registrul r1 citește cuvântul la adresa din registrul r0
ldrb r1, @ în registrul r1 citește octetul la adresa din registrul r0
ldreq r2, @ citire de cuvinte condiționată
ldrgtb r2, @ octet condiționat citit
ldr r3, @ citiți cuvântul la adresa 8 relativ la adresa din registrul r4
ldr r4, @ citiți cuvântul la adresa -16 relativ la adresa din registrul r5

După compilarea acestui text, puteți vedea codurile reale ale acestor comenzi:

0: e5901000 ldr r1,
4: e5d01000 ldrb r1,
8: 05912000 ldreq r2,
c:c5d12000 ldrbgt r2,
10: e5943008 ldr r3,
14: e5154010 ldr r4,

În exemplul de mai sus, folosesc doar ldr , dar str este folosit aproape în același mod.

Există moduri de acces la memorie pre-index și post-index write-back. În aceste moduri, indicatorul de acces la memorie este actualizat înainte sau după executarea instrucțiunii. Dacă sunteți familiarizat cu limbajul de programare C, atunci sunteți familiarizat cu constructele de acces pointer precum ( *psource++;) sau ( a=*++psource;). În procesorul ARM, acest mod de acces la memorie este doar implementat. Când este executată o comandă de citire, două registre sunt actualizate simultan - registrul receptor primește valoarea citită din memorie și valoarea din registrul-pointer către celula de memorie se deplasează înainte sau înapoi.

Scrierea acestor comenzi, în opinia mea, este oarecum ilogic. Este nevoie de mult timp să te obișnuiești.

ldr r3, ! @psrc++; r3 = *psrc;
ldr r3, , #4 @ r3 = *psrc; psrc++;

Prima comandă ldr incrementează mai întâi indicatorul, apoi îl citește. A doua comandă citește mai întâi, apoi crește indicatorul. Valoarea pointerului psrc este în registrul r0 .

Toate exemplele de mai sus au fost pentru cazul în care bitul I 25 din codul de comandă a fost resetat. Dar încă se poate instala! Atunci valoarea câmpului Offset nu va conține o constantă numerică, ci al treilea registru implicat în operație. Mai mult decât atât, valoarea celui de-al treilea registru poate fi încă pre-deplasată!

Iată exemple de posibile variante de cod:

0: e7921003 ldr r1, @ adresă de citire - suma valorilor din registrele r2 și r3
4: e7b21003 ldr r1, ! @ la fel, dar după citirea r2 va fi incrementat cu valoarea de la r3
8: e6932004 ldr r2, , r4 @ va citi mai întâi la r3, apoi r3 va fi incrementat cu r4
c: e7943185 ldr r3, @ citiți adresa r4+r5*8
10: e7b43285 ldr r3, ! @ adresa pentru a citi r4+r5*32, după citirea r4 va fi setat la valoarea acestei adrese
14: e69431a5 ldr r3, , r5, lsr #3 @ citiți adresa r4, după executarea comenzii r4 va fi setat la r4+r5/8

Iată variantele comenzilor de citire/scriere din procesorul ARM v2a.

La modelele mai vechi de procesoare ARM, această varietate de instrucțiuni este și mai mare.
Acest lucru se datorează faptului că procesorul permite, de exemplu, să citească nu numai cuvinte (numere de 32 de biți) și octeți, ci și jumătate de cuvinte (16 biți, 2 octeți). Apoi sufixul „h”, din cuvântul jumătate de cuvânt, este adăugat la comenzile ldr / str. Comenzile vor arăta ca ldrh sau strh. Există, de asemenea, comenzi pentru a încărca jumătăți de cuvinte ldrsh sau octeți ldrsb interpretați ca numere cu semn. În aceste cazuri, bitul cel mai semnificativ al cuvântului de bandă sau octetul încărcat este înmulțit în cei mai semnificativi biți ai întregului cuvânt din registrul de destinație. De exemplu, încărcarea semi-cuvântului 0xff25 cu comanda ldrsh are ca rezultat 0xffffff25 în registrul de destinație.

Citiri și scrieri multiple.

Comenzile ldr /str nu sunt singurele pentru accesarea memoriei. Procesorul ARM are, de asemenea, instrucțiuni care vă permit să efectuați transfer de bloc - puteți încărca conținutul mai multor cuvinte consecutive din memorie în mai multe registre deodată. De asemenea, este posibil să scrieți succesiv valorile mai multor registre în memorie.

Mnemonicii comenzilor de transfer de bloc încep cu rădăcina ldm (LoaD Multiple) sau stm (Store Multiple). Dar apoi, ca de obicei în ARM, povestea începe cu sufixe.

În general, comanda arată astfel:

op(cond)(mod) Rd(, {Register list} !}

Sufixul (Cond) este de înțeles, aceasta este condiția pentru executarea comenzii. Sufixul (modul) este modul de transmisie, despre asta mai târziu. Rd este un registru care specifică adresa de bază din memorie de citit sau scris. Un semn de exclamare după registrul Rd indică faptul că acesta va fi modificat după o operație de citire/scriere. Lista registrelor care sunt încărcate din memorie sau descărcate în memorie este (Register list) .

Lista de registre este specificată în acolade separate prin virgule sau ca un interval. De exemplu:

stm r0,(r3,r1, r5-r8)

Scrierea în memorie va fi făcută în neregulă. Lista indică pur și simplu ce registre vor fi scrise în memorie și atât. Codul de comandă are 16 biți rezervați pentru Lista de registre, la fel ca și numărul de registre din banca procesorului. Fiecare bit din acest câmp indică ce registru va fi implicat în operație.

Acum pentru modul citire/scriere. Există unde să te încurci. Faptul este că diferite nume de mod pot fi folosite pentru aceeași acțiune.

Dacă faci o mică digresiune lirică, atunci trebuie să vorbești despre... stiva. Stiva este o modalitate de a accesa datele de tip LIFO - Last In First Out (wiki) - last in, first out. Stiva este utilizată pe scară largă în programare la apelarea procedurilor și salvarea stării registrelor la intrarea în funcții și restabilirea acestora la ieșire, precum și la trecerea parametrilor procedurilor apelate.

Stiva din memorie este, cine ar fi crezut, patru tipuri.

Primul tip este complet descendent. Acesta este momentul în care indicatorul de stivă indică un element de stivă ocupat, iar stiva crește în direcția adreselor descrescătoare. Când trebuie să puneți un cuvânt pe stivă, atunci mai întâi indicatorul stivei este decrementat (Decrement Before), apoi cuvântul este scris la adresa indicatorului stivei. Când trebuie să eliminați un cuvânt de calculator din stivă, atunci cuvântul este citit la valoarea curentă a indicatorului stivei, apoi indicatorul se mută în sus (Incrementează după).

Al doilea tip este complet ascendent. Stiva nu crește în jos, ci în sus, spre adrese mari. Pointerul indică și elementul ocupat. Când trebuie să puneți un cuvânt pe stivă, atunci mai întâi indicatorul stivei este incrementat, apoi cuvântul este scris de indicator (Increment Before). Când este necesar să scoatem din stivă, citim mai întâi de indicatorul de stivă, deoarece indică către elementul ocupat, apoi indicatorul de stivă este decrementat (Decrement After).

Al treilea tip este Empty Descending. Stiva crește în jos, ca în cazul Full Descending , dar diferența este că pointerul stivei indică o celulă neocupată. Astfel, atunci când trebuie să puneți un cuvânt pe stivă, se face imediat o înregistrare, apoi indicatorul stivei este decrementat (Decrement After). La scoaterea din stivă, indicatorul este mai întâi incrementat, apoi citit (Increment Before).

Al patrulea tip este Empty Ascending. Sper că totul este clar - stiva crește. Indicatorul stivei indică un element gol. Apăsați pe stivă înseamnă să scrieți un cuvânt la adresa indicatorului stivei și să incrementați indicatorul stivei (Increment After ). Ieșiți din stivă - micșorați indicatorul stivei și citiți cuvântul (Decrement Before ).

Astfel, în timpul operațiunilor cu stiva, trebuie să măriți sau să micșorați indicatorul - (Incrementare/Scădere) înainte sau după (Înainte/După) citire/scriere în memorie, în funcție de tipul stivei. Procesoarele Intel, de exemplu, au comenzi speciale pentru lucrul cu stiva, cum ar fi PUSH (puneți un cuvânt pe stivă) sau POP (trageți un cuvânt din stivă). Nu există comenzi speciale în procesorul ARM, dar sunt folosite comenzile ldm și stm.

Dacă implementați stiva folosind instrucțiunile procesorului ARM, atunci obțineți următoarea imagine:

De ce aceeași echipă trebuia să primească nume diferite? Nu înțeleg deloc... Aici, desigur, trebuie menționat că standardul stivei pentru ARM este încă Full Descending.

Indicatorul stivei de pe un procesor ARM este registrul sp sau r13. Acesta este, în general, un astfel de acord. Desigur, scrierea stm sau citirea ldm se poate face și cu alte registre de bază. Cu toate acestea, trebuie să vă amintiți cum diferă registrul sp de alte registre - poate fi diferit în diferite moduri de operare a procesorului (USR, SVC, IRQ, FIRQ), deoarece au propriile bănci de registre.

Și încă o notă. Scrieți în codul de asamblare ARM o linie ca apăsați (r0-r3), Sigur ca poti. Dar de fapt va fi aceeași comandă stmfd sp!,(r0-r3).

În cele din urmă, voi da un exemplu de cod de asamblare și textul său compilat dezasamblat. Avem:


stmfd sp!,(r0-r3)
stmdb sp!,(r0-r3)
apăsați (r0-r3)

@aceste trei instrucțiuni sunt aceleași și fac același lucru
pop(r0-r3)
ldmia sp!,(r0-r3)
ldmfd r13!,(r0-r3)

Stmfd r4,(r0-r3,r5,r8)
stmea r4!,(r0-r3,r7,r9,lr,pc)
ldm r5,(r0,buc)

Primim după compilare:

0: e92d000f push(r0, r1, r2, r3)
4: e92d000f apăsare (r0, r1, r2, r3)
8: e92d000f push (r0, r1, r2, r3)
c:e8bd000f pop(r0, r1, r2, r3)
10: e8bd000f pop (r0, r1, r2, r3)
14: e8bd000f pop (r0, r1, r2, r3)
18: e904012f stmdb r4, (r0, r1, r2, r3, r5, r8)
1c: e8a4c28f stmia r4!, (r0, r1, r2, r3, r7, r9, lr, pc)
20: e8958001 ldm r5, (r0, pc)

Tranziții în programe.

Programarea nu este posibilă fără tranziții. În orice program, există atât execuția ciclică a codului, cât și apelurile de proceduri, funcții, există și execuția condiționată a secțiunilor de cod.

Există doar două comenzi în procesorul Amber ARM v2a: b (din cuvântul Branch - ramură, tranziție) și bl (Sucursală cu Link - tranziție cu salvarea adresei de retur).

Sintaxa comenzii este foarte simplă:

b(cond) etichetă
bl(cond) etichetă

Este clar că orice tranziție poate fi condiționată, adică în program pot exista astfel de cuvinte ciudate formate din rădăcinile „b” și „bl” și sufixe de condiție (Cond), cuvinte ciudate:

beq, bne, bcs, bhs, bcc, blo, bmi, bpl, bvs, bvc, bhi, bls, bge, bgt, ble, bal, b

bleq, blne, blcs, blhs, blcc, bllo, blmi, blpl, blvs, blvc, blhi, blls, blge, blgt, blle, blal, bl

Varietatea este uimitoare, nu-i așa?

Instrucțiunea de salt conține un offset de 24 de biți. Adresa de salt este calculată ca suma valorii curente a indicatorului pc și a numărului de offset deplasat cu 2 biți la stânga, interpretat ca un număr cu semn:

PC nou = pc + Offset*4

Astfel, intervalul de salt este de 32 MB înainte sau înapoi.

Să luăm în considerare ce este o sucursală cu salvarea adresei de retur bl. Această comandă este folosită pentru a apela subrutine. O caracteristică interesantă a acestei instrucțiuni este că adresa de retur din procedura atunci când procedura este apelată nu este stocată pe stivă, ca în cazul procesoarelor Intel, ci în registrul obișnuit r14 . Apoi, pentru a reveni din procedură, nu este necesară o comandă ret specială, ca și în cazul acelorași procesoare Intel, dar pur și simplu puteți copia valoarea lui r14 înapoi pe pc . Acum este clar de ce registrul r14 are un nume alternativ lr (Link Register).

Luați în considerare rutina outbyte din proiectul hello-world pentru Amber SoC.

000004a0<_outbyte>:
4a0: e59f1454 ldr r1, ; 8fc< адрес регистра данных UART >
4a4: e59f3454 ldr r3, ; 900< адрес регистра статуса UART >
4a8: e5932000 ldr r2, ; citiți starea curentă
4ac: e2022020 și r2, r2, #32
4b0: e3520000 cmp r2, #0; verificați dacă UART nu este ocupat
4b4: 05c10000 strbeq r0, ; scrieți un caracter în UART numai dacă nu este ocupat
4b8: 01b0f00e movseq pc, lr ; revenire condiționată de la procedură dacă UART nu era ocupat
4bc: 1affff9 bne 4a8<_outbyte+0x8>; buclă pentru a verifica starea UART

Cred că din comentariile acestui fragment este clar cum funcționează această procedură.

O altă notă importantă despre tranziții. Registrul r15 (pc) poate fi utilizat în operații aritmetice sau logice normale ca registru de destinație. Așadar, o comandă de genul add pc,pc,#8 este destul de o instrucțiune pentru a trece la o altă adresă.

Mai trebuie făcută o remarcă despre tranziții. Procesoarele ARM mai vechi au instrucțiuni suplimentare de ramificație bx, blx și blj. Acestea sunt comenzi pentru trecerea la fragmente de cod cu un sistem de comandă diferit. Bx /blx vă permite să faceți o tranziție la codul THUMB pe 16 biți al procesoarelor ARM. Blj este un apel de procedură al sistemului de comandă Jazelle (suport pentru limbajul Java în procesoarele ARM). Amber ARM v2a nu are aceste comenzi.

Salutare tuturor!
Prin ocupație, sunt programator Java. Ultimele luni de muncă m-au făcut să fac cunoștință cu dezvoltarea pentru Android NDK și, în consecință, să scriu aplicații native în C. Aici m-am lovit de problema optimizării bibliotecilor Linux. Mulți s-au dovedit a fi absolut neoptimizați pentru ARM și au încărcat puternic procesorul. Anterior, practic nu am programat în assembler, așa că la început a fost dificil să încep să învăț acest limbaj, dar cu toate acestea am decis să încerc. Acest articol este scris, ca să spunem așa, de la un începător pentru începători. Voi încerca să descriu elementele de bază pe care le-am studiat deja, sper că cineva va fi interesat. În plus, voi fi bucuros să primesc critici constructive din partea profesioniștilor.

Introducere
Deci, mai întâi, să ne dăm seama ce este ARM. Wikipedia oferă această definiție:

Arhitectura ARM (Advanced RISC Machine, Acorn RISC Machine, advanced RISC machine) este o familie de nuclee de microprocesoare licențiate pe 32 și 64 de biți dezvoltate de ARM Limited. Compania este angajată exclusiv în dezvoltarea de nuclee și instrumente pentru acestea (compilatoare, instrumente de depanare etc.), câștigând din licențierea arhitecturii către producători terți.

Dacă cineva nu știe, acum majoritatea dispozitivelor mobile, tabletele sunt dezvoltate pe această arhitectură specială a procesorului. Principalul avantaj al acestei familii este consumul redus de energie, datorită căruia este adesea folosit în diverse sisteme încorporate. Arhitectura a evoluat de-a lungul timpului, iar de la ARMv7 au fost definite 3 profiluri: 'A' (aplicatie) - aplicatii, 'R' (timp real) - in timp real, 'M' (microcontroller) - microcontroler. Poți citi istoria dezvoltării acestei tehnologii și alte date interesante pe Wikipedia sau google-l pe Internet. ARM acceptă diferite moduri de operare (Thumb și ARM, în plus, a apărut recent Thumb-2, care este un amestec de ARM și Thumb). În acest articol, vom lua în considerare modul ARM în sine, în care este executat un set de instrucțiuni pe 32 de biți.

Fiecare procesor ARM este construit din următoarele blocuri:

  • 37 de registre (dintre care doar 17 sunt vizibile în timpul dezvoltării)
  • Unitate logică aritmetică (ALU) - execută sarcini aritmetice și logice
  • Baril shifter - un dispozitiv conceput pentru a muta blocuri de date cu un anumit număr de biți
  • CP15 - un sistem special care controlează coprocesoarele ARM
  • Decodor de instrucțiuni - se ocupă de transformarea unei instrucțiuni într-o secvență de micro-operații
Acestea nu sunt toate componentele ARM, dar aprofundarea în jungla construcției procesoarelor nu este inclusă în subiectul acestui articol.
Execuția conductei
Procesoarele ARM folosesc o conductă în 3 etape (de la ARM8, a fost implementată o conductă în 5 etape). Să luăm în considerare o conductă simplă folosind procesorul ARM7TDMI ca exemplu. Execuția fiecărei instrucțiuni constă din trei pași:

1. Etapa de eșantionare (F)
În această etapă, instrucțiunile curg de la RAM la conducta procesorului.
2. Etapa de decodare (D)
Instrucțiunile sunt decodificate și tipul lor este recunoscut.
3. Etapa de execuție (E)
Datele intră în ALU și sunt executate, iar valoarea rezultată este scrisă în registrul specificat.

Dar atunci când dezvoltați, trebuie să țineți cont de faptul că există instrucțiuni care folosesc mai multe cicluri de execuție, de exemplu, încărcarea (LDR) sau stocarea. Într-un astfel de caz, faza de execuție (E) este împărțită în faze (E1, E2, E3...).

Execuție condiționată
Una dintre cele mai importante funcții ale asamblatorului ARM este execuția condiționată. Fiecare instrucțiune poate fi executată condiționat și pentru aceasta sunt folosite sufixe. Dacă se adaugă un sufix la numele unei instrucțiuni, atunci parametrii sunt verificați înainte de a fi executat. Dacă parametrii nu se potrivesc cu condiția, atunci instrucțiunea nu este executată. Sufixe:
MI - număr negativ
PL - pozitiv sau zero
AL - executa întotdeauna instrucțiunea
Există mult mai multe sufixe de execuție condiționată. Citiți restul sufixelor și exemplelor din documentația oficială: documentația ARM
Acum este timpul să luați în considerare...
Elementele de bază ale sintaxei asamblatorului ARM
Pentru cei care au lucrat anterior cu asamblator, acest articol poate fi sărit peste. Pentru toți ceilalți, voi descrie elementele de bază ale lucrului cu acest limbaj. Deci, fiecare program în limbaj de asamblare este format din instrucțiuni. Instrucțiunea este creată astfel:
(etichetă) (instrucțiune|operanzi) (@ comentariu)
Eticheta este un parametru opțional. O instrucțiune este direct un mnemonic pentru o instrucțiune către procesor. Instrucțiunile de bază și utilizarea lor vor fi discutate în continuare. Operanzi - constante, adrese de registru, adrese din RAM. Comentariul este un parametru opțional care nu afectează execuția programului.
Înregistrați nume
Sunt permise următoarele nume de registru:
1.r0-r15

3.v1-v8 (registri variabile, de la r4 la r11)

4.sb și SB (registru static, r9)

5.sl și SL (r10)

6.fp și FP (r11)

7.ip și IP (r12)

8.sp și SP (r13)

9.lr și LR (r14)

10.buc și PC (contor de programe, r15).

Variabile și constante
În asamblatorul ARM, ca orice alt limbaj de programare (practic), pot fi folosite variabile și constante. Ele sunt împărțite în următoarele tipuri:
  • Numeric
  • joc de inteligență
  • Şir
Variabilele numerice sunt inițializate astfel:
un SETA 100; se creează o variabilă numerică „a” cu valoarea 100.
Variabile șir:
improba SETS „literal”; variabila improbabilă este creată cu valoarea „literal”. ATENŢIE! Valoarea variabilei nu poate depăși 5120 de caractere.
Variabilele booleene folosesc valorile TRUE și, respectiv, FALSE.
Exemple de instrucțiuni de asamblare ARM
În acest tabel, am adunat principalele instrucțiuni care vor fi necesare pentru dezvoltarea ulterioară (la etapa cea mai de bază :):

Pentru a consolida utilizarea instrucțiunilor de bază, să scriem câteva exemple simple, dar mai întâi avem nevoie de un lanț de instrumente pentru braț. Lucrez pe Linux, așa că am ales: frank.harvard.edu/~coldwell/toolchain (arm-unknown-linux-gnu toolchain). Se pune la fel de ușor ca decojirea perelor, ca orice alt program pe Linux. În cazul meu (fedora rusă) aveam nevoie doar să instalez pachete rpm de pe site.
Acum este timpul să scriem cel mai simplu exemplu. Programul va fi absolut inutil, dar principalul lucru este că va funcționa :) Iată codul pe care ți-l ofer:
start: @ Un șir opțional care indică începutul programului mov r0, #3 @ Încărcați valoarea 3 în registrul r0 mov r1, #2 @ Faceți același lucru cu registrul r1, doar acum cu valoarea 2 adăugați r2, r1, r0 @ Adăugați valorile lui r0 și r1, scrieți răspunsul la r2 mul r3, r1, r0 @ Înmulțiți valoarea registrului r1 cu valoarea registrului r0, scrieți răspunsul la r3 stop: b stop @ Linia de terminare a programului
Compilăm programul până când obținem fișierul .bin:
/usr/arm/bin/arm-unknown-linux-gnu-as -o arm.o arm.s /usr/arm/bin/arm-unknown-linux-gnu-ld -Ttext=0x0 -o ​​​​arm. elf arm .o /usr/arm/bin/arm-unknown-linux-gnu-objcopy -O binary arm.elf arm.bin
(codul este în fișierul arm.s, iar lanțul de instrumente în cazul meu este în directorul /usr/arm/bin/)
Dacă totul a mers bine, veți avea 3 fișiere: arm.s (codul real), arm.o, arm.elf, arm.bin (programul executabil real). Pentru a testa funcționarea programului, nu este necesar să aveți propriul dispozitiv de braț. Este suficient să instalați QEMU. Pentru trimitere:

QEMU este un software gratuit și open source pentru emularea hardware-ului diferitelor platforme.

Include emularea procesoarelor Intel x86 și a dispozitivelor I/O. Poate emula 80386, 80486, Pentium, Pentium Pro, AMD64 și alte procesoare compatibile x86; PowerPC, ARM, MIPS, SPARC, SPARC64, m68k - doar parțial.

Rulează pe Syllab, FreeBSD, FreeDOS, Linux, Windows 9x, Windows 2000, Mac OS X, QNX, Android și multe altele.

Deci, pentru a emula arm, aveți nevoie de qemu-system-arm. Acest pachet este în yum, așa că, dacă aveți Fedora, puteți sări peste bătăi de cap și să rulați comanda:
yum instalează qemu-system-arm

Apoi, trebuie să pornim emulatorul ARM, astfel încât să execute programul nostru arm.bin. Pentru a face acest lucru, vom crea un fișier flash.bin, care va fi memoria flash pentru QEMU. Este foarte ușor să faci asta:
dd if=/dev/zero of=flash.bin bs=4096 count=4096 dd if=arm.bin of=flash.bin bs=4096 conv=notrunc
Acum încărcăm QEMU cu memoria flash primită:
qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
La ieșire, veți obține ceva de genul acesta:

$ qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
Monitor QEMU 0.15.1 - tastați „help” pentru mai multe informații
(qemu)

Programul nostru arm.bin a trebuit să schimbe valorile a patru registre, așa că să ne uităm la aceste registre pentru a verifica funcționarea corectă. Acest lucru se face cu o comandă foarte simplă: registrele de informații
La ieșire, veți vedea toate cele 15 registre ARM, iar patru dintre ele vor avea valori modificate. Verificați:) Valorile registrului sunt aceleași pe care v-ați aștepta după executarea programului:
(qemu) registre de informații R00=00000003 R01=00000002 R02=00000005 R03=00000006 R04=00000000 R05=00000000 R06=00000000 R06=00000000 R07=0000000000000000000000000000000 000000 R10=00000000 R11=00000000 R12=00000000 R13=00000000 R14= 00000000 R15=00000010 PSR=400001d3 -Z-- A svc32

P.S. În acest articol, am încercat să descriu elementele de bază ale programării în ARM assembler. Sper ca ti-a placut! Acest lucru este suficient pentru a pătrunde în continuare în jungla acestui limbaj și pentru a scrie programe în ea. Dacă totul merge, voi scrie în continuare despre ceea ce învăț eu însumi. Dacă există erori, vă rugăm să nu dați lovitura, deoarece sunt nou în asamblare.

Salutare tuturor!
Prin ocupație, sunt programator Java. Ultimele luni de muncă m-au făcut să fac cunoștință cu dezvoltarea pentru Android NDK și, în consecință, să scriu aplicații native în C. Aici m-am lovit de problema optimizării bibliotecilor Linux. Mulți s-au dovedit a fi absolut neoptimizați pentru ARM și au încărcat puternic procesorul. Anterior, practic nu am programat în assembler, așa că la început a fost dificil să încep să învăț acest limbaj, dar cu toate acestea am decis să încerc. Acest articol este scris, ca să spunem așa, de la un începător pentru începători. Voi încerca să descriu elementele de bază pe care le-am studiat deja, sper că cineva va fi interesat. În plus, voi fi bucuros să primesc critici constructive din partea profesioniștilor.

Introducere
Deci, mai întâi, să ne dăm seama ce este ARM. Wikipedia oferă această definiție:

Arhitectura ARM (Advanced RISC Machine, Acorn RISC Machine, advanced RISC machine) este o familie de nuclee de microprocesoare licențiate pe 32 și 64 de biți dezvoltate de ARM Limited. Compania este angajată exclusiv în dezvoltarea de nuclee și instrumente pentru acestea (compilatoare, instrumente de depanare etc.), câștigând din licențierea arhitecturii către producători terți.

Dacă cineva nu știe, acum majoritatea dispozitivelor mobile, tabletele sunt dezvoltate pe această arhitectură specială a procesorului. Principalul avantaj al acestei familii este consumul redus de energie, datorită căruia este adesea folosit în diverse sisteme încorporate. Arhitectura a evoluat de-a lungul timpului, iar de la ARMv7 au fost definite 3 profiluri: 'A' (aplicatie) - aplicatii, 'R' (timp real) - in timp real, 'M' (microcontroller) - microcontroler. Poți citi istoria dezvoltării acestei tehnologii și alte date interesante pe Wikipedia sau google-l pe Internet. ARM acceptă diferite moduri de operare (Thumb și ARM, în plus, a apărut recent Thumb-2, care este un amestec de ARM și Thumb). În acest articol, vom lua în considerare modul ARM în sine, în care este executat un set de instrucțiuni pe 32 de biți.

Fiecare procesor ARM este construit din următoarele blocuri:

  • 37 de registre (dintre care doar 17 sunt vizibile în timpul dezvoltării)
  • Unitate logică aritmetică (ALU) - execută sarcini aritmetice și logice
  • Baril shifter - un dispozitiv conceput pentru a muta blocuri de date cu un anumit număr de biți
  • CP15 - un sistem special care controlează coprocesoarele ARM
  • Decodor de instrucțiuni - se ocupă de transformarea unei instrucțiuni într-o secvență de micro-operații
Acestea nu sunt toate componentele ARM, dar aprofundarea în jungla construcției procesoarelor nu este inclusă în subiectul acestui articol.
Execuția conductei
Procesoarele ARM folosesc o conductă în 3 etape (de la ARM8, a fost implementată o conductă în 5 etape). Să luăm în considerare o conductă simplă folosind procesorul ARM7TDMI ca exemplu. Execuția fiecărei instrucțiuni constă din trei pași:

1. Etapa de eșantionare (F)
În această etapă, instrucțiunile curg de la RAM la conducta procesorului.
2. Etapa de decodare (D)
Instrucțiunile sunt decodificate și tipul lor este recunoscut.
3. Etapa de execuție (E)
Datele intră în ALU și sunt executate, iar valoarea rezultată este scrisă în registrul specificat.

Dar atunci când dezvoltați, trebuie să țineți cont de faptul că există instrucțiuni care folosesc mai multe cicluri de execuție, de exemplu, încărcarea (LDR) sau stocarea. Într-un astfel de caz, faza de execuție (E) este împărțită în faze (E1, E2, E3...).

Execuție condiționată
Una dintre cele mai importante funcții ale asamblatorului ARM este execuția condiționată. Fiecare instrucțiune poate fi executată condiționat și pentru aceasta sunt folosite sufixe. Dacă se adaugă un sufix la numele unei instrucțiuni, atunci parametrii sunt verificați înainte de a fi executat. Dacă parametrii nu se potrivesc cu condiția, atunci instrucțiunea nu este executată. Sufixe:
MI - număr negativ
PL - pozitiv sau zero
AL - executa întotdeauna instrucțiunea
Există mult mai multe sufixe de execuție condiționată. Citiți restul sufixelor și exemplelor din documentația oficială: documentația ARM
Acum este timpul să luați în considerare...
Elementele de bază ale sintaxei asamblatorului ARM
Pentru cei care au lucrat anterior cu asamblator, acest articol poate fi sărit peste. Pentru toți ceilalți, voi descrie elementele de bază ale lucrului cu acest limbaj. Deci, fiecare program în limbaj de asamblare este format din instrucțiuni. Instrucțiunea este creată astfel:
(etichetă) (instrucțiune|operanzi) (@ comentariu)
Eticheta este un parametru opțional. O instrucțiune este direct un mnemonic pentru o instrucțiune către procesor. Instrucțiunile de bază și utilizarea lor vor fi discutate în continuare. Operanzi - constante, adrese de registru, adrese din RAM. Comentariul este un parametru opțional care nu afectează execuția programului.
Înregistrați nume
Sunt permise următoarele nume de registru:
1.r0-r15

3.v1-v8 (registri variabile, de la r4 la r11)

4.sb și SB (registru static, r9)

5.sl și SL (r10)

6.fp și FP (r11)

7.ip și IP (r12)

8.sp și SP (r13)

9.lr și LR (r14)

10.buc și PC (contor de programe, r15).

Variabile și constante
În asamblatorul ARM, ca orice alt limbaj de programare (practic), pot fi folosite variabile și constante. Ele sunt împărțite în următoarele tipuri:
  • Numeric
  • joc de inteligență
  • Şir
Variabilele numerice sunt inițializate astfel:
un SETA 100; se creează o variabilă numerică „a” cu valoarea 100.
Variabile șir:
improba SETS „literal”; variabila improbabilă este creată cu valoarea „literal”. ATENŢIE! Valoarea variabilei nu poate depăși 5120 de caractere.
Variabilele booleene folosesc valorile TRUE și, respectiv, FALSE.
Exemple de instrucțiuni de asamblare ARM
În acest tabel, am adunat principalele instrucțiuni care vor fi necesare pentru dezvoltarea ulterioară (la etapa cea mai de bază :):

Pentru a consolida utilizarea instrucțiunilor de bază, să scriem câteva exemple simple, dar mai întâi avem nevoie de un lanț de instrumente pentru braț. Lucrez pe Linux, așa că am ales: frank.harvard.edu/~coldwell/toolchain (arm-unknown-linux-gnu toolchain). Se pune la fel de ușor ca decojirea perelor, ca orice alt program pe Linux. În cazul meu (fedora rusă) aveam nevoie doar să instalez pachete rpm de pe site.
Acum este timpul să scriem cel mai simplu exemplu. Programul va fi absolut inutil, dar principalul lucru este că va funcționa :) Iată codul pe care ți-l ofer:
start: @ Un șir opțional care indică începutul programului mov r0, #3 @ Încărcați valoarea 3 în registrul r0 mov r1, #2 @ Faceți același lucru cu registrul r1, doar acum cu valoarea 2 adăugați r2, r1, r0 @ Adăugați valorile lui r0 și r1, scrieți răspunsul la r2 mul r3, r1, r0 @ Înmulțiți valoarea registrului r1 cu valoarea registrului r0, scrieți răspunsul la r3 stop: b stop @ Linia de terminare a programului
Compilăm programul până când obținem fișierul .bin:
/usr/arm/bin/arm-unknown-linux-gnu-as -o arm.o arm.s /usr/arm/bin/arm-unknown-linux-gnu-ld -Ttext=0x0 -o ​​​​arm. elf arm .o /usr/arm/bin/arm-unknown-linux-gnu-objcopy -O binary arm.elf arm.bin
(codul este în fișierul arm.s, iar lanțul de instrumente în cazul meu este în directorul /usr/arm/bin/)
Dacă totul a mers bine, veți avea 3 fișiere: arm.s (codul real), arm.o, arm.elf, arm.bin (programul executabil real). Pentru a testa funcționarea programului, nu este necesar să aveți propriul dispozitiv de braț. Este suficient să instalați QEMU. Pentru trimitere:

QEMU este un software gratuit și open source pentru emularea hardware-ului diferitelor platforme.

Include emularea procesoarelor Intel x86 și a dispozitivelor I/O. Poate emula 80386, 80486, Pentium, Pentium Pro, AMD64 și alte procesoare compatibile x86; PowerPC, ARM, MIPS, SPARC, SPARC64, m68k - doar parțial.

Rulează pe Syllab, FreeBSD, FreeDOS, Linux, Windows 9x, Windows 2000, Mac OS X, QNX, Android și multe altele.

Deci, pentru a emula arm, aveți nevoie de qemu-system-arm. Acest pachet este în yum, așa că, dacă aveți Fedora, puteți sări peste bătăi de cap și să rulați comanda:
yum instalează qemu-system-arm

Apoi, trebuie să pornim emulatorul ARM, astfel încât să execute programul nostru arm.bin. Pentru a face acest lucru, vom crea un fișier flash.bin, care va fi memoria flash pentru QEMU. Este foarte ușor să faci asta:
dd if=/dev/zero of=flash.bin bs=4096 count=4096 dd if=arm.bin of=flash.bin bs=4096 conv=notrunc
Acum încărcăm QEMU cu memoria flash primită:
qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
La ieșire, veți obține ceva de genul acesta:

$ qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
Monitor QEMU 0.15.1 - tastați „help” pentru mai multe informații
(qemu)

Programul nostru arm.bin a trebuit să schimbe valorile a patru registre, așa că să ne uităm la aceste registre pentru a verifica funcționarea corectă. Acest lucru se face cu o comandă foarte simplă: registrele de informații
La ieșire, veți vedea toate cele 15 registre ARM, iar patru dintre ele vor avea valori modificate. Verificați:) Valorile registrului sunt aceleași pe care v-ați aștepta după executarea programului:
(qemu) registre de informații R00=00000003 R01=00000002 R02=00000005 R03=00000006 R04=00000000 R05=00000000 R06=00000000 R06=00000000 R07=0000000000000000000000000000000 000000 R10=00000000 R11=00000000 R12=00000000 R13=00000000 R14= 00000000 R15=00000010 PSR=400001d3 -Z-- A svc32

P.S. În acest articol, am încercat să descriu elementele de bază ale programării în ARM assembler. Sper ca ti-a placut! Acest lucru este suficient pentru a pătrunde în continuare în jungla acestui limbaj și pentru a scrie programe în ea. Dacă totul merge, voi scrie în continuare despre ceea ce învăț eu însumi. Dacă există erori, vă rugăm să nu dați lovitura, deoarece sunt nou în asamblare.

1. Contorul ceasului în timp real trebuie să fie activat (1); Bitul de selectare a sursei ceasului este șters (2) dacă ceasul nu este de la ceasul principal.

2. Unul sau ambii biți de selectare a evenimentului de întrerupere (3) trebuie să fie setați. Și se alege ce evenimente vor declanșa cererea de întrerupere (5).

3. Trebuie setate măștile pentru evenimente de întrerupere (4, 7).

2.5 Despre programarea ARM7 în asamblator

Setul de instrucțiuni ARM7 (Secțiunea 1.4) include doar 45 de instrucțiuni, care sunt destul de complexe datorită varietății de metode de adresare, câmpuri condiționate și modificatori. Programul în limbaj de asamblare este greoi și

Cu greu de citit. Prin urmare, asamblatorul este rar folosit în programare pentru arhitectura ARM7.

Cu toate acestea, limbajul C de nivel înalt ascunde multe caracteristici arhitecturale de la programator. Programatorul practic nu atinge proceduri precum alegerea modului kernel, alocarea memoriei pentru stivă și gestionarea întreruperilor. Pentru a învăța aceste proceduri, este util să scrieți cel puțin un program simplu în limbaj de asamblare.

În plus, chiar și atunci când se folosește C, trebuie să se recurgă la limbajul de asamblare.

1. Ar trebui controlat Compilatorul C, ținând evidența dacă a exclus instrucțiuni importante în timpul optimizării, considerându-le inutile. Compilatorul generează cod extrem de ineficient pentru o operație relativ simplă, din cauza optimizării insuficiente. Pentru a vă asigura că compilatorul folosește cu adevărat acele resurse hardware care sunt concepute pentru a crește eficiența unui anumit algoritm.

2. În timpul căutării erorilor sau cauzelor excepțiilor (Secțiunea 2.4.1).

3. Pentru a obține un cod care este absolut optim în ceea ce privește performanța sau consumul de memorie (secțiunile 2.2.20, 3.1.5).

Luați în considerare tehnicile de bază pentru compilarea unui program în asamblator

Cu scopul de a demonstra întregul cod executat de microcontroler, așa cum este și fără intermediar compilator C.

Procedura de creare a unui proiect bazat pe asamblare este aproape aceeași ca și pentru programele C (secțiunile 2.3.1–2.3.3). Există doar două excepții:

a) extensia *.S este atribuită fișierului text sursă;

b) aici se presupune că fișierul STARTUP.S nu este conectat la program.

2.5.1 Reguli de bază pentru scrierea programelor în asamblator

Se obișnuiește să se aranjeze textul programului în asamblator în patru coloane. Putem spune că fiecare linie este formată din patru câmpuri și anume: câmpuri de etichete, operații, operanzi, comentarii. Câmpurile sunt separate între ele prin file sau spații.

Domeniile principale sunt operațiile și operanzii. Operațiile valide și sintaxa acestora sunt date în tabelul (1.4.2)

O etichetă este o reprezentare simbolică a adresei unei comenzi. Peste tot, în locul etichetei, se va înlocui adresa comenzii precedată de etichetă. Cel mai adesea, etichetele sunt folosite în comenzile de transfer de control. Fiecare etichetă trebuie să fie unică și este opțională. Spre deosebire de multe alte versiuni, în asamblatorul RealView, etichetele nu se termină cu două puncte (":").

Comentariile sunt opțional plasate la sfârșitul unei linii și separate prin punct și virgulă ("; ").

Să luăm un exemplu simplu.

2.5.2 Pseudocomenzi

Asamblatorul RealView acceptă așa-numitele pseudo-comenzi. O pseudo-instrucțiune este o notație mnemonică care nu corespunde de fapt cu sistemul de instrucțiuni al procesorului, dar este înlocuită cu una sau (mai rar) mai multe instrucțiuni. Pseudo-comenzile sunt un fel de macrocomenzi și servesc la simplificarea sintaxei. Lista pseudo-comenzilor acceptate este dată în tabelul (2.5.1).

2.5.3 Directive de asamblare

Spre deosebire de instrucțiuni, directivele nu creează cod executabil care este încărcat în memoria microcontrolerului. Directivele sunt doar instrucțiuni pentru asamblator, ele controlează formarea codului executabil.

Să aruncăm o privire la directivele de asamblare RealView 4 utilizate în mod obișnuit.

Constanta numelui EQU

Atribuie desemnatorul simbolic Nume constantei, care devine sinonim pentru constantă. Scopul principal este introducerea denumirilor registrelor de control,

Nume ZONA, Opțiuni

Definește o regiune de memorie cu numele dat. Parametrii specifică scopul unei zone de memorie, cum ar fi DATE (date) sau CODE (cod). Adresele zonei definite depind de destinația selectată. Zona COD este localizată începând de la adresa 0x00000000, zona DATE - de la adresa 0x40000000. Programul trebuie să aibă o regiune CODE numită RESET. Constantele alocate în memoria programului ar trebui să fie declarate într-o secțiune cu o pereche de parametri CODE, READONLY.

Desemnează punctul de intrare în program, arată „începutul” acestuia. O astfel de directivă trebuie să fie întotdeauna prezentă într-un program. De obicei plasat imediat după directiva AREA RESET, CODE.

Tabelul 2.5.1 - Pseudo-comenzi acceptate de asamblatorul RealView 4

Notație mnemonică

Operațiune

Implementare efectivă

și sintaxă

ADR(Cond)

a înregistra

Adăugarea sau scăderea unei constante dintr-un PC co-

comenzi ADD sau SUB

ADRL(Cond.)

a înregistra

Dublu ADD sau SUB cu PC

(gamă extinsă de adrese)

ASR(Cond)(S)

Deplasare aritmetică la dreapta

ASR(Cond)(S)

operand de schimbare

LDR(Cond)

a înregistra

adresare (PC + offset direct)

Plasarea unei constante

în memoria programului

LDR (din adresa index-

ție. Offset-ul este PC.

LSL(Cond)(S)

Deplasare logică la stânga

LSL(Cond)(S)

operand de schimbare

LSR(Cond)(S)

Schimbarea logica la dreapta

LSR(Cond)(S)

operand de schimbare

POP(Conv.)

Restaurați registrele din stivă

Recuperare

registre

echipă

LDMIA R13!,(...)

PUSH(Cond)

Conservare

registre

echipă

STMDB R13!,(...)

ROR(Cond)(S)

Invarte spre dreapta

ROR(Cond)(S)

operand de schimbare

RRX(Conv.)(S)

Rotiți direct

transferați la 1 cifră

operand de schimbare

Nume SPACE Dimensiune

Rezervă memorie pentru stocarea datelor cu dimensiunea dată. Numele devine sinonim cu adresa spațiului rezervat. Unitatea spațiului de adrese permite ca această directivă să fie aplicată atât memoriei permanente, cât și aleatorii. Scopul principal este de a crea variabile globale în RAM (în zona DATE).

Etichetă DCB/DCW/DCD Constant

„Cosă” datele (constantele numerice) în memoria programului. Eticheta devine sinonimă cu adresa la care vor fi scrise datele. Diferite directive (DCB , DCW și DCD ) sunt utilizate pentru date de diferite dimensiuni: octet, cuvânt de 16 biți, cuvânt de 32 de biți (respectiv).

Servește ca semn de sfârșit de fișier. Tot textul după această directivă este ignorat de către asamblator.

2.5.4 Macrocomenzi

O macrocomandă este o piesă predefinită a unui program care efectuează o operațiune comună. Spre deosebire de subrutinele numite cu comenzi de transfer de control, utilizarea macro-urilor nu reduce performanța, dar nu reduce consumul de memorie de program. Pentru că la fiecare apel macro, asamblatorul înglobează întregul său text în program.

Următoarea construcție este utilizată pentru a declara o macrocomandă

$Parameter1, $Parameter2, ...

Parametrii vă permit să modificați textul unei macrocomenzi de fiecare dată când este apelată. În interiorul (în corpul) unei macrocomenzi, parametrii sunt utilizați și cu semnul „$” precedent. În locul parametrilor din corpul macro, sunt înlocuiți parametrii specificați în timpul apelului.

Macro-ul se numește astfel:

Nume Parametrul1, Parametrul2,...

Este posibil să se organizeze verificarea stării și ramificarea.

IF "$Parameter" == "Valoare"

Vă rugăm să rețineți că acest design nu duce la o verificare a programului a stării de către microcontroler. Condiția este verificată de către asamblator în timpul formării codului executabil.