Instrucțiuni de asamblare și instrucțiuni de mașină. Instructiuni de asamblare si masini Registre de uz general

Scriu un program în assembler (x86) pentru proiector. Concluzia este că există un mod de comutare automată a diapozitivelor. Este necesar să amânați expunerea de diapozitive în trepte de 10 secunde. M-au ajutat să fac o asemenea întârziere. Mai jos sunt două proceduri (crearea întârzierii și reducerea întârzierii)

MakeDelay Proc Near mov al,Timer ;valoare de întârziere în intervalul de la 10 la 90 de unități shr al,4 mov ah,al xor al,al ror ax,2 mov word ptr Delay+1,ax mov byte ptr Delay,0 mov byte ptr Delay + 3.0 ret MakeDelay Endp

mov ax,word ptr Delay sau ax,word ptr Delay + 2 cmp ax,0 ; je nxtslide ;Da - mergeți la selecția slide sub word ptr Delay,1 ;Nu - mergeți la micșorare sbb word ptr Delay + 2,0 ;Delays jmp ext5 ;Exit subrutine

În prima procedură, nu este deloc clar cum au ajuns la un astfel de algoritm de întârziere. Și în procedura de decrementare a întârzierii, nu este clar de ce să faci o disjuncție. Problema este că implementarea întârzierii este foarte dependentă de sistemul de operare. Și, de exemplu, setarea valorii la 60 în Virtual Windows xp are ca rezultat o întârziere de 65 de secunde, iar în Windows 7 50 de secunde. Vă rog să mă ajutați să scap de această mizerie.

Cod pentru sarcină: „Assembler x86”

textual

Lista de programe

ACP proc aproape valoarea corectă cu ADC push ax; salvează registrele utilizate în această rutină push dx; mov al,01h ;porniți „Start” out 03h,al ; Așteptați: în al,02h ; așteptați ca „Rdy” să se aprindă test al,01h ; jz Aşteptare ; mov al,0 ;resetează „Start” out 03h,al; in al,01h ;cititi valoarea pe registrul de intrare mov ah,10d ; mov dl,0FFh ;încărcați în dl numărul maxim care poate fi aplicat registrului de intrare mul ah ;înmulțiți numărul din registrul de intrare cu limita superioară a ADC (tensiune max.) div dl ;împarți rezultatul la maxim valoare pe registrul de intrare și obțineți numărul din intervalul 1..10 mov skor_ACP,al pop dx pop ax ret ACP endp

Poate fi considerat ca codare automată(vezi mai jos), extins prin construcții . Este, în esență, dependent de platformă. Limbajele de asamblare pentru diverse platforme hardware sunt incompatibile, deși pot fi în general similare.

În rusă, poate fi numit simplu " asamblator” (expresii precum „scrieți un program în asamblator” sunt tipice), ceea ce, strict vorbind, nu este adevărat, deoarece asamblator utilitarul pentru traducerea unui program cu limbaj de asamblareîn codul computerului.

Definiție generală

Limbajul de asamblare este o notație folosită pentru a reprezenta programele scrise în codul mașinii într-o formă care poate fi citită. Limbajul de asamblare permite programatorului să folosească coduri de operare mnemonice alfabetice, să atribuie nume simbolice registrelor și memoriei computerului la propria discreție și, de asemenea, să stabilească scheme de adresare care sunt convenabile pentru el (de exemplu, index sau indirect). În plus, vă permite să utilizați diverse sisteme calcul (de exemplu, zecimal sau hexazecimal) pentru a reprezenta constante numerice și face posibilă marcarea liniilor de program cu etichete cu nume simbolice, astfel încât acestea să poată fi accesate (după nume, nu după adresă) din alte părți ale programului (de exemplu, pentru a transfera controlul ) .

Se realizează traducerea unui program în limbaj de asamblare în cod de mașină executabil (calcul de expresii, extinderea macrocomenzilor, înlocuirea mnemotecilor cu coduri de mașină propriu-zise și adrese simbolice cu adrese absolute sau relative). asamblator- un program de traducere, care a dat numele limbajului de asamblare.

Instrucțiunile din limbajul de asamblare corespund unu la unu instrucțiunilor procesorului. De fapt, ele reprezintă o formă simbolică de notare care este mai convenabilă pentru o persoană - mnemocoduri- comenzile și argumentele lor. În acest caz, mai multe variante de instrucțiuni ale procesorului pot corespunde unei instrucțiuni de limbaj de asamblare.

În plus, limbajul de asamblare permite utilizarea etichetelor simbolice în locul adreselor celulelor de memorie, care, în timpul asamblarii, sunt înlocuite cu adrese absolute sau relative calculate de asamblator sau linker, precum și așa-numitele directive(instrucțiuni de asamblare care nu sunt traduse în instrucțiuni de mașină ale procesorului, dar sunt executate de către asamblator însuși).

Directivele de asamblare permit, în special, includerea blocurilor de date, setarea asamblarii unui fragment de program după condiție, setarea valorilor de etichetă, utilizarea macro-urilor cu parametri.

Fiecare model (sau familie) de procesoare are propriul set - sistem - de instrucțiuni și limbajul de asamblare corespunzător. Cele mai populare sintaxe ale limbajului de asamblare sunt sintaxa Intel și sintaxa AT&T.

Există calculatoare care implementează limbaj de programare la nivel înalt (Fort, Lisp, El-76) ca limbaj de mașină. De fapt, în astfel de computere ele îndeplinesc rolul de limbaje de asamblare.

Capabilități

Utilizarea limbajului de asamblare oferă programatorului o serie de caracteristici care de obicei nu sunt disponibile atunci când programează în limbaje de nivel înalt. Cele mai multe dintre ele sunt legate de apropierea limbajului de platforma hardware.

  • Abilitatea de a folosi pe deplin toate caracteristicile platformei hardware permite, teoretic, să scrieți cel mai rapid și mai compact cod posibil pentru un procesor dat. Un programator calificat, de regulă, este capabil să optimizeze semnificativ programul în comparație cu un traducător dintr-un limbaj de nivel înalt în unul sau mai mulți parametri și să creeze un cod apropiat de Pareto optim (de regulă, viteza programului este realizat prin prelungirea codului și invers):
    • datorită utilizării mai raționale a resurselor procesorului, de exemplu, cea mai eficientă plasare a tuturor datelor inițiale în registre, este posibil să se elimine accesul inutil la RAM;
    • datorită optimizării manuale a calculelor, inclusiv utilizării mai eficiente a rezultatelor intermediare, cantitatea de cod poate fi redusă și viteza programului poate fi mărită.
  • Abilitatea de a accesa direct hardware-ul și, în special, porturile I/O, adresele de memorie specifice, registrele procesorului (cu toate acestea, această capacitate este limitată semnificativ de faptul că în multe sisteme de operare accesul direct din programele de aplicație pentru a scrie în registrele perifericelor). echipamentul este blocat pentru funcționarea sistemului de fiabilitate).

Utilizarea assemblerului nu are aproape nicio alternativă la crearea:

  • driverele hardware și nucleul sistemului de operare (cel puțin, subsisteme dependente de mașină ale nucleului OS), când este importantă coordonarea funcționării dispozitivelor periferice cu procesorul central;
  • programe care trebuie stocate într-un ROM limitat și/sau rulat pe dispozitive cu performanțe limitate („firmware” de computere și diverse dispozitive electronice)
  • Componente specifice platformei ale compilatoarelor și interpreților limbilor de nivel înalt, biblioteci de sistem și cod care implementează compatibilitatea cu platformele.

