Kod maszynowy to program komputerowy napisany w języku maszynowym. Wykorzystuje on zestaw instrukcji danej architektury komputera. Zazwyczaj jest zapisany w postaci binarnej. Kod maszynowy jest najniższym poziomem oprogramowania. Inne języki programowania są tłumaczone na kod maszynowy, aby komputer mógł je wykonać.

Instrukcja mówi procesowi, jaką operację ma wykonać. Każda instrukcja składa się z opcode (kodu operacji) i operandu (operandów). Operandami są zazwyczaj adresy pamięci lub dane. Zestaw instrukcji to lista kodów operacyjnych dostępnych dla danego komputera. Kod maszynowy jest tym, do czego kod asemblera i inne języki programowania są kompilowane lub interpretowane.

Konstruktorzy programów przekształcają kod w inny język lub kod maszynowy. Kod maszynowy jest czasami nazywany kodem natywnym. Jest to używane, gdy mówimy o rzeczach, które działają tylko na niektórych komputerach.

Struktura instrukcji

Typowa instrukcja w kodzie maszynowym składa się z dwóch głównych części:

  • Opcode – identyfikuje operację (np. dodawanie, skok, załadunek danych).
  • Operandy – określają źródła i cele operacji (np. rejestry, adresy pamięci, stałe).

Format instrukcji zależy od architektury. W architekturach RISC (np. ARM) instrukcje mają zwykle stałą długość (np. 32 bity), co upraszcza dekodowanie. W architekturach CISC (np. x86) występują instrukcje o zmiennej długości, co pozwala na gęstsze kodowanie, ale wymaga bardziej złożonego dekodera.

Tryby adresowania

Instrukcje mogą korzystać z różnych trybów adresowania, które określają, jak interpreter instrukcji odnajduje dane:

  • Natychmiastowy (immediate) – operand jest podany bezpośrednio w instrukcji.
  • Rejestrowy (register) – operand to wartość przechowywana w rejestrze procesora.
  • Bezp. (direct) – instrukcja zawiera adres pamięci.
  • Pośredni (indirect) – instrukcja wskazuje na miejsce, gdzie znajduje się adres docelowy.
  • Indeksowy (indexed) – adres obliczany jest jako suma podstawy i przesunięcia (przydatne w operacjach na tablicach).
  • Względny (relative) – adres skoku wyrażony jest względem bieżącej pozycji instrukcji (częste w skokach i gałęziach).

Przedstawienie i reprezentacja

Kod maszynowy jest zapisywany jako sekwencja bitów. W praktyce często przedstawia się go w postaci heksadecymalnej (łatwiejszej do czytania) lub jako ciąg bajtów. Przykładowo pojedyncza instrukcja może odpowiadać kilku bajtom, np. 0xB8 0x04 0x00 0x00 0x00 (x86 — załaduj stałą do rejestru).

Ważnym aspektem jest endianness — porządek bajtów w pamięci (big-endian vs little-endian), co wpływa na interpretację wielobajtowych danych i instrukcji.

Tłumaczenie i wykonanie

Kod źródłowy w językach wysokiego poziomu trafia do kodu maszynowego przy użyciu różnych narzędzi:

  • Asembler – bezpośrednio tłumaczy instrukcje asemblerowe na odpowiadające im ciągi bajtów (kod maszynowy).
  • Kompilator – zamienia kod wysokiego poziomu na kod maszynowy (np. C → kod maszynowy). Często stosuje optymalizacje.
  • Interpreter – wykonuje kod "na żywo", często tłumacząc polecenia na instrukcje maszynowe w locie.
  • JIT (Just-In-Time) – dynamiczne kompilowanie fragmentów kodu (np. w środowiskach uruchomieniowych JVM czy .NET) do kodu natywnego w czasie działania programu.

W niektórych systemach pośrednie formaty (np. bytecode) są interpretowane lub kompilowane do kodu maszynowego dopiero w czasie wykonywania.

Zastosowania

Kod maszynowy jest wykorzystywany wszędzie tam, gdzie liczy się wydajność lub bezpośrednia kontrola nad sprzętem:

  • systemy operacyjne i sterowniki urządzeń,
  • oprogramowanie wbudowane (firmware) i sterowniki mikrokontrolerów,
  • krytyczne aplikacje czasu rzeczywistego,
  • optymalizowane biblioteki numeryczne i gry,
  • reverse engineering, analiza złośliwego oprogramowania i badania bezpieczeństwa.

Bezpieczeństwo i debugowanie

Kod maszynowy ma bezpośredni wpływ na bezpieczeństwo systemu. Błędy w kodzie maszynowym lub w tłumaczeniu mogą prowadzić do luk (np. przepełnienia bufora) i umożliwiać ataki (wstrzyknięcie kodu, eskalacja przywilejów). Technologie takie jak DEP (Data Execution Prevention) czy ASLR (Address Space Layout Randomization) ograniczają ryzyko nadużyć.

Do analizy i debugowania kodu maszynowego używa się narzędzi takich jak debugery (np. gdb), disassemblery i dekompilatory (np. objdump, IDA, Ghidra) oraz emulatorów. Te narzędzia pozwalają na śledzenie wykonania, badanie rejestrów i pamięci oraz analizę sekwencji instrukcji.

Przenośność i kod natywny

Kod maszynowy jest z natury zależny od architektury procesora — to, co działa na jednej rodzinie procesorów, może nie działać na innej. Dlatego mówi się o kodzie natywnym w kontekście konkretnej platformy. Aby osiągnąć przenośność, stosuje się warstwy abstrakcji (np. maszyny wirtualne, biblioteki przenośne) lub rekompilację dla różnych architektur.

Podsumowując, kod maszynowy to podstawowa forma instrukcji rozumianych przez procesor. Zrozumienie jego struktury, trybów adresowania i sposobów generowania jest kluczowe dla inżynierii oprogramowania niskiego poziomu, optymalizacji wydajności oraz zapewnienia bezpieczeństwa systemów komputerowych.