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).

Structura unui program în limbaj de asamblare

Î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):

GasNasm
.section .datasection .dataîncepe o secţiune pentru date iniţializate
    var1: .int 1234    var1 dw 1234int var1; (în C/C++)
    array: .byte 1, 2, 3, 4    array db 1, 2, 3, 4char array[]={1,2,3,4};
    nume: .string "Enter Your Name: "      nume db "Enter Your Name: "  char nume[]="Enter Your Name: ";
.section .bsssection .bssîncepe o secţiune pentru alocare de date
    .lcomm buffer, 100    buffer resb 100rezervă 100 bytes de memorie
.section .textsection .textîncepe secţiunea de cod (sau "text")
    .globl _start    global _startdeclară punctul de intrare în execuţie
       _start:       _start:adresa de început a execuţiei
          movl $nume, %edx          mov edx, numeinstrucţ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.).

Sintaxa de asamblare AT&T şi sintaxa Intel

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).

Apeluri de sistem (întreruperi software)

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.

O problemă cu litere şi coduri ASCII

Î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.

Pe Linux, folosind asamblorul Gas

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.

Pe Windows, folosind asamblorul Nasm

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).