Separat, se poate observa că, cu ajutorul unui program de dezasamblare, este posibilă convertirea unui program compilat într-un program în limbaj de asamblare. În cele mai multe cazuri, aceasta este singura modalitate (deși extrem de consumatoare de timp) de a face inginerie inversă a algoritmilor programului dacă codul sursă într-un limbaj de nivel înalt nu este disponibil.

Restricții

Aplicație

Din punct de vedere istoric, dacă codurile de mașină sunt considerate prima generație de limbaje de programare, atunci limbajul de asamblare poate fi considerat a doua generație de limbaje de programare. Deficiențele limbajului de asamblare, complexitatea dezvoltării sistemelor software mari pe acesta au dus la apariția limbajelor de a treia generație - limbaje de programare de nivel înalt (cum ar fi Fortran, Lisp, Cobol, Pascal, C etc. ). Limbajele de programare de nivel înalt și succesorii lor sunt utilizate în prezent în principal în industria tehnologiei informației. Cu toate acestea, limbajele de asamblare își păstrează nișa datorită avantajelor lor unice în ceea ce privește eficiența și capacitatea de a utiliza pe deplin caracteristicile specifice ale unei anumite platforme.

Programele sau fragmentele lor sunt scrise în limbaj de asamblare în cazurile în care sunt critice:

  • performanta (soferi, jocuri);
  • cantitatea de memorie utilizată (sectoare de pornire, software încorporat (ing. embedded), programe pentru microcontrolere și procesoare cu resurse limitate, viruși, protecție software).

Folosind programarea în limbaj de asamblare, sunt produse următoarele:

  • Optimizarea secțiunilor de programe critice pentru viteză în programe în limbaje de nivel înalt, cum ar fi C++ sau Pascal. Acest lucru este valabil mai ales pentru consolele de jocuri, care au o performanță fixă, și pentru codecurile multimedia, care tind să consume mai puțin resurse și mai rapide.
  • Crearea sistemelor de operare (OS) sau a componentelor acestora. În prezent, marea majoritate a sistemelor de operare sunt scrise în limbaje de nivel superior (în principal C, un limbaj de nivel înalt care a fost creat special pentru a scrie una dintre primele versiuni de UNIX). Piesele de cod dependente de hardware, cum ar fi încărcătorul sistemului de operare, stratul de abstractizare hardware și nucleul, sunt adesea scrise în limbaj de asamblare. De fapt, există foarte puțin cod de asamblare în nucleele Windows sau Linux, deoarece autorii se străduiesc să asigure portabilitatea și fiabilitatea, dar există totuși. Unele sisteme de operare amatoare, cum ar fi MenuetOS și KolibriOS, sunt scrise în întregime în limbaj de asamblare. În același timp, MenuetOS și KolibriOS sunt plasate pe o dischetă și conțin o interfață grafică cu mai multe ferestre.
  • Programarea microcontrolerelor (MC) și a altor procesoare încorporate. Potrivit profesorului Tanenbaum, dezvoltarea MC repetă dezvoltarea istorică a computerelor moderne. Acum (2013), limbajul de asamblare este foarte des folosit pentru programarea MK (deși limbaje precum C sunt, de asemenea, utilizate pe scară largă în acest domeniu). În MK, trebuie să mutați octeți și biți individuali între diferite celule de memorie. Programarea MK este foarte importantă, deoarece, potrivit lui Tanenbaum, într-o mașină și un apartament al unei persoane civilizate moderne, există, în medie, 50 de microcontrolere.
  • Crearea de drivere. Driverele (sau unele dintre modulele lor software) programează în limbaj de asamblare. Deși în prezent, driverele tind să scrie și în limbi de nivel înalt (este mult mai ușor să scrieți un driver de încredere într-o limbă de nivel înalt) datorită cerințelor crescute de fiabilitate și performanțe suficiente ale procesoarelor moderne (viteza asigură sincronizarea proceselor în dispozitiv și procesor) și perfecțiunea suficientă a compilatoarelor cu limbaje de nivel înalt (absența transferurilor inutile de date în codul generat), marea majoritate a driverelor moderne sunt scrise în limbaj de asamblare. Fiabilitatea driverelor joacă un rol special, deoarece în Windows NT și UNIX (inclusiv Linux) driverele rulează în modul kernel al sistemului. O eroare subtilă într-un driver poate prăbuși întregul sistem.
  • Crearea de antivirusuri și alte programe de protecție.
  • Scrierea codului pentru bibliotecile de nivel scăzut ale traducătorilor de limbaje de programare.

Conectarea programelor în diferite limbi

Deoarece de mult timp doar fragmente de programe au fost adesea codificate în limbaj de asamblare, acestea trebuie legate de restul sistemului software scris în alte limbaje de programare. Acest lucru se realizează în două moduri principale:

  • În etapa de compilare - inserarea fragmentelor de asamblare (eng. asamblator inline) în codul sursă al unui program într-un limbaj de nivel înalt folosind directive speciale de limbaj. Metoda este convenabilă pentru transformări simple de date, dar este imposibil să se realizeze un cod de asamblare cu drepturi depline cu date și subrutine, inclusiv subrutine cu multe intrări și ieșiri care nu sunt acceptate de un limbaj de nivel înalt.
  • În stadiul de link la compilarea separată. Pentru ca modulele composabile să interacționeze, este suficient ca funcțiile importate (definite în unele module și utilizate în altele) să suporte anumite convenții de apelare. Modulele separate pot fi scrise în orice limbă, inclusiv limbajul de asamblare.

Sintaxă

Sintaxa limbajului de asamblare este determinată de setul de instrucțiuni al unui anumit procesor.

Set de comenzi

Comenzile tipice ale limbajului de asamblare sunt (majoritatea exemplelor sunt date pentru sintaxa Intel pentru arhitectura x86):

  • Comenzi de transfer de date (mov etc.)
  • Comenzi aritmetice (add, sub, imul etc.)
  • Operații logice și pe biți (sau , și , xor , shr , etc.)
  • Programe comenzi de control al fluxului (jmp , loop , ret , etc.)
  • Instrucțiuni de întrerupere a apelului (uneori denumite instrucțiuni de control): int
  • Comenzi I/O către porturi (in , out)
  • Microcontrolerele și microcalculatoarele sunt, de asemenea, caracterizate prin comenzi care efectuează verificări și tranziții în funcție de condiție, de exemplu:
  • cjne - sari daca nu este egal
  • djnz - decrementează, iar dacă rezultatul este diferit de zero, atunci sari
  • cfsneq - comparați, iar dacă nu este egal, săriți următoarea comandă

Instrucțiuni

Format tipic de înregistrare a comenzilor

[etichetă:] [ [prefix] mnemocod [operand (, operand)] ] [ ;comentar]

Unde mnemocod- mnemonic direct al instrucțiunii către procesor. Pot fi adăugate prefixe (repetiții, modificări de tip de adresare etc.).

Mnemonicile folosite sunt de obicei aceleași pentru toate procesoarele din aceeași arhitectură sau familie de arhitecturi (printre cele cunoscute sunt x86, ARM, SPARC, PowerPC, procesor M68k și mnemonice controler). Acestea sunt descrise în specificațiile procesorului. Posibile excepții:

  • dacă asamblatorul folosește sintaxa AT&T multiplatformă (mnemonicele originale sunt convertite în sintaxa AT&T);
  • dacă inițial existau două standarde pentru scrierea mnemotecilor (sistemul de instrucțiuni a fost moștenit de la procesorul altui producător).

