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).
Î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.
Deplasări logice
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.
Pentru explicații legate de operațiile pe biți în C urmăriți ghidul de la adresa Operații pe biți în C.
Î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.
Sistemul hexazecimal este utilizat pentru a reprezenta adresele la care se află datele în memorie și pentru a vizualiza aceste date într-un mod mai ușor de interpretat decât o secvență compusă doar din 0 și 1. Imaginea de mai jos oferă un exemplu în acest sens:
În partea stângă avem adresele din memorie unde se află date. La adresa 0x0009FA08 primii 4 octeți începând cu offset-ul 0x02 sunt 0x01, 0x00, 0xFF, 0xFF. Aceștia pot reprezenta un întreg pe 4 octeți, 4 caractere, 2 întregi pe 2 octeți. Folosind baza 16 putem să interpretăm datele reușind astfel să intuim ce ar putea să reprezinte acestea.
În memoria unui calculator o valoare este memorată pe un număr fix de biți. În funcție de arhitectură fiecare procesor poate accesa un număr maxim de biți în cadrul unei operații, acest număr de biți reprezintă dimensiunea cuvântului (word size).
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 |
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 |
În limbajul C un pointer este o variabilă a cărei valoare este adresa unei alte variabile. Ne putem gândi la un pointer ca un intermediar, și anume o variabilă care indică către o locație finală sau către un alt intermediar după cum se poate vedea în imaginea și codul de mai jos.
#include <stdio.h> int main() { int v; int *p; /* pointer la un întreg pe 32 biți */ int **pp; /* pointer la un pointer care conține conține adresa unui întreg pe 32 biți */ /* Pentru a accesa adresa unei variabile în C se folosește operatorul &(referențiere) */ p = &v; /* p conține adresa valorii v */ pp = &p; /* pp conține adresa adresei valorii v */ v = 69; /* Pentru a accesa valoarea de la adresa memorată de un pointer se folosește operatorul *(dereferențiere) */ printf("v(%d) - *p(%d) - **pp(%d)\n", v, *p, *(*pp)); /* se va afișa v(69) - *p(69) - **pp(69) */ return 0; }
În C se poate defini un pointer către oricare dintre tipurile de date care există în limbaj cât și pentru void. Un void pointer diferă de un pointer către un tip de date explicit prin faptul că un void pointer NU poate fi folosit în operații cu pointeri, void neavând o dimensiune clară. Un exemplu de bază unde se folosesc pointerii și operațiile cu pointeri este alocarea și parcurgerea unui tablou de valori:
#include <stdio.h> #include <stdlib.h> #define ARR_LENGTH 5 int main() { int *arr, i; arr = (int *)malloc(sizeof(int) * ARR_LENGTH); // arr = (int *)calloc(ARR_LENGTH, sizeof(int)); for (i = 0; i < ARR_LENGTH; ++i) { /* * arr + i iterează prin adresele fiecărui element din tablou însă adresa arr + i nu crește cu i ci cu i * sizeof(int), arr fiind un pointer la int * în C nu se vede și nu este necesară această operație dar mai târziu în limbajul de asamblare va fi necesar */ printf("arr[%d] = %d: ", i, *(arr + i)); } free(arr); return 0; }
Pointerii oferă o flexibilitate mare în ce privește accesul la memorie. Mai jos este un exemplu în acest sens care verifică dacă un sistem este little sau big endian, folosind cast între diverse tipuri de pointeri.
#include <stdio.h> int main() { int v = 0x00000001; unsigned char *first_byte = (unsigned char *)&v; if (*first_byte == 0x01) printf("little-endian\n"); else printf("big-endian\n"); return 0; }
git pull origin master
din interiorul directorului în care se află repository-ul (~/Desktop/iocla
).Recomandarea este să îl actualizați cât mai frecvent, înainte să începeți lucrul, pentru a vă asigura că aveți versiunea cea mai recentă.
Dacă doriți să descărcați repository-ul în altă locație, folosiți comanda git clone https://github.com/systems-cs-pub-ro/iocla ${target}
.
Pentru mai multe informații despre folosirea utilitarului git
, urmați ghidul de la https://gitimmersion.com.
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:
Pentru compilare și execuție puteți folosi:
Veți rezolva exercițiul plecând de la fișierul len_xor.c aflat în directorul 2-len_xor.
Pentru un șir de caractere dat, să se afișeze:
Folosiți cât de mult posibil operații cu pointeri și operații pe biți!
Pentru șirul de caractere “ababababacccbacbacbacbacbabc”:
Adresele de mai sus sunt orientative!
Veți rezolva exercițiul plecând de la fișierul mirror.c aflat în directorul 3-mirror.
Folosind operații cu pointeri implementați un program în C care inversează un șir de caractere. Funcția mirror trebuie să realizeze rotirea datelor din șirul de caractere in-place (la ieșirea din funcție, șirul dat ca intrare va conține șirul inversat).
Veți rezolva exercițiul plecând de la fișierul rotations.c aflat în directorul 4-rotations.
Implementați în C rotație stânga și rotație dreapta pentru numere întregi pe 32 de biți.
Veți rezolva exercițiul plecând de la fișierul odd_even.c aflat în directorul 5-odd_even.
Să se parcurgă un tablou de întregi pe 32 de biți folosind operații cu pointeri și să se afișeze în binar numerele pare și în hexazecimal numerele impare.
Soluțiile pentru exerciții sunt disponibile aici.