Programowanie w języku C/C++ » Artykuły

Jak programować w Linuksie

  • 2010-10-31 18:27
  • 13 komentarzy
  • 5243 odsłony
  • Oceń ten tekst jako pierwszy
Ten artykuł wymaga dopracowania!

Jeżeli możesz popraw ten artykuł według zaleceń, które możesz znaleźć na stronie Artykuły do poprawy. Po dopracowaniu tego tekstu można usunąć ten komunikat.



Wymagania ogólne:
  • mózg
  • trochę wolnego czasu i chęci

Wymagania szczegółowe:
  • znajomość języka C
  • znajomość systemu Linux

Wymagania techniczne:
  • Linux + biblioteki (development)

Spis treści

     1 Wprowadzenie
     2 Funkcje systemowe i biblioteczne: porównanie.
          2.1 Kompilacja
     3 Inne funkcje systemowe.
     4 4. Procedury biblioteczne {<stdio.h>}.
          4.1 4.1. Funkcje wyświetlające.
          4.2 4.2. Funkcje pobierające.
     5 Katalogi i pliki
          5.1 Pliki
          5.2 Katalogi
     6 Krótko o procesach.
     7 Programowanie sieciowe


                                                       

Wprowadzenie


Systemy Unix (Linux) są w ogromnym stopniu oparte na systemie plików, prawie wszystko może być potraktowane jako plik. Wystarczy zaglądnąć do katalogu /dev. Znajdują się tam plikowe reprezentacje urządzeń fizycznych. Np. żeby zamontować sobie dysk C:, na którym masz DOS-a wpisujesz:
mount /dev/hda1 /mnt/c

o czym jako użytkownik Linuksa na pewno wiesz. Pamięć również jest traktowana jest jako plik w tym systemie. Jako plik traktuje się również gniazdka sieciowe, a więc za pomocą funkcji systemowych pisze się do nich i czyta jak do i z plików. Z tego względu umiejętność programowania w Unixie, to umiejętność posługiwania się funkcjami obsługującymi pliki. Oczywiście to nie wszysko (dobrze by było ;). Z drugiej jednak strony budowa systemów Unixowych jest dosyć prosta i przejrzysta (of kors na odpowiednim poziomie).

Tutorial ten ma Ci pokazać podstawy programowania w Linuksie. Aby osiągnąć więcej trzeba pisać i czytać kod, którego jest mnóstwo w sieci. Mam nadzieję, że gdy zobaczysz jakiś exploit, to będziesz mniej więcej rozumiał o co w nim chodzi i nie będziesz się nim bezmyślnie posługiwał.


Funkcje systemowe i biblioteczne: porównanie.


Pliki w systemie Linux można na poziomie programowania obsługiwać na dwa sposoby: za pomocą funkcji systemowych oraz bibliotecznych, przy czym funkcji bibliotecznych jest więcej i są one bardziej przyjazne.

Funkcje systemowe to takie funkcje, które wykorzystują gotową obsługę udostępnioną przez jądro systemu. Jądro jest cały czas podczas działania systemu w pamięci i kontroluje wszystkie procesy i każdy dostęp do systemu plików. Również funkcje biblioteczne używają funkcji systemowych.

Podstawowa różnica między tymi dwoma sposobami leży w kodzie samego programu. Kiedy program korzysta z funkcji systemowych, sam kod nie jest dołączany do programu - program zleca to zadanie dla jądra (jest to zwykle wykonywane za pomocą przerwań programowych). W przypadku biblioteki kod w niej zawarty jest dołączany do programu.

Dlaczego zatem używa się funkcji bibliotecznych? Przede wszystkim funkcje te są bardziej przyjazne - nie trzeba na przykład przejmować się obsługą buforów, o wiele trudniej jest się pomylić. Funkcje biblioteczne są też bardziej rozbudowane.

Poniżej znajdują się przykłady prostych programów, które wykonują te same zadania wykorzystując funkcje systemowe lub biblioteczne.

system.c
/*wykorzytanie funkcji systemowych do operacji na pliku*/
/*program liczy znaki w pliku 'plik' Ilosc znakow to rownoczesnie 
  wielkosc pliku w bajtach*/
 
#include <stdlib.h> //wykorzystanie dla funkcji exit()
#include <fcntl.h>
 
#define BUFROZ 512
main()
{
    int desp;                //deskryptor pliku
    char buf[BUFROZ];
    ssize_t czytaj;
    long licz_zn; 
    /*
    */
    if((desp = open("plik", O_RDONLY)) == -1) //w wypadku błędu funkcja
                                                //open zwraca -1
    {
        printf("Nie moge otworzyc pliku 'plik'n");
        exit(1);
    }
 
    while((czytaj = read(desp, buf, BUFROZ)) >0)
        licz_zn+=czytaj;
 
    printf("Ilosc znakow w pliku wynosi : %ldn", licz_zn);
 
    close(desp);
 
    exit(0);        
}


Jak widzimy program użył kilku funkcji systemowych: open, read, close.



Funkcja open:
int open(const char *ścieżka, int flagi, [mode_t mode]);

służy do otwierania pliku. Drugi argument tej funkcji jest typu int. Wykorzystujemy tu odpowiednie flagi (w naszym przypadku O_RDONLY), które są zdefiniowane w pliku nagłówkowym < fcntl.h >:

O_RDONLY - otwórz tylko do odczytu
O_WRONLY - otwórz tylko do zapisu
O_RDWR - otwórz do odczytu i zapisu

Trzeci argument [mode_t mode] jest używany jedynie z flagą O_CREAT oznaczający utworzenie pliku, natomiast argument [mode_t mode] będzie ustawiał prawa dostępu (np. 0622).



Funkcja read:
ssize_t read(desp, buf, BUFROZ);

służy do czytania z otworzonego pliku. Argumenty:
desp - deskryptor pliku uzyskany podczas wywołania funkcji open
buf - bufor, który będzie zawierał odczytane z pliku dane
BUFROZ - rozmiar bufora (w jakich porcjach będzie funkcja read czytać)

Zaleca się używanie BUFROZ równego wielokrotności 512. Jeżeli funkcja zwróci wartość ssize_t <0 oznacza to koniec pliku. (-1 - EOF)



Funkcja close:
close(desp);

służy do zamknięcia pliku poprzez zwolnienie deskryptora.

proced.c
/*wykorzystanie procedur bibliotecznych*/
/*program liczy znaki w 'plik'*/
 
#include <stdio.h> //biblioteka I/O 
#include <stdlib.h>
 
main()
{
    FILE *p;
    int c;
    long licz_zn = 0;
 
    if ((p = fopen("plik", "r")) == NULL)
    {
        printf("Nie moge otworzyc pliku 'plik'n");
        exit(1);
    }
 
    while ((c = getc(p)) != EOF)
        {
            licz_zn+=1;
        }    
 
    printf("Ilosc znakow w pliku wynosi : %ldn", licz_zn);        
 
    fclose(p);
 
    exit(0);        
}




Funkcja fopen i fclose:
        FILE* fopen(const char *nazwa_pliku, const char *typ);
        int fclose(FILE* stream);


Plik jest identyfikowany za pomocą wskaźnika do struktury FILE.
typ:
"r" - tylko do odczytu
"w" - tylko do zapisu (jesli nie istnieje utwórz)
"a" - do odczytu, dane będą dodawane na końcu pliku
"r+"- do odczytu i zapisu
"w+"- do odczytu i zapisu (jesli nie istnieje utwórz)
"a+" - do odczytu i zapisu, dane będą dodawane na końcu pliku



Funkcja getc:
        int getc(FILE *p);

pobiera numer znaku z pliku opisanego przez FILE

system2.c
/*   kopiuje plik1 do plik2  używając funkcji systemowych*/
 
#include <unistd.h>
#include <fcntl.h>
 
#define BUFSIZE 512
#define PERM        0644
 
/* funkcja kopiujaca */
int copyfile(const char *plik1, const char *plik2)
{
    int infile, outfile;
    ssize_t nread;
    char buffer[BUFSIZE];
 
    if((infile = open(plik1, O_RDONLY)) == -1)
        return(-1);
 
    if((outfile = open(plik2, O_WRONLY|O_CREAT|O_TRUNC, PERM)) == -1)
    {
        close(infile);
        return(-2);
    }        
 
    /*teraz czytaj z plik1 po BUFSIZE znakow naraz*/
    while((nread = read(infile, buffer, BUFSIZE))>0)
    {
        /*zapisz bufor do ploku wyjsciowego*/
        write(outfile, buffer, nread);
    }
    close(infile);
    close(outfile);
 
}
/////////////////////////////
int main(int argc, char** argv)
{
    if (argc<3)
    {
     printf("Uzyj: %s plik_zrodlo plik_celn",argv[0]);
     exit(1);
    }
 
    copyfile(argv[1],argv[2]);
}


Program używa znanych już funkcji (oprócz write) do skopiowania jednego pliku do drugiego.



Funkcja write:
ssize_t write(int desp, const char buffer, ssize_t nread);

służy do zapisu danych do pliku o deskryptorze desp, danych zawartych w buffer w liczbie ssize_t nread.