De exemplu, procesorul Zilog Z80 a moștenit setul de instrucțiuni Intel 8080, l-a extins și a schimbat mnemonicii (și desemnările registrului) în felul său. Procesoarele Motorola Fireball au moștenit setul de instrucțiuni Z80, reducându-l puțin. În același timp, Motorola a revenit oficial la mnemonicii Intel și în acest moment jumătate dintre asamblatorii Fireball lucrează cu mnemonicii Intel și jumătate cu mnemonicii Zilog.

directive

Un program în limbaj de asamblare poate conține directive: instrucțiuni care nu se traduc direct în instrucțiuni de mașină, dar controlează funcționarea compilatorului. Setul și sintaxa lor variază semnificativ și depind nu de platforma hardware, ci de traducătorul utilizat (dând naștere la dialecte ale limbilor din cadrul aceleiași familii de arhitecturi). Ca „set gentleman” de directive, se pot distinge următoarele:

  • definirea datelor (constante și variabile),
  • gestionarea organizării programului în memorie și a parametrilor fișierului de ieșire,
  • setarea modului compilatorului,
  • tot felul de abstracții (adică elemente ale limbajelor de nivel înalt) - de la proiectarea procedurilor și funcțiilor (pentru a simplifica implementarea paradigmei de programare procedurală) până la structuri și bucle condiționate (pentru paradigma de programare structurată),

Exemplu de program

Exemple de programe Bună, lume! pentru diferite platforme și diferite dialecte:

SECTION .data msg: db " Hello , world " , 10 len: equ $-msg SECTION .text global _start _start: mov edx , len mov ecx , msg mov ebx , 1 ; stdout mov eax , 4 ; scrie(2) int 0x80 mov ebx , 0 mov eax , 1 ; exit(2) int 0x80

SECȚIUNE .data msg: db " Bună ziua , lume " , 10 len: equ $-msg SECȚIUNE .text global _start syscall: int 0x80 ret _start: push len push msg push 1 ; stdout mov eax , 4 ; write(2) call syscall add esp , 3 * 4 push 0 mov eax , 1 ; exit(2) apel syscall

386 .model plat , opțiune stdcall casemap : niciunul include \ masm32 \ include \ windows.inc include \ masm32 \ include \ kernel32.inc includelib \ masm32 \ lib \ kernel32.lib .data msg db " Salut , lume " , 13 , 10 len equ $-msg .data ? scris dd? .code start: push - 11 call GetStdHandle push 0 push OFFSET scris push len push OFFSET msg push eax call WriteFile push 0 call ExitProcess terminare start

format PE consolă intrarea început include " include \ win32a.inc " secțiunea " .data " date lizibile mesaj scriptabil db " Bună , lume! " , 0 secțiune " .code " cod citibil executabil începe: ; CINVOKE macro în FASM. ; Vă permite să apelați funcții CDECL. cinvoke printf , mesaj cinvoke getch ; INVOKE este o macrocomandă similară pentru funcțiile STDCALL. invocați ExitProcess , secțiunea 0 „ .idata ” importați nucleul bibliotecii care pot fi citite , „ kernel32.dll ” , \ msvcrt , „ msvcrt.dll ” import kernel , \ ExitProcess , „ ExitProcess ” import msvcrt , \ printf , „ printf ” , \ getch , „_getch”

;yasm-1.0.0-win32.exe -f win64 HelloWorld_Yasm.asm;setenv /Release /x64 /xp ;link HelloWorld_Yasm.obj Kernel32.lib User32.lib /entry:main /subsystem:windows /LARGEADDRESSAWARE:NU biți 64 global principal extern MessageBoxA extern ExitProcess secțiune .data mytit db " Lumea pe 64 de biți a Windows și asamblatorului... " , 0 mymsg db " Bună lume ! " , 0 secțiune .text main: mov r9d , 0 ; uType = MB_OK mov r8 , mytit ; LPCSTR lpCaption mov rdx , mymsg ; LPCSTR lpText mov rcx , 0 ; hWnd = HWND_DESKTOP apel MessageBoxA mov ecx , eax ; uExitCode = MessageBox(...) apelează ExitProcess ret

Secțiunea ".data" salut: .asciz "Bună ziua!\n" .secțiunea ".text" .align 4 .global main main: salvați %sp , - 96 , %sp ! alocă memorie mov 4 , %g1 ! 4 = SCRIERE (apel de sistem) mov 1 , %o0 ! 1 = STDOUT set salut , %o1 mov 14 , %o2 ! numărul de caractere ta 8 ! apel de sistem! ieșire din program mov 1 , %g1 ! mutați 1 (exit () syscall ) în %g1 mov 0 , %o0 ! mutați 0 (adresa de retur) în %o0 ta 8 ! apel de sistem

org 7 C00h use16 jmp code nop db " hellowrd " SectSize dw 00200 h ClustSize db 001 h ResSecs dw 00001 h FatCnt db 002 h RootSiz dw 000 E0h TotSecs dw Media dw 00 hs2000 dw 00000 0000000 h HidnSec dw 00000 h cod: cli mov ax , cs mov ds , axe mov ss , axe mov sp , 7 c00h sti mov ax , 0 b800h mov es , axe mov di , 200 mov ah , 2 mov msg_print , 2 mov msg_ al ,[ cs : bx ] mov [ es : di ], ax inc bx add di , 2 cmp bx , MessEnd jnz msg_print loo: jmp loo MessStr equ $ Mesaj db " Hello , World ! " MessEnd equ $

Istorie și terminologie

Acest tip de limbaj și-a primit numele de la numele traducătorului (compilatorului) din aceste limbi - asamblator (asamblator englez - asamblator). Numele se datorează faptului că programul a fost „asamblat automat” și nu a fost introdus manual comandă cu comandă direct în coduri. În același timp, există o confuzie de termeni: asamblatorul este adesea numit nu numai traducător, ci și limbajul de programare corespunzător („programul de asamblare”).

Scuză-mă, ai un minut să vorbești despre salvatorul nostru, asamblatorul? În ultimul articol, am scris prima noastră aplicație Hello World în asma, am învățat cum să o compilam și să o depanăm și, de asemenea, am învățat cum să facem apeluri de sistem în Linux. Astăzi ne vom familiariza direct cu instrucțiunile de asamblare, conceptul de registre, stiva și toate acestea. Asamblatoarele pentru arhitecturile x86 (alias i386) și x64 (alias amd64) sunt foarte asemănătoare și, prin urmare, nu are sens să le luăm în considerare în articole separate. Mai mult, voi încerca să mă concentrez pe x64, pe parcurs notând diferențele față de x86, dacă există. Următoarele presupune că știți deja, de exemplu, cum diferă o stivă de o grămadă și nu este nevoie să explicați astfel de lucruri.

Registre de uz general

Un registru este o bucată mică (de obicei de 4 sau 8 octeți) de memorie dintr-un procesor cu extrem de mare viteză acces. Registrele sunt împărțite în registre motiv specialși registrele generale. Acum suntem interesați de registrele de uz general. După cum puteți ghici din nume, programul poate folosi aceste registre pentru propriile nevoi, după bunul plac.

