<docere>http://www.docere.ro/ |

Instrumente de investigare; zona de date şi zona de cod
Să zicem că vrem să vedem reprezentările în memorie pentru diverse valori… În acest scop, am putea scrie un program simplu - definim nişte variabile şi afişăm adresele şi valorile aferente.
În C, &VAR dă adresa variabilei VAR, iar *ADR dă valoarea memorată la adresa păstrată de variabila (pointer) ADR. Operatorii de referenţiere/dereferenţiere & şi * ţin cont de tipul variabilei - mai precis, de dimensiunea zonei alocate. De exemplu, dacă &VAR = 1000 (VAR are adresa 1000) şi VAR este de tip int, iar sizeof(int) = 4 — atunci &VAR+1 este adresa 1004 (adresa valorii de tip int "următoare" celeia de la adresa lui VAR), nu 1001. Pentru a referi octeţii componenţi şi nu "întreaga" valoare (de 4 octeţi) reprezentată de VAR, trebuie să convertim &VAR (adresă de valori int) în adresă de valori de tip char, folosind (char*) &VAR.
/* "limb1.c" */ #include <stdio.h> unsigned int j = 1234; /* 'j' şi 'tc' sunt variabile globale */ unsigned char tc[6] = {'a', 'z', 'A', 'Z', '0', '9'}; main() { int offs; /* 'offs' şi 'adr' sunt variabile locale */ unsigned char* adr = (unsigned char*) &j; printf("adresa variabilei 'j': %p => valoare: %u (pentru 'int' se alocă %u octeţi)\n", &j, *(&j), sizeof(int)); printf("adresa tabloului 'tc': %p (are alocaţi %u octeţi)\n", tc, sizeof(tc)); printf("\n10 locaţii de memorie, începând de la adresa variabilei globale 'j':\n"); for(offs = 0; offs < 10; offs++, adr++) printf("%p => %0X ('%c')\n", adr, *adr, *adr); printf("\nadresa variabilei locale 'offs': %p => valoare: %u\n", &offs, *(&offs)); printf("\nadresa codului funcţiei 'main': %p\n", &main); }
Considerăm chiar două variante, pentru obţinerea unui "executabil": întâi, folosind GCC (compilatorul standard adoptat pe Linux şi care se poate folosi şi pe alte sisteme de operare); apoi, folosind un compilator Borland C++ pe un sistem Windows. Vom reda rezultatele execuţiei şi le vom interpreta în contextul nostru de discuţie.
Pe Linux, deschizând un terminal şi lansând de pe linia de comandă gcc limb1.c -o limb1.exe - obţinem fişierul executabil "limb1.exe" (de aprox. 7Ko); lansând apoi "limb1.exe", obţinem:
adresa variabilei 'j': 0x80497a8 => valoare: 1234 (pentru 'int' se alocă 4 octeţi)
adresa tabloului 'tc': 0x80497ac (are alocaţi 6 octeţi)
10 locaţii de memorie, începând de la adresa variabilei globale 'j':
0x80497a8 => D2 ('�)
0x80497a9 => 4 ('')
0x80497aa => 0 ('')
0x80497ab => 0 ('')
0x80497ac => 61 ('a')
0x80497ad => 7A ('z')
0x80497ae => 41 ('A')
0x80497af => 5A ('Z')
0x80497b0 => 30 ('0')
0x80497b1 => 39 ('9')
adresa variabilei locale 'offs': 0xbfc7a24c => valoare: 10
adresa codului funcţiei 'main': 0x80483a4
Iar pe Windows, având instalat un compilator BCC.EXE ("the 16-bit command-line compiler Borland C++ Version 5") — putem proceda astfel: accesăm meniul Run... din bara de Start şi lansăm un terminal cu C:\WINDOWS\System32\cmd.exe; intrăm în directorul în care avem fişierul-sursă cd C:\Teste şi lansăm BCC: C:\Teste> C:\bc5\BIN\bcc limb1.c. În urma compilării şi invocării editorului de legături (Turbo Link), obţinem în directorul nostru de lucru fişierul executabil "limb1.exe" (de peste 31 KB) - pe care-l lansăm apoi (de la prompt): C:\Teste> limb1.exe > limb1.txt (am redirecţionat ieşirea către "limb1.txt", pentru a transfera apoi rezultatele respective pe Linux).
adresa variabilei 'j': 00A8 => valoare: 1234 (pentru 'int' se alocÄ 2 octeÅ£i)
adresa tabloului 'tc': 00AA (are alocaţi 6 octeţi)
10 locaţii de memorie, începând de la adresa variabilei globale 'j':
00A8 => D2 ('Ò')
00A9 => 4 ('')
00AA => 61 ('a')
00AB => 7A ('z')
00AC => 41 ('A')
00AD => 5A ('Z')
00AE => 30 ('0')
00AF => 39 ('9')
00B0 => 61 ('a')
00B1 => 64 ('d')
adresa variabilei locale 'offs': FFF4 => valoare: 10
adresa codului funcţiei 'main': 0293
Sau, folosind de această dată compilatorul pe 32 de biţi, bcc32.exe:
C:\Teste>bcc32 limb1.c
Borland C++ 5.0 for Win32 Copyright (c) 1993, 1996 Borland International
limb1.c:
Warning limb.c 22: Function should return a value in function main
Turbo Link Version 1.6.72.0 Copyright (c) 1993,1996 Borland International
C:\Teste>limb1
adresa variabilei 'j': 00407074 => valoare: 1234 (pentru 'int' se alocă 4 octeţi)
adresa tabloului 'tc': 00407078 (are alocaţi 6 octeţi)
10 locaţii de memorie, începând de la adresa variabilei globale 'j':
00407074 => D2 ('╥')
00407075 => 4 ('♦')
00407076 => 0 (' ')
00407077 => 0 (' ')
00407078 => 61 ('a')
00407079 => 7A ('z')
0040707A => 41 ('A')
0040707B => 5A ('Z')
0040707C => 30 ('0')
0040707D => 39 ('9')
adresa variabilei locale 'offs': 0012FF88 => valoare: 10
adresa codului funcţiei 'main': 0040107C
În limb1.c s-au declarat câteva variabile (bineînţeles că puteam considera şi mai multe). Variabilele globale sunt reprezentate într-o zonă de memorie contiguă, în ordinea în care apar ele în fişierul-sursă; variabilele locale au în mod clar, o zonă de reprezentare separată de aceea a variabilelor globale; zona care corespunde instrucţiunilor de executat (codul funcţiei main()) este separată faţă de celelalte zone de memorie.
Vedem că s-a folosit adresare pe 32 de biţi, respectiv pe 16 biţi - după caz. Adresele sunt reprezentate numai prin "offset" (nu ca Segment:Offset); este de intuit că locaţiile respective fac parte dintr-un acelaşi segment de memorie (o anumită porţiune a lui - pentru date şi o alta, disjunctă de prima - pentru cod). Este instructiv de experimentat cât de puţin cu programul — adăugând încă nişte variabile, recompilând şi reexecutând în diverse contexte - de exemplu după lansarea prealabilă a altor aplicaţii, sau folosind diverse opţiuni de compilare (de exemplu, privind "alinierea în memorie").
De observat că reprezentarea în memorie corespunde formatului little-endian (specifică microprocesoarelor INTEL): variabila j are reprezentarea (D2, 4, 0, 0) (respectiv, pe numai doi octeţi (D2, 0)), la adresa cea mai mică fiind octetul D2 (şi avem 0xD2 = 210, iar 210 + 4*256 = 1234 = valoarea lui j).
De observat şi că GCC (pe Linux) foloseşte codificarea "universală" Unicode (UTF-8), care reprezintă caracterele prin coduri de lungime variabilă (BCC foloseşte codul ASCII, în care fiecare caracter este reprezentat pe un octet): caracterului ţ îi corespunde un cod de doi octeţi (reprezentând î) pe care BCC i-a văzut ca atare (individual), dar GCC i-a interpretat ca reprezentând împreună un caracter. Sunt de reţinut codurile ASCII care s-au afişat: pentru 'a' 0x61, pentru 'A' 0x41 şi pentru '0' 0x30.
Privind diferenţa sensibilă de dimensiune a codului executabil (7 Ko faţă de 31 Ko), explicaţia poate decurge logic astfel: pentru obţinerea executabilului, editorul de legături trebuie să "lege" codul obiect produs de compilator pentru programul-sursă, de codul corespunzător bibliotecilor incluse (în cazul de faţă, codul funcţiei printf() din stdio); această "legare" se poate face în două moduri, după cum compilatorul respectiv este integrat sau nu, în sistemul de operare; GCC există pe orice sistem Linux, pe când BCC poate să fie sau poate să nu fie instalat, pe sistemul Windows respectiv; în cazul sistemului Linux va fi suficient pentru "legare" să se precizeze adresa codului funcţiei printf(), pe când în celălalt caz acest cod trebuie efectiv încorporat ca atare în codul executabil.
Pentru investigarea memoriei şi a modului de funcţionare a programelor există instrumente software specializate, numite debugger. Pe de o parte, acestea permit vizualizarea conţinutului diverselor zone de memorie, inclusiv a regiştrilor CPU/FPU; pe de altă parte, permit execuţia programului în regim "pas cu pas" - posibilă datorită faptului că CPU oferă suportul hardware necesar (vizibil prin prezenţa "flagului" Trap flag (single step) în registrul FLAGS).
Folosind butonul Start ("click here to begin") şi opţiunea Run... se lansează întâi interpretorul de comenzi cmd.exe; bara de titlu a fereastrei obţinute conţine un buton (etichetat "C:\") prin a cărui punctare (prin click) se deschide un meniu care pe lângă opţiunile obişnuite pentru "butonul din stânga-sus al ferestrei Windows" (Restore, Move, Close, etc.), conţine şi un submeniu Edit care oferă opţiuni de selectare a conţinutului ferestrei şi de Copy - permiţând astfel ca rezultatele obţinute prin folosirea în fereastra respectivă a diverselor comenzi să poată fi transferate interactiv, de exemplu într-un fişier-text.
Reproducem parţial, o asemenea sesiune de lucru; după ce s-a lansat debug, s-a folosit comanda r (afişează regiştrii) şi apoi comanda ? ("Help" asupra comenzilor disponibile):
Microsoft Windows XP [Version 5.1.2600] (C) Copyright 1985-2001 Microsoft Corp. C:\Documents and Settings\vb>debug -r AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=135C ES=135C SS=135C CS=135C IP=0100 NV UP EI PL NZ NA PO NC 135C:0100 0000 ADD [BX+SI],AL DS:0000=CD -? assemble A [address] dump D [range] enter E address [list] fill F range list go G [=address] [addresses] hex H value1 value2 load L [address] [drive] [firstsector] [number] move M range address name N [pathname] [arglist] proceed P [=address] [number] quit Q register R [register] search S range list trace T [=address] [value] unassemble U [range] write W [address] [drive] [firstsector] [number]
Comanda R a afişat conţinutul curent al regiştrilor de 16 biţi, pe două linii: întâi, regiştrii "generali" (implicaţi de exemplu în operaţii aritmetice sau folosiţi pentru a indica "offset"-uri); pe a doua linie apar regiştrii de segment, registrul "Instruction Pointer" şi starea curentă a 8 dintre flagurile microprocesorului (de exemplu, NC semnalează "Not Carry", iar NZ "No Zero"). Pe a treia linie este indicată, pe trei coloane, instrucţiunea care ar fi executată dacă s-ar tasta imediat comanda T "Trace"; pe prima coloană este adresa acestei instrucţiuni - 135C:0100, adică observând linia de deasupra CS:IP (registrul CS conţine 135C, iar IP=0100); a doua coloană redă codul maşină corespunzător instrucţiunii respective, iar a treia coloană transcrie instrucţiunea în limbaj de asamblare.
Propunem un mic experiment cu debug, pentru a face o serie de clarificări privind regiştrii CPU, reprezentarea în memorie, instrucţiunile CPU şi limbajul de asamblare.
C:\>debug -a 135E:0100 mov AX, 415A ; AX = 0x415A (adica "AZ") 135E:0103 mov word ptr [DI], AX ; DS:[DI] <-- AX 135E:0105 -u 100 104 135E:0100 B85A41 MOV AX,415A 135E:0103 8905 MOV [DI],AX
Primind comanda a, debug asamblează instrucţiunea indicată, adică: determină "codul-maşină" corespunzător (folosind un tabel propriu de mnemonice - coduri) şi îl înscrie la adresa CS:IP curentă (CS = 0x135E indică adresa de bază a segmentului de cod, iar IP = 0x100 este "offset"ul instrucţiunii). Prima instrucţiune a fost asamblată începând de la adresa CS:0100, iar a doua - de la CS:0103; deducem că octeţii de "cod-maşină" corespunzători primei instrucţiuni ocupă locaţiile de offseturi 0x100, 0x101 şi 0x102 (iar codul-maşină corespunzător celor două instrucţiuni este cuprins între CS:0100 şi CS:0104 inclusiv - de unde şi comanda "u 100 104").
mov este mnemonica folosită în multe limbaje de asamblare pentru instrucţiunile de transfer: mov Dest, Sursă transferă date (copiază) de la Sursă la Destinaţie (în unele limbaje, sintaxa este inversată: mov Sursă, Dest).
Prin comanda u, debug dezasamblează codul-maşină de la adresa specificată, afişând un tabel cu trei coloane: adresa codului-maşină, octeţii care constituie împreună codul-maşină şi "traducerea" corespunzătoare în limbaj de asamblare. Secvenţa de octeţi 0xB85A41 reprezintă "codul-maşină" al primei instrucţiuni; primul, 0xB8 este opcode ("operation code"), specificând microprocesorului operaţia de efectuat - "încarcă în registrul AX o valoare", anume 0x415A (şi vedem că octeţii acesteia sunt inversaţi în memorie, după modelul "little-endian").
Să trecem la executarea celor două instrucţiuni:
-t AX=415A BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=135E ES=135E SS=135E CS=135E IP=0103 NV UP EI PL NZ NA PO NC 135E:0103 8905 MOV [DI],AX DS:0000=20CD -t …… -d DS:0 F 135E:0000 5A 41 FF 9F 00 9A EE FE-1D F0 4F 03 C2 0D 8A 03 ZA........O.....
Primind comanda t, debug pune în execuţie instrucţiunea asamblată la adresa CS:IP curentă (CS:0x0100, în cazul primei instrucţiuni). Execuţia de către CPU a unui instrucţiuni este un proces "neinteruptibil" şi care durează unul sau mai mulţi "clock cycles"; la finalul execuţiei, IP indică adresa relativă a următoarei instrucţiuni de executat (aici, CS:0x0103).
A doua comandă t a pus în execuţie instrucţiunea mov [DI], AX, prin care conţinutul lui AX este copiat în memorie, anume în segmentul DS = 0x135E la offsetul indicat de registrul DI; în memorie, octeţii lui AX sunt inversaţi ("ZA"), fiind vorba de formatul "little-endian".
Comanda d afişează conţinutul memoriei de la adresa indicată (aici de la adresa DS:0), pe trei coloane: prima coloană conţine adresa primului octet din cei 16 afişaţi pe coloana a doua; ultima coloană redă interpretarea ASCII (înlocuind cu ".", dacă octetul respectiv nu se încadrează în gama codurilor ASCII a caracterelor "tipăribile").
Putem adăuga noi instrucţiuni, de exemplu pentru a înscrie "az" după "ZA" în zona DS:DI:
-a 135E:0105 add AX, 2020 ; 0x41 + 0x20 = 0x61 = 'a'; 0x5A + 0x20 = 0x7A = 'z' 135E:0108 xchg AL, AH ; "exchange" AL cu AH 135E:010A mov word ptr [DI+2], AX 135E:010D -t
add realizează aici operaţia "AX += 0x2020", care transformă octeţii 'A', 'Z' din AH şi respectiv AL în 'a' şi 'z'; apoi, xchg interschimbă între ei AH şi AL (obţinem AX=617A), încât la depunerea în memorie octeţii să apară în ordinea firească "az":
AX=617A BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=135E ES=135E SS=135E CS=135E IP=0108 NV UP EI PL NZ NA PO NC 135E:0108 86C4 XCHG AL,AH -t AX=7A61 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=135E ES=135E SS=135E CS=135E IP=010A NV UP EI PL NZ NA PO NC 135E:010A 894502 MOV [DI+02],AX DS:0002=9FFF -t…… -d DS:0 F 135E:0000 5A 41 61 7A 00 9A EE FE-1D F0 4F 03 C2 0D 8A 03 ZAaz......O.....
Programul DEBUG a fost creat de Tim Paterson, autorul original al sistemului de operare MS-DOS. Este un instrument uşor de folosit şi care serveşte încă (utilizatorilor individuali, sau în medii universitare) pentru studiul limbajului de asamblare - deşi nu recunoaşte decât setul de instrucţiuni Intel 8086/8087; Microsoft® nu l-a "updatat" - din fericire! - probabil pentru că a preferat să comercializeze propriile instrumente de programare: asamblorul MASM (Microsoft Macro Assembler), compilatorul şi debuggerul CodeView, şi apoi Microsoft Visual Studio.
ORAR orarul şcolii
SitSco situaţie şcolară
ŞAH prin corespondenţă
doChess a Javascript chess engine
doPGN a Javascript PGN-browser
Cal++ ambiţiile Calului
aşaAzis momente lingvistice
Comentarii
—cum ar trebui calculată Media şcolară?
completely rethink the browser:
Google chrome