Flaga O_TRUNC razem z O_CREAT sprawia, że jeśli plik już istnieje zostanie on obcięty do zerowej długości. Oczywiście jeśli nie chcemy stracić ważnych danych powinniśmy użyć razem O_CREAT|O_EXCL wtedy funkcja open zwróci błąd jeżeli już taki plik istnieje.

Jeszcze małe wyjaśnienie funkcji int main(int argc, char** argv):
int argc - to liczba argumentów podanych w linii poleceń, w naszym przypadku musimy podać ich trzy: nazwa_programu plik_zrodlo plik_cel
char** argv - jest to tablica wskaźników pokazujących na nasze argumenty podane w linii poleceń podczas uruchamiania skompilowanego programu (np. argv[0] wskazuje na nazwę naszego programu).

proced2.c
/*program wykorzystauje procedury biblioteczne*/
/*
program kopiuje plik1 do plik2
*/
 
#include <stdio.h>
#include <stdlib.h>
 
/*funkcja kopiujaca*/
int copyfile(const char *plik1, const char *plik2)
{
    FILE *in, *out;
    int c;
 
    if ((in = fopen(plik1,"r")) == NULL)
        return(-1);
 
    if ((out = fopen(plik2,"w")) == NULL)
    {
        fclose(in);
        return(-2);
    }                
 
    while((c = getc(in)) != EOF)
        putc(c, out);
 
    fclose(in);
    fclose(out);        
}
////////////////////////////////
 int main(int argc, char** argv)
 {
     if(argc<3)
     {
         printf("Uzyj: %s plik_zrodlo plik_celn",argv[0]);
         exit(1);
     }
 
     copyfile(argv[1],argv[2]);
 }




Funkcja putc:        
int putc(int c, FILE* out);


pisze do out -opisany przez FILE- znak c.




Kompilacja


W Linuksie kompilację wykonuje się w następujący sposób:
gcc [opcje] plik.c

Zajmijmy się trochę opcjami.

Kompilacja gcc plik.c automatycznie tworzy plik binarny o nazwie a.out. Jeżeli chcemy aby plik wyjściowy miał konkretną nazwę używamy opcji -o.
gcc -osystem system.c

w ten sposób kompilator utworzy plik system gotowy do uruchomienia. Inną ciekawą opcją jest -S - kompilator tworzy plik wyjściowy, a raczej przejściowy, w asemblerze. Po wpisaniu polecenia
gcc -S system.c

kompilator utworzy plik system.s. Dla naszego przykładu wygląda on tak:

.file        "system.c"
        .version        "01.01"
gcc2_compiled.:
.section        .rodata
.LC0:
        .string        "plik"
        .align 32
.LC1:
        .string        "Nie moge otworzyc pliku 'plik'n"
        .align 32
.LC2:
        .string        "Ilosc znakow w pliku wynosi : %ldn"
.text
.align 4
.globl main
        .type         main,@function
main:
        pushl %ebp
        movl %esp,%ebp
        subl $512,%esp
        pushl %edi
        pushl %esi
        pushl %ebx
        pushl $0
        pushl $.LC0
        call open
        movl %eax,%esi
        addl $8,%esp
        cmpl $-1,%esi
        jne .L17
        pushl $.LC1
        call printf
        pushl $1
        call exit
        .p2align 4,,7
.L17:
        leal -512(%ebp),%ebx
        jmp .L18
        .p2align 4,,7
.L20:
        addl %eax,%edi
.L18:
        pushl $512
        pushl %ebx
        pushl %esi
        call read
        addl $12,%esp
        testl %eax,%eax
        jg .L20
        pushl %edi
        pushl $.LC2
        call printf
        pushl %esi
        call close
        pushl $0
        call exit
.Lfe1:
        .size         main,.Lfe1-main
        .ident        "GCC: (GNU) egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)"


Jak widać polecenia asemblerowe różnią się nieco od tych znanych z kompilatrów dosowych. W Linuksie dodajemy do większości poleceń literę "l" np.
movl %eax,%esi -kopiuj wartość z rej. esi do eax

Litery "l" nie dodaje się do polecenia call (chyba wiadomo dlaczego) i do funkcji skoków.

W samym kodzie źródłowym polecenia asemblerowe trzeba poprzedzić odpowiednią dyrektywą np.
asm("movl %esp,%eax");

asm wstawiamy przed każdym poleceniem lub
 __asm__("clcn"
                "1:t"
                "lodslnt"
                "adcl %%eax, %%ebxnt"
                "loop 1bnt"
                "adcl $0, %%ebxnt"
                "movl %%ebx, %%eaxnt"
                "shrl $16, %%eaxnt"
                "addw %%ax, %%bxnt"
                "adcw $0, %%bx");

asm używamy do zgrupowania naszych asemblerowych poleceń. Oczywiście mam świadomość, że asemblera będziesz używać bardzo rzadko lub wcale, jednak jest on dość często używany w różnych exploitach. Programowanie na niskim poziomie daje możliwość pełnej kontroli nad komputerem, ale jest stosunkowo trudne.

Chciałbym przedstawić jeszcze jeden argument wydawany kompilatorowi. Jest to argument optymalizacji -O[numer_optymalizacji]. Generalnie preferuje się optymalizację na poziomie 2, zwiększanie jego poziomu daje często odwrotne do zamieżonych rezultaty. Przykład:

gcc -osystem -O2 system.c


W ten sposób nasz kompilator spróbuje poprawić szybkość i zmniejszyć wielkość programu wynikowego. Aby sprawdzić w jaki sposób to robi można kompilować z opcją -S przed optymalizacją i po niej, a póżniej porównywać kod przejściowy.


Inne funkcje systemowe.


Oprócz funkcji open z flagą O_CREAT do tworzenia pliku można wykorzystać funkcję systemową creat.

Funkcja creat:
int creat(const *ścieżka, mode_t mode);

np.
        int desp;
        desp = creat("/home/reksio/plik", 0622);




Do zmiany pozycji wskaźnika odczytu-zapisu wykorzystujemu funkcję
systemową lseek.

Funkcja lseek:
off_t lseek(int desp, off_t offset, int pozycja_poczatkowa);

Do programu należy dołączyć plik nagłówkowy <unistd.h>.
Przykład użycia:
        off_t pozycja;
        int desp;
        desp = open("plik", O_RDONLY);        
        pozycja = lseek(desp, (off_t)+10, SEEK_SET);

Pozycja wskaźnika jest w tym przypadku 10 bajtem liczonym od początku pliku.

Argument [int pozycja_poczatkowa] może przybierać następujące wartości:
SEEK_SET - początek pliku
SEEK_CUR - bieżace położenie
SEEK_END - koniec pliku



Za pomocą funkcji lseek można w łatwy sposób sprawdzić wielkość pliku np.
        int desp;
        off_t w_pliku;
        desp = open("plik", O_RDONLY);
        w_pliku = lseek(desp, (off_t)0, SEEK_END);


Do obsługi błędów można użyć pliku nagłówkowego <errno.h>, która za pomocą zmiennej errno przekazuje numer błędu, który z kolei jest powiązany z komunikatami o błędach. Aby użyć świadomie tego numeru trzeba znać listę kodów błędów. Na szczęście Linux dostarcza funkcję biblioteczną perror, która pobiera kod błędu i pokazuje odpowiedni komunikat (jeżeli twoje ustawienia lokalizacyjne są konfigurowane na język polski, komunikaty o błędach będą również po polsku).

np.
#include <fcntl.h>
#include <errno.h>
main()
{
        int desp;
 
        if ((desp = open("plik", O_RDONLY)) == -1)
                perror("Blad podczas otwarcia pliku");
}


Dzięki funkcji perror w razie błędu (-1) zostanie pobrany odpowiedni kod błędu i wyświetlony czytelny komunikat. W naszym przypadku w razie nieistnienia pliku "plik" w bieżącym katalogu zostanie wyświetlony komunikat:
<quote>Blad podczas otwarcia pliku: Nie ma takiego pliku lub katalogu</quote>

Do usuwania plików można zastosować dwie metody: unlink i remove. Remove korzysta z stdio.h

Funkcja unlink i remove:
 
        #include <unistd.h>
        int unlink(const char *ścieżka)
 
        #include <stdio.h>
        int remove(const char *ścieżka)



         

4. Procedury biblioteczne {<stdio.h>}.


Plik stdio.h jest również nazywany Standardową Biblioteką I/O. Głównym zadaniem tej biblioteki jest ułatwienie dostępu do pliku oraz zapewnienie efektywnej operacji wejścia/wyjścia. Część funkcji była już pokazana wcześniej: fopen, fclose, getc, putc. Do SB I/O należą również funkcje:printf, scanf, getchar. Są jedne z częściej używanych funkcji służących do komunikacji z użytkownikiem.

4.1. Funkcje wyświetlające.


Funkcja printf:
int printf(const char *kom, arg1, arg2....arg3);

wyświetla ciąg znaków zawarty w między " ". Dodatkowow można wysyłać tzw. conversions specifications. Np.
 
        pi = 3.1415926
        printf("tLiczba pi wynosi: %fn", pi);


