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

Un exemplu de programare în limbaj de asamblare
În şcoli se folosesc compilatoarele de la Borland, cu licenţă comercială specifică (de fapt, compilatoarele sunt implicate numai la nivelul Integrated Development Environment pentru DOS/Windows). Borland a introdus pe piaţă şi asamblorul TASM Turbo Assembler, interoperabil în general cu celelalte produse Borland. Există o versiune "free" Borland C++ v.5.5, care include compilatorul bcc32.exe, linker-ul tlink32.exe, etc. - dar nu şi TASM (care ar trebui achiziţionat separat).
Alternativa achiziţionării de software comercial este investigarea zonei free software…
nasm Netwide Assembler este un asamblor pentru Intel x86 (şi pentru mai multe sisteme de operare), sub licenţa LGPL (la fel de exemplu, ca şi Mozilla). Pentru Windows, se poate descărca nasm-2.02-dos.zip DOS 32-bit binaries, de la //sourceforge.net/projects/nasm; după dezarhivare, se poate consulta fişierul text "nasmdoc", pentru documentare asupra instalării şi folosirii asamblorului.
Gas The GNU Assembler este asamblorul folosit de GNU. Gas (ca executabil - as) este "back-end"-ul implicit pentru GCC (colecţie GNU de compilatoare pentru C/C++, Java, Fortran, Ada, şi alte limbaje de programare), fiind utilizat între altele şi pentru a compila Linux-ul. Multe programe GNU au fost portate pe Microsoft Windows, Mac OS X şi chiar pe Unix (constatându-se adesea că sunt mai bune decât programele comerciale pe care le înlocuiesc); de exemplu, Cygwin sau Mingw portează diverse pachete GNU - în particular GCC şi implicit, Gas - pe un sistem Windows.
În cele ce urmează încercăm să vizăm pe un exemplu concret, ambele asambloare: Gas şi respectiv, nasm; descriem şi folosim diverse categorii de instrucţiuni CPU/FPU, câteva apeluri de sistem (pentru DOS/Windows, respectiv Linux), diverse directive de asamblare şi desigur - pe cât se poate - ilustrăm sau clarificăm diverse chestiuni fundamentale (reprezentarea numerelor, codul ASCII, etc.; elaborarea şi dezvoltarea programelor).
Într-un limbaj de nivel înalt, un program este format de obicei din declaraţii globale (cu sau fără iniţializare), o funcţie principală (care conţine punctul de începere a execuţiei programului) şi diverse alte funcţii; tot aşa, un program în limbaj de asamblare conţine o secţiune de date iniţializate, una de date neiniţializate (pentru care doar se alocă memorie) şi o secţiune de cod (desigur, directivele prin care se introduc aceste secţiuni pot să difere, în funcţie de asamblorul folosit):
| Gas | Nasm | |
|---|---|---|
| .section .data | section .data | începe o secţiune pentru date iniţializate |
| var1: .int 1234 | var1 dw 1234 | int var1; (în C/C++) |
| array: .byte 1, 2, 3, 4 | array db 1, 2, 3, 4 | char array[]={1,2,3,4}; |
| nume: .string "Enter Your Name: " | nume db "Enter Your Name: " | char nume[]="Enter Your Name: "; |
| .section .bss | section .bss | începe o secţiune pentru alocare de date |
| .lcomm buffer, 100 | buffer resb 100 | rezervă 100 bytes de memorie |
| .section .text | section .text | începe secţiunea de cod (sau "text") |
| .globl _start | global _start | declară punctul de intrare în execuţie |
| _start: | _start: | adresa de început a execuţiei |
| movl $nume, %edx | mov edx, nume | instrucţiunile programului... |
Asamblorul primeşte un astfel de fişier şi creează codul obiect, prin translatarea mnemonicelor de instrucţiuni în cod-maşină specific microprocesorului destinaţie şi prin transformarea în adrese relative a simbolurilor (etichete) existente în programul sursă; în codul-obiect, secţiunile sunt nişte blocuri disjuncte de adrese consecutive, având fiecare adresa de bază zero (nu corespund unor adrese reale). Fişierul-obiect trebuie transmis (direct sau indirect) unui program linker, care va constitui fişierul executabil, relocatând adresele încât acestea să corespundă unor adrese reale şi adăugând fişierului obiect un header cu informaţii despre secţiunile programului (adresă, dimensiune, etc.).
Nasm foloseşte sintaxa Intel, ca şi multe alte asambloare; Gas foloseşte sintaxa AT&T, standard pe sistemele Unix (dar Gas oferă suport şi pentru sintaxă Intel, prin directiva de asamblare .intel_syntax; iar pe de altă parte, există programe care convertesc dintr-o sintaxă în cealaltă).
Formatul instrucţiunilor (cei doi operanzi sunt inversaţi)
în sintaxa AT&T: Mnemonică Sursă, Destinaţie,
în sintaxa Intel: Mnemonică Destinaţie, Sursă.
În sintaxa AT&T operanzii registru trebuie prefixaţi cu % (mov %eax, %ebx), iar operanzii imediaţi (numere constante, adrese constante) trebuie prefixaţi cu $ (mov $4, %eax faţă de "mov eax, 4").
În sintaxa AT&T mnemonicele instrucţiunilor care angajează referinţe de memorie trebuie sufixate cu b (referinţă de byte, 8 biţi), w (= word, 16 biţi), l (= long, 32 biţi), etc. - permiţând asamblorului să determine dimensiunea operandului implicat (sufixul poate lipsi dacă operanzii implicaţi nu referă memoria); în sintaxa Intel, determinarea dimensiunii operanzilor de memorie necesită prefixarea cu byte ptr, sau cu word ptr, respectiv dword ptr. De exemplu, pentru a încărca în registrul AL un octet de la adresa "foo" - AT&T: movb foo, %al; Intel: mov al, byte ptr foo.
Particularităţile sintactice AT&T au justificările lor; de exemplu, indicarea regiştrilor prin prefixul "%" face posibilă includerea simbolurilor externe C fără vreun risc de confuzie (şi fără a cere prefixarea acestor simboluri externe cu "_", precum în cazul altor asambloare).
Uneori execuţia unui program trebuie întreruptă, pentru a deservi evenimente care impun un răspuns prompt. Întreruperile hardware sunt "cerute" de diverse dispozitive externe - tastatură, mouse, CD-ROM, etc. - al căror hardware propriu este conectat la un controller programabil de întreruperi (circuitul I8259, pe platformele Intel) cu care şi microprocesorul are două legături (doi pini prin care percepe semnalele şi codurile de întrerupere de la I8259). Întreruperile software sunt emise de un program în execuţie, în general în acele momente când execuţia ajunge în situaţia de a necesita acces direct la hardware; de regulă, numai sistemul de operare are acces direct la hardware - aplicaţiile obişnuite trebuie să apeleze la sistemul de operare pentru asemenea operaţii. Sistemul de operare oferă o gamă suficient de largă de funcţii standard pentru realizarea operaţiilor obişnuite: citire de la tastatură într-un buffer de memorie, deschiderea unui fişier pentru citire/scriere, crearea unui director, deservirea operaţiilor obişnuite cu mouse-ul, etc.; adresele de intrare în funcţiile respective sunt înregistrate într-o anumită zonă de memorie (numită de obicei tabela vectorilor de întrerupere).
Instrucţiunea INT 0x21 pentru DOS/Windows şi analog, INT 0x80 pentru Linux - întrerupe execuţia programului din care este lansată şi "apelează" funcţia a cărei adresă este înregistrată în tabele vectorilor de întrerupere la intrarea de rang 4*0x21 = 0x84 (= 132) şi respectiv 4*0x80 = 0x200 (=512) pentru Linux; această funcţie constituie interfaţa între aplicaţiile obişnuite şi serviciile oferite de către sistemul de operare. Protocolul asumat de această interfaţă presupune că în registrul AH (sau în EAX) a fost transmis "numărul serviciului", iar în alţi regiştri au fost transmişi anumiţi parametri corespunzători serviciului solicitat sistemului de operare; pe baza numărului existent în AH (sau în EAX) se determină intrarea în tabela vectorilor de întrerupere corespunzătoare acelui serviciu şi - după salvarea pe stivă a tuturor regiştrilor - se apelează funcţia a cărei adresă este indicată în tabel la intrarea respectivă, furnizându-i ca parametri valorile primite în ceilalţi regiştri; la încheierea execuţiei funcţiei apelate, se reconstituie de pe stivă regiştrii salvaţi iniţial şi se revine în aplicaţia care solicitase serviciul respectiv.
În alfabetul standard avem 26 litere (mari sau mici) şi - fiindcă 26 < 32 = 25 = 1000002 - rezultă că sunt suficienţi 5 biţi pentru a reprezenta valorile 1..26, prin 1 = 00001, 2 = 00010, 3 = 00011, ..., 26 = 11010.
Prefixând cu 010 rezultă codurile ASCII ale majusculelor, 'A' = 01000001 = 0x41 = 65, 'B' = 01000010 = 0x42 = 66, ..., 'Z' = 01011010 = 0x5A = 90; înlocuind prefixul cu 011, rezultă codurile ASCII ale literelor mici 'a' = 01100001 = 0x61 = 97, 'b' = 01100010 = 0x62 = 97, ..., 'z' = 01111010 = 0x7A = 122.
Prefixarea se poate face folosind operatorul logic OR între prefixul respectiv şi valoarea de "prefixat"; de exemplu, "01100000" OR "00000001" = 0x60 OR 0x01 = 0x61 = 'a' (codul literei 'a'). Fiindcă diferenţa celor două prefixe este "0110000" - "01000000" = 0x60 - 0x40 = 0x20, rezultă că oricare literă mică diferă (în privinţa codului ASCII) de litera mare omonimă prin 0x20 ('w' - 'W' = 0x20, oricare ar fi litera 'w').
Ne propunem un program în limbaj de asamblare, prin care să verificăm pur si simplu, mica "teorie" de mai sus: înscrie într-o zonă de memorie numerele de câte 8 biţi 1..26; prefixează octeţii din zona respectivă cu "010" şi afişează literele obţinute; comută prefixul pe "011" şi afişează literele rezultate.
Fişierele sursă pentru Gas au extensia standard (neobligatorie totuşi) .s (sau .S; de altfel, cu opţiunea -S compilatorul GCC generează codul în limbaj de asamblare); fişierele obiect produse de asamblor au extensia standard .o. Opţiunea de asamblare -g determină includerea în fişierul obiect a unui tabel de simboluri, care - asociind simbolurile existente în programul-sursă cu adresele stabilite de asamblor pentru ele - va permite folosirea ulterioară mai comodă (mai "umană"), a diverselor instrumente de investigare a codului (a debugg-erelor).
### Asamblare "limb.s" cu: as -g -a -o limb.o limb.s > limb.lst ### Obţine executabilul cu: ld -o limb limb.o .section .bss .lcomm litere, 26 # alocă 26 octeţi .section .data br: .string "\n" # codul ASCII/C de trecere pe rând nou .equ to_upp, 0x40 # litere mari = 1..26 OR 0x40 ('A' = 0x41) .equ to_low, 0x60 # litere mici = 1..26 OR 0x60 ('a' = 0x61) .macro write STR, STR_SIZE # scrie 'STR_SIZE' octeţi de la adresa 'STR' mov $4, %eax # în fişierul cu descriptorul EBX (stdout = 1) mov $1, %ebx movl \STR, %ecx movl \STR_SIZE, %edx int $0x80 # apelează kernelul Linux pentru serviciul 4 (= write) .endm .macro Exit mov $1, %eax # apelul de sistem 1 ("exit") încheie execuţia şi mov $0, %ebx # redă controlul către sistemul de operare int $0x80 .endm .section .text .globl _start # _start este numele standard (neobligatoriu) _start: # al punctului de intrare în execuţie (IP = _start) movb $to_upp, %al # AL = 0x40 call transform # constituie codurile ASCII de majuscule şi afişează movb $to_low, %al # AL = 0x60 call transform # constituie codurile ASCII de litere mici şi afişează Exit # încheie şi redă controlul sistemului de operare transform: # subrutină pentru constituirea la adresa 'litere' a mov $26, %ecx # codurilor ASCII de litere mari sau mici, movl $litere, %edi # prin OR între valorile 1..26 şi registrul AL mov $1, %ah # (la apelare, AL conţine prefixul necesar) or %al, %ah # AH = codul ASCII al literei next: # repetă pentru cele 26 de litere movb %ah, (%edi) # înscrie litera la adresa pointată de EDI inc %ah # AH = codul ASCII al literei următoare inc %edi # EDI pointează locul următoarei litere de înscris loop next # ECX--; dacă ECX > 0 atunci reia de la adresa 'next' write $litere, $26 # scrie pe ecran cele 26 de litere write $br, $1 # şi trece pe rândul de ecran următor ret # încheie 'transform', reluând execuţia programului apelant
Executând programul (pentru verificare), obţinem cele două rânduri de litere:
vb@debian:~/lar$ ./limb
ABCDEFGHIJKLMNOPQRSTUVWXYZ
abcdefghijklmnopqrstuvwxyz
vb@debian:~/lar$
După asamblare, s-a obţinut fişierul obiect limb.o; îl putem dezasambla cu objdump, pentru a constata că:
— toate simbolurile folosite în sursa limb.s au fost "eliminate", fiind înlocuite prin adrese sau valori; de exemplu, 'transform' a devenit adresa 0x0000001a (iar "call transform" a devenit "call 1a"); simbolurile definite prin directiva .equ au fost înlocuite prin valorile respective (faptul că apar şi etichetele simbolice pe listingul de dezasamblare se datorează opţiunii de asamblare -g, prin care s-a cerut asamblorului să includă în fişierul obiect şi un tabel de simboluri);
— macrourile au fost expandate, în locul unde au fost invocate;
— secţiunea de date "nu apare" pe listing, decât în mod implicit: de exemplu, la adresa 0x0000001f avem codul instrucţiunii "mov $0x0,%edi" care corespunde instrucţiunii "movl $litere, %edi" din limb.s şi deducem că etichetei "litere" îi corespunde adresa relativă 0 din segmentul de date.
vb@debian:~/lar$ objdump -d limb.o;
limb.o: file format elf32-i386
Disassembly of section .text:
00000000 <_start>:
0: b0 40 mov $0x40,%al
2: e8 13 00 00 00 call 1a <transform>
7: b0 60 mov $0x60,%al
9: e8 0c 00 00 00 call 1a <transform>
e: b8 01 00 00 00 mov $0x1,%eax
13: bb 00 00 00 00 mov $0x0,%ebx
18: cd 80 int $0x80
0000001a <transform>:
1a: b9 1a 00 00 00 mov $0x1a,%ecx
1f: bf 00 00 00 00 mov $0x0,%edi
24: b4 01 mov $0x1,%ah
26: 08 c4 or %al,%ah
00000028 <next>:
28: 88 27 mov %ah,(%edi)
2a: fe c4 inc %ah
2c: 47 inc %edi
2d: e2 f9 loop 28 <next>
2f: b8 04 00 00 00 mov $0x4,%eax
34: bb 01 00 00 00 mov $0x1,%ebx
39: b9 00 00 00 00 mov $0x0,%ecx
3e: ba 1a 00 00 00 mov $0x1a,%edx
43: cd 80 int $0x80
45: b8 04 00 00 00 mov $0x4,%eax
4a: bb 01 00 00 00 mov $0x1,%ebx
4f: b9 00 00 00 00 mov $0x0,%ecx
54: ba 01 00 00 00 mov $0x1,%edx
59: cd 80 int $0x80
5b: c3 ret
vb@debian:~/lar$
Procedând la fel cu fişierul executabil produs de linkerul ld (standard pentru Unix), vedem că secţiunile din program au fost relocatate corespunzător ("zona de cod" are adresa relativă 0x8048074; "litere" are adresa 0x80490d8; etc.):
vb@debian:~/lar$ objdump -d limb
limb: file format elf32-i386
Disassembly of section .text:
08048074 <_start>:
8048074: b0 40 mov $0x40,%al
8048076: e8 13 00 00 00 call 804808e <transform>
804807b: b0 60 mov $0x60,%al
804807d: e8 0c 00 00 00 call 804808e <transform>
8048082: b8 01 00 00 00 mov $0x1,%eax
8048087: bb 00 00 00 00 mov $0x0,%ebx
804808c: cd 80 int $0x80
0804808e <transform>:
804808e: b9 1a 00 00 00 mov $0x1a,%ecx
8048093: bf d8 90 04 08 mov $0x80490d8,%edi
8048098: b4 01 mov $0x1,%ah
804809a: 08 c4 or %al,%ah
0804809c <next>:
804809c: 88 27 mov %ah,(%edi)
804809e: fe c4 inc %ah
80480a0: 47 inc %edi
80480a1: e2 f9 loop 804809c <next>
80480a3: b8 04 00 00 00 mov $0x4,%eax
80480a8: bb 01 00 00 00 mov $0x1,%ebx
80480ad: b9 d8 90 04 08 mov $0x80490d8,%ecx
80480b2: ba 1a 00 00 00 mov $0x1a,%edx
80480b7: cd 80 int $0x80
80480b9: b8 04 00 00 00 mov $0x4,%eax
80480be: bb 01 00 00 00 mov $0x1,%ebx
80480c3: b9 d0 90 04 08 mov $0x80490d0,%ecx
80480c8: ba 01 00 00 00 mov $0x1,%edx
80480cd: cd 80 int $0x80
80480cf: c3 ret
vb@debian:~/lar$
Dacă am vrea să urmărim execuţia pas cu pas a programului, putem folosi GDB GNU's GDB Debugger.
Preferăm să folosim nasm în cel mai simplu mod: nu precizăm vreun format de fişier executabil (prin opţiunea -f), obţinând formatul executabil implicit bin (binar, în fişierul "limb.exe"); nu pretindem informaţie de depanare (prin opţiunea -g). Fişierul "limb.lst" (prin opţiunea -l) va conţine într-un format lizibil rezultatul asamblării.
;;; pe Windows: nasm limb.asm -o limb.exe -l limb.lst section .bss litere resb 26 ; aloca 26 octeti section .data br db "\n" ; codul ASCII/C de trecere pe rand nou (Unix) to_upp equ 40h ; litere mari = 1..26 OR 0x40 ('A' = 0x41) to_low equ 60h ; litere mici = 1..26 OR 0x60 ('a' = 0x61) %macro write 2 ; scrie %2 octeţi de la adresa %1 mov ah, 40h ; in fisierul cu descriptorul BX mov bx, 1 ; descriptorul 1 corespunde ecranului (stdout) mov cx, %2 ; numarul de octeti de scris mov dx, %1 ; adresa octetilor de scris int 21h ; apeleaza functia DOS pentru serviciul indicat in AH (scriere in fisier) %endmacro %macro Exit 0 mov ax, 4C00h ; functia DOS pentru exit (AH = 0x4C) int 21h %endm section .text start: ; punctul de intrare in execuţie (IP = start) mov al, to_upp ; AL = 0x40 call transform ; constituie codurile ASCII de majuscule si afiseaza mov al, to_low ; AL = 0x60 call transform ; constituie codurile ASCII de litere mici si afiseaza Exit ; incheie si reda controlul sistemului de operare transform: ; subrutina pentru constituirea la adresa litere a mov ecx, 26 ; codurilor ASCII de litere mari sau mici, mov edi, litere ; prin OR intre valorile 1..26 si registrul AL mov ah, 1 ; (la apelare, AL conţine prefixul necesar) or ah, al ; AH = codul ASCII al literei next: ; repeta pentru cele 26 de litere mov [edi], ah ; inscrie litera la adresa pointata de EDI inc ah ; AH = codul ASCII al literei urmatoare inc edi ; EDI pointeaza locul urmatoarei litere de inscris loop next ; ECX--; daca ECX > 0 atunci reia de la adresa next write litere, 26 ; scrie pe ecran cele 26 de litere write br, 1 ; si trece pe randul de ecran urmator ret ; incheie transform, reluand executia programului apelant
Executând şi dezasamblând cu ndisasm (inclus în pachetul Nasm):

