Hvordan går vi fra montering til maskinkode (kodegenerering)

Er det en enkel måte å visualisere trinnet mellom å samle kode til maskinkode?

Hvis du for eksempel åpner en binær fil i notisblokken, ser du en tekstformatert fremstilling av maskinkoden. Jeg antar at hver byte (symbol) du ser er det tilsvarende ascii-tegnet for den «s binære verdien?

Men hvordan går vi fra montering til binær, hva skjer bak kulissene ??

Svar

Se på instruksjonsdokumentasjonen, så finner du oppføringer som denne fra en pic-mikrokontroller for hver instruksjon:

eksempel addlw instruksjon

Linjen «koding» forteller hvordan den instruksjonen ser ut i binær. I dette tilfellet begynner det alltid med fem, deretter en bryr seg ikke (som kan være enten en eller null), så står «k» for den bokstavelige du legger til.

de første bitene kalles en «opcode», de er unike for hver instruksjon. CPUen ser i utgangspunktet på opcode for å se hvilken instruksjon det er, så vet den å dekode «k» -ene som et tall som skal legges til.

Det er kjedelig, men ikke så vanskelig å kode og dekode. Jeg hadde en undervisningskurs der vi måtte gjøre det for hånd i eksamener.

For å faktisk lage en full kjørbar fil, må du også gjøre ting som å tildele minne, beregne grenforskyvninger og legge det i en format som ELF , avhengig av operativsystem.

Svar

Monteringsopkoder har for det meste en en-til-en korrespondanse med de underliggende maskininstruksjonene. Så alt du trenger å gjøre er å identifisere hver opode på monteringsspråket, kartlegge den til den tilsvarende maskininstruksjonen, og skrive maskininstruksjonen ut til en fil sammen med tilhørende parametere (hvis noen). Deretter gjentar du prosessen for hver ekstra opode i kildefilen.

Det tar selvfølgelig mer enn det å lage en kjørbar fil som vil lastes og kjøres ordentlig på et operativsystem, og de fleste anstendige montører gjør har noen ekstra muligheter utover enkel kartlegging av opkoder til maskininstruksjoner (for eksempel makroer, for eksempel).

Svar

Det første det du trenger er noe sånt som denne filen . Dette er instruksjonsdatabasen for x86-prosessorer som brukes av NASM-samleren (som jeg hjalp til med å skrive, men ikke delene som faktisk oversetter instruksjoner). La oss velge en vilkårlig linje fra databasen:

 ADD rm32,imm8 [mi: hle o32 83 /0 ib,s] 386,LOCK  

Hva dette betyr er at den beskriver instruksjonen ADD. Det er flere varianter av denne instruksjonen, og den spesifikke som blir beskrevet her er varianten som tar enten et 32-biters register eller en minneadresse og legger til en umiddelbar 8-biters verdi (dvs. en konstant som er direkte inkludert i instruksjonen). Et eksempel på monteringsinstruksjon som vil bruke denne versjonen er denne:

 add eax, 42  

Nå, du må ta tekstinntastingen og analysere den i individuelle instruksjoner og operander. For instruksjonen ovenfor vil dette trolig resultere i en struktur som inneholder instruksjonen, ADD, og en rekke operander (en referanse til registeret EAX og verdien 42). Når du har denne strukturen, går du gjennom instruksjonsdatabasen og finner linjen som samsvarer med både instruksjonsnavnet og typene operandene. Hvis du ikke finner et samsvar, er det en feil som må presenteres for brukeren («ulovlig kombinasjon av opcode og operander» eller lignende er vanlig tekst).

Når vi har fikk linjen fra databasen, vi ser på den tredje kolonnen, som for denne instruksjonen er:

 [mi: hle o32 83 /0 ib,s]  

Dette er et sett med instruksjoner som beskriver hvordan du genererer maskinkodeinstruksjonene som kreves:

  • mi er en beskrivelse av operandene: en a modr/m (register eller minne) operand (som betyr at vi «må legge til en modr/m byte til slutten på instruksjonen, som vi senere kommer til) og en umiddelbar instruksjon (som vil bli brukt i beskrivelsen av instruksjonen).
  • Neste er hle. Dette identifiserer hvordan vi håndterer «lås» -prefikset. Vi har ikke brukt «lås», så vi ignorerer den.
  • Neste er o32. Dette forteller oss at hvis vi samler kode for en 16- bit-utdataformat, trenger instruksjonen et overordnet prefiks for operand-størrelse.Hvis vi produserte 16-bits utdata, ville vi produsere prefikset nå (0x66), men jeg antar at vi ikke fortsetter.
  • Neste er 83. Dette er en bokstav i heksadesimal. Vi sender den ut.
  • Neste er /0 Dette spesifiserer noen ekstra biter vi trenger i modr / m bytem, og får oss til å generere det. modr/m byte brukes til å kode registre eller indirekte minnereferanser. Vi har en slik operand, et register. Registeret har et nummer som er spesifisert i en annen datafil :

     eax REG_EAX reg32 0  
  • Vi sjekker at reg32 er enig med den nødvendige størrelsen på instruksjonen fra den opprinnelige databasen (den gjør det). 0 er registerets nummer. En modr/m byte er en datastruktur spesifisert av prosessoren, som ser slik ut:

      (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)  
  • Fordi vi jobber med et register, er mod -feltet 0b11.

  • reg -feltet er nummeret på registeret vi bruker, 0b000
  • Fordi det bare er ett register i denne instruksjonen, må vi fylle ut rm -feltet med noe. Det var det de ekstra dataene spesifisert i /0 var for, så vi la det i rm -feltet, 0b000.
  • modr/m byte er derfor 0b11000000 eller 0xC0. Vi sender dette.
  • Neste er ib,s. Dette spesifiserer en signert øyeblikkelig byte. Vi ser på operandene og bemerker at vi har en umiddelbar tilgjengelig verdi. Vi konverterer den til en signert byte og sender den ut (42 => 0x2A).