po kolei:
t - znak tabulacji przesuwa ciąg wyświetlanych znaków
%f - wyświetli wartość typu float ze zmiennej pi
n - przejście do następnej linii

a więc:
<quote>Liczba pi wynosi: 3.1415926</quote>

Do wyświetlania znaku % trzeba użyć %%.
Po znaku % używa się róznych liter w celu wyświetlenia zmiennych różnych typów, a więc:

%d - typu int
%u - unsigned int
%o - typ int ósemkowo
%x - typ int szesnastkowo
%X - typ int szesnastkowo ale z dużymi literami ABCDEF
%ld - typ long

%f - typ float lub double
%e - typ float lub double w formie naukowej(wykładniczej)
%g - w zależności od wielkości wyświetli tak jak %f lub %e

%c - typu char
%s - wskaźnik znakowy (napis)



Funkcja fprintf:
int fprintf(FILE *plik, const char *kom, arg1, arg2...arg3);

służy do wyświetlania komunikatów o błędach.

Za każdym razem gdy uruchamiany jest program Linuks otwiera trzy pliki:
stdin - standardowe wejście(klawiatura)
stdout - standardowe wyjście(ekran)
stderr - standardowe wyjście komunikatów o błędach        
Wszystkie są ponumerowane odpowiednio 0, 1, 2. Z reguły używa się ich znakowych odpowiedników.

Rozgraniczenie stdout i stderr wynika z tego, że nie zawsze jest potrzeba wysyłania błędów na ekran, równie dobrze można odpowiednie komunikaty wysyłać do pliku.

Funkcja fprintf jest prawie identyczna jak printf, jedynie pierwszy argument zawiera stderr. Np.
fprintf(stderr, "BLAD nr %d w %s", arg1, arg2);




Funkcja sprintf:
int sprintf(char *string, const char *kom, arg1, arg2...argn);

nie jest typową funkcją wypisującą, gdyż wyprowadza komunikat do tablicy
znakowej string. Np.
          char prmpt[81];
          sprintf(prmpt,"%s ",ProgName);





Poniższy program pokazuje parametry środowiska oraz argumenty wpisane w linii poleceń:

#include <stdio.h>
#include <stdlib.h>
 
int i=0;
main(int argc, char* argv[], char* env[])
{
    printf("Parametry srodowiska Linux: n");
    do {
        printf("%s n", env[i]);
        i++;
    }
    while (env[i]!=NULL);
            printf("Lista parametrow programu: n");
 
   for(i=1;i<=argc-1;i++) printf("%s n", argv[i]);
 
    printf("Nazwa programu: n");
    printf("%s n", argv[0]);
    return 0;
}


4.2. Funkcje pobierające.


Są to funkcje odwrotne do funkcji wypisujących. Pobierają one z klawiatury lub z pliku i zapisują do zmiennej odpowiedniego typu.



Funkcja scanf:
int scanf(const char *napis, &zmienna1, &zmienna2...);

pobiera ze standardowego wejścia (stdin) i zapisuje w zmiennej. Znak & (ampresand) to adres w pamięci, gdzie znajduje się zmienna. Np.
        int a, b;
        float c;
 
        scanf("%2d %2d %f", &a, &b, &c);




Funkcja fscanf:
int fscanf(FILE *wskaz, const char *napis, &zmienna1, &zmienna...);


czyta z pliku wskazywanego przez wskaz



Funkcja sscanf:
int sscanf(const char *string, const char *napis, &zmienna1...);

pobiera dane z tablicy string i wpisuje do zmiennych.



Inną często używaną funkcją ze Standardowej Biblioteki I/O jest getchar.
Czyta ona jeden znak ze standardowego wejścia programu (zwykle klawiatura)

getchar:
int getchar(void);

Np.
        int z;
 
        z = getchar();
        if (z==10) printf("Nacisnales ENTER");



Poniższy przykład usuwa plik w argumencie podanym jako ścieżka, program czeka na potwierdzenie operacji.

/*del --program do kasowania pliku*/
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
 
FILE *p;
char *uzycie = "Uzycie: del sciezka_do_plikun";
char c[0];
 
int usun(const char *plik)
{
        /*sprawdzanie za pomoca funkcji open czy plik istnieje*/        
        if ((p = fopen(plik,"r")) == NULL)        
        {                
                fprintf(stderr, "Plik %s nie istniejen",plik);                
                return(-1);        
        }        
 
        fclose(p);        
 
        printf("Czy mam na pewno usunac %s? T/Nn", plik);                
        scanf("%s", c);        
 
        if(c[0] == 't' || c[0] == 'T')        
        {                
                remove(plik);                
                printf("Plik usunietyn");        
                return(0);
        }
 
//        printf("%sn",c);
        printf("Anulowano...n");
 
}
 
int main(int argc, char** argv)
{
        if (argc<2)        
        {                
                fprintf(stderr, uzycie);                
                exit(1);        
        }        
 
        usun(argv[1]);
        exit(0);
}


Istnieje oczywiście większa liczba funkcji SB I/O. Jeżeli jesteś ich ciekawy to zaglądnij do stdio.h (/usr/include).


Katalogi i pliki


Pliki


Katalogi i pliki to podstawa każdego systemu plików. W Unixach plik traktowany jest w specjalny sposób. Oprócz tego każdy plik jest czyjąś własnością, a to daje pewne przywileje jego właścicielowi, który może z tym plikiem zrobić co zechce. Przede wszystkim właściciel pliku może nadawać odpowiednie uprawnienia według własnego uznania. Uprawnienia określają prawa dostępu dla różnych użytkowników. Istnieją ich trzy typy:
  • właściciel
  • grupa do której należy właściciel
  • pozostali użytkownicy.

Prawa dostępu to:
  • czytanie z pliku (4)
  • pisanie do niego (2)
  • wykonywanie (jeżeli jest to plik wykonywalny) (1).

System przechowuje o każdym pliku dokładne dane, które można podglądnąć używając funkcji stat zawartej w <sys/stat.h>. Najczęściej uprawnienia ustawia się za pomocą zapisu ósemkowego np.

0777 - każdy użytkownik może czytać, pisać i wykonywać plik(4+2+1),
0644 - właściciel może czytać i pisać, osoby należące do tej samej grupy
       mogą czytać, czytać mogą pozostali użytkownicy,

Oprócz tego możemy plikom nadać dodatkowe uprawnienia:

04000 - sławny SUID, czyli chwilowe uprawnienia właścieiela są nadawane
        każdemu, kto jest właścicielem procesu. Jeżeli właścicielem jest
        root w trakcie działania programu z suidem kazdy ma uprawnienia
        root'a
02000 - SGID - to samo co SUID tylko dla grupy
01000 - SVTX - obecnie wykorzystywany tylko dla katalogów (bit cachujący)

Do sprawdzenia, czy plik jest dostępny używa się funkcji access



Funkcja access:
        #include <unistd.h>
        int access(const char *ścieżka, int mode);


Argument [int mode]:
R_OK - czy proces może czytać?
W_OK - czy proces może pisać?
X_OK - czy proces może wykonać plik?

W razie błędu funkcja zwraca wartość -1

/*program sprawdza czy jest możliwy dostęp do pliku*/
 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
int main(int argc, char** argv)
{
        if (argc<2)
        {
                printf("Uzycie: %s plikn", argv[0]);
                exit(1);
        }
 
        if(access(argv[1], R_OK) == -1)
                fprintf(stderr, "Nie mozesz czytac z pliku %sn", argv[1]);
 
        printf("Czytanie z pliku dozwolonen");
 
        if(access(argv[1], W_OK) == -1)
                fprintf(stderr, "Nie mozesz pisać do pliku %sn", argv[1]);
 
        printf("Pisanie do pliku dozwolonen");
 
        if(access(argv[1], X_OK) == -1)
                fprintf(stderr, "Nie mozesz uruchamiać pliku %sn", argv[1]);
 
        printf("Uruchamianie pliku dozwolonen");
 
        exit(0);
 
}




Do zmiany uprawnień dla pliku służy funkcja chmod (podobnie jak z linii
poleceń).

Funkcja chmod:
#inlcude <sys/types.h>
#include <sys/stat.h>
int chmod(const char *ścieżka, mode_t nowy);

Funkcja ta może być używana przez właściciela pliku lub administratora.

        if(chmod("/etc/passwd",0777) == -1)
                perror("");





Do zmiany właściciela pliku oraz grupy służy funkcja chown.

Funkcja chown:

        #include <sys/types.h>
        #include <unistd.h>
 
        int chown(const char *ścieżka, uid_t id_wlaściciela, gid_t id_grupy);


np.
        int wartosc
        if(wartosc = chown("/etc/passwd", 44, 7) == -1)
                perror("");


Możliwa jest przekazanie komuś właśności pliku. Taka operacja nie może być anulowana. Chroni to przed kradzieżą przywilejów.