Pe x86, sunt disponibile opt registre de uz general pe 32 de biți - eax, ebx, ecx, edx, esp, ebp, esi și edi. Registrele nu au un tip predefinit, adică pot fi tratate ca numere întregi semnate sau nesemnate, pointeri, booleeni, coduri de caractere ASCII și așa mai departe. Deși în teorie aceste registre pot fi utilizate în orice fel, în practică fiecare registru este de obicei folosit într-un mod anume. Deci, esp indică partea de sus a stivei, ecx joacă rolul unui contor, iar eax este rezultatul unei operații sau proceduri. Există registre de 16 biți ax, bx, cx, dx, sp, bp, si și di, care sunt cei mai puțin semnificativi 16 biți din registrele de 32 de biți corespunzătoare. De asemenea, sunt disponibile registrele de 8 biți ah, al, bh, bl, ch, cl, dh și dl, care reprezintă octeții mari și inferiori ai ax, bx, cx și, respectiv, dx.

Luați în considerare un exemplu. Să presupunem că sunt executate următoarele trei instrucțiuni:

(gdb) x/3i $buc
=> 0x8048074: mov $0xaabbccdd,%eax
0x8048079: mov $0xee,%al
0x804807b: mov $0x1234,%ax

Înregistrați valorile după ce ați scris 0 la eax X AABBCCDD:

(gdb) p/x $eax
$1 = 0xaabbccdd
(gdb) p/x $ax
$2 = 0xccdd
(gdb) p/x $ah
$3 = 0xcc
(gdb) p/x $al
$4 = 0xdd

Valorile după scrierea 0 la înregistrarea al X EE:

(gdb) p/x $eax
$5 = 0xaabbccee
(gdb) p/x $ax
$6 = 0xccee
(gdb) p/x $ah
$7 = 0xcc
(gdb) p/x $al
8 USD = 0xee

Înregistrați valori după ce ați scris 0 în ax X 1234:

(gdb) p/x $eax
9 USD = 0xaabb1234
(gdb) p/x $ax
10 USD = 0x1234
(gdb) p/x $ah
11 USD = 0x12
(gdb) p/x $al
12 USD = 0x34

După cum puteți vedea, nimic complicat.

Notă: Sintaxa GAS vă permite să specificați în mod explicit dimensiunile operanzilor folosind sufixele b (octeți), w (cuvânt, 2 octeți), l (cuvânt lung, 4 octeți), q (cuvânt patru, 8 octeți) și altele. De exemplu, în locul comenzii mov $0xEE , % al poti sa scrii movb $0xEE , %al , în loc de mov $0x1234 , % axmovw $0x1234 , %ax , si asa mai departe. În GAS modern aceste sufixe sunt opționale și eu personal nu le folosesc. Dar nu vă alarmați dacă le vedeți în codul altcuiva.

Pe x64, dimensiunea registrului a fost mărită la 64 de biți. Registrele corespunzătoare sunt denumite rax, rbx și așa mai departe. În plus, există șaisprezece registre de uz general în loc de opt. Registrele suplimentare sunt denumite r8, r9, ..., r15. Registrele corespondente, care reprezintă cei 32, 16 și 8 biți inferiori, se numesc r8d, r8w, r8b și, prin analogie, pentru registrele r9-r15. În plus, au apărut registre, care sunt cei 8 biți inferiori ai registrelor rsi, rdi, rbp și rsp - sil, dil, bpl și respectiv spl.

Despre adresare

După cum sa menționat deja, registrele pot fi tratate ca pointeri către datele din memorie. Pentru a dereferinta astfel de indicatori, se foloseste o sintaxa speciala:

mov(%rsp) , %rax

Această intrare înseamnă „citiți 8 octeți din adresa din registrul rsp și stocați-i în registrul rax”. Când un program este pornit, rsp indică în partea de sus a stivei, care stochează numărul de argumente transmise programului (argc), pointerii către acele argumente, precum și variabilele de mediu și alte informații. Astfel, ca urmare a executării instrucțiunii de mai sus (desigur, cu condiția ca înaintea acesteia să nu fi fost executate alte instrucțiuni), numărul de argumente cu care a fost lansat programul va fi scris în rax.

Într-o comandă, puteți specifica adresa și offset-ul (atât pozitiv, cât și negativ) în raport cu aceasta:

mov 8 (% rsp ), % rax

Această intrare înseamnă „luați rsp, adăugați-i 8, citiți 8 octeți la adresa rezultată și puneți-i în rax”. Astfel, rax va conține adresa șirului reprezentând primul argument al programului, adică numele fișierului executabil.

Când lucrați cu matrice, poate fi convenabil să faceți referire la un element la un index specific. Sintaxa relevanta:

# instrucțiunea xchg schimbă valori
xchg 16 (% rsp , % rcx , 8 ) , % rax

Se citește astfel: „calculați rcx*8 + rsp + 16 și schimbați 8 octeți (dimensiunea registrului) la adresa rezultată și valoarea registrului rax.” Cu alte cuvinte, rsp și 16 joacă în continuare rolul unui offset, rcx joacă rolul unui index în matrice, iar 8 este dimensiunea elementului matrice. Când utilizați această sintaxă, singurele dimensiuni permise ale elementelor sunt 1, 2, 4 și 8. Dacă este necesară o altă dimensiune, puteți utiliza înmulțirea, deplasarea binară și alte instrucțiuni, pe care le vom analiza în continuare.

În sfârșit, următorul cod este și el valabil:

Date
mesaj:
. ascii „Bună, lume!\n”
. text

Globl_start
_start:
# resetează rcx
xor %rcx , %rcx
msg mov(,% rcx , 8 ) , % al
msj mov, %ah

În sensul că nu puteți specifica un registru cu un offset sau niciun registru deloc. Ca rezultat al executării acestui cod, codul ASCII al literei H, sau 0, va fi scris în registrele al și ah. X 48.

În acest context, aș dori să menționez încă o instrucțiune de asamblare utilă:

# rax:= rcx*8 + rax + 123
lea 123 (% rax , % rcx , 8 ) , % rax

Instrucțiunea lea este foarte utilă, deoarece vă permite să efectuați înmulțiri și adunări multiple simultan.

fapte amuzante! Pe x64, codul octet al instrucțiunii nu utilizează niciodată decalaje pe 64 de biți. Spre deosebire de x86, instrucțiunile operează adesea nu cu adrese absolute, ci cu adrese relativ la adresa instrucțiunii în sine, ceea ce vă permite să accesați cel mai apropiat +/- 2 GB de RAM. Sintaxa relevanta:

movb msg(% rip) , % al

Să comparăm lungimile codurilor operaționale mov „regulate” și „relative” (objdump -d ):

4000b0: 8a 0c 25 e8 00 60 00 mov 0x6000e8,%cl
4000b7: 8a 05 2b 00 20 00 mov 0x20002b(%rip),%al # 0x6000e8

După cum puteți vedea, mov-ul „relativ” este, de asemenea, cu un octet mai scurt! Ce fel de registru este acest rip vom afla puțin mai jos.

Pentru a scrie valoarea completă de 64 de biți în registru, este furnizată o instrucțiune specială:

movabs $0x1122334455667788 , %rax

Cu alte cuvinte, procesoarele x64 codifică instrucțiunile la fel de puțin ca și procesoarele x86, iar în zilele noastre nu prea are sens să folosești procesoare x86 în sisteme cu câțiva gigaocteți de RAM sau mai puțin (dispozitive mobile, frigidere, cuptoare cu microunde și așa mai departe ). Sunt șanse ca procesoarele x64 să fie și mai eficiente datorită mai multor registre disponibile și dimensiune mai mare aceste registre.

Operatii aritmetice

