Is er een gemakkelijke manier om de stap tussen het assembleren van code naar machinecode te visualiseren?
Als u bijvoorbeeld een binair bestand opent in kladblok, ziet u een tekstueel opgemaakte weergave van machinecode. Ik neem aan dat elke byte (symbool) die je ziet, het corresponderende ascii-teken is voor de binaire waarde ervan?
Maar hoe gaan we van assembly naar binair, wat gebeurt er achter de schermen ??
Answer
Kijk naar de instructieset documentatie, en je zult items zoals deze vinden van een pic-microcontroller voor elke instructie:
De regel “codering” vertelt hoe die instructie eruitziet in binair. In dit geval begint het altijd met 5 enen, dan een don “t-care bit (wat één of nul kan zijn), dan staat de” k “voor de letterlijke waarde die je toevoegt.
De De eerste paar bits worden een “opcode” genoemd en zijn uniek voor elke instructie. De CPU kijkt in feite naar de opcode om te zien welke instructie het is, en vervolgens weet hij de “k” en te decoderen als een toe te voegen getal.
Het is vervelend, maar niet zo moeilijk om te coderen en decoderen. Ik had een undergrad-klas waar we het met de hand moesten doen bij examens.
Om daadwerkelijk een volledig uitvoerbaar bestand te maken, moet je ook dingen doen zoals geheugen toewijzen, vertakkingsverschuivingen berekenen en het in een formaat zoals ELF , afhankelijk van uw besturingssysteem.
Antwoord
Assembly-opcodes hebben voor het grootste deel een één-op-één overeenkomst met de onderliggende machine-instructies. U hoeft dus alleen maar elke opcode in de assembleertaal te identificeren, deze toe te wijzen aan de corresponderende machine-instructie en de machine-instructie naar een bestand te schrijven, samen met de bijbehorende parameters (indien aanwezig). Je herhaalt dan het proces voor elke extra opcode in het bronbestand.
Natuurlijk is er meer nodig om een uitvoerbaar bestand te maken dat correct kan worden geladen en uitgevoerd op een besturingssysteem, en de meeste fatsoenlijke assemblers doen dat hebben enkele extra mogelijkheden naast het eenvoudig toewijzen van opcodes aan machine-instructies (zoals macros, bijvoorbeeld).
Antwoord
De eerste wat je nodig hebt is zoiets als dit bestand . Dit is de instructiedatabase voor x86-processors zoals gebruikt door de NASM-assembler (die ik heb helpen schrijven, hoewel niet de delen die de instructies daadwerkelijk vertalen). Laten we een willekeurige regel uit de database kiezen:
ADD rm32,imm8 [mi: hle o32 83 /0 ib,s] 386,LOCK
Dit betekent dat het beschrijft de instructie ADD
. Er zijn meerdere varianten van deze instructie, en de specifieke die hier wordt beschreven, is de variant die een 32-bits register of geheugenadres nodig heeft en een onmiddellijke 8-bits waarde toevoegt (d.w.z. een constante die direct in de instructie is opgenomen). Een voorbeeld van een montage-instructie voor deze versie is deze:
add eax, 42
Nu, u moet uw tekstinvoer nemen en deze in individuele instructies en operanden parseren. Voor de bovenstaande instructie zou dit waarschijnlijk resulteren in een structuur die de instructie ADD
en een reeks operanden bevat (een verwijzing naar het register EAX
en de waarde 42
). Zodra u deze structuur heeft, doorloopt u de instructiedatabase en vindt u de regel die overeenkomt met zowel de instructienaam als de typen operanden. Als u geen overeenkomst vindt, is dat een fout die aan de gebruiker moet worden gepresenteerd (“illegale combinatie van opcode en operanden” of iets dergelijks is de gebruikelijke tekst).
Zodra we haal de regel uit de database, we kijken naar de derde kolom, die voor deze instructie is:
[mi: hle o32 83 /0 ib,s]
Dit is een reeks instructies die beschrijven hoe de vereiste machinecode-instructie wordt gegenereerd:
- De
mi
is een beschrijving van de operanden: één amodr/m
(register of geheugen) operand (wat betekent dat we “eenmodr/m
byte moeten toevoegen aan het einde van de instructie, waar we later op terugkomen) en één een onmiddellijke instructie (die zal worden gebruikt in de beschrijving van de instructie). - Het volgende is
hle
. Dit geeft aan hoe we omgaan met het voorvoegsel “lock”. We hebben “lock” niet gebruikt, dus we negeren het. - De volgende is
o32
. Dit vertelt ons dat als we de code opnieuw samenstellen voor een 16- bit uitvoerformaat, heeft de instructie een overschrijvingsprefix voor operandgrootte nodig.Als we 16-bits uitvoer zouden produceren, zouden we “nu het voorvoegsel produceren (0x66
), maar ik” neem aan dat we dat niet zijn en ga door. - De volgende is
83
. Dit is een letterlijke byte in hexadecimaal. We voeren deze uit. -
De volgende is
/0
. Dit specificeert enkele extra bits die we nodig hebben in de modr / m bytem, en zorgt ervoor dat we deze genereren. Demodr/m
byte wordt gebruikt om registers of indirecte geheugenreferenties te coderen. We hebben een enkele operand, een register. Het register heeft een nummer dat wordt gespecificeerd in een ander gegevensbestand :eax REG_EAX reg32 0
-
We controleren of
reg32
het eens is met de vereiste grootte van de instructie uit de originele database (dat doet het). De0
is het nummer van het register. Eenmodr/m
byte is een datastructuur gespecificeerd door de processor, die er als volgt uitziet:(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)
-
Omdat we met een register werken, is het veld
mod
0b11
. - Het veld
reg
is het nummer van het register dat we” gebruiken,0b000
- Omdat deze instructie maar één register bevat, moeten we het veld
rm
met iets invullen. Dat is waar de extra gegevens gespecificeerd in/0
voor waren, dus we hebben die in hetrm
veld,0b000
. - De
modr/m
byte is daarom0b11000000
of0xC0
. We voeren dit uit. - De volgende is
ib,s
. Dit specificeert een gesigneerde onmiddellijke byte. We kijken naar de operanden en zien dat we een onmiddellijke beschikbare waarde. We converteren het naar een byte met teken en voeren het uit (42
=>0x2A
).
De volledig samengestelde instructie is daarom: 0x83 0xC0 0x2A
. Stuur het naar uw uitvoermodule, samen met de opmerking dat geen van de bytes geheugenreferenties zijn (de uitvoermodule moet wellicht weten als ze dat doen).
Herhaal voor elke instructie. Houd labels bij, zodat u weet wat u moet invoegen als er naar wordt verwezen. Voeg faciliteiten toe voor macros en richtlijnen die worden doorgegeven aan de uitvoermodules van uw objectbestand. En dit is eigenlijk hoe een assembler werkt.
Reacties
Antwoord
In de praktijk is een assembler gewoonlijk “niet rechtstreeks een binair uitvoerbaar bestand produceren, maar sommige objectbestand (om later naar de linker te sturen). Er zijn echter uitzonderingen (u kunt sommige assemblers gebruiken om direct een binair uitvoerbaar bestand ; ze komen niet vaak voor).
Merk allereerst op dat veel assemblers tegenwoordig gratis softwareprogrammas zijn. Dus download en compileer op uw computer de bron code van GNU als (een deel van binutils ) en van nasm . Bestudeer vervolgens hun broncode. Trouwens, ik raad aan om Linux voor dat doel te gebruiken (het is een zeer ontwikkelaarsvriendelijk en gratis softwarevriendelijk besturingssysteem).
Het objectbestand geproduceerd door een assembler bevat met name een codesegment en verplaatsingsinstructies . Het is georganiseerd in een goed gedocumenteerd bestandsformaat, dat afhankelijk is van het besturingssysteem. Onder Linux is dat formaat (gebruikt voor objectbestanden, gedeelde bibliotheken, kerndumps en uitvoerbare bestanden) ELF . Dat objectbestand wordt later ingevoerd in de linker (die uiteindelijk een uitvoerbaar bestand produceert). Verhuizingen worden gespecificeerd door de ABI (bijv. x86-64 ABI ). Lees het boek van Levine Linkers and Loaders voor meer.
Het codesegment in zon objectbestand bevat machinecode met gaten (te vullen, met behulp van verplaatsingsinformatie, door de linker). De (verplaatsbare) machinecode gegenereerd door een assembler is duidelijk specifiek voor een instructieset architectuur .De ISAs van x86 of x86-64 (gebruikt in de meeste laptop- of desktopprocessors) zijn vreselijk complex in hun details. Maar een vereenvoudigde subset, genaamd y86 of y86-64, is uitgevonden voor onderwijsdoeleinden. Lees dias erop. Andere antwoorden op deze vraag verklaren daar ook een beetje van. Misschien wil je een goed boek lezen over computerarchitectuur .
De meeste assemblers werken in twee passen , waarbij de tweede verplaatsing uitzendt of een deel van de uitvoer van de eerste doorgang corrigeert. Ze gebruiken nu de gebruikelijke parsing-technieken (dus lees misschien The Dragon Book ).
Hoe een uitvoerbaar bestand wordt gestart door het besturingssysteem kernel (bijv. hoe de execve
systeemaanroep werkt op Linux ) is een andere (en complexe) vraag. Het zet gewoonlijk een virtuele adresruimte op (in het proces waarbij dat execve (2) …) en initialiseer vervolgens de interne processtatus (inclusief user-mode registers). Een dynamische linker -zoals ld-linux.so (8) op Linux- zou betrokken zijn tijdens runtime. Lees een goed boek, zoals Besturingssysteem: drie eenvoudige onderdelen . De OSDEV -wiki geeft ook nuttige informatie.
PS. Uw vraag is zo breed dat u er meerdere boeken over moet lezen. Ik heb enkele (zeer onvolledige) referenties gegeven. Je zou er meer moeten vinden.
Opmerkingen
- Met betrekking tot objectbestandsindelingen, voor een beginner ‘ d raad aan om te kijken naar het RDOFF-formaat geproduceerd door NASM. Dit is opzettelijk ontworpen om zo eenvoudig en realistisch mogelijk te zijn en toch in verschillende situaties te werken. De NASM-bron bevat een linker en een lader voor het formaat. (Volledige openbaarmaking – ik heb deze allemaal ontworpen en geschreven)
$ 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
… ja, je ‘ hebt helemaal gelijk. 🙂