Język programowania niskiego poziomu - Low-level programming language
Język niskiego poziomu jest językiem programowania , który zapewnia niewielką lub żadną abstrakcją z komputera za zestaw instrukcji architektury -commands lub funkcji na mapie językowych, które są strukturalnie podobne do instrukcji procesora. Ogólnie odnosi się to do kodu maszynowego lub języka asemblera . Ze względu na niską (stąd słowo) abstrakcję między językiem a językiem maszynowym, języki niskiego poziomu są czasami opisywane jako „bliskie sprzętowi”. Programy napisane w językach niskiego poziomu wydają się być stosunkowo nieprzenośne , ponieważ są zoptymalizowane pod kątem określonego typu architektury systemu.
Języki niskiego poziomu mogą konwertować na kod maszynowy bez kompilatora lub interpretera — języki programowania drugiej generacji używają prostszego procesora zwanego asemblerem — a wynikowy kod działa bezpośrednio na procesorze. Program napisany w języku niskiego poziomu może działać bardzo szybko i zajmuje mało pamięci . Równoważny program w języku wysokiego poziomu może być mniej wydajny i zużywać więcej pamięci. Języki niskopoziomowe są proste, ale uważane za trudne w użyciu ze względu na liczne szczegóły techniczne, o których programista musi pamiętać. Dla porównania, język programowania wysokiego poziomu izoluje semantykę wykonania architektury komputerowej od specyfikacji programu, co upraszcza programowanie.
Kod maszynowy
Kod maszynowy to jedyny język, który komputer może przetwarzać bezpośrednio bez wcześniejszej transformacji. Obecnie programiści prawie nigdy nie piszą programów bezpośrednio w kodzie maszynowym, ponieważ wymaga to zwrócenia uwagi na liczne szczegóły, które język wysokiego poziomu obsługuje automatycznie. Ponadto wymaga zapamiętania lub wyszukania kodów numerycznych dla każdej instrukcji i jest niezwykle trudny do zmodyfikowania.
Prawdziwy kod maszynowy to strumień nieprzetworzonych, zwykle binarnych danych. Programista kodowanie w „kod maszynowy” normalnie kody instrukcji i danych w bardziej czytelnej formie jak po przecinku , ósemkowym lub szesnastkowym , który jest tłumaczony do formatu wewnętrznego przez program nazywany ładowarka lub przełączana do pamięci komputera z przedniego panelu .
Chociaż niewiele programów jest napisanych w języku maszynowym, programiści często stają się biegli w ich odczytywaniu poprzez pracę ze zrzutami pamięci lub debugowanie z panelu przedniego.
Przykład: Funkcja w szesnastkowej reprezentacji 32-bitowego kodu maszynowego x86 do obliczenia n- tej liczby Fibonacciego :
8B542408 83FA0077 06B80000 0000C383 FA027706 B8010000 00C353BB 01000000 B9010000 008D0419 83FA0376 078BD989 C14AEBF1 5BC3
język programowania
Języki drugiej generacji zapewniają jeden poziom abstrakcji nad kodem maszynowym. W pierwszych dniach kodowania na komputerach takich jak TX-0 i PDP-1 pierwszą rzeczą, jaką zrobili hakerzy z MIT, było napisanie asemblerów. Język asemblerowy ma niewielką semantykę lub specyfikację formalną, będąc jedynie mapowaniem symboli czytelnych dla człowieka, w tym adresów symbolicznych, na kody operacyjne , adresy , stałe numeryczne, łańcuchy i tak dalej. Zazwyczaj jedna instrukcja maszynowa jest reprezentowana jako jeden wiersz kodu asemblera. Asemblery tworzą pliki obiektowe, które mogą łączyć się z innymi plikami obiektowymi lub być ładowane samodzielnie.
Większość asemblerów dostarcza makra do generowania typowych sekwencji instrukcji.
Przykład: Ten sam kalkulator liczb Fibonacciego jak powyżej, ale w języku asemblerowym x86-64 używającym składni AT&T :
_fib:
movl $1, %eax
xorl %ebx, %ebx
.fib_loop:
cmpl $1, %edi
jbe .fib_done
movl %eax, %ecx
addl %ebx, %eax
movl %ecx, %ebx
subl $1, %edi
jmp .fib_loop
.fib_done:
ret
W tym przykładzie kodu funkcje sprzętowe procesora x86-64 (jego rejestry ) są nazywane i manipulowane bezpośrednio. Funkcja ładuje swoje dane wejściowe z %edi zgodnie z ABI Systemu V i wykonuje obliczenia, manipulując wartościami w rejestrach EAX , EBX i ECX , aż do zakończenia i powrotu. Zauważ, że w tym języku asemblerowym nie ma koncepcji zwracania wartości. Po zapisaniu wyniku w rejestrze EAX polecenie RET po prostu przenosi przetwarzanie kodu do lokalizacji kodu przechowywanej na stosie (zazwyczaj instrukcja bezpośrednio po tej, która wywołała tę funkcję) i to od autora kodu wywołującego Wiedz, że ta funkcja przechowuje swój wynik w EAX i stamtąd go pobiera. język asemblerowy x86-64 nie narzuca standardu zwracania wartości z funkcji (a więc w rzeczywistości nie ma pojęcia o funkcji); to od kodu wywołującego zależy sprawdzenie stanu po powrocie procedury, jeśli musi wyodrębnić wartość.
Porównaj to z tą samą funkcją w C:
unsigned int fib(unsigned int n) {
if (!n)
return 0;
else if (n <= 2)
return 1;
else {
unsigned int a, c;
for (a = c = 1; ; --n) {
c += a;
if (n <= 2) return c;
a = c - a;
}
}
}
Ten kod jest bardzo podobny w strukturze do przykładu języka asemblerowego, ale istnieją znaczące różnice pod względem abstrakcji:
- Wejście (parametr n ) jest abstrakcją, która nie określa żadnej lokalizacji pamięci na sprzęcie. W praktyce kompilator C stosuje jedną z wielu możliwych konwencji wywoływania, aby określić miejsce przechowywania danych wejściowych.
- Wersja w języku asemblerowym ładuje parametr wejściowy ze stosu do rejestru iw każdej iteracji pętli zmniejsza wartość w rejestrze, nigdy nie zmieniając wartości w lokalizacji pamięci na stosie. Kompilator C może załadować parametr do rejestru i zrobić to samo lub zaktualizować wartość, gdziekolwiek jest przechowywana. To, który wybierze, jest decyzją implementacyjną całkowicie ukrytą przed autorem kodu (i taką bez skutków ubocznych , dzięki standardom języka C).
- Zmienne lokalne a, b i c to abstrakcje, które nie określają żadnej konkretnej lokalizacji pamięci na sprzęcie. Kompilator C decyduje, jak faktycznie przechowywać je dla architektury docelowej.
- Funkcja return określa wartość do zwrócenia, ale nie dyktuje, w jaki sposób ma być zwracana. Kompilator C dla dowolnej architektury implementuje standardowy mechanizm zwracania wartości. Kompilatory dla architektury x86 zazwyczaj (ale nie zawsze) używają rejestru EAX do zwracania wartości, tak jak w przykładzie języka asemblerowego (autor przykładu języka asemblerowego wybrał skopiowanie konwencji C, ale język asemblerowy tego nie wymaga).
Te abstrakcje sprawiają, że kod C jest kompilowalny bez modyfikacji na dowolnej architekturze, dla której napisano kompilator C. Kod języka asemblera x86 jest specyficzny dla architektury x86.
Programowanie niskopoziomowe w językach wysokiego poziomu
Pod koniec lat 60. języki wysokiego poziomu, takie jak PL/S , BLISS , BCPL , rozszerzony ALGOL (dla dużych systemów Burroughs ) i C zawierały pewien stopień dostępu do funkcji programowania niskiego poziomu. Jedną z metod jest montaż Inline , w którym kod asemblera jest osadzony w języku wysokiego poziomu, który obsługuje tę funkcję. Niektóre z tych języków umożliwiają także dyrektywom optymalizacji kompilatora zależnych od architektury dostosowanie sposobu, w jaki kompilator używa architektury procesora docelowego.