Luați în considerare operațiile aritmetice de bază:

# inițializați valorile registrului
mov 123 USD , %rax
mov 456 USD , %rcx

# increment: rax = rax + 1 = 124
inc%rax

# decrement: rax = rax - 1 = 123
dec%rax

# adăugare: rax = rax + rcx = 579
adăugați % rcx , % rax

# scădere: rax = rax - rcx = 123
sub % rcx , % rax

# schimbare semn: rcx = - rcx = -456
neg %rcx

Aici și mai jos, operanzii pot fi nu numai registre, ci și zone de memorie sau constante. Dar ambii operanzi nu pot fi locații de memorie. Această regulă se aplică tuturor instrucțiunilor de asamblare x86/x64, cel puțin celor discutate în acest articol.

Exemplu de multiplicare:

mov 100 USD , % al
mov $3 , %cl
mul % cl

În acest exemplu, instrucțiunea mul înmulțește al cu cl și stochează rezultatul înmulțirii în perechea de registre al și ah. Astfel, ax va lua valoarea 0 X 12C sau 300 în notație zecimală. În cel mai rău caz, poate dura până la 2*N octeți pentru a stoca rezultatul înmulțirii a doi valori de N octeți. În funcție de dimensiunea operandului, rezultatul este stocat în al:ah, ax:dx, eax:edx sau rax:rdx. Mai mult, primul dintre aceste registre și argumentul transmis instrucțiunii sunt întotdeauna folosite ca multiplicatori.

Înmulțirea semnată se face exact în același mod folosind instrucțiunea imul. În plus, există variante de imul cu două și trei argumente:

mov 123 USD , %rax
mov 456 USD , %rcx

#rax=rax*rcx=56088
imul % rcx , % rax

#rcx=rax*10=560880
imul $10 , % rax , % rcx

Instrucțiunile div și idiv fac opusul mul și imul. De exemplu:

mov $0 , %rdx
mov 456 USD , %rax
mov $123 , %rcx

# rax = rdx:rax / rcx = 3
# rdx = rdx:rax % rcx = 87
div %rcx

După cum puteți vedea, a fost obținut rezultatul unei diviziuni întregi, precum și restul divizării.

Acestea nu sunt toate instrucțiuni aritmetice. De exemplu, există și adc (adăugare, ținând cont de steagul de transport), sbb (scădere, ținând cont de împrumut), precum și instrucțiuni corespunzătoare acestora care stabilesc și șterg steagurile corespunzătoare (ctc, clc) și multe altele. Dar ele sunt mult mai puțin comune și, prin urmare, nu sunt luate în considerare în cadrul acestui articol.

Operații logice și pe biți

După cum sa menționat deja, nu există o tastare specială în asamblatorul x86/x64. Prin urmare, nu vă mirați că nu are instrucțiuni separate pentru efectuarea operațiilor booleene și instrucțiuni separate pentru efectuarea operațiilor pe biți. În schimb, există un set de instrucțiuni care funcționează cu biți, iar modul de interpretare a rezultatului depinde de programul specific.

Deci, de exemplu, calculul celei mai simple expresii logice arată astfel:

mov $0 , % rax # a = fals
mov $1 , % rbx # b = adevărat
mov $0 , % rcx # c = fals

# rdx:= a || !(b && c)
mov % rcx , % rdx # rdx = c
și % rbx , % rdx # rdx &= b
nu %rdx#rdx=~rdx
sau % rax , % rdx # rdx |= a
și $1 , % rdx # rdx &= 1

Rețineți că aici am folosit un bit cel mai puțin semnificativ în fiecare dintre registrele de 64 de biți. Astfel, se formează gunoi în biții înalți, pe care îi resetăm la zero cu ultima comandă.

O altă instrucțiune utilă este xor (exclusiv sau). În expresiile booleene, xor este folosit rar, dar adesea resetează registrele. Dacă te uiți la codurile operaționale ale instrucțiunilor, devine clar de ce:

4000b3: 48 31 db xor %rbx,%rbx
4000b6: 48 ff c3 inc %rbx
4000b9: 48 c7 c3 01 00 00 00 mov $0x1,%rbx

După cum puteți vedea, instrucțiunile xor și inc sunt codificate cu doar trei octeți fiecare, în timp ce instrucțiunea mov care face același lucru ocupă până la șapte octeți. Fiecare caz individual, desigur, este mai bine să fie evaluat separat, dar regula euristică generală este aceasta - cu cât codul este mai scurt, cu atât se potrivește mai mult în memoria cache a procesorului, cu atât funcționează mai rapid.

În acest context, ar trebui să ne amintim și instrucțiunile pentru deplasarea biților, testul biților (testul biților) și scanarea biților (scanarea biților):

# pune ceva în registru
movabs $0xc0de1c0ffee2beef , %rax

# deplasați la stânga cu 3 biți
# rax = 0x0de1c0ffee2beef0
shl $4 , % rax

# deplasați la dreapta 7 biți
#rax = 0x001bc381ffdc57dd
shr $7 , % rax

# rotiți la dreapta cu 5 biți
#rax=0xe800de1c0ffee2be
ror $5 , % rax

# rotiți la stânga cu 5 biți
#rax = 0x001bc381ffdc57dd
rola 5 $, % rax

# idem + set bit (test de biți și setați)

bts 13 $, % rax

# idem + bit de resetare (test de biți și resetare)
#rax=0x001bc381ffdc57dd, CF=1
btr $13, % rax

# idem + bit invers (test de biți și complement)
#rax=0x001bc381ffdc77dd, CF=0
btc $13, % rax

# găsiți cel mai puțin semnificativ octet diferit de zero (scanare de biți înainte)
#rcx=0, ZF=0
bsf %rax , %rcx

# găsiți cel mai semnificativ octet diferit de zero (scanare inversă de biți)
#rdx=52, ZF=0
bsr % rax , % rdx

# dacă toți biții sunt zero, ZF = 1, valoarea rdx este nedefinită
xor % rax , % rax
bsf %rax , %rdx

Există, de asemenea, deplasări de biți semnate (sal, sar), deplasări ciclice cu un flag de transport (rcl, rcr) și deplasări duble de precizie (shld, shrd). Dar nu sunt folosite atât de des și te vei sătura să enumerați toate instrucțiunile în general. Prin urmare, vă las studiul lor ca teme.

Condiționale și bucle

Unele steaguri au fost menționate mai sus de mai multe ori, de exemplu, steagul de transfer. Steaguri sunt biți din registrul special eflags / rflags (numele de pe x86 și, respectiv, x64). Acest registru nu poate fi accesat direct de instrucțiuni mov, add și similare, dar este schimbat și utilizat indirect de diverse instrucțiuni. De exemplu, flag-ul de transport (CF) deja menționat este stocat în bitul zero al eflags / rflags și este utilizat, de exemplu, în aceeași instrucțiune bt. Alte steaguri utilizate frecvent includ steagul zero (ZF, al 6-lea bit), steagul semnului (SF, al 7-lea bit), pavilionul de direcție (DF, al 10-lea bit) și pavilionul de depășire (OF, al 11-lea bit).

Un alt dintre aceste registre implicite ar trebui să se numească eip / rip, care stochează adresa instrucțiunii curente. De asemenea, nu poate fi accesat direct, dar este vizibil în GDB împreună cu eflags / rflags dacă spui info registers și este schimbat indirect toata lumea instrucțiuni. Cele mai multe instrucțiuni pur și simplu cresc eip / rip cu lungimea acelei instrucțiuni, dar există excepții de la această regulă. De exemplu, instrucțiunea jmp sare pur și simplu la adresa dată:

