Masinakood on arvutiprogramm, mis on kirjutatud masinakeeles — otseselt protsessori täidetavates nullide ja ühtede vormis esinevates käskudes. Masinakood kasutab konkreetse arvutiarhitektuuri käsukirjakomplekti (instruction set). Tavaliselt esitatakse see binaarses kujus (bitijada), kuigi inimloetavaks muutmiseks kasutatakse sageli heksade või binaari tekstiesitust. Masinakood on tarkvara madalaim tase: kõik kõrgemtasemelised programmeerimiskeeled tõlgitakse või kompileeritakse lõpuks masinkoodiks, et protsessor saaks juhiseid täita.

Mis koosneb käsust?

Käsk (instruktsioon) ütleb protsessorile, millist toimingut sooritada. Üldjuhul koosneb käsk kahest osast:

  • Opkood (operatsioonikood) — määrab tehtava operatsiooni (nt liitmine, liigutamine registritesse, tingimuslik hüppamine).
  • Operand(id) — andmed või viited andmetele, millega opkood töötab. Operandid võivad olla registreid, mäluaadresse, konstante (otseandmeid) või mälli/registreid identifitseerivaid viiteid.

Käskude komplekt on loetelu opkoodidest, mis on antud arhitektuuris toetatud. Masinakood on see vorm, milleks koostekood ja muud programmeerimiskeeled lõpuks kompileeritakse või milleks neid tõlgitakse, et protsessor saaks programmi täita.

Instruktsiooniformaat ja binaarne struktuur

Masinkoodi binaarne struktuur varieerub arhitektuuri järgi, kuid tüüpilised kujundid sisaldavad:

  • fikseeritud või muutuva pikkusega käsud (nt 32-bitised fikseeritud instruktsioonid RISC-arkitektuurides või erineva pikkusega CISC-instruktsioonid);
  • bitiväljad opkoodi, lähte- ja sihtregistrite, adreseerimisrežiimide ning otseste konstantide jaoks;
  • mälu- ja registriaadressimise bitikoodid ning tingimuslipud ja seisundiindikaatorid.

Näiteks lihtsas kujuteldavas 16-bitises instruktsioonis võivad esimesed 6 bitti olla opkood, järgnevad 5 bitti lähteregistri valimiseks ja viimased 5 bitti sihtregi või adreksi jaoks. Tegelikes protsessorites on sõnumformaatid ja bitipositsioonid dokumenteeritud arhitektuuri spetsifikatsioonis.

Erinevad adreseerimisviisid

  • Registriaadressimine — operand on registris;
  • Otsene mäluaadressimine — operand asub märgitud mäluaadressil;
  • Indekseerimine ja nihkearvestus — mäluaadress arvutatakse registri ja nihke põhjal;
  • Implicit (implitsiitne) — operand on ettenähtud registris või operaator kasutab fikseeritud andmeid;
  • Immediataadressimine — operand on osa instruktsioonist (konstant).

Kuidas masinakood tekib ja milliseid tööriistu kasutatakse?

Masinakoodi loovad või genereerivad tavaliselt:

  • Programmi koostajad (assmblerid) — tõlgivad assemblerkoodi (inimloetav mnemoonika) otse masinkoodiks;
  • Kompleerijad ja lingijad — kompileerivad kõrgemas keeles kirjutatud lähtekoodi (nt C, C++) masinakeelseks objektkoodiks ning lingivad erinevad objektfailid ja teegid käivitatavaks failiks (nt ELF, PE);
  • Tõlgid (interpreters) — mõnikord häälestavad käskude seeria reaalajas tõlke- või JIT-koodina masinakeelde.

Valminud masinkood salvestatakse sageli käivitatavatesse failidesse (nt Windowsi PE, Linuxi ELF), kuid protsessis kasutatav binaarne kujul mälus on sisuliselt sama.

Masinakood vs assembler ja kõrgemad keeled

  • Assembler-kood on inimloetavam esitus masinakoodist, kus opkoodidel on mnemoonilised nimed (nt MOV, ADD) — assembleri abil saab lihtsasti masinkoodi genereerida.
  • Kõrgema taseme keeled (nt Python, Java, C) annavad abstraktsiooni ja kirjutusmugavuse; need vajavad kompilaatorit või tõlki, et tekitada masinkoodi või virtuaalmasina bitijada.
  • Mõnikord kutsutakse masinkoodi emakeelseks koodiks, eriti kui käsitletakse programmi, mis töötab ainult konkreetsel riistvaral või arhitektuuril.

Arhitektuurilised erinevused: RISC ja CISC

RISC (Reduced Instruction Set Computer) ja CISC (Complex Instruction Set Computer) erinevused mõjutavad masinkoodi ülesehitust:

  • RISC: lihtsam ja tavaliselt fikseeritud pikkusega instruktsioonid, väike arv opkoode, millele järgnevad registrioperandid — tulemuseks eeldatavalt kiirem ja optimeeritavam masinkood;
  • CISC: rohkem ja keerukamaid instruktsioone, muutuva pikkusega kood, mitmed adreseerimisviisid — tihti kompileeritakse kõrgema taseme keele konstruktsioonid otse keerukamateks instruktsioonide jadadeks.

Bitijada ja endianness

Binaarne esitlus sõltub ka arvuti endianness'ist — kuidas mitme baitilise väärtuse baitid mällu paigutatakse:

  • Little-endian — madalaima suurusjärgu bait (least significant byte) salvestatakse madalamale aadressile;
  • Big-endian — kõrgeima suurusjärgu bait salvestatakse madalamale aadressile.

Endianness ei muuda opsüntaksi, kuid mõjutab mälust lugemist/kirjutamist madalama taseme andmetöötluses ja binaarsete failide ülekandmises.

Turvalisus, disassembel ja analüüs

Masinakood võib sisaldada ka pahatahtlikku koodi; seetõttu kasutatakse tööriistu nagu disassemblerid (nt IDA Pro, Ghidra) ja dekompilaatorid, et analüüsida käivitatavat binaari. Disassembleerimine teisendab masinkoodi tagasi assemblerilaadseks mnemoonikaks, mis aitab aru saada programmi struktuurist ja käitumisest.

Lõpetuseks — miks masinakood oluline on

Masinakood on oluline, sest see on otseselt see, mida riistvara täidab. Mõistes masinakoodi struktuuri, opkoode ja adreseerimisviise, saavad arendajad optimeerida jõudlust, parandada turvalisust ja teha madala taseme tõrkeotsingut. Lisaks annab masinakood teadmise selle kohta, kuidas tarkvara ja riistvara omavahel suhtlevad ning milliste piirangutega tuleb arvestada süsteemitasandil.