Assembler x86 Tutorial - Jak nauczyłem się assemblera


Spis treści



Wstęp

Gdy zacząłem już rozumieć paradygmaty programowania wysokopoziomowego (pisałem apki webowe, desktopowe), chciałem wiedzieć co jest pod spodem.

Jak to wszystko naprawdę działa, zacząłem szukać w internecie fraz typu programowanie niskopoziomowe. Natknąłem się na assemblera. Już wcześniej wiedziałem, co to jest, ale w ogóle nie rozumiałem tego języka. Myślałem, że pisze się w nim pod jakieś archaiczne 8-bitowe mikrokontrolery.

Czyli są różne assemblery?

Różne architektury procesorów, mają różne rozmiary rejestrów oraz różne instrukcje (rzecz jasna często się one pokrywają).

Generalnie assembler to język programowania niskiego poziomu, w którym jest bardzo duża zgodność między instrukcjami języka a instrukcjami w kodzie maszynowym.

Tutaj skupimy się na assemblerze pod architekturę x86 (to ta 32-bitowa).

Kod assemblera różni się też w zależności od kompilatora i składni (syntaxu).

Istnieją dwie składnie językowe assemblera:

  • Intel
  • AT&T

Czym róźnią się te składnie?

Tutaj prosty przykład operacji na pamięci:

Intel mov eax, [ebx] mov eax, [ebx+3]

AT&T movl (%ebx), %eax movl 3(%ebx), %eax

Składnia Intela zapewne wydaje się być prostsza. Składnia AT&T jest bardziej zbliżona do tego, jak wyglądają instrukcje w kodzie maszynowym.

Sam posługuje się zawsze składnią Intela.

Programowanie Niskopoziomowe - Co to jest?

Programowanie niskopoziomowe - w językach niskiego poziomu jedna operacja elementarna odpowiada najczęściej jednej operacji elementarnej procesora.

Operacje elementarne - ale co to znaczy?

Operacja elementarna - jest przykładowo operacją porównania dwóch liczb ze sobą.

W assemblerze x86 porównanie dwóch liczb wyglądałoby następująco: cmp eax, 3 ; <-- jedna operacja w języku assembler

W kodzie maszynowym x86 ta instrukcja wygląda tak: 83 F8 03 <-- jedna operacja w języku maszynowym procesora

Widać tutaj, jak ilość operacji w assemblerze pokrywa się z ilością operacji w języku maszynowym procesora.

Rejestry Procesora - Co to jest?

Na samym początku nauki assemblera nie rozumiałem, czym są rejestry procesora. W programowaniu mamy pojęcie zmiennej - zmienna jest zapisywana w pamięci operacyjnej (RAM) w danych strukturach systemu operacyjnego.

Rejestr - jest małą komórką pamięci w procesorze, dostęp do niej jest o wiele szybszy, niż do pamięci operacyjnej.

Każdy 64-bitowy procesor posiada rejestry 64-bitowe, które dzielą się na 32-bitowe, te na 16-bitowe, a te na 8-bitowe.

Tabela rejestrów (ogólnego przeznaczenia) procesora powinna wszystko rozjaśnić.

64 bit32 bit16 bit8 bit
raxeaxaxal
rbxebxbxbl
rbxecxcxcl
rdxedxdxdl
rsiesisisil
rdiedididil


Lecz to nie wszystko! Istnieją również rejestry specjalnego przeznaczenia, o czym pisałem w książce: Assembler x86 - Programowanie i Podstawy Systemów Operacyjnych. Aktualnie trwa na nią promocja -43%.


Czy C/C++ Jest Niskopoziomowe

Nie! Zdecydowanie C i C++ nie są językami niskopoziomowymi.

Owszem, C czy C++ są na niższym poziomie od języków, takich jak C#, Java czy Python, ponieważ między innymi są kompilowane bezpośrednio do kodu maszynowego danej architektury, ale nie są językami niskiego poziomu.

Pierwszy Kod Assemblera - Hello World Assembler x86

Teraz wreszcie trochę praktyki!

Kompilator, z którego będziemy korzystać to NASM. Kod będzie działał pod systemem GNU/Linux - tak też polecam zaczynać naukę, od pisania assemblera pod Linuxa.

Sekcje w Programie

Jeśli nie programowałeś w językach, takich jak C/C++, to pewnie nie spotkałeś się z pojęciem sekcji w programie.

Sekcje - segregują dane w pliku, poszczególne bajty w pliku mogą należeć tylko do jednej sekcji. Przykład? - sekcja text - przechowuje wykonywalny kod, jej zawartość nie może nachodzić na inne sekcje np. data.

Sekcji jest kilka:

  • text
    • wykonywalny kod programy
  • data
    • statyczne, globalne zmienne ze zdefiniowaną wartością
  • bss
    • zmienne bez zdefiniowanej wartości (lub równe zero)

Pierwszy Kod - Assembler exit process

section .text

    global _start

_start:
    mov ebx, 123
    mov eax, 1
    int 0x80

Na początku zdefiniowałem sekcję text, następnie jest dyrektywa global _start ona wskazuje na etykietę _start od której ma zacząć się wykonywać program.

Assembler - Etykiety

Etykiety - to w skrócie miejsca w kodzie, do których można skoczyć lub się odwołać.

Odwołanie (instrukcja call), to nie to samo, co skok (instrukcja jmp), ale tego tematu nie będę jeszcze poruszał.

Składnia nazwa:

Przykład ``` exit: mov ebx, 0 mov eax, 1 int 0x80

_start: jmp exit ```

Teraz możemy wrócić do kodu.

To co tutaj się dzieje - to wywołanie systemowe (syscall).

Wywołanie systemowe (syscall) - to w skrócie sposób komunikacji (interfejs) pomiędzy programem użytkownika, a jądrem systemu operacyjnego. Więcej o wywołaniach systemowych napisałem w książce Assembler x86 - Programowanie i Podstawy Systemów Operacyjnych.

(tak, średniki w assemblerze to komentarze, a znaki tabulacji nie mają znaczenia :))

mov ebx, 123 ; exit status
mov eax, 1   ; syscall number - 1 odpowiada sys_exit
int 0x80     ; przerwanie z numerem 0x80 - czyli linux syscall

Kompilacja Assemblera

Niestety NASM nie posiada linkera, który pozwoliłby na stworzenie pliku wynokywalnego w formacie ELF.

kompilacja:

nasm -f elf32 main.asm -o main.o

linkowanie:

ld -m elf_i386 main.o -o main

$ ./main
$ echo $?
 123

Działa! Mamy exit status 123.

Co dalej - Książka Assembler

Więcej na temat assemblera i systemów operacyjnych zostało opisane w książce: Assembler x86 - Programowanie i Podstawy Systemów Operacyjnych. Aktualnie trwa na nią promocja -43%.



Podobne artykuły