De asemenea, putem folosi debug, cu care putem urmări şi execuţia pas cu pas a programului (folosind comanda t). Putem constata şi acum (am mai făcut-o şi anterior) că deşi debug nu recunoaşte mnemonicele de regiştri extinşi, pune în execuţie instrucţiunile respective (de exemplu, instrucţiunea "inc EDI" de cod 0x6647 este redată de comanda de dezasamblare u prin "DB 66" - 0x66 este octetul-prefix pentru date de 32 biţi - şi apoi "inc DI").

De data aceasta, prin executarea programului - literele au fost afişate toate pe acelaşi rând; de vină este interpretarea diferită a caracterului \n (emis prin instrucţiunea "write br, 1", care în cazul Linux a determinat trecerea pe următorul rând de ecran, la începutul acestuia).
Primele 32 de coduri ASCII sunt coduri de control; codurile 0x0A Line Feed şi 0x0D Carriage Return au fost destinate pentru controlul unei imprimante (sau al afişării pe ecran), anume: LF determină mutarea capului de imprimare cu o linie mai jos, iar CR determină mutarea capului de imprimare la marginea stângă (fără trecere pe următoarea linie); cu timpul, CR a fost de asemenea, asignat tastei ENTER, semnalând că s-a încheiat introducerea unui text de la tastatură.
Aceste coduri au fost implementate ca atare (păstrând semnificaţia originală) în multe protocoale de comunicaţie serială şi în sisteme de operare precum DOS/Windows; dar limbajul C şi Unix-ul au redefinit caracterul LF (0x0A) drept newline, însemnând acum combinarea operaţiilor "line feed" şi "carriage return" într-o singură operaţie (în multe limbaje, acest caracter de control este desemnat prin "\n") - motivarea fiind că este de dorit ca apăsând tasta ENTER să treci nu doar "dedesubt", dar chiar "dedesubt şi la începutul rândului".
Pe Linux, putem face următorul experiment simplu: creem un fişier text "test.txt" conţinând două rânduri; afişăm "test.txt" în hexazecimal (şi constatăm separarea rândurilor prin 0x0A); convertim "text.txt" la formatul DOS şi afişăm din nou (constatând acum că separarea rândurilor se face prin 0x0D şi 0x0A):
vb@debian:~/lar$ cat > test.txt aaa bbb vb@debian:~/lar$ hexdump -C test.txt 00000000 61 61 61 0a 62 62 62 |aaa.bbb| vb@debian:~/lar$ unix2dos test.txt vb@debian:~/lar$ hexdump -C test.txt 00000000 61 61 61 0d 0a 62 62 62 |aaa..bbb|
Folosind iarăşi hexdump (de pe Linux), putem vedea fişierul binar "limb.exe" (produs de nasm pe Windows, cum am arătat mai sus):
vb@debian:~/lar$ hexdump -C nasm/limb.exe 00000000 b0 40 e8 0a 00 b0 60 e8 05 00 b8 00 4c cd 21 66 |.@....`.....L.!f| 00000010 b9 1a 00 00 00 66 bf 48 00 00 00 b4 01 08 c4 67 |.....f.H.......g| 00000020 88 27 fe c4 66 47 e2 f7 b4 40 bb 01 00 b9 1a 00 |.'..fG...@......| 00000030 ba 48 00 cd 21 b4 40 bb 01 00 b9 01 00 ba 44 00 |.H..!.@.......D.| 00000040 cd 21 c3 00 5c 6e |.!..\n|
Confruntând cu dezasamblările prezentate mai sus, constatăm că "limb.exe" conţine nimic altceva decât codurile binare ale instrucţiunilor (conformându-se vechiului format de executabil .COM din DOS). Alte formate de fişier-executabil (produse prin intermediul unui linker, din fişierul-obiect) au în plus un header specific, cu informaţii de relocatare a secţiunilor şi eventual cu informaţii de depanare (tabele de simboluri); de exemplu, se poate vedea cu hexdump fişierul executabil rezultat în primul caz (când am folosit Gas şi linkerul ld pe Linux).
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