Există o modalitate ușoară de a vizualiza pasul dintre asamblarea codului la codul mașinii?
De exemplu, dacă deschideți un fișier binar în notepad, vedeți o reprezentare formatată text a codului mașinii. Presupun că fiecare octet (simbol) pe care îl vedeți este caracterul ascii corespunzător pentru valoarea sa binară?
Dar cum mergem de la asamblare la binar, ce se întâmplă în spatele scenei ??
Răspuns
Uită-te la documentația setului de instrucțiuni și vei găsi intrări ca aceasta din un microcontroler pic pentru fiecare instrucțiune:
Linia „codificare” spune cum arată acea instrucțiune în binar. În acest caz, începe întotdeauna cu 5, apoi un bit care nu are grijă (care poate fi unul sau zero), apoi „k” reprezintă literalul pe care îl adăugați.
primii câțiva biți sunt numiți „opcode”, sunt unici pentru fiecare instrucțiune. CPU se uită practic la codul opțional pentru a vedea ce instrucțiune este, apoi știe să decodeze „k”-urile ca număr care trebuie adăugat.
Este plictisitor, dar nu atât de dificil de codificat și decodat. Am avut o clasă de licență în care a trebuit să o facem manual la examene.
Pentru a crea de fapt un fișier executabil complet, trebuie să faceți lucruri precum alocarea de memorie, calcularea compensărilor de ramură și punerea acestuia într-un format ca ELF , în funcție de sistemul de operare.
Răspuns
Codurile opționale de asamblare au, în cea mai mare parte, o corespondență unu-la-unu cu instrucțiunile subiacente ale mașinii. Deci, tot ce trebuie să faceți este să identificați fiecare opcode în limbajul de asamblare, să îl mapați la instrucțiunile corespunzătoare ale mașinii și să scrieți instrucțiunile mașinii într-un fișier, împreună cu parametrii corespunzători (dacă există). Apoi repetați procesul pentru fiecare cod opțional suplimentar din fișierul sursă.
Desigur, este nevoie de mai mult decât atât pentru a crea un fișier executabil care să se încarce și să ruleze în mod corespunzător pe un sistem de operare, iar majoritatea asamblatoarelor decente o fac au unele funcții suplimentare dincolo de simpla mapare a codurilor opționale la instrucțiunile mașinii (cum ar fi macrocomenzile, de exemplu).
Răspuns
Primul lucru de care aveți nevoie este ceva de genul acest fișier . Aceasta este baza de date de instrucțiuni pentru procesoarele x86, așa cum este utilizată de asamblorul NASM (pe care am ajutat-o să scriu, deși nu părțile care traduc efectiv instrucțiunile). Permite să alegem o linie arbitrară din baza de date:
ADD rm32,imm8 [mi: hle o32 83 /0 ib,s] 386,LOCK
Ce înseamnă asta este că descrie instrucțiunea ADD
. Există mai multe variante ale acestei instrucțiuni, iar cea specifică care este descrisă aici este varianta care ia fie un registru pe 32 de biți, fie o adresă de memorie și adaugă o valoare imediată de 8 biți (adică o constantă direct inclusă în instrucțiune). Un exemplu de instrucțiune de asamblare care ar folosi această versiune este următorul:
add eax, 42
Acum, trebuie să luați textul introdus și să-l analizați în instrucțiuni și operanzi individuali. Pentru instrucțiunea de mai sus, aceasta ar rezulta probabil într-o structură care conține instrucțiunea, ADD
și o matrice de operanzi (o referință la registrul EAX
și valoarea 42
). Odată ce aveți această structură, rulați prin baza de date de instrucțiuni și găsiți linia care se potrivește atât cu numele instrucțiunii, cât și cu tipurile de operanzi. Dacă nu găsiți o potrivire, aceasta este o eroare care trebuie prezentată utilizatorului („combinația ilegală de opcode și operanzi” sau similar este textul obișnuit).
Odată ce „am văzut a primit linia din baza de date, ne uităm la a treia coloană, care pentru această instrucțiune este:
[mi: hle o32 83 /0 ib,s]
Acesta este un set de instrucțiuni care descrie cum se generează instrucțiunile codului mașinii care sunt „necesare:
-
mi
este o descriere a operanzilor: un amodr/m
(registru sau memorie) operand (ceea ce înseamnă că va trebui să adăugăm unmodr/m
octet la sfârșitul instrucțiunii, la care vom ajunge mai târziu) și unul o instrucțiune imediată (care va fi utilizată în descrierea instrucțiunii). - Următorul este
hle
. Aceasta identifică modul în care gestionăm prefixul „blocare”. Nu am folosit „blocarea”, așa că o ignorăm. - Următorul este
o32
. Aceasta ne spune că, dacă asamblăm codul pentru un 16- format de ieșire pe biți, instrucțiunea are nevoie de un prefix de suprascriere a dimensiunii operandului.Dacă am produce ieșire pe 16 biți, am produce acum prefixul (0x66
), dar voi presupune că nu suntem și vom continua. - Următorul este
83
. Acesta este un octet literal în hexazecimal. L-am redat. -
Următorul este
/0
. Aceasta specifică câțiva biți suplimentari de care vom avea nevoie în bytemul modr / m și ne determină să îl generăm. Octetulmodr/m
este utilizat pentru a codifica registre sau referințe indirecte de memorie. Avem un singur astfel de operand, un registru. Registrul are un număr, care este specificat în alt fișier de date :eax REG_EAX reg32 0
-
Verificăm dacă
reg32
este de acord cu dimensiunea necesară a instrucțiunii din baza de date originală (o face).0
este numărul registrului. Unmodr/m
octet este o structură de date specificată de procesor, care arată astfel:(most significant bit) 2 bits mod - 00 => indirect, e.g. [eax] 01 => indirect plus byte offset 10 => indirect plus word offset 11 => register 3 bits reg - identifies register 3 bits rm - identifies second register or additional data (least significant bit)
-
Deoarece lucrăm cu un registru, câmpul
mod
este0b11
. - Câmpul
reg
este numărul registrului pe care îl utilizăm,0b000
- Deoarece există un singur registru în această instrucțiune, trebuie să completăm câmpul
rm
cu ceva. Pentru asta erau datele suplimentare specificate în/0
, așa că am pus-o în câmpulrm
,0b000
. - Octetul
modr/m
este, prin urmare,0b11000000
sau0xC0
. Lansăm acest lucru. - Următorul este
ib,s
. Acesta specifică un octet imediat semnat. Ne uităm la operanzi și observăm că avem un imediat valoare disponibilă. Îl convertim într-un octet semnat și îl redăm (42
=>0x2A
).
Instrucțiunea completă asamblată este, prin urmare: 0x83 0xC0 0x2A
. Trimiteți-o către modulul de ieșire, împreună cu o notă că niciunul dintre octeți nu constituie referințe de memorie (este posibil ca modulul de ieșire să știe dacă o fac).
Repetați pentru fiecare instrucțiune. Țineți evidența etichetelor, astfel încât să știți ce să inserați atunci când sunt „referențiate”. Adăugați facilități pentru macrocomenzi și directive care sunt transmise modulelor de ieșire ale fișierului obiect. Și acesta este practic modul în care funcționează un asamblator.
Comentarii
Răspuns
În practică, un asamblor de obicei, nu produce direct niște executabile , dar unele fișier obiect (care urmează să fie alimentat mai târziu către linker ). Cu toate acestea, există excepții (puteți utiliza unii asamblori pentru a produce direct un executabil binar ; sunt mai puțin frecvente).
Mai întâi, observați că mulți asamblori sunt astăzi programe software gratuit . Deci descărcați și compilați pe computer sursa cod de GNU as (o parte din binutils ) și de nasm . Apoi studiați codul sursă al acestora. BTW, vă recomand să utilizați Linux în acest scop (este un sistem de operare foarte ușor de dezvoltat și de software gratuit).
Fișierul obiect produs de un asamblator conține în special un segment de cod și instrucțiuni de relocare . Este organizat într-un format de fișier bine documentat, care depinde de sistemul de operare. Pe Linux, acel format (utilizat pentru fișiere obiect, biblioteci partajate, dumpuri de bază și executabile) este ELF . Acel fișier obiect este introdus ulterior în linker (care produce în cele din urmă un executabil). Relocările sunt specificate de ABI (de ex. x86-64 ABI ). Citiți cartea lui Levine Linkere și încărcătoare pentru mai multe.
Segmentul de cod dintr-un astfel de fișier obiect conține codul mașinii cu găuri (care trebuie completat, cu ajutorul informațiilor de relocare, de către linker). Codul mașinii (relocabil) generat de un asamblator este evident specific unui set de instrucțiuni arhitectură .ISA-urile x86 sau (utilizate în majoritatea procesoarelor pentru laptop sau desktop) complexe în detaliile lor. Dar un subset simplificat, numit y86 sau y86-64, a fost inventat în scopuri didactice. Citiți diapozitive pe ele. Alte răspunsuri la această întrebare explică, de asemenea, un pic din asta. Poate doriți să citiți o carte bună despre Arhitectura computerelor .
Majoritatea asamblatoarelor lucrează în două treceri , a doua emițând relocare sau corectând o parte din ieșirea primei treceri. Ei folosesc acum tehnici obișnuite de analiză (deci citiți poate Cartea Dragonului ).
Cum este pornit un executabil de către sistemul de operare kernel (de exemplu, modul în care funcționează apelul de sistem execve
pe Linux ) este o întrebare diferită (și complexă). De obicei, configurează un anumit spațiu virtual de adrese (în procesul făcând acest lucru execve (2) …) apoi reinitializați starea internă a procesului (inclusiv registrele user-mode ). Un linker dinamic -com ld-linux.so (8) pe Linux- ar putea fi implicat în timpul rulării. Citiți o carte bună, cum ar fi Sistem de operare: trei piese ușoare . Wiki-ul OSDEV oferă, de asemenea, informații utile.
PS. Întrebarea dvs. este atât de amplă încât trebuie să citiți mai multe cărți despre aceasta. Am dat câteva referințe (foarte incomplete). Ar trebui să le găsiți mai multe.
Comentarii
- În ceea ce privește formatele de fișiere obiect, pentru un începător I ‘ d recomand să vă uitați la formatul RDOFF produs de NASM. Acest lucru a fost conceput în mod intenționat pentru a fi cât se poate de simplu cât mai realist și să funcționeze în continuare într-o varietate de situații. Sursa NASM include un linker și un încărcător pentru format. (Dezvăluire completă – am proiectat și am scris toate acestea)
$ cat > test.asm bits 32 add eax,42 $ nasm -f bin test.asm -o test.bin $ od -t x1 test.bin 0000000 83 c0 2a 0000003
… da, ‘ ai destulă dreptate. 🙂