Den komplette instruksjonen er derfor: 0x83 0xC0 0x2A. Send den til utdatamodulen din, sammen med en merknad om at ingen av byte utgjør minnereferanser (utdatamodulen trenger kanskje å vite hvis de gjør det).

Gjenta for hver instruksjon. Hold oversikt over etikettene slik at du vet hva du skal sette inn når de er referert til. Legg til fasiliteter for makroer og direktiver som blir sendt til objektfilutdata-modulene. Og dette er i utgangspunktet hvordan en montør fungerer.

Kommentarer

  • Takk. Flott forklaring, men burde ikke være ‘ t » 0x83 0xC0 0x2A » i stedet for » 0x83 0xB0 0x2A » fordi 0b11000000 = 0xC0
  • @Kamran – $ 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, du ‘ har helt rett. 🙂

Svar

I praksis er en montør produserer vanligvis ikke direkte noe binært kjørbart , men noen objektfil (skal mates senere til linker ). Det er imidlertid unntak (du kan bruke noen samlere til å produsere direkte noen binær kjørbar ; de er uvanlige).

Legg først merke til at mange montører i dag er gratis programvare programmer. Så last ned og kompiler kilden på datamaskinen din kode for GNU as (en del av binutils ) og av nasm . Studer deretter kildekoden deres. BTW, jeg anbefaler å bruke Linux til det formålet (det er et veldig utviklervennlig og gratis programvarevennlig OS).

Objektfilen produsert av en samler inneholder spesielt et kodesegment og flytting instruksjoner. Den er organisert i et godt dokumentert filformat, som avhenger av operativsystemet. På Linux er det formatet (brukt til objektfiler, delte biblioteker, kjernedumper og kjørbare filer) ELF . Objektfilen blir senere lagt inn til linker (som til slutt produserer en kjørbar). Flytting er spesifisert av ABI (f.eks. x86-64 ABI ). Les Levines bok Linkers and Loaders for mer.

Kodesegmentet i en slik objektfil inneholder maskinkode med hull (skal fylles ut, ved hjelp av omplasseringsinformasjon, av linkeren). (Flyttbar) maskinkoden generert av en samler er åpenbart spesifikk for et instruksjonssett arkitektur . x86 eller x86-64 (brukes i de fleste bærbare eller stasjonære prosessorer) ISA-er er veldig komplisert i detaljene. Men en forenklet delmengde, kalt y86 eller y86-64, er oppfunnet for undervisningsformål. Les lysbildene på dem. Andre svar på dette spørsmålet forklarer også litt av det. Det kan være lurt å lese en god bok om Computer Architecture .

De fleste montører jobber i to passerer , den andre sender ut flytting eller korrigerer noe av utdataene fra det første passet. De bruker nå vanlige parsing teknikker (så les kanskje The Dragon Book ).

Hvordan en kjørbar fil startes av OS kjernen (f.eks. hvordan execve systemanrop fungerer på Linux ) er et annet (og komplekst) spørsmål. Det setter vanligvis opp noe virtuelt adresseområde (i prosessen gjør det execve (2) …), initialiser deretter prosessens interne tilstand på nytt (inkludert brukermodus -registre). En dynamisk linker – slik som ld-linux.so (8) på Linux- kan være involvert i kjøretid. Les en god bok, for eksempel Operativsystem: Three Easy Pieces . OSDEV wiki gir også nyttig informasjon.

PS. Spørsmålet ditt er så bredt at du trenger å lese flere bøker om det. Jeg har gitt noen (veldig ufullstendige) referanser. Du bør finne flere av dem.

Kommentarer

  • Når det gjelder objektfilformater, for en nybegynner ‘ Jeg anbefaler å se på RDOFF-format produsert av NASM. Dette ble med vilje utformet for å være så enkelt som realistisk mulig og fremdeles fungere i en rekke situasjoner. NASM-kilden inkluderer en linker og en laster for formatet. (Full avsløring – jeg designet og skrev alle disse)

Legg igjen en kommentar

Din e-postadresse vil ikke bli publisert. Obligatoriske felt er merket med *