Pentru a înțelege cum funcționează un program în limbaj de asamblare, este necesară o înțelegere a modului în care sunt organizate calculatoarele, a modului în care acestea par să funcționeze la un nivel foarte scăzut. La nivelul cel mai simplist, calculatoarele au trei părți principale:
- memoria principală sau RAM care conține date și instrucțiuni,
- un procesor, care prelucrează datele prin executarea instrucțiunilor, și
- intrare și ieșire (uneori prescurtat I/O), care permit calculatorului să comunice cu lumea exterioară și să stocheze date în afara memoriei principale pentru a le putea recupera ulterior.
Memorie principală
În majoritatea computerelor, memoria este împărțită în bytes. Fiecare octet conține 8 biți. Fiecare octet din memorie are, de asemenea, o adresă, care este un număr care indică locul în care se află octetul în memorie. Primul octet din memorie are adresa 0, următorul are adresa 1 și așa mai departe. Împărțirea memoriei în octeți o face adresabilă pe octeți, deoarece fiecare octet primește o adresă unică. Adresele memoriilor de octeți nu pot fi utilizate pentru a se referi la un singur bit al unui octet. Un octet este cea mai mică bucată de memorie care poate fi adresată.
Chiar dacă o adresă se referă la un anumit octet din memorie, procesoarele permit utilizarea mai multor octeți de memorie la rând. Cea mai frecventă utilizare a acestei caracteristici este folosirea a 2 sau 4 octeți într-un rând pentru a reprezenta un număr, de obicei un număr întreg. Uneori se folosesc și octeți unici pentru a reprezenta numere întregi, dar, deoarece au o lungime de numai 8 biți, pot conține numai 28 sau 256 de valori posibile diferite. Utilizarea a 2 sau 4 octeți într-un rând crește numărul de valori posibile diferite la 216 , 65536 sau, respectiv, 232 , 4294967296.
Atunci când un program folosește un octet sau un număr de octeți la rând pentru a reprezenta ceva precum o literă, un număr sau orice altceva, acei octeți sunt numiți un obiect, deoarece toți fac parte din același lucru. Chiar dacă obiectele sunt toate stocate în octeți identici de memorie, ele sunt tratate ca și cum ar avea un "tip", care spune cum ar trebui să fie înțeleși octeții: fie ca un număr întreg, fie ca un caracter sau ca un alt tip (cum ar fi o valoare non-integrală). Codul mașinii poate fi considerat, de asemenea, ca un tip care este interpretat ca instrucțiuni. Noțiunea de tip este foarte, foarte importantă, deoarece definește ce se poate face și ce nu se poate face cu obiectul și cum se interpretează octeții obiectului. De exemplu, nu este validă stocarea unui număr negativ într-un obiect de tip număr pozitiv și nu este validă stocarea unei fracții într-un număr întreg.
O adresă care indică (este adresa unui) obiect multibyte este adresa primului octet al acelui obiect - octetul care are cea mai mică adresă. Ca o paranteză, un lucru important de reținut este faptul că nu se poate spune care este tipul unui obiect - sau chiar dimensiunea acestuia - după adresa sa. De fapt, nu puteți spune ce tip este un obiect nici măcar uitându-vă la el. Un program în limbaj de asamblare trebuie să țină evidența adreselor de memorie care conțin ce obiecte și cât de mari sunt aceste obiecte. Un program care face acest lucru este sigur din punct de vedere al tipului, deoarece face numai lucruri cu obiectele care sunt sigure pentru tipul lor. Un program care nu o face probabil că nu va funcționa corect. Rețineți că majoritatea programelor nu stochează de fapt în mod explicit care este tipul unui obiect, ci doar accesează obiectele în mod consecvent - același obiect este întotdeauna tratat ca fiind de același tip.
Procesorul
Procesorul rulează (execută) instrucțiuni, care sunt stocate ca cod mașină în memoria principală. Pe lângă posibilitatea de a accesa memoria pentru stocare, majoritatea procesoarelor dispun de câteva spații mici, rapide și de dimensiuni fixe pentru păstrarea obiectelor cu care se lucrează în acel moment. Aceste spații se numesc registre. Procesoarele execută de obicei trei tipuri de instrucțiuni, deși unele instrucțiuni pot fi o combinație a acestor tipuri. Mai jos sunt prezentate câteva exemple ale fiecărui tip în limbajul de asamblare x86.
Instrucțiuni care citesc sau scriu în memorie
Următoarea instrucțiune în limbaj de asamblare x86 citește (încarcă) un obiect de 2 octeți din octetul de la adresa 4096 (0x1000 în hexazecimal) într-un registru de 16 biți numit "ax":
În acest limbaj de asamblare, parantezele pătrate din jurul unui număr (sau al unui nume de registru) înseamnă că numărul trebuie folosit ca adresă către datele care trebuie utilizate. Utilizarea unei adrese pentru a indica datele se numește indirection. În următorul exemplu, fără parantezele pătrate, un alt registru, bx, primește de fapt valoarea 20 încărcată în el.
Deoarece nu a fost folosită nicio indirectă, valoarea reală a fost introdusă în registru.
În cazul în care operanzii (elementele care vin după mnemonic) apar în ordine inversă, o instrucțiune care încarcă ceva din memorie în loc să scrie în memorie:
Aici, memoria de la adresa 1000h primește valoarea bx. Dacă acest exemplu este executat imediat după cel precedent, cei 2 octeți de la 1000h și 1001h vor fi un întreg de 2 octeți cu valoarea 20.
Instrucțiuni care efectuează operații matematice sau logice
Unele instrucțiuni fac lucruri precum scăderea sau operații logice, cum ar fi nu:
Exemplul de cod mașină de mai devreme din acest articol ar fi acest lucru în limbaj de asamblare:
Aici, 42 și ax sunt adunate, iar rezultatul este stocat înapoi în ax. În ansamblul x86 este posibilă și combinarea unui acces la memorie și a unei operații matematice în felul următor:
Această instrucțiune adaugă valoarea numărului întreg de 2 octeți stocat la 1000h la ax și stochează răspunsul în ax.
Această instrucțiune calculează or a conținutului registrelor ax și bx și stochează rezultatul în ax.
Instrucțiuni care decid care va fi următoarea instrucțiune
De obicei, instrucțiunile sunt executate în ordinea în care apar în memorie, care este ordinea în care sunt tastate în codul de asamblare. Procesorul pur și simplu le execută una după alta. Cu toate acestea, pentru ca procesoarele să facă lucruri complicate, trebuie să execute instrucțiuni diferite în funcție de datele care le-au fost furnizate. Capacitatea procesoarelor de a executa instrucțiuni diferite în funcție de rezultatul unui lucru se numește ramificare. Instrucțiunile care decid care ar trebui să fie următoarea instrucțiune se numesc instrucțiuni de ramificare.
În acest exemplu, să presupunem că cineva dorește să calculeze cantitatea de vopsea de care va avea nevoie pentru a picta un pătrat cu o anumită lungime a laturii. Cu toate acestea, datorită economiei de scară, magazinul de vopsele nu va vinde mai puțin decât cantitatea de vopsea necesară pentru a picta un pătrat de 100 x 100.
Pentru a-și da seama de cantitatea de vopsea de care vor avea nevoie în funcție de lungimea pătratului pe care vor să-l picteze, au elaborat acest set de pași:
- scădem 100 din lungimea laturii
- dacă răspunsul este mai mic decât zero, se stabilește lungimea laturii la 100
- se înmulțește lungimea laturii cu ea însăși
Acest algoritm poate fi exprimat în următorul cod, unde ax este lungimea laturii.
mov bx, ax sub bx, 100 jge continue mov ax, 100 continue: mul ax
Acest exemplu introduce câteva lucruri noi, dar primele două instrucțiuni sunt cunoscute. Acestea copiază valoarea lui ax în bx și apoi scad 100 din bx.
Unul dintre elementele noi din acest exemplu se numește etichetă, un concept întâlnit în limbajele de asamblare în general. Etichetele pot fi orice dorește programatorul (cu excepția cazului în care este numele unei instrucțiuni, ceea ce ar deruta asamblorul). În acest exemplu, eticheta este "continue". Aceasta este interpretată de asamblor ca fiind adresa unei instrucțiuni. În acest caz, este vorba de adresa mult ax.
Un alt concept nou este cel al steagurilor. Pe procesoarele x86, multe instrucțiuni setează "stegulețe" în procesor, care pot fi folosite de următoarea instrucțiune pentru a decide ce să facă. În acest caz, dacă bx a fost mai mic de 100, sub va seta un indicator care spune că rezultatul a fost mai mic de zero.
Următoarea instrucțiune este jge, care este prescurtarea de la "jump if greater than or equal to". Este o instrucțiune de ramificare. În cazul în care indicatoarele din procesor specifică faptul că rezultatul a fost mai mare sau egal cu zero, în loc să treacă la instrucțiunea următoare, procesorul va trece la instrucțiunea de la eticheta continue, care este mul ax.
Acest exemplu funcționează bine, dar nu este ceea ce ar scrie majoritatea programatorilor. Instrucțiunea subtract a setat corect flag-ul, dar modifică și valoarea pe care operează, ceea ce a necesitat copierea ax-ului în bx. Cele mai multe limbaje de asamblare permit instrucțiuni de comparație care nu modifică niciunul dintre argumentele care le sunt transmise, dar care, totuși, setează corect stegulețele, iar asamblarea x86 nu face excepție.
cmp ax, 100 jge continue mov ax, 100 continue: mul ax
Acum, în loc să scădem 100 din ax, să vedem dacă acel număr este mai mic decât zero și să îl atribuim înapoi la ax, ax rămâne neschimbat. Stegulețele sunt în continuare setate în același mod, iar saltul este în continuare efectuat în aceleași situații.
Intrare și ieșire
În timp ce intrările și ieșirile reprezintă o parte fundamentală a calculului, nu există un singur mod de a le realiza în limbajul de asamblare. Acest lucru se datorează faptului că modul în care funcționează I/O depinde de configurația calculatorului și de sistemul de operare pe care îl rulează, nu doar de tipul de procesor pe care îl are. În secțiunea de exemple de mai jos, exemplul Hello World utilizează apeluri ale sistemului de operare MS-DOS, iar exemplul următor utilizează apeluri BIOS.
Este posibil să se facă I/O în limbajul de asamblare. Într-adevăr, limbajul de asamblare poate exprima, în general, tot ceea ce poate face un calculator. Cu toate acestea, chiar dacă există instrucțiuni de adăugare și de ramificare în limbajul de asamblare care vor face întotdeauna același lucru, nu există instrucțiuni în limbajul de asamblare care să facă întotdeauna I/O.
Este important de reținut faptul că modul în care funcționează I/O nu face parte din niciun limbaj de asamblare, deoarece nu face parte din modul de funcționare al procesorului.