06.doc

(549 KB) Pobierz
Rozdział X

 

Rozdział 6. ¨ Podstawy programowania dla hakerów              317



Rozdział 6.
Podstawy programowania dla hakerów

Język C

Dla każdego hakera, młodego czy starego, mniej lub bardziej doświadczonego, znajomość języka C jest jednym z fundamentów wiedzy. Niemal wszystkie narzędzia i programy, stosowane w trakcie analiz sieci i włamań, powstają właśnie w tym języku. Również w niniejszej książce większość przedstawianego kodu to właśnie kod źródłowy w języku C. Programy te można modyfikować, dostosowywać do własnych potrzeb i odpowiednio kompilować.

W pracy nad niniejszym rozdziałem wykorzystano obszerne fragmenty pracy guru programowania Matthew Proberta. Mają one pełnić funkcję wprowadzenia do programowania w języku C i umożliwić stosowanie przedstawianych w książce (i załączonych na CD-ROM-ie) listingów programów. Pełny kurs języka znajdziesz w niejednej książce wydawnictwa Helion.

Język C wyróżniają następujące cechy, które omawiamy niżej.

t   Blokowe konstrukcje sterowania wykonywaniem programu (typowe dla większości języków wysokiego poziomu).

t   Swobodne operowanie podstawowymi obiektami „maszynowymi” (takimi jak bajty) i możliwość odwoływania się do nich przy użyciu dowolnej, wymaganej w danej sytuacji, perspektywy obiektowej (typowe dla języków asemblerowych).

t   Możliwość wykonywania operacji zarówno wysokiego poziomu (na przykład arytmetyka zmiennoprzecinkowa), jak i niskiego poziomu (zbliżonych do instrukcji języka maszynowego), co umożliwia tworzenie kodu wysoce zoptymalizowanego bez utraty jego przenośności.

Przedstawiony w niniejszym rozdziale opis języka C bazować będzie na funkcjach oferowanych przez większość kompilatorów dla komputerów PC. Powinien dzięki temu umożliwić rozpoczęcie tworzenia prostych programów osobom nieposiadającym szerokiej wiedzy o języku (uwzględnimy między innymi funkcje zapisane w pamięci ROM i funkcje DOS-u).

Przyjmujemy założenie, że masz, drogi Czytelniku, dostęp do kompilatora C i odpowiedniej dokumentacji funkcji bibliotecznych. Programy przykładowe powstały w Turbo C firmy Borland; większość elementów niestandardowych tego narzędzia uwzględniono również w późniejszych edycjach Microsoft C.

Wersje języka C

W pierwotnej edycji języka C (jeszcze przed publikacją Kernighana i Ritchie’ego, The C Programming Language, Prentice-Hall 1988 (polskie wydanie: Język ANSI C, Wydawnictwa Naukowo-Techniczne 1994)) zintegrowane operatory przypisania (+=, *= itd.) definiowane były odwrotnie (tj. =+, =* itd.). Znakomicie utrudniało to interpretację wyrażeń takich jak:

x=-y

co mogłoby znaczyć

x = x - y  

lub

   x = (-y)

Ritchie szybko zauważył dwuznaczność takiego zapisu i zmodyfikował go do postaci znanej dzisiaj (+=, *= itd.). Mimo to wciąż stosowanych jest wiele odmian będących rodzajem wypośrodkowania między pierwotną wersją języka C Kernighana i Ritchie’ego a językiem ANSI C. Różnice między nimi dotyczą przede wszystkim:

t   wprowadzenia prototypów funkcji i zmiany preambuły definicji funkcji, aby dostosować ją do stylu prototypów,

t   wprowadzenia znaku wielokropka (...) do oznaczenia list argumentów o zmiennej długości,

t   wprowadzenia słowa kluczowego void (dla funkcji, które nie zwracają wartości) i typu void * dla ogólnych zmiennych wskaźnikowych,

t   wprowadzenie w preprocesorze mechanizmów scalania ciągów, wklejania elementu (token-pasting) i zamiany na ciąg (string-izing),

t   dodanie w preprocesorze translacji „trygrafów” (trigraph) — trójznakowych sekwencji reprezentujących znaki specjalne,