# resetează rax
xor % rax , % rax
jmp în continuare
# această instrucțiune va fi omisă
inc%rax
Următorul:
inc%rax

Ca rezultat, valoarea lui rax va fi egală cu unu, deoarece prima instrucțiune inc va fi omisă. Rețineți că adresa de salt poate fi scrisă și într-un registru:

xor % rax , % rax
mov $next, %rcx
jmp*%rcx
inc%rax
Următorul:
inc%rax

Cu toate acestea, în practică, un astfel de cod este cel mai bine evitat, deoarece încalcă predicția ramurilor și, prin urmare, este mai puțin eficient.

Notă: GAS permite ca etichetelor să li se dea nume numerice precum 1: , 2: , și așa mai departe și să sară la cea mai apropiată etichetă anterioară sau următoare cu un număr dat cu instrucțiuni precum jmp1bși jmp 1f. Acest lucru este destul de util, deoarece uneori poate fi dificil să veniți cu nume semnificative pentru etichete. Detalii pot fi găsite.

Salturile condiționate sunt de obicei implementate cu instrucțiunea cmp, care compară cei doi operanzi ai săi și setează steagurile adecvate, urmate de o instrucțiune din familiile je, jg ​​și similare:

cmp %rax , %rcx

eu 1f # sari dacă este egal (egal)
jl 1f # sari dacă semnează mai puțin (mai puțin)
jb 1f # săriți dacă nesemnați mai puțin decât (mai jos)
jg 1f # sări dacă semnul este mai mare decât (mai mare)
ja 1f # săriți dacă nu este semnat mai mare decât (mai sus)

Există, de asemenea, instrucțiuni jne (săriți dacă nu este egal), jle (săriți dacă este semnat mai mic sau egal), jna (săriți dacă nu este semnat mai mare decât) și altele asemenea. Principiul denumirii lor, sper, este evident. În loc de je / jne, jz / jnz este adesea scris, deoarece instrucțiunile je / jne pur și simplu verifică valoarea lui ZF. Există, de asemenea, instrucțiuni care verifică alte steaguri - js, jo și jp, dar în practică sunt rar folosite. Toate aceste instrucțiuni reunite sunt denumite în mod obișnuit jcc. Adică, în loc de condiții specifice, se scriu două litere „c”, din „condiție”. puteți găsi un tabel rezumativ bun al tuturor instrucțiunilor jcc și ce steaguri verifică.

În plus față de cmp, declarația de testare este adesea folosită:

test %rax , %rax
jz 1f # sări dacă rax == 0
js 2f # sari daca rax< 0
1 :
# ceva cod
2 :
# un alt cod

fapte amuzante! Interesant, cmp și test sunt în esență aceleași cu sub și și, doar că nu își schimbă operanzii. Aceste cunoștințe pot fi folosite pentru a executa o ramură sub sau și și o ramură condiționată în același timp, fără instrucțiuni suplimentare cmp sau de testare.

O altă instrucțiune asociată cu săriturile condiționate este următoarea.

jrcxz 1f
# ceva cod
1 :

Instrucțiunea jrcxz sare doar dacă valoarea registrului rcx este zero.

cmovge %rcx , %rax

Instrucțiunile familiei cmovcc (mutare condiționată) funcționează ca mov, dar numai atunci când condiția specificată este îndeplinită, prin analogie cu jcc.

setnz % al

Instrucțiunile setcc setează un registru sau un octet în memorie la 1 dacă condiția specificată este adevărată și la 0 în caz contrar.

cmpxchg % rcx , (% rdx )

Comparați rax cu piesa de memorie dată. Dacă este egal, setați ZF și stocați valoarea registrului specificat la adresa specificată, în acest exemplu rcx. În caz contrar, ștergeți ZF și încărcați valoarea din memorie în rax. De asemenea, ambii operanzi pot fi registre.

cmpxchg8b(%rsi)
cmpxchg16b(%rsi)

Instrucțiunea cmpxchg8b este necesară în cea mai mare parte pe x86. Funcționează similar cu cmpxchg, doar că compară și schimbă 8 octeți deodată. Registrele edx:eax sunt folosite pentru comparație, iar registrele ecx:ebx stochează ceea ce vrem să scriem. Instrucțiunea cmpxchg16b, pe același principiu, compară și schimbă 16 octeți simultan pe x64.

Important! Rețineți că fără prefixul de blocare, toate aceste instrucțiuni de comparare și schimb nu sunt atomice.

mov $10 , %rcx
1 :
# ceva cod
bucla 1b
# bucla 1b
# loopnz 1b

Instrucțiunea buclă decrementează valoarea registrului rcx cu unu, iar dacă după aceea rcx != 0 , sare la eticheta dată. Instrucțiunile loopz și loopnz funcționează în mod similar, doar condițiile sunt mai complicate - (rcx != 0) && (ZF == 1) și respectiv (rcx != 0) && (ZF == 0).

Nu este nevoie de un creier pentru a-și da seama de construcții if-then-else sau de bucle for/while cu aceste instrucțiuni, așa că hai să mergem mai departe.

Operații „în șir”.

Luați în considerare următoarea bucată de cod:

mov $str1, %rsi
mov $str2, % edi
cld
cmpsb

Registrele rsi și rdi sunt umplute cu adresele a două șiruri. Comanda cld șterge indicatorul de direcție (DF). Instrucțiunea care face opusul se numește std. Apoi intră în joc instrucțiunea cmpsb. Compară octeții (%rsi) și (%rdi) și setează steaguri în funcție de rezultatul comparării. Atunci, dacă DF = 0, rsi și rdi cresc cu unul (numărul de octeți din ceea ce am comparat), altfel scad. Instrucțiunile similare cmpsw, cmpsl și cmpsq compară cuvinte, cuvinte lungiși, respectiv, patru cuvinte.

Instrucțiunile cmps sunt interesante deoarece pot fi folosite cu prefixul rep, repe (repz) și repne (repnz). De exemplu:

mov $str1, %rsi
mov $str2, % edi
mov $len, %rcx
cld
repe cmpsb
jne nu_egal

Prefixul rep repetă instrucțiunea de câte ori este specificat în registrul rcx. Prefixele repz și repnz fac același lucru, dar numai după fiecare execuție a instrucțiunii, ZF este verificat suplimentar. Bucla se termină dacă ZF = 0 în cazul lui c repz și dacă ZF = 1 în cazul repnz. Deci, codul de mai sus verifică egalitatea între două buffer-uri de aceeași dimensiune.

Instrucțiuni similare movs mută datele din buffer-ul a cărui adresă este specificată în rsi în buffer-ul a cărui adresă este specificată în rdi (ușor de reținut - rsi înseamnă sursă, rdi înseamnă destinație). Instrucțiunea stos umple bufferul la adresa din rdi cu octeții în rax (sau eax, sau ax, sau al, în funcție de instrucțiunea particulară). Instrucțiunile lods fac invers - copiați octeții la adresa specificată în rsi în registrul rax. În cele din urmă, instrucțiunile scas caută octeții din registrul rax (sau registrele mai mici corespunzătoare) în buffer-ul indicat de rdi. La fel ca cmps, toate aceste instrucțiuni funcționează cu prefixe rep, repz și repnz.