Za pomocą funkcji link można utworzyć do pliku tzw. twarde łącze. Utworzenie twardego łącza sprawia, że tzw. licznik łączy jest zwiększony. Dopiero usunięcie wszystkich twardych łączy oznacza pełne usunięcie pliku z systemu (funkcja unlink). Licznik łaczy powinien dla tego pliku wynosić 0. Funkcja link nie tworzy kopii pliku, sprawia, że z innego miejsca mamy łączność z plikiem macierzystym. Łącze twarde jest swoistym wskaźnikiem do pliku.


Funkcja link:

#include <unistd.h>
int link(const char *ścieżka_do_pliku_macierzystego, const char *nowa);


np.

link("/home/neo/matrix", "/home/cartman/platrix");


W razie błędu funkcja zwraca -1.



Jak już wcześniej wspomniałem istnieje metoda poznania wszystkich własności
pliku, które zawiera system plików.



Funkcja stat:

#include <sys/types.h>
#inlcude <sys/stat.h>
int stat(const char *ścieżka, struct stat *statystyka);

Strukturę stat najlepiej poznać w praktyce. Oto kod wykorzystujący stat:

/*program pokazuje strukture stat dla podanego pliku*/
 
#include <stdio.h>
#include <sys/stat.h>
 
int main(int argc, char** argv)
{
    struct stat statystyka;
 
    if(argc<2)
    {
        printf("Uzycie: %s plikn",argv[0]);
        exit(1);
    }
 
        if(stat(argv[1], &statystyka) == -1)
        {        
                perror("");
                return(-1);    
        }
 
    printf("Dane dla %s odczytane za pomoca funkcji 'stat' :n", argv[1]);
    printf("Urzadzenie logiczne (st_dev)   : %dn", statystyka.st_dev);
    printf("Numer i-wezla (st_ino)   : %dn", statystyka.st_ino);
    printf("Uprawnienia (st_mode)  : %dn", statystyka.st_mode);
    printf("Liczba laczy (st_nlink) : %dn", statystyka.st_nlink);
    printf(" UID (st_uid) : %dn", statystyka.st_uid);
    printf(" GID (st_gid) : %dn", statystyka.st_gid);
    printf("Opis urzadzenia (st_rdev) : %dn", statystyka.st_rdev);
    printf("Wielkosc pliku (st_size) : %ldn", statystyka.st_size);   
    printf("Ostatni odczyt (st_atime) : %dn", statystyka.st_atime);   
    printf("Ostatnia modyfikacja (st_mtime) : %dn", statystyka.st_mtime);   
    printf("Ostatnia zmiana w 'stat' (st_ctime) : %dn", statystyka.st_ctime);   
    printf("Wielkosc bloku I/O (st_blksize) : %ldn", statystyka.st_blksize);   
    printf("Ilosc fiz. blokow dla pliku (st_blocks) : %ldn", statystyka.st_blocks);   
 
}


  • st_dev i st_ino jednoznacznie identyfikują plik, st_dev jest numerem
   urządzenia logicznego, na którym znajduje się plik (numer ten można
   również poznać przeglądając /proc/sys/kernel/real-root-dev), st_ino to numer i-węzła,
   i-węzeł opisuje plik (katalog) w hierarchicznej strukturze systemu
   plików (drzewo katalogów)

  • st_mode opisuje uprawnienia do pliku (12 najmłodszych bitów), można
   również dowiedzieć się jakie urządzenie reprezentuje plik - 060000 to
   urządzenie blokowe, 020000 to urządzenie znakowe

  • st_nlink pokazuje ilość łączy dla pliku

  • st_uid i st_gid pkazują numer id użytkownika i grupy

  • st_rdev pokazuje numer urządzenia nadrzędengo i podrzędnego, numery
   te są potrzebne do jednoznacznej identyfikacji sterownika, z którego
   dane uzrządzenie korzysta. Numery nadrzędne przydzielone statycznie
   są pokazane w /proc/devices. Lepszym sposobem na poznanie tych numerów
   jest polecenie ls -l np. w katalogu /dev:

        brw-rw----   1 root     disk       3,   1 May  5  1998 hda1
        brw-rw----   1 root     disk       3,  10 May  5  1998 hda10
        brw-rw----   1 root     disk       3,  11 May  5  1998 hda11
        brw-rw----   1 root     disk       3,  12 May  5  1998 hda12
        brw-rw----   1 root     disk       3,  13 May  5  1998 hda13
        brw-rw----   1 root     disk       3,  14 May  5  1998 hda14
        brw-rw----   1 root     disk       3,  15 May  5  1998 hda15
        brw-rw----   1 root     disk       3,  16 May  5  1998 hda16
        brw-rw----   1 root     disk       3,   2 May  5  1998 hda2
        brw-rw----   1 root     disk       3,  64 May  5  1998 hdb
        brw-rw----   1 root     disk       3,  65 May  5  1998 hdb1
        brw-rw----   1 root     disk       3,  74 May  5  1998 hdb10
        brw-rw----   1 root     disk       3,  75 May  5  1998 hdb11

   litera b oznacza, że jest to urządzenie blokowe, numer 3 oznacza, że
   wszystkie pokazane urządzenia są obsługiwane przez
   ten sam sterownik, numer podrzędny pozwala sterownikowi jednoznaczenie
   rozumieć, które urządzenie ma być rzeczywiście obsługiwane

  • st_size to jedno z ważniejszych z punktu widzenia użytkownika pole,
   jest wielkością jaką zajmuje plik

  • st_atime, st_mtime, st_ctime to w kolejności: czas ostatniego odczyty,
   czas ostatniej modyfikacji, czas ostatniej zmiany informacji w strukturze
   stat

  • st_blksize to wielkość bloku I/O w systemie plików, w moim przypadku
   wartość 4k (dysk podzielony na 4 kilowe klastry)

  • st_blocks jest liczbą bloków jaką zajmuje fizycznie plik, wilekość nie pokrywa
   się z rzeczywistą wielkością pliku (jeżeli wielkość pliku = 10k, to plik
   zajmuje 3 bloki fizyczne czyli 12k). Im mniejsza wielkość bloku I/O w
   systemie plików, tym mniejsze straty pojemności dysku.
   

Katalogi


Katalogi są w systemach Unix traktowane prawie tak samo jak pliki. Jedyna,
ale bardzo ważna różnica to hierarchia jaką tworzą katalogi. Katalogi
porządkują system plików tworząc drzewo katalogów. Katalog jest kontenerem
dla plików. Katalogami są również . i .. - są to łącza do bieżącego katalogu
i jego przodka. Katalog / jest korzeniem. (wypróbuj polecenie ls -ld)

Każdy katalog jest identyfikowany za pomocą tzw. i-węzła np:

                                 /[1]
                                 ..[1]
                                 |
                                home[4]
                                /  
                                 ..[4]
                            [5]neo  cartman[6]        

Dla przykładu korzeń będzie miał i-węzeł [1], home[4] itd. Katalog .. w neo i cartman ma ten sam i-węzeł co home, podobnie .. i /.

Podobnie jak pliki również katalogi posiadają swych właścicieli jednak prawa dostępu interpretuje się w nieco inny sposób:

  • prawo odczytu oznacza możliwość wyświetlenia zawartości katalogu
  • prawo zapisu umożliwia utworzenie i usunięcie plików w katalogu (oczywiście dostęp do już istniejących plików zależy od uprawnień na poziomie pliku)
  • prawo wykonywania pozwala wejść do katalogu za pomocą polecenia cd

Do obsługi katalogów na poziomie programowania istnieje oddzielna rodzina
funkcji.

Tworzenie i usuwanie katalogów:

Funkcja mkdir i rmdir:

        #include <sys/types.h>
        #include <sys/stat.h>

        int mkdir(const char *ścieżka, mode_t mode);

np:

        int wartosc;
        wartosc = mkdir("/home/uzyt1/kat1", 0644);

        #include <unistd.h>

        int rmdir(const char *ścieżka);

Funkcja usuwania zadziała tylko wtedy, gdy katalog będzie pusty tzn. będzie
zawierał (.) i (..).




Otwieranie i zamykanie katalogów:



Funkcje opendir i closedir:

        #include <sys/types.h>
        #include <dirent.h>

        DIR *opendir(const char *nazwa_katalogu);

<dirent.h> zawiera definicję DIR (strumień katalogu). Porównaj z FILE.
W razie błędu funkcja zwraca NULL.

        #include <dirent.h>

        int closedir(DIR *wskanik_dir);

Np.


        DIR *wks_dir;

        if((wks_dir = opendir("/home/uzyt1/kat1")) == NULL)
        {
                perror("");
                exit(1);
        }
        ...
        closedir(wsk_dir);



Funkcja readdir i rewinddir:

        #include <sys/types.h>
        #include <dirent.h>

        struct dirent *readdir(DIR *wks_dir);
        void rewinddir(DIR *wks_dir);

readdir - służy do odczytania zawartości katalogu dochodząc od początku
do końca (wskaźnik NULL)
rewinddir - ustawia wskaźnik na początek katalogu

Struktura dirent znajduje się w pliku /bits/dirent.h dołączonym do dirent.h
i wygląda ona następująco:
struct dirent
  {
  1. ifndef __USE_FILE_OFFSET64
    __ino_t d_ino;
    __off_t d_off;
  1. else
    __ino64_t d_ino;
    __off64_t d_off;
  1. endif
    unsigned short int d_reclen;
    unsigned char d_type;
    char d_name[256];                /* We must not include limits.h! */
  };