t   dodanie w preprocesorze dyrektywy #pragma i formalizacja pseudofunkcji declared(),

t   wprowadzenie ciągów i znaków wielobajtowych, zapewniających obsługę języków narodowych,

t   wprowadzenie słowa kluczowego signed (jako uzupełnienie słowa unsigned, stosowane w deklaracjach liczb całkowitych) i jednoargumentowego operatora plus (+).

Klasyfikowanie języka C

Szerokie możliwości języka C, dopuszczenie bezpośredniego operowania na adresach i danych w pamięci oraz strukturalne podejście do programowania sprawiają, że język ten klasyfikuje się jako „język programowania średniego poziomu”. Znajduje to wyraz w mniejszej liczbie gotowych rozwiązań niż w językach wysokiego poziomu, takich jak BASIC, ale wyższym poziomie strukturalnym niż niskopoziomowy Assembler.

Słowa kluczowe

Pierwotna edycja języka C definiuje 27 słów kluczowych. Komitet ANSI dodał do nich 5 nowych. Wynikiem są dwa standardy języka, choć norma ANSI przejęła większość elementów od Kerninghana i Ritchie’ego. Oto lista:

auto

double

int

struct

break

else

long

switch

case

enum

register

typedef

char

extern

return

union

const

float

short

unsigned

continue

for

signed

void

default

goto

sizeof

volatile

do

if

static

while

Warto zwrócić uwagę, że niektóre kompilatory C wprowadzają dodatkowe słowa kluczowe, specyficzne dla środowiska sprzętowego. Warto zapoznać się z nimi.

Struktura języka C

Język C wymaga programowania strukturalnego. Oznacza to, że na program składa się pewna grupa nawzajem wywołujących się bloków kodu. Dostępne są różnorodne polecenia służące do konstruowania pętli i sprawdzania warunków:

do-while, for, while, if, case

Blok programu w języku C ujmowany jest w nawiasy klamrowe ({}). Może on być kompletną procedurą, nazywaną funkcją lub częścią kodu funkcji. Przyjrzyjmy się przykładowi:

if (x < 10)

{

  a = 1;

  b = 0;

}

Instrukcje wewnątrz nawiasów klamrowych wykonane zostaną tylko wtedy, gdy spełniony zostanie warunek x < 10.

Jako kolejny przykład przedstawimy pełny blok kodu funkcji, zawierający wewnątrz blok pętli:

int GET_X()

{

  int  x;

 

  do

  {

    printf ("\nWprowadz liczbe z zakresu od 0 do 10 ");

    scanf("%d",&x);

  }

  while(x < 0 || x > 10);

  return(x);

}

Zwróćmy uwagę, że każdy wiersz instrukcji zakończony jest średnikiem, o ile nie jest sygnałem początku bloku kodu (w takim przypadku kolejnym znakiem jest nawias klamrowy). Język C rozpoznaje wielkość liter, ale nie bierze pod uwagę białych znaków. Odstępy między poleceniami są pomijane, stąd konieczność użycia średnika, aby oznaczyć koniec wiersza. Tego rodzaju podejście powoduje, że następujące polecenia interpretowane są jako identyczne:

x = 0;

x        =0;

x=0;

Ogólna postać programu w języku C jest następująca:

t   instrukcje preprocesora kompilacji,

t   globalne deklaracje danych.

t   deklaracje i definicje funkcji (włączając w to zawartość programu):

typ-zwracany main (lista parametrów)

{

  instrukcje

}

typ-zwracany f1 (lista parametrów)

{

  instrukcje

}

typ-zwracany f2 (lista parametrów)

{

  instrukcje

}

.

typ-zwracany fn (lista parametrów)

{

  instrukcje

}

Komentarze

Podobnie jak większość języków, C pozwala umieszczać w kodzie programu komentarze. Ich ogranicznikami są symbole /* i */:

/* To jest wiersz komentarza w języku C */