Pe baza acestor instrucțiuni, procedurile memcmp, memcpy, strcmp și similare sunt ușor de implementat. Este interesant că, de exemplu, pentru a reseta memoria, inginerii Intel recomandă utilizarea pe procesoare moderne rep stosb, adică resetați octet cu octet și nu, să zicem, cu patru cuvinte.

Manipularea stivelor și proceduri

Cu o stivă, totul este foarte simplu. Instrucțiunea push își împinge argumentul în stivă, iar instrucțiunea pop scoate o valoare din stivă. De exemplu, dacă uitați temporar de instrucțiunea xchg, atunci puteți schimba valoarea a două registre astfel:

împinge %rax
mov % rcx , % rax
pop %rcx

Există instrucțiuni care împing și pop registrul rflags / eflags pe stivă:

pushf
# face ceva care schimbă steaguri
popf
# steaguri restaurate, este timpul să faceți jcc

Și astfel, de exemplu, puteți obține valoarea steagului CF:

pushf
pop %rax
și $1 , %rax

Pe x86, există și instrucțiuni pusha și popa care salvează și restabilesc valorile tuturor registrelor din stivă. În x64, aceste instrucțiuni nu mai sunt disponibile. Aparent, pentru că există mai multe registre și registrele în sine sunt acum mai lungi - a devenit mult mai costisitor să le salvați și să le restaurați pe toate.

Procedurile sunt de obicei „create” folosind instrucțiunile de apel și retragere. Instrucțiunea de apel împinge adresa în stivă următoarea instrucțiuneși transferă controlul către adresa specificată în argument. Instrucțiunea ret citește adresa de retur din stivă și transferă controlul asupra acesteia. De exemplu:

someproc:
# prologul procedurii tipice
# de exemplu, alocați 0x10 octeți pe stivă pentru variabilele locale
# rbp - indicator către cadrul stivei
apăsați %rbp
mov % rsp , % rbp
sub $0x10, % rsp

# un fel de calcul aici...
mov $1 , %rax

# epilog procedura tipică
adăugați $0x10 , %rsp
pop %rbp

# procedura de ieșire
ret

start:
# ca și în cazul jmp, adresa de salt poate fi într-un registru
suna someproc
test %rax , %rax
eroare jnz

Notă: Un prolog și un epilog similare pot fi scrise folosind instrucțiunile introduceți $0x10 , $0și părăsi. Dar aceste instrucțiuni sunt rareori folosite în zilele noastre, deoarece sunt mai lente de executat datorită suportului suplimentar pentru procedurile imbricate.

De regulă, valoarea returnată este transmisă în registrul rax sau, dacă dimensiunea sa nu este suficient de mare, este scrisă în structura, a cărei adresă este transmisă ca argument. Pe problema trecerii argumentelor. Există o mulțime de convenții de apelare. În unele, toate argumentele sunt întotdeauna trecute prin stivă (o întrebare separată este în ce ordine) și procedura în sine este responsabilă pentru ștergerea stivei de argumente, în altele, unele dintre argumente sunt trecute prin registre, iar altele prin stivă , iar apelantul este responsabil pentru ștergerea stivei de argumente, plus o mulțime de opțiuni în mijloc, cu reguli separate pentru alinierea argumentelor pe stivă, transmițând acest lucru dacă este un limbaj OOP și așa mai departe. În cazul general, pentru o arhitectură arbitrară, un compilator și un limbaj de programare, convenția de apelare poate fi orice.

eu] ;
}
returnează haș;
}

Lista de dezasamblare (atunci când este compilată cu -O0, comentariile sunt ale mele):

# prologul procedurii tipice
# register rsp nu se modifică, deoarece procedura nu apelează niciunul
# alte proceduri
400950: 55 apăsați %rbp
400951: 48 89 e5 mov %rsp,%rbp

# inițializarea variabilelor locale:
# -0x08(%rbp) - const caracter nesemnat *date (8 octeți)
# -0x10(%rbp) - const size_t data_len (8 octeți)
# -0x14(%rbp) - hash int nesemnat (4 octeți)
# -0x18(%rbp) - int i (4 octeți)
400954: 48 89 7d f8 mov %rdi,-0x8(%rbp)
400958: 48 89 75 f0 mov %rsi,-0x10(%rbp)
40095c: c7 45 ec 4b 43 41 48 movl $0x4841434b,-0x14(%rbp)
400963: c7 45 e8 00 00 00 00 movl $0x0,-0x18(%rbp)

#rax:= i. dacă data_len este atins, ieșiți din buclă
40096a: 48 63 45 e8 movslq -0x18(%rbp),%rax
40096e: 48 3b 45 f0 cmp -0x10(%rbp),%rax
400972: 0f 83 28 00 00 00 jae 4009a0

# eax:= (hash<< 5) + hash
400978: 8b 45ec mov -0x14(%rbp),%eax
40097b: c1 e0 05 shl $0x5,%eax
40097e: 03 45 ec adăugați -0x14(%rbp),%eax

# eax += date[i]
400981: 48 63 4d e8 movslq -0x18(%rbp),%rcx
400985: 48 8b 55 f8 mov -0x8(%rbp),%rdx
400989: 0f b6 34 0a movzbl(%rdx,%rcx,1),%esi
40098d: 01 f0 adauga %esi,%eax

# hash:= eax
40098f: 89 45 ec mov %eax,-0x14(%rbp)

# i++ și mergeți la începutul buclei
400992: 8b 45 e8 mov -0x18(%rbp),%eax
400995: 83 c0 01 adăugați $0x1,%eax
400998: 89 45 e8 mutare %eax,-0x18(%rbp)
40099b: e9 ca ff ff ff jmpq 40096a

# valoarea returnată (hash) este introdusă în registrul eax
4009a0: 8b 45ec mov -0x14(%rbp),%eax

# epilog tipic
4009a3: 5d pop %rbp
4009a4: c3 retq

Aici am întâlnit două instrucțiuni noi - movs și movz. Funcționează exact ca mov, doar că extind un operand la dimensiunea celui de-al doilea, semnat și, respectiv, nesemnat. De exemplu, instrucțiunea movzbl (%rdx,%rcx,1),%esi citește un octet (b) la adresa (%rdx,%rcx,1) , îl extinde într-un cuvânt lung (l) adăugând zerouri (z) ) și pune rezultatul în registrul esi.

După cum puteți vedea, două argumente au fost trecute procedurii prin registrele rdi și rsi. Se pare că folosește o convenție numită System V AMD64 ABI. Acesta se pretinde a fi standardul de facto pentru x64 pe sistemele *nix. Nu văd niciun motiv să spun descrierea acestei convenții aici, cititorii interesați pot citi descrierea completă la linkul furnizat.

Concluzie

Inutil să spun că, în cadrul unui articol, nu este posibil să descriem întregul asamblator x86 / x64 (mai mult, nu sunt sigur că eu însumi știu bine. întregul). Cel puțin, subiecte precum operațiunile cu numere în virgulă mobilă, instrucțiuni MMX, SSE și AVX, precum și tot felul de instrucțiuni exotice precum lidt, lgdt, bswap , rdtsc, cpuid, movbe, xlatb sau prefetch au fost lăsate în urmă. scene. Voi încerca să le acopăr în articolele viitoare, dar nu promit nimic. De asemenea, trebuie remarcat faptul că în rezultatul objdump -d pentru majoritatea programelor reale, foarte rar veți vedea altceva decât ceea ce este descris mai sus.