Generalnie wykorzystuje się d_ino(zakres i-węzła katalogu) oraz d_name(nazwa
pliku o max. dlugości [256]).

Np.
        struct dirent *dr;
        DIR *wsk_dir;

        if((wks_dir = opendir(argv[1])) == NULL)
        {
                perror("");
                exit(1);
        }

        while(dr=readdir(wsk_dir))
        {
                if (dr->d_ino != 0)
                        printf("%sn",dr->d_name);
        }



Funkcja chdir i getcwd:

        #include <unistd.h>

        int chdir(const char *ścieżka);

sprawia, że ścieżka staje się nowym katalogiem bieżącym dla programu.

        #include <unistd.h>

        char *getcwd(char *nazwa_katalogu, ssize_t rozmiar);

funkcja wpisuje do [nazwa_katalogu] bieżący katalog roboczy o rozmiarze
[rozmiar]

Np.
                char n_kat[50];
                getcwd(n_kat, 50);
                printf("katalog biezacy to %sn",n_kat);

w razie błędu funkcja zwraca NULL.



 
Czasami istnieje potrzeba przejścia naszego drzewa katalogów poczynając od
określonego miejsca. Do tego najlepiej wykorzystać gotową funkcję ftw:




Funkcja ftw:

        #include <ftw.h>
        int ftw(const char *ścieżka, int(*funkcja) (), int głębokość);

- pierwszy parametr jest ścieżką do katalogu, od którego funkcja rozpocznie
  rekurencyjne przeszukiwanie

- drugi parametr to funkcja, która zostanie uruchomiona za każdym razem ,gdy
  zmieni się katalog przeszukiwań

- parametr trzeci to liczba otwartych katalogów (deskryptory), należy uważać
  aby nie przekroczyć ich maksymalnej liczby, którą moze obsługiwać system;
  jeżeli przeszukujemy większą liczbę katalogów to można ustawić ten parametr
  na liczbę większą niż 1, w ten sposób zwiększy się nieco szybkość
  przeszukiwania (katalogi nie będą otwierane po każdej zmianie w ścieżce
  przeszukiwań tylko od razu zostanie pobrany z pamięci kolejny deskryptor)

Funkcja wywoływana za każdym razem po zmianie ścieżki bieżącej powinna
być wywoływana w następujący sposób:

int funkcja(const char *nazwa, const struct stat *statystyka, int typ)

- nazwa to wskaźnik do nazwy bieżącego katalogu(pliku)
- statystyka to wskaźnik do struktury stat, za każdym razem dla katalogu
  lub pliku są pobierane dane i umieszczane w strukturze stat (tak jak
  po wywołaniu funkcji stat)
- typ określa z jakiego rodzaju obiektem mamy doczynienia:

        FTW_F - plik
        FTW_D - katalog
        FTW_DNR - katalog bez możliwości odczytu
        FTW_SL - łącze symboliczne
        FTW_NS - dla tego obiektu nie można wypełnić stat (nie jest łączem
                   symbolicznym)
       

Poniżej znajduje się krótki program korzystający z tej funkcji. Program
pokazuje, które pliki lub katalogi mają uprawnienia 0777.


/*program korzystajacy z ftw*/

  1. include <stdlib.h>
  2. include <sys/stat.h>
  3. include <ftw.h>

int szukaj(const char *nazwa, const struct stat *statystyka, int typ)
{
   
    if(typ == FTW_D)
        printf("%-79sr",nazwa);        
       
    if (typ == FTW_D && (statystyka->st_mode&0777) == 0777)                
        printf("r%-20st%dt%3otkatalogn",nazwa, statystyka->st_ino,
                statystyka->st_mode&0777);        
       
    if (typ == FTW_F && (statystyka->st_mode&0777) == 0777)                
        printf("r%-20st%dt%3otplikn",nazwa, statystyka->st_ino,
                statystyka->st_mode&0777);        
       
    return 0;        
}

main()
{
    printf("SCIEZKAtt   I-WEZELtPRAWAtTYPn");
    ftw("/", szukaj, 1);        //zaczynamy szukać od korzenia
    printf("n");
    exit(0);
}
--cut--





Krótko o procesach.


Każdy uruchomiony program jest w systemie Linux określany jako proces.
Każdemu procesowi zostaje przydzielona wirtualna przestrzeń adresowa.
Linux rozróżnia trzy typy procesów:
- interaktywne - uruchamiane przez powłokę i przez nią kontrolowane
- wsadowe
- rezydentne (demony)
System identyfikuje procesy przydzielając im numer(PID). Za pomocą polecenia
ps można z linii powłoki zobaczyć jakie procesy są uruchomione. Za pomocą
polecenia kill [numer] można usunąć proces. Stosuje się to głównie w stosunku
do niedziałających procesów lub do przeresetowania serwerów.

Każdy proces może utworzyć proces potomny. Funkcja jaka jest
używana w tym celu nosi nazwę fork().



Funkcja fork:

        #include <sys/types.h>
        #include <unistd.h>

        pid_t fork(void);

powoduje utworzenie przez jądro nowego procesu, który jest kopią procesu
wywołującego. Proces potomny jest wierną, ale samodzielnie działającą
kopią procesu rodzicielskiego - oznacza to, że proces potomny to coś zupełnie
różnego od tzw. wątku. Wątek posiada kopię kodu programu, ale korzysta z tych
samych zmiennych, co program rodzicielski.

pid_t to liczba identyfikująca proces potomny




Funkcja fork bardzo często jest wykorzystywana w programach sieciowych typu
serwer.

Obsługa procesów to bardzo złożony problem.
Nam wystarczy na razie posługiwanie się funkcją fork().


Programowanie sieciowe


Pisania programów działających w sieci to bardzo ważna umiejętność.
Na szczęście system Linux daje nam odpowiednie narzędzia, aby w prosty
sposób poznać tę "tajemną wiedzę".

Na początku warto się zastanowić w jaki sposób następuje poączenie między
dwoma komputerami.
Weźmy dwa hosty o numerach 204.23.104.8 i 192.168.20.2. Aby połączenie
doszło do skutku trzeba ustalić kilka podstawowych rzeczy:
protokół - określony jednoznacznie mechanizm za pomocą którego będzie
             możliwa wymiana danych (TCP, UDP lub inny )
port - numer 16-bitowy określający z jakim procesem aplikacji ma nastąpić
         połączenie (telnet - numer 23)
gniazdo - można o nim myśleć na dwa sposoby: pierwszy z punktu widzenia
            systemu to numer jednoznacznie identyfikujący proces , port, który
            jest przydzielany dynamicznie, drugi z punktu widzenia programisty
            to specjalny plik (gniazdko), z którym powiązane są odpowiednie
            struktury i który jest końcowym i początkowym elementem wirtualnego
            połączenia. Określenie plik w stosunku do gniazda oznacza tu, że
            o gniazdku można myśleć jako o pliku (deskryptor).

Port przydzielany dynamicznie        to port, który jest przypisany przez system
do programu (klienta). O ile dla sesji telnetowej mamy z góry
założony numer 23- port z którym zawsze trzeba się połączyć, aby móc
skorzystać z usługi, o tyle klient ,aby mógł być jednoznacznie identyfikowany
przez serwer i system hosta, musi mieć przydzielony port (pierwszy wolny
w systemie)

Przykład: (dla TCP)

1.HOST (192.168.20.2)                
Uruchomienie klienta        
system przydziela                
wolny numer portu
1034        

2.SERWER (204.23.104.8)
Serwer odbiera rządanie
od 192.168.20.2:1034
zapamiętuje je

3.SERWER (204.23.104.8)
Serwer wysyła sygnał gotowości
na przyjmowanie poleceń

Numery portów przydzielone statycznie do określonych usług znajdują się
w pliku /etc/services.

Aby utworzyć gniazdko sieciowe musimy wypełnić pola w odpowiednich
strukturach. Ogólna struktura adresu gniazda jest zdefiniowana
w pliku nagłówkowym <sys/socket.h>:
struct sockaddr
{        sa_family_t sa_family; /*typ*/
        char                sa_data[]; /*adres*/
};

jest to gniazdo ogólne (może również służyć do komunikacji między procesami).
Gniazdo do komunikacji sieciowej zostało zdefiniowane w <netinet/in.h>:
struct sockaddr_in{
        sa_family_t                sin_family; /*rodzaj adresu internetowego*/
        in_port_t                sin_port;        /*numer portu*/
        struct in_addr        sin_addr;        /*specjalna struktura do przechowania IP*/
        unsigned char        sin_zero[8] /*wypełnienie*/
};

Aby utworzyć gniazdo musimy określić jego rodzaj za pomocą funkcji socket.



Funkcja socket:

        #include <sys/socket.h>
        int socket(int rodzaj, int typ, int protokół);

argument 1 - rodzaj adresów:
AF_INET - adresy internetowe
AF_UNIX - do komunikacji między procesami znajdującymi się na tym
            samym komputerze

