Pentru reprezentarea informațiilor (instrucțiuni și date), calculatoarele folosesc sistemul binar (baza 2). În cazul scrierii programelor în limbaj de asamblare este preferat sistemul hexazecimal (baza 16), pentru că scutește programatorul de scrierea șirurilor lungi de 1 și 0, iar conversia din/în binar se poate face mult mai ușor decât în cazul sistemului zecimal (baza 10).
Să presupunem următoarea instrucțiune în limbajul C:
char a = 113;
Această instrucțiune are ca efect stocarea în memorie a unui octet cu valoarea 113, care va fi reprezentat în forma binară: 0b01110001. Aceeași valoare poate fi scrisă în hexazecimal ca 0x71.
Înainte de a începe lucrul cu limbajul de asamblare, este necesar să ne familiarizăm cu sistemele binar și hexazecimal și cu modalitățile de conversie între ele.
În sistemul binar (baza 2), valorile sunt reprezentate ca un șir de 0 și 1. Fiecare cifră din șir reprezintă un bit, iar un grup de 8 biți formează un octet (byte). Un grup de 4 biți poartă denumirea de nibble, sau half-byte.
În sistemul hexazecimal (baza 16), valorile sunt reprezentate sub forma unui șir de caractere din intervalul '0'-'9' sau 'a'-'f'. Un octet este format din două astfel de caractere, deci fiecare caracter corespunde unui grup de 4 biți (un nibble).
Un număr convertit din baza X în baza 10 are valoarea egală cu suma produselor dintre fiecare cifră din numărul în baza X și X la puterea egală cu poziția cifrei în numărul respectiv (numărarea se face de la dreapta la stânga, începând cu 0).
0xD9B1 = 1*160 + 11*161 + 9*162 + 13*163 = 55729
După cum am precizat anterior, o cifră din cadrul unui număr în hexazecimal corespunde unui grup de 4 biți (un nibble). Astfel, pentru a converti un număr din hexazecimal în binar este suficient să transformăm fiecare cifră în grupul de 4 biți echivalent.
Astfel, numărul obținut în binar este 0b1101100110110001.
Operația inversă, conversia din binar în hexazecimal se poate face convertind fiecare grup de 4 biți în cifra corespunzătoare în hexazecimal.
În memoria unui calculator o valoare este memorată pe un număr fix de biți. Dimensiunea cuvântului (word size) arhitecturii unui procesor reprezintă numărul maxim de biți cu care procesorul îi poate accesa printr-o singură operație.
Dimensiunile tipurilor de date uzuale folosite în C sunt dependente atât de procesor, cât și de platforma cu ajutorul căreia a fost compilat programul (sistem de operare, compilator). În tabelul de mai jos sunt prezentate dimensiunile tipurilor de date pe un procesor cu dimensiunea cuvântului arhitecturii de 32 de biți, în cazul în care programul este compilat folosind gcc, sub Linux.
Tip de date | Număr biți | Număr octeți |
---|---|---|
char | 8 | 1 |
short | 16 | 2 |
int | 32 | 4 |
long | 32 | 4 |
long long | 64 | 8 |
pointer | 32 | 4 |
În cazul în care un număr trebuie reprezentat în binar pe un număr de biți mai mare decât numărul maxim alocat, atunci vor fi păstrați doar biții care încap (începând din partea dreaptă), iar restul sunt ignorați. Această situație poartă denumirea de integer overflow.
Un overflow poate să apară și în cazul în care se efectuează o operație (de exemplu adunare), iar rezultatul se dorește să fie reprezentat pe același număr de biți ca și operanzii. În acest caz este posibil ca rezultatul să nu încapă pe numărul respectiv de biți, iar primul bit (din stânga) să fie ignorat. O soluție folosită în procesoare pentru această situație este memorarea separată a unui bit în cazul în care operația duce la integer overflow, numit bit de depășire (carry bit).
Să presupunem că dorim să adunăm două valori stocate pe un octet, iar rezultatul să fie memorat tot pe un octet.
O1 = 171 = 0b10101011 O2 = 113 = 0b01110001 Rezultatul așteptat: 171+113 = 0b100011100 Rezultatul memorat pe 8 biți: 0b00011100 Carry bit = 1
Folosind N biți putem memora 2N valori, de exemplu între 0 și 2N-1. Aceste valori sunt fără semn (toate sunt pozitive), pentru că nu putem face deosebirea între cele pozitive și negative. Dacă dorim și reprezentarea valorilor negative, trebuie să alocăm o parte din combinațiile binare posibile pe N biți pentru aceste valori.
În practică se împart combinațiile posibile astfel:
De exemplu, pentru valori reprezentate pe 8 biți, împărțirea se face astfel:
Această reprezentare a fost aleasă pentru că prezintă o serie de avantaje:
Pentru negarea unui număr (transformarea în complement față de 2) se procedează astfel:
Pentru reprezentarea valorilor mai mari de un octet există două metode posibile, ambele folosite în practică:
Exemplu: Dorim să stocăm valoarea 0x4a912480 în memorie pe 32 de biți (4 octeți), începând cu adresa 0x100, folosind cele două metode:
Metoda | Adresa 0x100 | Adresa 0x101 | Adresa 0x102 | Adresa 0x103 |
---|---|---|---|---|
Little-Endian | 0x80 | 0x24 | 0x91 | 0x4a |
Big-Endian | 0x4a | 0x91 | 0x24 | 0x80 |
1. Valorile mici (pe un octet) sunt stocate mereu la aceeași adresă, indiferent de dimensiunea tipului de date folosit.
Exemplu: Dorim stocarea valorii 0x49 la adresa 0x2000 cu tipurile char, short și int:
Tip de date | 0x2000 | 0x2001 | 0x2002 | 0x2003 |
---|---|---|---|---|
char | 0x49 | - | - | - |
short | 0x49 | 0x00 | - | - |
int | 0x49 | 0x00 | 0x00 | 0x00 |
2. Oferă ușurință în efectuarea operațiilor aritmetice. Majoritatea operațiilor se efectuează începând cu cel mai puțin semnificativ octet, iar acesta este stocat primul în cadrul acestui mod de reprezentare, deci putem efectua operațiile pe mai mulți octeți parcurgând operanzii de la adresa cea mai mică adresă la cea mai mare.
1. Valorile nu necesită transformări în momentul în care se transmit pe rețea. Pentru a se putea realiza comunicația între două calculatoare care folosesc metode diferite de reprezentare, toate valorile transmise sunt reprezentate în formatul Network byte order, care este echivalent cu Big-Endian.
2. Valorile pe mai mulți octeți sunt mai ușor de citit în momentul examinării unei zone de memorie.
Deplasările logice dreapta/stânga presupun mutarea cu o poziție a fiecărui bit. Cum rezultatul trebuie să fie pe același număr de biți ca valoarea inițială, primul bit este pierdut, iar spațiul gol este completat cu bitul 0.
Deplasarea aritmetică stânga este identică cu cea logică. În schimb, deplasarea aritmetică dreapta păstrează semnul valorii (în cazul în care primul bit este 1, deci numărul este negativ, rezultatul trebuie să fie tot negativ).
Similar, deplasarea unei valori cu N poziții la dreapta este echivalentă cu împărțirea valorii respective la 2N (și rotunjirea rezultatului la partea întreagă).
Operația de rotire (numită și deplasare circulară) este similară cu cea de deplasare, singura diferență fiind aceea că spațiul gol generat de deplasare este înlocuit cu bitul eliminat.
Majoritatea procesoarelor permit un tip special de rotire, și anume rotire folosind un bit auxiliar. De obicei, ca bit auxiliar se folosește bitul de carry. În acest caz, spațiul gol este înlocuit cu bitul auxiliar, iar bitul eliminat este memorat în acest bit.
1. (4p) Efectuați următoarele conversii între sisteme de numerație:
a. Din decimal în binar și hexazecimal:
b. Convertiți în zecimal:
c. Din hexazecimal în binar:
d. Din binar în hexazecimal:
2. (1p) Aflați dimensiunile principalelor tipuri de date din C pe sistemele din laborator. (char, short, int, unsigned int, long, long long, pointer). Hint: sizeof.
3. (1p) xxd este un utilitar Linux ce permite afișarea fișierelor binare în diferite formate. Puteți găsi o versiune pentru Windows aici: xxd.zip.
Se dă fișierul binar din arhiva următoare: binary_file.zip. Să se afișeze folosind xxd conținutul acestui fișier în următoarele formate:
4. (1p) Scrieți un program C cu ajutorul căruia să afișați următorul șir hexazecimal ca text: 48455820526f636b73210a.
Hint 2: În C puteți folosi codurile în format hexazecimal în locul caracterelor ASCII în șiruri de caractere cu ajutorul prefixului \x. Exemplu: char a[] = “\x41\x42\x43”; este echivalent cu char a[] = “ABC”; (puteți vedea aici un tabel cu codurile caracterelor ASCII)
5. (1p) Se dau următoarele declarații de variabile în C:
#include <stdio.h> void main() { unsigned int a = 4127; int b = -27714; unsigned long c = 0x12345678; char d[] = {'I', 'O', 'C', 'L', 'A'}; // TODO }
Observați cum sunt memorate aceste variabile în memorie.
6. (1p) Afișați valorile variabilelor c, d și e din programul de mai jos și explicați rezultatele (puteți converti valorile în binar pentru a observa mai ușor cauzele):
#include <stdio.h> void main() { short a = 20000; short b = 14000; short c = a + b; unsigned short d = 3 * a + b; short e = a << 1; // TODO }
7. (1p) Scrieți un program C cu ajutorul căruia să efectuați operația XOR între următoarele șiruri haxazecimale (octet cu octet) și afișați rezultatul ca text (hint: operatorul ^):