Un alt subiect interesant lăsat în culise sunt operațiunile atomice, barierele de memorie, spinlock-urile și atât. De exemplu, compararea și schimbarea sunt adesea implementate pur și simplu ca o instrucțiune cmpxchg prefixată cu lock . Prin analogie, sunt implementate un increment atomic, decrement și așa mai departe. Din păcate, toate acestea atrag un subiect pentru un articol separat.

Ca surse de informații suplimentare, vă putem recomanda cartea Modern X86 Assembly Language Programming și, bineînțeles, manuale de la Intel. Cartea x86 Assembly de pe wikibooks.org este, de asemenea, destul de bună.

Din referințele online pentru instrucțiunile de asamblare, ar trebui să acordați atenție următoarelor:

Cunoașteți limbajul de asamblare și, dacă da, considerați utile aceste cunoștințe?

Aceste informații au fost postate inițial pe pagina Explicații pentru tabelul cheie. Dar apoi s-a decis ca aceste lungi argumente generale să fie puse pe o pagină separată. Totuși, după un astfel de transfer, aceste argumente au crescut puțin mai mult. Acum, probabil, sunt potrivite doar pentru secțiunea „Note diverse”...

Instrucțiuni de asamblare și instrucțiuni de mașină

În primul rând, nu trebuie să uităm că instrucțiunile în limbajul de asamblare și instrucțiunile în limbajul mașinii sunt două lucruri diferite. Deși este clar că aceste două concepte sunt strâns legate.

O instrucțiune de asamblare este un nume mnemonic. Pentru procesoarele din familia x86, acest nume este scris în engleză. De exemplu, comanda de adăugare are numele ADĂUGA, iar comanda de scădere are un nume SUB.

Echipă afișează numele comenzii în limbaj de asamblare.

Baza unei instrucțiuni de mașină este opcode, care este pur și simplu un număr. Pentru procesoarele x86 (cu toate acestea, și pentru alte procesoare), se obișnuiește să se utilizeze numere hexazecimale. (În treacăt, observăm că numerele octale au fost adoptate pentru calculatoarele sovietice, a existat mai puțină confuzie cu ele, deoarece astfel de numere constau numai din numere și nu conțin litere).

În tabelele acestui manual din coloana Codul arată codul operațional al instrucțiunii mașinii și în coloană Format arată formatul instrucțiunii mașinii.

Putem presupune că numărul de instrucțiuni de mașină diferite pentru un procesor dat este egal cu numărul de coduri de operare posibile. După format, puteți afla din ce componente constă o anumită instrucțiune de mașină. Instrucțiunile mașinii diferite pot avea formate diferite. Opcode-ul unei instrucțiuni de mașină definește complet formatul acesteia.

Adesea, o instrucțiune de asamblare are mai multe variante diferite de instrucțiuni de mașină. Mai mult, formatele acestor comenzi de mașină pot fi diferite pentru diferite opțiuni.

De exemplu, instrucțiunea de asamblare ADD are zece variante de instrucțiuni de mașină cu coduri operaționale diferite. Dar există mai puține formate diferite, doar trei. Și fiecare dintre aceste trei formate necesită diferite tipuri de operanzi atunci când scrieți o instrucțiune în limbaj de asamblare.

Este important de remarcat aici că toate aceste zece instrucțiuni ale mașinii efectuează aceeași operație elementară, care în limbajul de asamblare se numește ADD.

Și, prin urmare, se dovedește că pare a fi posibil să se raționeze astfel: procesorul poate efectua atâtea operațiuni elementare diferite câte instrucțiuni de asamblare există. Cu toate acestea, acest principiu simplu mai are nevoie de rezerve și note. Deoarece unele dintre comenzile de asamblare au și sinonime.

O listă generală a tuturor instrucțiunilor procesorului poate fi construită în moduri diferite, alegând o ordine diferită a instrucțiunilor. Principalele două moduri sunt.

Metoda (1). Luați comenzile din limbajul de asamblare ca bază și aranjați comenzile în ordine alfabetică. Apoi se pot obține astfel de tabele. Toate comenzile în ordine alfabetică (pe scurt)

Metoda (2). Luați ca bază codul operațional al instrucțiunii mașinii și aranjați instrucțiunile în ordinea codurilor operaționale. În acest caz, ar fi mai bine dacă lista generală este împărțită în două părți, pentru a face liste separate pentru comenzile cu un opcode de un octet și pentru comenzile cu un opcode de doi octeți. Primul octet de cod operațional Al doilea octet de cod operațional

Desigur, există și o a treia cale, care este de obicei folosită în manuale. Împărțiți toate comenzile în grupuri după semnificația lor și studiați-le în grupuri, începând cu cele mai simple.

Octet opcode principal

În sistemul de comandă x86, un octet (256 de combinații diferite) nu a fost suficient pentru a codifica toate comenzile. Prin urmare, codul operațional dintr-o instrucțiune de mașină ocupă fie un octet, fie doi octeți.

Dacă primul octet conține codul 0F, apoi codul operațional este format din doi octeți.

Dacă codul operațional dintr-o instrucțiune de mașină constă dintr-un octet, atunci acest singur octet este octetul principal al codului operațional. Și conținutul acestui octet determină care este operația.

Dacă codul operațional dintr-o comandă de mașină constă din doi octeți, atunci nu primul, ci al doilea octet va fi octetul principal și definitoriu din codul operațional.

În tabelele manuale care arată codarea instrucțiunilor mașinii, octetul principal al codului de operare este de obicei afișat de două ori, mai întâi în coloana „Cod” ca număr hexazecimal, iar apoi în coloana „Format” sub formă de opt liniuțe condiționate , pe care sunt marcați biți speciali, dacă există în octetul opcode principal.

Paginile principale ale manualului

Referință pentru instrucțiunile procesorului x86 - pagina principală (aici este o hartă a tuturor paginilor de manual)

Assembler este un limbaj de programare de nivel scăzut care este folosit pentru a programa diferite procesoare, microprocesoare și microcontrolere. Acest test ia în considerare asamblatorul pentru procesoare x86.

Programele în limbaj de asamblare constau dintr-un set de instrucțiuni specifice. Aceste comenzi sunt apoi, cu ajutorul unui traducător, convertite în cod mașină, care este apoi executat de procesorul central. Cu ajutorul comenzilor, puteți efectua calcule aritmetice, puteți lucra cu memorie și porturi etc.

De obicei, asamblatorul este utilizat atunci când este necesar să se optimizeze secțiunile critice de cod pentru viteză, în driverele de dispozitiv, în viruși și alte programe malware, în sisteme de operare, compilatoare etc.

Publicul țintă al testului Assembler x86

Testul testează cunoștințele limbajului de asamblare și arhitectura x86. Testul este axat mai mult pe cunoștințele practice ale limbajului și arhitecturii și, prin urmare, va fi de interes pentru programatorii de sistem și studenții pentru a testa cunoștințele și, de asemenea, util pentru toți programatorii pentru a îmbunătăți cunoștințele despre arhitectura computerelor și programarea de nivel scăzut.

Structura de testare în asamblatorul x86

Următoarele subiecte pot fi identificate în mod arbitrar:

  • Probleme generale
  • Moduri de funcționare a procesorului (real, protejat)
  • Instrucțiuni pentru procesor

Dezvoltarea în continuare a testului de asamblare x86

În viitor, intenționăm să adăugăm întrebări pe subiecte neacoperite (FPU, lucrul cu dispozitive / porturi). De asemenea, există un test de nivel intermediar în dezvoltare, care va fi disponibil în curând pentru promovare.