argument 2 - typ gniazda:
SOCK_STREAM - tryb połączeniowy, wykorzystuje strumień, jest to protokół
                  TCP
SOCK_DGRAM - tryb bezpołączeniowy oparty na datagramach, jest to protokół
                 UDP (jest to tryb zawodny, ale szybciej działający, dość rzadko
                        się go używa)

argument 3 - określa, który protokół powinien być używany przez gniazdo
ustawiany na zero        (domyślny protokół: dla SOCK_STREAM TCP, dla SOCK_DGRAM UDP)




Do gniazda przypisywane są dwa adresy: hosta i serwera. Adres hosta jest
dopisywany automatycznie przez system. Adres serwera trzeba podać. Można
zrobić to na dwa sposoby:

1. Jeżeli klient ma służyć do łączenia się z jednym określonym adresem
możemy ustawić to na sztywno wypełniając strukturę:

struct sockaddr_in adres; //utworzenie struktury adres typu sockaddr_in

adres.sin_addr.s_addr = inet_addr("204.23.104.8");//wypełnienie adresem

2. O wiele częściej istnieje potrzeba podania adresu (również nazwy).
Do tego służy funkcja gethostbyname.



Funkcja gethostbyname:
        #include <netdb.h>
 
        struct hostent *gethostbyname(const char *name)

szuka numeru IP dla podanej nazwy, jeżeli od razu podamy numer funkcja
wypełnia strukturę hostent (funkcja poszukuje IP w kolejności zdefiniowanej
w systemie np. plik /etc/hosts a później serwer DNS)

Struktura hostent znajduje się w netdb.h.
struct hostent {
char        *h_name;        //nazwa hosta
char **h_aliases; //lista aliasów
int        h_addrtype; //typ adresu zdefiniowany w <sys/socket.h>
int        h_length; //rozmiar adresu w bajtach (dla IP zawsze 4)
char        **h_addr_list;
#define h_addr h_addr_list[0]; //lista adresów IP
};





Kiedy mamy wszystkie potrzebne dane należy nawiązać połączenie za pomocą
funkcji connect.



Funkcja connect:
        #include <sys/types.h>
        #include <sys/socket.h>
 
        int connect(int desp, const struct sockaddr *adres, size_t add_len);

- 1 -deskryptor gniazda uzyskany po wywołaniu funkcji socket
         (deskryptor ten ma takie samo znaczenie jak w przypadku plików
          dlatego też do obsługi gniazda można użyć funkcji read i write)
- 2 - adres do struktury typu sockaddr
- 3 - wielkość struktury w bajtach




Gdy zostaje nawiązane połączenie należy napisać do gniazda coś sensownego,
wszystko zależy z jaką usługą mamy doczynienia. Dla każdej usługi istnieje
pewien z góry określony sposób komunikacji (standardy komunikacji dla usług
są podawane w dokumentach RFC (Request For Comments)).