(Równie często korzysta się z komentarzy jednoliniowych, otrzymywanych poprzez sekwencję //, np.:

   //To też jest wiersz komentarza  P.B.)

Biblioteki

Programy w języku C kompiluje się i łączy z funkcjami bibliotecznymi, dostarczanymi wraz z kompilatorem. Na biblioteki składają się funkcje standardowe, których działanie zdefiniowane zostało w normie ANSI. Ich powiązanie z konkretnym kompilatorem zapewnia dostosowanie do platformy sprzętowej. Wynika stąd, że standardowa funkcja biblioteczna printf() działa tak samo w systemach DEC VAX i IBM PC, choć różni się jej, zapisany w bibliotece, kod maszynowy. Programista C nie musi zagłębiać się w zawartość bibliotek, wymagana jest jedynie umiejętność ich stosowania i znajomość działania funkcji, które pozostają niezmienne na każdym komputerze.

Tworzenie programów

Kompilacja

Zanim zajmiemy się funkcjami, poleceniami, sekwencjami i innymi zaawansowanymi zagadnieniami, przyjrzyjmy się praktycznemu przykładowi, w którym doprowadzimy do skompilowania kodu. Kompilowanie programów C jest stosunkowo prostą czynnością, jednak różni się zależnie od stosowanego kompilatora. Kompilatory wyposażone w menu umożliwią skompilowanie, skonsolidowanie i uruchomienie programu jednym wciśnięciem klawisza. Podchodząc jednak do zagadnienia możliwie uniwersalnie i tradycyjnie, przeprowadzimy poniżej całą procedurę w oparciu o wiersz poleceń.

W dowolnym edytorze wprowadzamy poniższy fragment kodu i zapisujemy plik jako przyklad.c:

/*

  przykładowy komunikat tekstowy

*/

#include<stdio.h>

void main()

{

  printf( "Hello!\n" );

}

Kolejnym krokiem jest skompilowanie kodu do postaci pliku programu — dopiero wtedy można będzie go uruchomić (czy też wykonać). W wierszu poleceń w tym samym katalogu, w którym zapisaliśmy plik przyklad.c, wprowadzamy następujące polecenie kompilacji:

cc przyklad.c

Nie wolno zapominać, że składnia polecenia kompilacji zależy od kompilatora. Nasz przykład opiera się na standardzie języka C. Współcześnie jednak popularne jest stosowanie składni wywodzącej się z kompilatora GNU C:

gcc przyklad.c

Po wykonaniu takiego polecenia nasz kod jest już skompilowany i ma postać pliku programu, który możemy uruchomić. Wynik jego działania łatwo wydedukować z prostego kodu:

Hello!

Press any key to continue

To wszystko! Kompilowanie małych programów w C nie jest trudne, należy jedynie mieć świadomość szkodliwych niekiedy efektów ich działania. Programy przedstawiane na stronach tej książki i załączone na CD-ROM-ie są oczywiście znacznie bardziej skomplikowane, jednak zasady pozostają te same.

Typy danych

W języku C wyróżnia się cztery podstawowe typy danych: znakowy, całkowity, zmiennoprzecinkowy i nieokreślony. Odpowiadają im słowa kluczowe: char, int, float i void. Dalsze typy danych tworzy się na tej podstawie, dodając modyfikatory: signed (ze znakiem), unsigned (bez znaku), long (długa) i short (krótka). Modyfikator signed jest elementem domyślnym, co sprawia, że jego użycie może się okazać konieczne jedynie w wypadku gdy zastosowano przełącznik kompilacji nakazujący domyślne korzystanie ze zmiennych bez znaku. Rozmiar każdego typu danych zależy od platformy sprzętowej, jednak norma ANSI wyznacza pewne zakresy minimalne, zestawione w tabeli 6.1.

W praktyce tak określone konwencje oznaczają, że typ danych char nadaje się najlepiej do przechowywania zmiennych typu znacznikowego, takich jako kody stanu, o ograniczonym zakresie wartości. Można również korzystać z typu int. Gdy jednak zakres wartości nie przekracza 127 (lub 255 dla unsigned char), każda deklarowana w ten sposób zmienna przyczynia się do niepotrzebnego obciążania pamięci.

Natomiast trudniejsze jest pytanie o to, z którego typu liczb rzeczywistych korzystać float, double czy long double. Gdy wymagana jest dokładność, na przykład w aplikacji stosowanej...

Zgłoś jeśli naruszono regulamin