Poniżej znajduje się program korzystający z usługi finger. Po wpisaniu
dowolnego parametru program zapisuje w pliku odpowiednie struktury
(pod warunkiem, że mamy uprawnienia root'a)
/*                
*        Nazwa programu : k_finger+ 
*         
*                Opis   : klient uslugi finger z mozliwoscia podgladu 
*                         struktur
*
*         Sposob uzycia : k_finger+ host_docelowy uzytkownik [z]
*                         z-dowolny argument (pokazuje struktury)
*                    np : k_finger+ 192.168.20.203 root        
*
*             Wymagania : UNIX/LINUX
*                dodatkowe: mozliwosc podgladu struktur tylko z poziomu
*                           root'a lub z atrybutem +s dla programu
*                
*
*/
 
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
                            //MAIN
int main(int argc, char** argv)
{
    struct hostent *host; //deklaracja struktury host typu hostent
    struct servent *serv; //deklaracja struktury serv typu servent
    struct sockaddr_in adres; //deklaracja struktury adres typu sockaddr_in
    int gnz, i;
    char buf[1024];
    FILE *file;
 
//////////////////////////////////////////////////////////////////////////////
            printf("ntt------------------------------");
            printf("ntt  k_finger+ Ver.0.00002 -2000-");
            printf("ntt------------------------------nn");
 
//////////////////////////////////////////////////////////////////////////////    
    if (argc<3)
    {
        printf("Uzycie: %s [host_docelowy] [uzytkownik] [jakis arg do pok. strukt.]nn",argv[0]);
        return 1;
    }
 
    adres.sin_family=AF_INET; //adresy internetowe
 
    host=gethostbyname(argv[1]); //poszukiwanie IP dla nazwy serwera
    if (host!=NULL) //jeżeli powiodło się to wypełnij strukturę adres
    {
        bcopy(host->h_addr,(char *)&adres.sin_addr,host->h_length);
                    /////
    /////
    }
        else
        {
            perror ("Blad: Nie moge znalezc hosta_docelowego");
            return 1;
        }
 
   adres.sin_port=htons(79); //wpisz do struktury adres numer portu
                                //funkcja htons formatuje liczbę 79 (port fingera)
                                //w odpowiednio zrozumiały dla kompa sposób        
 
    gnz=socket(AF_INET,SOCK_STREAM,0); //utworzenie gniazda TCP (mamy deskryptor)
    if (gnz==-1)
    {
        perror("Blad funkcji socket!");
        return 1;
    }        
    if (connect(gnz, (struct sockaddr *)&adres, sizeof(adres))<0) //połącz
    {
        close(gnz); //w razie błędu zamknij gniazdo
         // i pokaż rodzaj błędu
        perror("Nie moge polaczyc!");
    }
 
        //komunikacja
    write (gnz, argv[2],strlen(argv[2])); //piszemy do gniazda użytkownika
    write (gnz, "rn",2);                        // i dodajemy znak powrotu karetki i końca linii
    while ((i=read(gnz,buf,sizeof(buf)))>0) // odbieramy dane z serwera
    write (0,buf,i); //wypisanie na ekran
 
    if(!geteuid()) //jeżeli jest to proces root'a 
            {
        if(argv[3]) // i podano dodatkowy parametr
            {
            file=fopen("struct.log","w");
            fprintf(file,"ntKontrolne sprawdzanie strukturnntStruktura hostent :n 
             th_name = %sntth_aliases = %sntth_addrtype = %d
             th_length = %dntth_addr = %s nntStruktura sockaddr_in :n
             tsin_family = %dnttsin_port = %d nttsin_addr = %s
             tsin_zero = %sn",
             host->h_name,host->h_addrtype,host->h_length,adres.sin_family,
             ntohs(adres.sin_port),adres.sin_zero);
             fclose(file);
            }
 
        }
 
    close(gnz);
 
    return 0;   
 
}


Plik struct.log może wyglądać tak:

Kontrolne sprawdzanie struktur

        Struktura hostent :
 
                     h_name = 192.168.20.203
                h_addrtype = 2
                     h_length = 4
       

        Struktura sockaddr_in :

                     sin_family = 2
                sin_port = 79
                     sin_zero = //tu mogą być jakieś śmiecie


W przypadku usługi finger serwer oczekuje od klienta podania nazwy użytkownika
oraz dodania znaków rn. Tylko tak sformatowany tekst jest zrozumiały dla
serwera - w innym przypadku serwer zakończył by połączenie zwracając błąd.

Inaczej jest w usługach typu SMTP - POP3, gdzie należy przeprowadzić
dłuższą konwersację z zastosowaniem określonych reguł - poleceń.

W przypadku SMTP połączenie wygląda następująco (klient):
1. helo
2. mail from:<e-mail>
3. rcpt to:<e-mail>
4. data
5. quit

Serwer za każdym razem zwraca odpowiedni numer jako odpowiedź (np. 250
oznacza poprawną pracę serwera i przyjęcie polecenia). Dokładny opis
protokołu znajduje się w dokumencie RFC 821.

Poniżej przykład klienta pocztowego korzystającego z SMTP.

/*
*
*                Nazwa programu : smtp_klient Ver.0.1
*
*                          Opis : program do wysylania poczty
*
*                        Uzycie : smtp_klient [do_kogo] [na_jaki_host]
*
*                     Wymagania : UNIX/LINUX + Development        
*
*/
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <errno.h>
#include <string.h>
///////////
///////////
struct hostent *host;
struct sockaddr_in adres;
int gnz,x;
FILE *d;
char buf1[15];
char buf2[40];
char buf3[1024];
///////////////////////////////////MAIN/MAIN/MAIN////////////////////////////
int main(int argc, char** argv)
{
    printf("nttt------------------------n");
    printf("ttt    SMTP-klient Ver.0.1n");
    printf("ttt------------------------nn");
 
    if (argc<3)
    {
        printf("Uzycie: %s [adresat] [host_docelowy]n",argv[0]);
        printf("Uwaga: program uzywa formy [email protected]_docelowyn");
        exit(0);
    }
 
    host=gethostbyname(argv[2]);
    if (!host)
    {
        perror("gethostbyname");
        exit(1);
    }    
    bzero((char *)&adres,sizeof(adres)); //zerowanie struktury adres
    fprintf(stderr,"Tworzenie wiadomosci: nn");
    bcopy(host->h_addr,(char*)&adres.sin_addr,host->h_length);
    adres.sin_family=host->h_addrtype;
    adres.sin_port=htons(25);
///////////////////////////////////////////////////////////////////////    
    if ((gnz=socket(AF_INET,SOCK_STREAM,0))==-1) 
    {
        perror("BLAD podczas tworzenia gniazda");
        return 1;
    }
    if (connect(gnz,(struct sockaddr *)&adres,sizeof(adres))<0)
    {
        close(gnz);
        switch (errno)
        {
            default : perror("Nie moge polaczyc!");
                      return 1;
        }
    }
 
    printf("<ADRES NADAWCY>: ");
    scanf("%s",&buf1);
    printf("n<TEMAT>: ");
    scanf("%s",&buf2);
    printf("n<TEKST>: "); //w tym miejscu powinna znalezc sie obsluga
                                //wpisywania dlgiego tekstu i konczenie go .
                                // jak w programie mail
    scanf("%s",&buf3);
 
 
    if (!(d=fdopen(gnz,"w"))) { perror(""); exit(0); }
//////////////////////////////////////////////////////////////////////    
    fprintf(d,"helo HOSTn");
    fprintf(d,"mail from: <%s>n",buf1);
    fprintf(d,"rcpt to: <%s>n",argv[1]);
    fprintf(d,"datan");
    fprintf(d,"To: %sn",argv[1]);
    fprintf(d,"Subject: %sn",buf2);
    fprintf(d,"%sn",buf3);
    fprintf(d,".n");
    fprintf(d,"quitn");
    fflush(d);        //wysłanie zawartosci bufora do pliku zwiazanego ze
                        //strumieniem (u nas plik to gniazdko)
    shutdown(gnz,2); //zamknięcie połaczenia
    close(gnz); //zwolnienie deskryptora gniazda
}


W kliencie SMTP użyłem nowej funkcji fdopen. Funkcja ta jest niemal
identyczna z fopen - różnica jest taka, że zamiast scieżki do pliku
podaje się deskryptor (u nas gnz uzyskany z funkcji socket()).
Funkcja shutdown przeprowadza proces zamykania połączenia (uwaga: nie zwalnia
deskryptora), jest to jeden ze sposobów zamykania połączenia.
Deksryptor zwalniamy przez close().

Skoro umiemy wysłać pocztę to przydał by się program do jego odbierania.
O ile do wysyłania korzystamy z protokołu SMTP (numer 25), o tyle do odbierania
używamy protokołu POP3. Wynika to z tego, że SMTP zmusza komputer docelowy
do ciagłego działania - inaczej poczta nie może być dostarczona. Dlatego
stworzono POP3 (wcześniej POP2). POP3 operuje na tzw. skrzynkach. Każda
skrzynka to nic innego jak konto (użytkownik+hasło) jednak bez możliwości
logowania (shell ustawiony na false).

SMTP jest instalowany domyślnie w systemie Linux. POP3 jest dodatkową
usługą i wymaga oddzielnego uruchomienia. Trzeba w pliku /etc/inetd.conf
odznaczyć zaremowaną linijkę do pop3 (usunąć #), a następnie zainstalować
pakiet imap. Po tym wystarczy przerestartować i mamy serwer poczty(!).

Poniżej znajduje się krótki klient dla pop-3.


/* pop.c
*
*                Nazwa programu : pop
*
*                          Opis : program do odbioru poczty (pop3)
*
*                        Uzycie : pop [host]
*
*                     Wymagania : UNIX/LINUX + Development        
*
*/
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <curses.h>
 
int gnz, i, q, lp=0;
char buf[1024];
char ret[20]={"RETR "};
char usun[20]={"DELE "};
char znak, wyb;
 
void przeglad();
void usuwanie();
 
WINDOW *okno;
 
 
int main(int ilarg, char** argv)
{
 
    struct hostent *host;
    struct servent *serv;
    struct sockaddr_in adres;
    char nazwa[100];
    char haslo[100];
    char uz[20]={"USER "};
    char ha[20]={"PASS "};
 
 
    if (ilarg<2) { printf("Uzycie: %s hostn,argv[0]");return 1;}    
 
    okno=initscr();      
    scrollok(okno,1);
    wrefresh(okno);
    cbreak();
 
    bzero((char*)&adres, sizeof(adres)); //zerowanie struktury adres
    adres.sin_family=AF_INET;
 
 
    host=gethostbyname(argv[1]);
    if(host!=NULL)
    bcopy(host->h_addr, (char*)&adres.sin_addr, host->h_length);
     else
     {     
      perror("Blad: nie moge znalezc hosta");
      endwin();
      return 1;
     }
 
 
     serv=getservbyname("pop-3", "tcp"); //łaczy sie z serwerem i sprawdza
                                                        //numer portu dla pop-3
     if(serv!=NULL)
     {
      adres.sin_port=serv->s_port;
      }
     else
     {
      perror("Blad getservbyname");
      endwin();
      return 1;
     }
 
     gnz=socket(PF_INET, SOCK_STREAM,0);
     if (gnz==-1)
     {
      perror("Blad funkcji socket");
      endwin();
      return 1;
     }
 
     if (connect(gnz, (struct sockaddr*)&adres, sizeof(adres))<0)
     {
      close(gnz);
       perror("Nie moge polaczyc");
       endwin();
      } 
 
 
 
    i=read(gnz, buf, sizeof(buf));
    write(0, buf, i);    
    do
    {
      wprintw(okno, "nPodaj nazwe skrzynki pocztowej: ");
      getstr(nazwa);
 
      for (q=5; q<=4+strlen(nazwa); q++)
      {
        uz[q]=nazwa[q-5];
      }
 
 
      wprintw(okno,"Podaj haslo: ");      
 
      q=0;
      do
      {
      znak=getch();
      mvwdelch(okno,2,13);
      if (znak==10) break; else haslo[q++]=znak;
      }while(znak!=10);    
        haslo[q]=0;    
 
 
      for (q=5; q<=4+strlen(haslo); q++)
      {
      ha[q]=haslo[q-5];
      }
 
 
      write(gnz, uz, strlen(uz));
      write(gnz,"rn",2);
      i=read(gnz, buf, sizeof(buf));
      //write(0, buf, i);
 
      if (buf[0]=='+')
      {
        write(gnz, ha, strlen(ha));
        write(gnz,"rn",2);
        i=read(gnz, buf, sizeof(buf));
        //write(0, buf, i);
      }
        clear();
 
 
      if(buf[0]=='-') wprintw(okno,"Blad logowania");
      lp++;      
    }while((buf[0]=='-')&&(lp<3));
 
    if(buf[0]=='+') wprintw(okno,"nMasz %c wiadomosci nn",buf[18]);
 
 
 
 
    if((buf[18]!='0')&&(buf[0]=='+'))
     {
      do
      {  
        wprintw(okno,"          MENU n");
        wprintw(okno,"       1-przeglad n");
        wprintw(okno,"       2-usuwanie n");
        wprintw(okno,"       3-koniec n"); 
 
        wyb=getch();     
        switch(wyb)
        {
          case '1': przeglad();
                  break;
          case '2': usuwanie();
                  break;
          case '3': break;
        }       
      }while(wyb!='3');        
    }
 
    wrefresh(okno);        
 
    write(gnz, "QUIT", strlen("QUIT"));
    write(gnz,"rn",2);    
 
    wprintw(okno,"n");
    wrefresh(okno);        
 
    i=read(gnz, buf, sizeof(buf));
    write(0, buf, i);
 
    close(gnz);
    endwin(); 
    return 0;
}
 
 
void przeglad()
{
    wprintw(okno,"nPodaj numer wiadomosci do przegladniecia : n");
    znak=getch();
    ret[5]=znak;
 
    clear();        //czyszczenie okna
    wrefresh(okno);
 
    write(gnz, ret, strlen(ret));
    write(gnz,"rn",2); 
 
    i=read(gnz, buf, sizeof(buf));
    write(0, buf, i);
    move(0,25);
}
 
void usuwanie()
{
    wprintw(okno,"nPodaj numer wiadomosci do usuniecia : n");
    znak=getch();
    usun[5]=znak;
 
    write(gnz, usun, strlen(usun));
    write(gnz,"rn",2); 
 
    clear();        //czyszczenie okna
    wrefresh(okno);
 
    i=read(gnz, buf, sizeof(buf));
    write(0, buf, i);
    move(0,25); //przesuwanie do punktu 0,25
}



Sam program nie jest jakąś rewelacją. Przy kompilacji należy dodać opcję
lcurses czyli w linii wpisujemy gcc -opop pop.c -lcurses. Wynika to z tego,
że wykorzystany tu został tryb pełnoekranowy z wykorzystaniem biblioteki
<curses.h>. Na początku deklarujemy chęć użycia okna WINDOW *okno:

    okno=initscr();      //inicjujemy okno
    wrefresh(okno);        //odświeżamy okno

Odświeżanie powinno następować po każdej mozliwej zmianie w oknie.
Polecenie wprintw to to samo co printf - należy tylko podać do jakiego
okna piszemy (okien moze być kilka). W programie użyto jeszcze jednej
ważnej funkcji: mvwdelch(okno,2,13); - przydała się ona w trakcie wpisywania
hasła do skrzynki, każdy wpisany znak jest kasowany poprzez przeniesienie
kursora do punktu ekranu 2,13 - dzięki temu wpisywane hasło jest niewidoczne.

Protokół jest opisany w RFC 1725 dlatego nie będę szczegółowo go opisywał.
Wymienię tylko podstawowe polecenia:
-USER [uzytkownik]
-PASS [haslo]
-RETR [numer] - pobranie konkretnej wiadomości
-DELE [numer] - skasowanie wiadomości
-QUIT - wyjście

Serwer zwraca w przypadku powodzenie +OK lub niepowodzenia -ERR.

Polecenia podstawowe są użyte w kodzie. Każdy protokół można sprawdzić
bez odpowiedniego klienta, wystarczy do tego telnet np.
  1. telnet host 110

Poniżej znajduje się prosty, ale nieco bliższy hakerce kod. Jest to skaner
portów:

/* -*-C-*- tcpprobe.c */
/* tcpprobe - report on which tcp ports accept connections */
/* IO ERROR, [email protected], Sep 15, 1995 */
 
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <netdb.h>
#include <signal.h>
 
 
 
int main(int argc, char **argv)
{
  int probeport = 0;
  struct hostent *host;
  int err, i, net, filedes;
  struct sockaddr_in sa;
 
  if (argc != 2) 
  {
    printf("Usage: %s hostnamen", argv[0]);
    exit(1);
  }
   for (i = 1; i < 1024; i++) 
  {
    strncpy((char *)&sa, "", sizeof sa);
    sa.sin_family = AF_INET;
 
    if (isdigit(*argv[1]))
      sa.sin_addr.s_addr = inet_addr(argv[1]);
 
    else if ((host = gethostbyname(argv[1])) != 0)
      strncpy((char *)&sa.sin_addr, (char *)host->h_addr, sizeof sa.sin_addr);
 
    else 
    {
      herror(argv[1]);
      exit(2);
    }
 
    sa.sin_port = htons(i);
    net = socket(AF_INET, SOCK_STREAM, 0);
 
    if (net < 0) 
    {
      perror("nsocket");
      exit(2);
    }
 
    err = connect(net, (struct sockaddr *) &sa, sizeof sa);
 
    if (err < 0) 
    {
      printf("%s %-5d %sr", argv[1], i, strerror(errno));
      fflush(stdout);
    } 
    else 
    {
         printf("%s %-5d accepted.                               n", 
                 argv[1], i);
 
      if (shutdown(net, 2) < 0) 
      {
        perror("nshutdown");
        exit(2);
      }
    }
 
    close(net);
 
  }
 
  printf("                                                                r");
  fflush(stdout);
 
  return (0);
 
}

Działanie tego skanera jest proste - łączy się (pętla) z kolejnymi numerami
portów w odległym hoście i sprawdza czy połączenie doszło do skutku, jeśli
tak to zwraca numer portu (accepted). Jest to podstawowe narzędzie pozwalające
zorientować się które usługi serwera są na chodzie. Można nim też sprawdzać,
czy ktoś nie założył sobie na serwerze backdoora pod jakimś
dziwnym portem (należałoby zwiększyć ilość portów do przeszukiwania of cos).

Program można nieco rozbudować np. robiąc plik z nazwami usług, jeżeli
jest to usługa z numerem 7, to powinna się znaleźć w siódemj linijce
w pliku. Za pomocą funkcji lseek można by odczytać nazwę usługi dla
odpowiadającego jej numeru (zmienna i). Prostsze mogłoby być utworzenie
tablicy z tymi nazwami (znajdują się w pliku /etc/services).




Pora teraz na przyjrzenie się programowaniu serwera. Proces serwera różni
się główni od klienta tym, że musi działać, być biernym i dostępnym
w momencie próby połączenia od strony klienta. Serwery popularnych usług
działają jako tak zwane demony (inaczej programy rezydentne).
Dodatkowo w systemie Linuks usługi takie jak: inetd i network są wymagane
przez wszystkie systemy sieciowe. Demon inetd (uruchamiany przez skrypt
/etc/ini.d/inet lub inny podczas ładowania systemu) jest używany do uruchamiania
większości usług sieciowych (tzw. usług na żądanie). Inetd na żądanie
z innego hosta uruchamia odpowiednią usługę. Dzięki temu liczba
demonów może być zmiejszona do minimum.

Zamieszczony przykład będzie uruchamiany w shellu.

Podstawowym zadaniem serwera jest nasłuchiwanie żądań połączeń od klientów.
Z tego też względu serwer zachowuje się biernie. Dodatkowo serwer może
działać na dwa sposoby: tworzy kolejkę połączeń, jest wieloprocesowy.
Kolejka połączeń sprawia, że w jednym momencie może zostać obsłużony
tylko jeden klient, reszta czeka w kolejce (jej max. ilość jest z góry
ustawiana). Lepszym sposobem jest zaprogramowanie serwera wieloprocesowego.
Do tego rozwiązania przydaje się właśnie funkcja fork(). Tworzy ona
dla każdego połączenia odrębny proces.

Aby ustawić serwer w tryb nasłuchiwania trzeba ustawić gniazdko nasłuchujące.
Używamy do tego celu trzech funkcji: socket, bind, listen.




Funkcja bind:
        #include <sys/socket.h>
 
        int bind(int desp, const struct sockaddr *nazwa, int dl_nazwy);

- desp - deskryptor gniazda uzyskany z funkcji socket
- nazwa - struktura sockaddr
- dl_nazwy - rozmiar struktury nazwa.

Funkcja ta określa numer portu - wszystkie żądania do tego portu będą
kierowane do gniazda serwera.



Funkcja listen:
        #include <sys/socket.h>
 
        int listen(int desp, int kolejka);

- desp - deskryptor gniazda uzyskany z funkcji socket
- kolejka - max. liczba nie obsłużonych nadchodzących połączeń

Funkcja listen może być użyta tylko w gniazdach typu strumieniowego (TCP).
Przepełnienie kolejki powoduje zwrócenie błędu do klienta. Nawet w przypadku
wykorzystania funkcji fork() kolejka powinna mieć odpowiedni rozmiar, gdyż
serwer może nie nadążać z tworzeniem procesów potomnych.




Po ustawieniu gniazda w tryb nasłuchujący serwer powinien móc obsłużyć
pierwsze żądanie z kolejki połaczeń. Do sprawdzania i akceptacji żądania
służy funkcja accept().



Funkcja accept:
        #include <sys/socket.h>
 
        int accept(int desp, struct sockaddr *adres, int *dl_adres);

- desp - deskryptor gniazda nasłuchującego
- adres - struktura sockaddr
- dl_adres - rozmiar struktury adres

Funkcja tworzy nowe gniazdo i zwraca do niego deskryptor. Nowe gniazdo
jest wypełniane adresem hosta żądającego.

13 komentarzy

rahzel 2007-08-01 09:49

swietny art oby więcej takich rzeczowych i KONTRETKNYCH artoow dla Linuxa. wiecej wiecej wiecej

Grymek 2007-07-01 15:53

poprawilem wiele w Programowanie sieciowe bo nie widzialem naglowkow include //

ali88 2007-06-24 14:05

Poprawiłem kilka kodów (tzn, wziąłem je w bloki code) bo widzę, że nikt nie wprowadzał poprawek :(. Mam nadzieję, że ktoś (może autor? :)) poprawi resztę, żeby było czytelniejsze.

Gryzli 2006-12-13 18:06

ciekawy art, dopiero teraz go zauwazylem :]
mala uwaga: w tej chwili niektore fragmenty sa nieczytelne (jak np. nazwy bibliotek w dyrektywie include)

Waldi__17 2004-01-31 20:44

Arcik jest gitt... teraz będe wprowadzał w życie te przykłądy :]

scorpio_n 2003-12-25 17:37

Art super;)
Sam jestem uzytkownkiem Linuxa od
niedawna i przyda sie na pewno.

marioc64 2003-12-02 12:42

w artykule w czesci o asmie jest napisane tak:
"movl %eax,%esi -kopiuj wartość z rej. esi do eax "
ta instrukcja skopiuje eax do esi, bo w skladnii asmowej AT&T czyta sie odwrotnie!!!


Rudy 2003-08-21 11:56

hm.. ja bym wolał żebyś z tego naprzykład sklepał kurs albo podzielił na kilka artów :))

lofix 2003-07-10 13:08

Wszystko ladnie, tylko staraj sie pisac wlasne materialy, a nie przepisywac gotowe...

lesny 2003-05-24 12:17

No właśnie przecież C jest stworzony do środowiska UNIX (Linux)  Tak trzymać ! Oby więcej w tym temacie

SAPER 2003-05-10 17:25

Do krótkich ten artykuł chyba nie należy....

Ciekawy. Przymierzam się do Lincha może wkońcu zainstaluje sobie...