Z Pamiętniczka Młodego Hackerkachacker.pl



Drogi Pamiętniczku …



01.06.2020

addressof.c

#include < stdio.h >

int main () {

int int_var = 5;

int * int_ptr;

int_ptr = & int_var; // wstaw adres int_var do int_ptr

}

Sam program nie wyprowadza niczego, ale prawdopodobnie można się domyślić, co się stanie, nawet przed debugowaniem za pomocą GDB.

reader@hacking:~/booksrc $ gcc -g addressof.c

reader@hacking:~/booksrc $ gdb -q ./a.out

Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1".

(gdb) list

1 #include < stdio.h >

2

3 int main() {

4 int int_var = 5;

5 int *int_ptr;

6

7 int_ptr = &int_var; // Put the address of int_var into int_ptr.

8 }

(gdb) break 8

Breakpoint 1 at 0x8048361: file addressof.c, line 8.

(gdb) run

Starting program: /home/reader/booksrc/a.out

Breakpoint 1, main () at addressof.c:8

8 }

(gdb) print int_var

$1 = 5

(gdb) print &int_var

$2 = (int *) 0xbffff804

(gdb) print int_ptr

$3 = (int *) 0xbffff804

(gdb) print &int_ptr

$4 = (int **) 0xbffff800

(gdb)

Jak zwykle ustawiany jest punkt przerwania i program jest wykonywany w debugerze. W tym momencie została wykonana większość programu. Pierwsze polecenie drukowania pokazuje wartość int_var, a drugie pokazuje jego adres przy użyciu operatora adresu. Następne dwa polecenia drukowania pokazują, że int_ptr zawiera adres int_var, a także pokazuje adres int_ptr dla dobrej miary. Dodatkowy operator jednoargumentowy o nazwie operator dereferencji istnieje do użycia ze wskaźnikami. Ten operator zwróci dane znalezione w adresie wskazywanym przez wskaźnik, zamiast samego adresu. Ma on postać gwiazdki przed nazwą zmiennej, podobnie jak deklaracja wskaźnika. Ponownie operator dereferencji istnieje zarówno w GDB, jak i w C. Używany w GDB, może pobrać wartość całkowitą int_ptr punktów do.

(gdb) print * int_ptr

$5 = 5

Kilka dodatków do kodu addressof.c (pokazanych w addressof2.c) pokaże wszystkie te koncepcje. Dodane funkcje printf () używają parametrów formatu, które wyjaśnię w następnej sekcji. Na razie skup się tylko na wynikach programu.

Powrót

02.06.2020

addressof2.c

#include < stdio.h >

int main() {

int int_var = 5;

int *int_ptr;

int_ptr = &int_var; // Put the address of int_var into int_ptr.

printf("int_ptr = 0x%08x\n", int_ptr);

printf("&int_ptr = 0x%08x\n", &int_ptr);

printf("*int_ptr = 0x%08x\n\n", *int_ptr);

printf("int_var is located at 0x%08x and contains %d\n", &int_var, int_var);

printf("int_ptr is located at 0x%08x, contains 0x%08x, and points to %d\n\n",

&int_ptr, int_ptr, *int_ptr);

}

Wyniki kompilowania i wykonywania adresu 2.c są następujące.

eader@hacking:~/booksrc $ gcc addressof2.c

reader@hacking:~/booksrc $ ./a.out

int_ptr = 0xbffff834 <

&int_ptr = 0xbffff830

*int_ptr = 0x00000005

int_var is located at 0xbffff834 and contains 5

int_ptr is located at 0xbffff830, contains 0xbffff834, and points to 5

reader@hacking:~/booksrc $

Gdy operatory jednoargumentowe są używane ze wskaźnikami, operator adresu może być traktowany jako ruch do tyłu, podczas gdy operator dereferencji przesuwa się do przodu w kierunku wskazywanym przez wskaźnik

Powrót

03.06.2020

Formatowanie łańcucha znaków

Funkcja printf() może być używana do wyświetlania więcej niż tylko stałych ciągów znaków. Ta funkcja może również wykorzystywać ciągi formatów do drukowania zmiennych w wielu różnych formatach. Łańcuch formatu to tylko ciąg znaków ze specjalnymi sekwencjami ucieczki, które informują funkcję wstawiania zmiennych wydrukowanych w określonym formacie w miejsce sekwencji specjalnej. Sposób, w jaki funkcja printf () była używana w poprzednich programach, technicznym ciągiem "Hello, world! \n" jest ciąg formatujący; jest on jednak pozbawiony specjalnych sekwencji ucieczki. Te sekwencje specjalne są również nazywane parametrami formatu, a dla każdej znalezionej w ciągu formatu oczekuje się, że funkcja przyjmie dodatkowy argument. Każdy parametr formatu rozpoczyna się znakiem procenta (%) i używa jednokrotnego skrótu bardzo podobnego do znaków formatujących używanych przez polecenie badania GDB.

Parametr : Typ wyjściowy

%d : dziesiętny

%u : dziesiętny bez znaku

%x : szesnastkowy

Wszystkie poprzednie parametry formatu otrzymują dane jako wartości, a nie wskaźniki do wartości. Istnieją również niektóre parametry formatu, które oczekują wskaźników, takich jak poniższe.

Parametr : Typ wyjściowy

%s : łańcuch znaków

%n : Liczba zapisanych do tej pory bajtów

Parametr formatujący %s oczekuje podania adresu pamięci; wypisuje dane na tym adresie pamięci, aż napotkany zostanie pusty bajt. Parametr %n jest unikalny, ponieważ faktycznie zapisuje dane. Oczekuje również, że otrzyma adres pamięci i zapisze liczbę bajtów, które zostały zapisane do tej pory w tym adresie pamięci. Na razie skupiamy się tylko na parametrach formatu używanych do wyświetlania danych. Program fmt_strings.c pokazuje przykłady różnych parametrów formatu.

fmt_strings.png

W poprzednim kodzie dodatkowe argumenty zmiennych są przekazywane do każdego wywołania printf () dla każdego parametru formatu w ciągu formatującym. Ostateczne wywołanie printf () używa argumentu A, który podaje adres zmiennej A. Kompilacja i wykonanie programu są następujące.

reader@hacking:~/booksrc $ gcc -o fmt_strings fmt_strings.c

reader@hacking:~/booksrc $ ./fmt_strings

[A] Dec: -73, Hex: ffffffb7, Unsigned: 4294967223

[B] Dec: 31337, Hex: 7a69, Unsigned: 31337

[field width on B] 3: '31337', 10: ' 31337', '00031337'

[string] sample Address bffff870

variable A is at address: bffff86c

reader@hacking:~/booksrc $

Pierwsze dwa wywołania printf() demonstrują wyświetlenie zmiennych A i B, używając różnych parametrów formatu. Ponieważ w każdym wierszu znajdują się trzy parametry formatu, zmienne A i B należy podać trzy razy. Parametr formatu %d dopuszcza wartości ujemne, a %u nie, ponieważ oczekuje wartości bez znaku. Kiedy zmienna A jest wyświetlana przy użyciu parametru formatu % u, pojawia się jako bardzo wysoka wartość. Wynika to z faktu, że A jest liczbą ujemną przechowywaną w dopełnieniu dwójki, a parametr formatu próbuje wydrukować ją tak, jakby była wartością bez znaku. Od dopełnienia dwójki odwraca wszystkie bity i dodaje jeden, bardzo wysokie bity, które były zerowe są teraz jednym. Trzecia linia w przykładzie, oznaczona [szerokość pola na B], pokazuje użycie opcji szerokości pola w parametrze formatu. Jest to tylko liczba całkowita, która określa minimalną szerokość pola dla tego parametru formatu. Jednak nie jest to maksymalna szerokość pola - jeśli wartość do wyprowadzenia jest większa niż szerokość pola, s zerokość pola zostanie przekroczona. Dzieje się tak, gdy używane jest 3, ponieważ dane wyjściowe wymagają 5 bajtów. Gdy jako szerokość pola zostanie użyta wartość 10, przed danymi wyjściowymi wyprowadzane jest 5 bajtów pustej przestrzeni. Dodatkowo, jeśli wartość szerokości pola zaczyna się od 0, oznacza to, że pole powinno być wypełnione zerami. Jeśli użyto na przykład 08, wyjście to 00031337. Czwarta linia, oznaczona [string], po prostu pokazuje użycie parametru formatu% s. Pamiętaj, że zmienna string jest w rzeczywistości wskaźnikiem zawierającym adres łańcucha, który działa cudownie, ponieważ parametr formatu% s oczekuje, że jego dane zostaną przekazane przez odniesienie. Ostatnia linia pokazuje tylko adres zmiennej A, używając operatora unarnego do dereferencji zmiennej. Ta wartość jest wyświetlana jako osiem cyfr szesnastkowych, wypełnionych zerami. Jak pokazują te przykłady, należy użyć %d dla dziesiętnych, %u dla bez znaku, a %x dla wartości szesnastkowych. Minimalne szerokości pól można ustawić, umieszczając liczbę tuż za znakiem procentu, a jeśli szerokość pola zaczyna się od 0, zostanie uzupełniona zerami. Parametr %s może być użyty do drukowania łańcuchów i powinien zostać przekazany adres łańcucha. Jak na razie dobrze. Łańcuchy formatowania są używane przez całą rodzinę standardowych funkcji we / wy, w tym scanf (), która zasadniczo działa podobnie do printf(), ale jest używana do wprowadzania danych zamiast danych wyjściowych. Jedną z głównych różnic jest to, że funkcja scanf() oczekuje, że wszystkie jej argumenty będą wskaźnikami, więc argumenty muszą w rzeczywistości być zmiennymi adresami, a nie same zmienne. Można to zrobić za pomocą zmiennych wskaźnikowych lub za pomocą operatora unarnego, aby pobrać adres normalnych zmiennych. Program input.c i wykonanie powinny pomóc w wyjaśnieniu

Powrót

04.06.2020

inputc.png

W pliku input.c funkcja scanf () służy do ustawiania zmiennej count. Dane wyjściowe poniżej pokazują jego zastosowanie.

reader@hacking:~/booksrc $ gcc -o input input.c

reader@hacking:~/booksrc $ ./input

Repeat how many times? 3

0 - Hello, world!

1 - Hello, world!

2 - Hello, world!

reader@hacking:~/booksrc $ ./input

Repeat how many times? 12

0 - Hello, world!

1 - Hello, world!

2 - Hello, world!

3 - Hello, world!

4 - Hello, world!

5 - Hello, world!

6 - Hello, world!

7 - Hello, world!

8 - Hello, world!

9 - Hello, world!

10 - Hello, world!

11 - Hello, world!

reader@hacking:~/booksrc $

Formatowanie ciągów jest używane dość często, więc ich znajomość jest cenna. Ponadto możliwość wyprowadzania wartości zmiennych pozwala na debugowanie w programie, bez użycia debuggera. Posiadanie jakiejś formy natychmiastowej informacji zwrotnej jest dość istotne dla procesu uczenia się hakerów, a coś tak prostego, jak drukowanie wartości zmiennej, może pozwolić na wiele zastosowań.

Powrót

05.06.2020

Konwersja typu

Typowanie jest po prostu sposobem na tymczasową zmianę typu danych zmiennej, pomimo tego, jak została pierwotnie zdefiniowana. Kiedy zmienna jest typowana do innego typu, kompilator jest zasadniczo informowany o traktowaniu tej zmiennej tak, jakby był nowym typem danych, ale tylko dla tej operacji. Składnia do typowania jest następująca:

(typecast_data_type) zmienna

Może to być używane w przypadku zmiennych całkowitych i zmiennoprzecinkowych, jak pokazuje typecasting.c Wyniki kompilowania i wykonywania typecasting.c są następujące.

reader @ hacking: ~ / booksrc $ gcc typecasting.c

reader @ hacking: ~ / booksrc $ ./a.out

[liczby całkowite] a = 13 b = 5

[pływaki] c = 2,000000 d = 2,600000

reader@ hacking: ~ / booksrc $

Jak wspomniano wcześniej, dzielenie liczby całkowitej 13 przez 5 zostanie zaokrąglone do błędnej odpowiedzi 2, nawet jeśli ta wartość jest przechowywana w zmiennej zmiennoprzecinkowej. Jednakże, jeśli te zmienne liczb całkowitych są typograficzne w zmiennoprzecinkowe, będą traktowane jako takie. Pozwala to na prawidłowe obliczenie wartości 2,6. Ten przykład jest przykładowy, ale tam, gdzie naprawdę rzuca się w cień, jest użycie zmiennej kursora. Mimo że wskaźnik jest po prostu adresem pamięci, kompilator języka C nadal wymaga typu danych dla każdego wskaźnika. Jednym z powodów tego jest próba ograniczenia błędów programowania. Wskaźnik całkowity powinien wskazywać tylko na dane całkowite, a wskaźnik powinien wskazywać tylko dane znakowe. Innym powodem jest arytmetyka wskaźnika. Liczba całkowita to cztery bajty, podczas gdy postać zajmuje tylko jeden bajt. Program pointer_types.c pokaże i wyjaśni te pojęcia dalej. Ten kod wykorzystuje parametr formatu% p do wyprowadzania adresów pamięci. Jest to skrót oznaczający wyświetlanie wskaźników i jest zasadniczo równoważny z 0x% 08x.

Powrót

06.06.2020

pointer_types.c
#include < stdio.h >
int main() {
int i;
char char_array[5] = {'a', 'b', 'c', 'd', 'e'};
int int_array[5] = {1, 2, 3, 4, 5};
char *char_pointer;
int *int_pointer;
char_pointer = char_array;
int_pointer = int_array;
for(i=0; i < 5; i++) { // Iterate through the int array with the int_pointer.
printf("[integer pointer] points to %p, which contains the integer %d\n",
int_pointer, *int_pointer);
int_pointer = int_pointer + 1;
}
for(i=0; i < 5; i++) { // Iterate through the char array with the char_pointer.
printf("[char pointer] points to %p, which contains the char '%c'\n",
char_pointer, *char_pointer);
char_pointer = char_pointer + 1;
}
}

W tym kodzie są zdefiniowane dwie tablice w pamięci - jedna zawierająca dane całkowite, a druga zawierająca dane znakowe. Dwa wskaźniki są również zdefiniowane, jeden z typem danych całkowitych i jeden z typem danych znakowych, i są one ustawione tak, aby wskazywały na początku odpowiednich macierzy danych. Dwie oddzielne pętle iterują po tablicach za pomocą arytmetyki wskaźników, aby dostosować wskaźnik do punktu przy następnej wartości. W pętlach, kiedy wartości całkowite i znakowe są faktycznie drukowane z parametrami formatu% d i% c, zauważ, że odpowiednie argumenty printf () muszą wyłuskać zmienne wskaźnika. Odbywa się to za pomocą operatora unarnego * i zostało zaznaczone powyżej pogrubioną czcionką.

reader@hacking:~/booksrc $ gcc pointer_types.c
reader@hacking:~/booksrc $ ./a.out
[integer pointer] points to 0xbffff7f0, which contains the integer 1
[integer pointer] points to 0xbffff7f4, which contains the integer 2
[integer pointer] points to 0xbffff7f8, which contains the integer 3
[integer pointer] points to 0xbffff7fc, which contains the integer 4
[integer pointer] points to 0xbffff800, which contains the integer 5
[char pointer] points to 0xbffff810, which contains the char 'a'
[char pointer] points to 0xbffff811, which contains the char 'b'
[char pointer] points to 0xbffff812, which contains the char 'c'
[char pointer] points to 0xbffff813, which contains the char 'd'
[char pointer] points to 0xbffff814, which contains the char 'e'
reader@hacking:~/booksrc $

Mimo że ta sama wartość 1 jest dodawana do int_pointer i char_pointer w ich odpowiednich pętlach, kompilator zwiększa adresy wskaźnika o różne kwoty. Ponieważ znak jest tylko 1 bajt, wskaźnik do następnego znaku będzie naturalnie równy 1 bajtowi. Ale ponieważ liczba całkowita jest 4 bajty, wskaźnik do następnej liczby całkowitej musi być 4 bajty ponad. W pointer_types2.c wskaźniki są zestawione w taki sposób, że int_pointer wskazuje na dane znakowe i na odwrót.

Powrót

07.06.2020

pointer_types2.c

#include < stdio.h >
int main() {
int i;
char char_array[5] = {'a', 'b', 'c', 'd', 'e'};
int int_array[5] = {1, 2, 3, 4, 5};
char *char_pointer;
int *int_pointer;
char_pointer = int_array; // The char_pointer and int_pointer now
int_pointer = char_array; // point to incompatible data types.
for(i=0; i < 5; i++) { // Iterate through the int array with the int_pointer.
printf("[integer pointer] points to %p, which contains the char '%c'\n",
int_pointer, *int_pointer);
int_pointer = int_pointer + 1;
}
for(i=0; i < 5; i++) { // Iterate through the char array with the char_pointer.
printf("[char pointer] points to %p, which contains the integer %d\n",
char_pointer, *char_pointer);
char_pointer = char_pointer + 1;
}
}
Dane wyjściowe poniżej pokazują ostrzeżenia wypływające z kompilatora.
reader@hacking:~/booksrc $ gcc pointer_types2.c
pointer_types2.c: In function `main':
pointer_types2.c:12: warning: assignment from incompatible pointer type
pointer_types2.c:13: warning: assignment from incompatible pointer type
reader@hacking:~/booksrc $

Aby zapobiec błędom programowania, kompilator ostrzega o wskaźnikach wskazujących na niekompatybilne typy danych. Ale kompilator i być może programista są jedynymi, którzy dbają o typ wskaźnika. W skompilowanym kodzie wskaźnik jest niczym więcej niż adresem pamięci, więc kompilator nadal będzie kompilował kod, jeśli wskaźnik wskazuje na niekompatybilny typ danych - po prostu ostrzega programistę, aby przewidzieć nieoczekiwane wyniki.

reader @ hacking: ~ / booksrc $ ./a.out
[wskaźnik całkowity] wskazuje na 0xbffff810, który zawiera znak "a"
[wskaźnik całkowity] wskazuje na 0xbffff814, który zawiera znak "e"
[wskaźnik całkowity] wskazuje na 0xbffff818, który zawiera znak "8"
[wskaźnik całkowity] wskazuje na 0xbffff81c, który zawiera znak "
[wskaźnik całkowity] wskazuje na 0xbffff820, który zawiera znak "?"
[char point] wskazuje na 0xbffff7f0, która zawiera liczbę całkowitą 1
[char point] wskazuje na 0xbffff7f1, która zawiera liczbę całkowitą 0
[char point] wskazuje na 0xbffff7f2, która zawiera liczbę całkowitą 0
[char point] wskazuje na 0xbffff7f3, która zawiera liczbę całkowitą 0
[char point] wskazuje na 0xbffff7f4, która zawiera liczbę całkowitą 2
reader @ hacking: ~ / booksrc $

Mimo że int_pointer wskazuje na dane znakowe, które zawierają tylko 5 bajtów danych, nadal jest wpisywane jako liczba całkowita. Oznacza to, że dodanie 1 do wskaźnika zwiększy adres o 4 za każdym razem. Podobnie, adres char_pointer jest zwiększany o 1 za każdym razem, przechodząc przez 20 bajtów danych całkowitych (pięć 4-bajtowych liczb całkowitych), jeden bajt na raz. Po raz kolejny widoczna jest kolejność bajtów z małych liczb całkowitych gdy 4-bajtowa liczba całkowita jest badana jeden bajt na raz. 4-bajtowa wartość 0x00000001 jest faktycznie przechowywana w pamięci jako 0x01, 0x00, 0x00, 0x00. Będą takie sytuacje, w których używasz wskaźnika wskazującego dane z typem konfliktu. Ponieważ typ wskaźnika określa rozmiar danych, które wskazuje, ważne jest, aby typ był poprawny. Jak widać poniżej w pointer_types3.c, typowanie jest po prostu sposobem na zmianę typu zmiennej w locie

Powrót

08.06.2020

pointer_types3.c

#include < stdio.h >

int main() {

int i;

char char_array[5] = {'a', 'b', 'c', 'd', 'e'};

int int_array[5] = {1, 2, 3, 4, 5};

char *char_pointer;

int *int_pointer;

char_pointer = (char *) int_array; // Typecast into the

int_pointer = (int *) char_array; // pointer's data type.

for(i=0; i < 5; i++) { // Iterate through the int array with the int_pointer.

printf("[integer pointer] points to %p, which contains the char '%c'\n",

int_pointer, *int_pointer);

int_pointer = (int *) ((char *) int_pointer + 1);

}

for(i=0; i < 5; i++) { // Iterate through the char array with the char_pointer.

printf("[char pointer] points to %p, which contains the integer %d\n",

char_pointer, *char_pointer);

char_pointer = (char *) ((int *) char_pointer + 1);

}

}

W tym kodzie, gdy początkowo ustawione są wskaźniki, dane są typowe w typie danych wskaźnika. To będzie uniemożliwić kompilatorowi C narzekanie na sprzeczne typy danych; jednak dowolna arytmetyczna wskazówka będzie nadal jest niepoprawny. Aby to naprawić, gdy 1 jest dodawany do wskaźników, muszą najpierw być typecast we właściwych danych wpisz, aby adres został zwiększony o prawidłową kwotę. Następnie ten wskaźnik musi być typecast z powrotem do typ danych wskaźnika po raz kolejny. Nie wygląda zbyt ładnie, ale działa.

reader@hacking:~/booksrc $ gcc pointer_types3.c

reader@hacking:~/booksrc $ ./a.out

[integer pointer] points to 0xbffff810, which contains the char 'a'

[integer pointer] points to 0xbffff811, which contains the char 'b'

[integer pointer] points to 0xbffff812, which contains the char 'c'

[integer pointer] points to 0xbffff813, which contains the char 'd'

[integer pointer] points to 0xbffff814, which contains the char 'e'

[char pointer] points to 0xbffff7f0, which contains the integer 1

[char pointer] points to 0xbffff7f4, which contains the integer 2

[char pointer] points to 0xbffff7f8, which contains the integer 3

[char pointer] points to 0xbffff7fc, which contains the integer 4

[char pointer] points to 0xbffff800, which contains the integer 5

reader@hacking:~/booksrc $

Oczywiście znacznie łatwiej jest po prostu użyć poprawnego typu danych dla wskaźników; jednak czasami pożądany jest ogólny wskaźnik bez typów. W języku C wskaźnik pustego jest wskaźnikiem bez typu, zdefiniowanym przez puste słowo kluczowe. Eksperymentowanie z wskaźnikami pustymi szybko ujawnia kilka rzeczy na temat wskaźników bez formy. Po pierwsze, wskaźniki nie mogą być odsyłane, chyba że mają typ. Aby pobrać wartość przechowywaną w adresie pamięci wskaźnika, kompilator musi najpierw wiedzieć, jaki to jest typ danych. Po drugie, puste wskaźniki muszą również być typograficzne przed wykonaniem wskaźnik arytmetyczny. Są to dość intuicyjne ograniczenia, co oznacza, że głównym celem pustego wskaźnika jest po prostu przytrzymanie adresu pamięci. Program pointer_types3.c można zmodyfikować tak, aby używał pojedynczego pustego wskaźnika przez typowanie go do właściwego typu za każdym razem, gdy jest używany. Kompilator wie, że pusty wskaźnik jest bez nazwy, więc każdy typ wskaźnika może być przechowywany w pliku void wskaźnik bez typowania. Oznacza to również, że pusta wskazówka musi zawsze być typecast podczas deereferencji, jednak. Różnice te można zaobserwować w pointer_types4.c, który używa pustego wskaźnika.

Powrót

09.06.2020

pointer_types4.c

#include < stdio.h >

int main() {

int i;

char char_array[5] = {'a', 'b', 'c', 'd', 'e'};

int int_array[5] = {1, 2, 3, 4, 5};

void *void_pointer;

void_pointer = (void *) char_array;

for(i=0; i < 5; i++) { // Iterate through the int array with the int_pointer.

printf("[char pointer] points to %p, which contains the char '%c'\n",

void_pointer, *((char *) void_pointer));

void_pointer = (void *) ((char *) void_pointer + 1);

}

void_pointer = (void *) int_array;

for(i=0; i < 5; i++) { // Iterate through the int array with the int_pointer.

printf("[integer pointer] points to %p, which contains the integer %d\n",

void_pointer, *((int *) void_pointer));

void_pointer = (void *) ((int *) void_pointer + 1);

}

}

Wyniki kompilowania i wykonywania pointer_types4.c są następujące.

reader@hacking:~/booksrc $ gcc pointer_types4.c

reader@hacking:~/booksrc $ ./a.out

[char pointer] points to 0xbffff810, which contains the char 'a'

[char pointer] points to 0xbffff811, which contains the char 'b'

[char pointer] points to 0xbffff812, which contains the char 'c'

[char pointer] points to 0xbffff813, which contains the char 'd'

[char pointer] points to 0xbffff814, which contains the char 'e'

[integer pointer] points to 0xbffff7f0, which contains the integer 1

[integer pointer] points to 0xbffff7f4, which contains the integer 2

[integer pointer] points to 0xbffff7f8, which contains the integer 3

[integer pointer] points to 0xbffff7fc, which contains the integer 4

[integer pointer] points to 0xbffff800, which contains the integer 5

reader@hacking:~/booksrc $

Kompilacja i wyjście tego wskaźnika_types4.c jest w zasadzie takie samo jak w przypadku pointer_types3.c. Wskaźnik pustych jest po prostu trzymaniem adresów pamięci, podczas gdy zakodowanie typu hard-coding mówi kompilatorowi, aby używał właściwych typów, gdy używany jest wskaźnik. Ponieważ typ jest traktowany przez typecasts, pusta wskazówka jest niczym więcej niż adresem pamięci. Przy typach danych definiowanych przez typowanie, wszystko, co jest wystarczająco duże, aby pomieścić czterobajtową wartość, może działać tak samo, jak puste pole. W pointer_types5.c do zapisania tego adresu używana jest liczba całkowita bez znaku

Powrót

10.06.2020

pointer_types5.c

#include < stdio.h >

int main() {

int i;

char char_array[5] = {'a', 'b', 'c', 'd', 'e'};

int int_array[5] = {1, 2, 3, 4, 5};

unsigned int hacky_nonpointer;

hacky_nonpointer = (unsigned int) char_array;

for(i=0; i < 5; i++) { // Iterate through the int array with the int_pointer.

printf("[hacky_nonpointer] points to %p, which contains the char '%c'\n",

hacky_nonpointer, *((char *) hacky_nonpointer));

hacky_nonpointer = hacky_nonpointer + sizeof(char);

}

hacky_nonpointer = (unsigned int) int_array;

for(i=0; i < 5; i++) { // Iterate through the int array with the int_pointer.

printf("[hacky_nonpointer] points to %p, which contains the integer %d\n",

hacky_nonpointer, *((int *) hacky_nonpointer));

hacky_nonpointer = hacky_nonpointer + sizeof(int);

}

}

Jest to raczej oklepane, ale ponieważ ta wartość całkowita jest typecast do odpowiednich typów wskaźników, gdy jest przypisany i odroczenie, wynik końcowy jest taki sam. Zauważ, że zamiast kilkukrotnego typowania tekstu do arytmetyki wskaźnika na niepodpisanej liczbie całkowitej (która nie jest nawet wskaźnikiem), funkcja sizeof () jest używana do osiągnięcia tego samego wyniku przy użyciu zwykłej arytmetyki.

reader@hacking:~/booksrc $ gcc pointer_types5.c

reader@hacking:~/booksrc $ ./a.out

[hacky_nonpointer] points to 0xbffff810, which contains the char 'a'

[hacky_nonpointer] points to 0xbffff811, which contains the char 'b'

[hacky_nonpointer] points to 0xbffff812, which contains the char 'c'

[hacky_nonpointer] points to 0xbffff813, which contains the char 'd'

[hacky_nonpointer] points to 0xbffff814, which contains the char 'e'

[hacky_nonpointer] points to 0xbffff7f0, which contains the integer 1

[hacky_nonpointer] points to 0xbffff7f4, which contains the integer 2

[hacky_nonpointer] points to 0xbffff7f8, which contains the integer 3

[hacky_nonpointer] points to 0xbffff7fc, which contains the integer 4

[hacky_nonpointer] points to 0xbffff800, which contains the integer 5

reader@hacking:~/booksrc $

Ważną rzeczą do zapamiętania na temat zmiennych w C jest to, że kompilator jest jedyną rzeczą, która dba o typ zmiennej. W końcu, po skompilowaniu programu, zmienne są niczym więcej niż adresami pamięci. Oznacza to, że zmienne jednego typu można łatwo zmusić do zachowywania się jak innego typu, mówiąc kompilatorowi, aby typował je w pożądanym typie.

Powrót

11.06.2020

Argumenty linii poleceń

Wiele niegraficznych programów otrzymuje dane wejściowe w postaci argumentów linii poleceń. W przeciwieństwie do wprowadzania za pomocą scanf (), argumenty wiersza poleceń nie wymagają interakcji użytkownika po rozpoczęciu wykonywania programu. To wydaje się być bardziej wydajne i jest przydatną metodą wprowadzania danych. W języku C można uzyskać dostęp do argumentów wiersza polecenia w funkcji main (), włączając dwa dodatkowe argumenty do funkcji: liczbę całkowitą i wskaźnik do tablicy łańcuchów. Liczba całkowita będzie zawierała liczbę argumentów, a tablica łańcuchów będzie zawierać każdy z tych argumentów. Program commandline.c i jego wykonanie powinny wyjaśniać rzeczy.

commandline.c

#include < stdio.h >

int main(int arg_count, char *arg_list[]) {

int i;

printf("There were %d arguments provided:\n", arg_count);

for(i=0; i < arg_count; i++)

printf("argument #%d\t-\t%s\n", i, arg_list[i]);

}

reader@hacking:~/booksrc $ gcc -o commandline commandline.c

reader@hacking:~/booksrc $ ./commandline

There were 1 arguments provided:

argument #0 - ./commandline

reader@hacking:~/booksrc $ ./commandline this is a test

There were 5 arguments provided:

argument #0 - ./commandline

argument #1 - this

argument #2 - is

argument #3 - a

argument #4 - test

reader@hacking:~/booksrc $

Argumentem zerowym jest zawsze nazwa wykonywanego pliku binarnego, a reszta tablicy argumentów (często nazywana wektorem argumentu) zawiera pozostałe argumenty jako ciągi. Czasami program będzie chciał użyć argumentu wiersza poleceń jako liczby całkowitej, w przeciwieństwie do łańcucha. Niezależnie od tego argument jest przekazywany jako ciąg; jednak istnieją standardowe funkcje konwersji. W przeciwieństwie do zwykłego typowania, funkcje te mogą w rzeczywistości przekształcić tablice znaków zawierające liczby w rzeczywiste liczby całkowite. Najczęstszą z tych funkcji jest atoi (), która jest skrótem od ASCII do liczby całkowitej. Ta funkcja przyjmuje jako argument wskaźnik do łańcucha i zwraca wartość całkowitą, którą reprezentuje. Obserwuj jego użycie w convert.c.

Powrót

12.06.2020

convert.c

#include < stdio.h >

void usage(char *program_name) {

printf("Usage: %s < message > <# of times to repeat>\n", program_name);

exit(1);

}

int main(int argc, char *argv[]) {

int i, count;

if(argc < 3) // If fewer than 3 arguments are used,

usage(argv[0]); // display usage message and exit.

count = atoi(argv[2]); // Convert the 2nd arg into an integer.

printf("Repeating %d times..\n", count);

for(i=0; i < count; i++)

printf("%3d - %s\n", i, argv[1]); // Print the 1st arg.

}

Wyniki kompilacji i wykonywania convert.c są następujące

reader@hacking:~/booksrc $ gcc convert.c

reader@hacking:~/booksrc $ ./a.out

Usage: ./a.out <# of times to repeat>

reader@hacking:~/booksrc $ ./a.out 'Hello, world!' 3

Repeating 3 times..

0 - Hello, world!

1 - Hello, world!

2 - Hello, world!

reader@hacking:~/booksrc $

W poprzednim kodzie instrukcja if zapewnia, że trzy argumenty zostaną użyte przed uzyskaniem dostępu do tych łańcuchów. Jeśli program próbuje uzyskać dostęp do pamięci, która nie istnieje lub program nie ma uprawnień do odczytu, program ulegnie awarii. W języku C ważne jest, aby sprawdzić te warunki i obsłużyć je w logice programu. Jeśli sprawdzanie błędów, jeśli instrukcja zostanie skomentowana, można zbadać to naruszenie pamięci. Program convert2.c powinien to wyjaśnić.

Powrót

13.06.2020

convert2.c

#include < stdio.h >

void usage(char *program_name) {

printf("Usage: %s < message > <# of times to repeat>\n", program_name);

exit(1);

}

int main(int argc, char *argv[]) {

int i, count;

// if(argc < 3) // If fewer than 3 arguments are used,

// usage(argv[0]); // display usage message and exit.

count = atoi(argv[2]); // Convert the 2nd arg into an integer.

printf("Repeating %d times..\n", count);

for(i=0; i < count; i++)

printf("%3d - %s\n", i, argv[1]); // Print the 1st arg.

}

Wyniki kompilacji i wykonywania convert2.c są następujące.

reader@hacking:~/booksrc $ gcc convert2.c

reader@hacking:~/booksrc $ ./a.out test

Segmentation fault (core dumped)

reader@hacking:~/booksrc $

Gdy program nie otrzymuje wystarczającej liczby argumentów wiersza poleceń, nadal próbuje uzyskać dostęp do elementów tablicy argumentów, nawet jeśli nie istnieją. Powoduje to awarię programu z powodu błędu segmentacji. Pamięć jest dzielona na segmenty (które zostaną omówione później), a niektóre adresy pamięci nie mieszczą się w granicach segmentów pamięci, do których program ma dostęp. Gdy program próbuje uzyskać dostęp do adresu, który jest poza granicami, nastąpi awaria i zginie w tak zwanym uszkodzeniu segmentacji. Ten efekt można dalej zbadać za pomocą GDB.

reader@hacking:~/booksrc $ gcc -g convert2.c

reader@hacking:~/booksrc $ gdb -q ./a.out

Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1".

(gdb) run test

Starting program: /home/reader/booksrc/a.out test

Program received signal SIGSEGV, Segmentation fault.

0xb7ec819b in ?? () from /lib/tls/i686/cmov/libc.so.6

(gdb) where

#0 0xb7ec819b in ?? () from /lib/tls/i686/cmov/libc.so.6

#1 0xb800183c in ?? ()

#2 0x00000000 in ?? ()

(gdb) break main

Breakpoint 1 at 0x8048419: file convert2.c, line 14.

(gdb) run test

The program being debugged has been started already.

Start it from the beginning? (y or n) y

Starting program: /home/reader/booksrc/a.out test

Breakpoint 1, main (argc=2, argv=0xbffff894) at convert2.c:14

14 count = atoi(argv[2]); // convert the 2nd arg into an integer

(gdb) cont

Continuing.

Program received signal SIGSEGV, Segmentation fault.

0xb7ec819b in ?? () from /lib/tls/i686/cmov/libc.so.6

(gdb) x/3xw 0xbffff894

0xbffff894: 0xbffff9b3 0xbffff9ce 0x00000000

(gdb) x/s 0xbffff9b3

0xbffff9b3: "/home/reader/booksrc/a.out"

(gdb) x/s 0xbffff9ce

0xbffff9ce: "test"

(gdb) x/s 0x00000000

0x0: < Address 0x0 out of bounds >

(gdb) quit

The program is running. Exit anyway? (y or n) y

reader@hacking:~/booksrc $

Program jest wykonywany z jednym argumentem testowym wiersza poleceń w GDB, co powoduje awarię programu. Polecenie where może czasem pokazywać użyteczne śledzenie stosu; jednak w tym przypadku stos był zbyt mocno zawalił się w wypadku. Punkt przerwania jest ustawiony na main, a program jest ponownie wykonywany, aby uzyskać wartość wektora argumentu (pogrubioną czcionką). Ponieważ wektor argumentu jest wskaźnikiem do listy łańcuchów, jest to właściwie wskaźnik do listy wskaźników. Za pomocą polecenia x / 3xw zbadaj pierwsze trzy adresy pamięci przechowywane w adresie argumentu pokazuje, że same są wskaźnikami do łańcuchów. Pierwszy to argument zerowy, drugi to argument testowy, a trzeci to zero, które jest poza granicami. Gdy program próbuje uzyskać dostęp do tego adresu pamięci, ulega awarii z błędem segmentacji.

Powrót

14.06.2020

. Zmienne zakresy

Inną ciekawą koncepcją dotyczącą pamięci w C jest zmienne określanie zakresu lub kontekst - w szczególności konteksty zmiennych w funkcjach. Każda funkcja ma własny zestaw zmiennych lokalnych, które są niezależne od wszystkiego. W rzeczywistości wiele połączeń do tej samej funkcji ma swoje własne konteksty. Możesz użyć funkcji printf () z ciągami formatów, aby szybko to zbadać, sprawdź to w scope.c.

scope.c #include < stdio.h >

void func3() {

int i = 11;

printf("\t\t\t[in func3] i = %d\n", i);

}

void func2() {

int i = 7;

printf("\t\t[in func2] i = %d\n", i);

func3();

printf("\t\t[back in func2] i = %d\n", i);

}

void func1() {

int i = 5;

printf("\t[in func1] i = %d\n", i);

func2();

printf("\t[back in func1] i = %d\n", i)

; }

int main() {

int i = 3;

printf("[in main] i = %d\n", i);

func1();

printf("[back in main] i = %d\n", i);

}

Wyjście tego prostego programu demonstruje zagnieżdżone wywołania funkcji.

reader@hacking:~/booksrc $ gcc scope.c

reader@hacking:~/booksrc $ ./a.out

[in main] i = 3

[in func1] i = 5

[in func2] i = 7

[in func3] i = 11

[back in func2] i = 7

[back in func1] i = 5

[back in main] i = 3

reader@hacking:~/booksrc $

W każdej funkcji zmienna i jest ustawiana na inną wartość i drukowana. Zauważ, że w funkcji main () zmienną i jest 3, nawet po wywołaniu func1 (), gdzie zmienną i jest 5. Podobnie, w func1 () zmienna i pozostaje 5, nawet po wywołaniu func2 (), gdzie i jest 7 , i tak dalej. Najlepszym sposobem na myślenie o tym jest to, że każde wywołanie funkcji ma swoją własną wersję zmiennej i. Zmienne mogą mieć także zasięg globalny, co oznacza, że będą zachowane we wszystkich funkcjach. Zmienne są globalne, jeśli są zdefiniowane na początku kodu, poza wszelkimi funkcjami. W poniższym kodzie przykładowym z zakresu 2.c zmienna j jest deklarowana globalnie i ustawiona na 42. Zmienna może być odczytywana i zapisywana przez dowolną funkcję, a zmiany w niej będą zachowane między funkcjami.

Powrót

15.06.2020

scope2.c

#include < stdio.h >

int j = 42; // j is a global variable.

void func3() {

int i = 11, j = 999; // Here, j is a local variable of func3().

printf("\t\t\t[in func3] i = %d, j = %d\n", i, j);

}

void func2() {

int i = 7;

printf("\t\t[in func2] i = %d, j = %d\n", i, j);

printf("\t\t[in func2] setting j = 1337\n");

j = 1337; // Writing to j

func3();

printf("\t\t[back in func2] i = %d, j = %d\n", i, j);

}

void func1() {

int i = 5;

printf("\t[in func1] i = %d, j = %d\n", i, j);

func2();

printf("\t[back in func1] i = %d, j = %d\n", i, j);

}

int main() {

int i = 3;

printf("[in main] i = %d, j = %d\n", i, j);

func1();

printf("[back in main] i = %d, j = %d\n", i, j);

}

Wyniki kompilowania i wykonywania scope2.c są następujące.

reader@hacking:~/booksrc $ gcc scope2.c

reader@hacking:~/booksrc $ ./a.out

[in main] i = 3, j = 42

[in func1] i = 5, j = 42

[in func2] i = 7, j = 42

[in func2] setting j = 1337

[in func3] i = 11, j = 999

[back in func2] i = 7, j = 1337

[back in func1] i = 5, j = 1337

[back in main] i = 3, j = 1337

reader@hacking:~/booksrc $

Na wyjściu zmienna globalna j jest zapisywana w funkcji func2 (), a zmiana zachowuje się we wszystkich funkcjach oprócz funkcji func3 (), która ma własną zmienną lokalną o nazwie j. W takim przypadku kompilator preferuje użycie zmiennej lokalnej. Ze wszystkimi tymi zmiennymi używającymi tych samych nazw, może to być trochę mylące, ale pamiętaj, że w końcu to tylko pamięć. Zmienna globalna j jest właśnie zapisywana w pamięci i każda funkcja ma dostęp do tej pamięci. Zmienne lokalne dla każdej funkcji są przechowywane we własnych miejscach w pamięci, niezależnie od identycznych nazw. Drukowanie adresów pamięci tych zmiennych da wyraźniejszy obraz tego, co się dzieje. W poniższym kodzie przykładowym na przykładzie 3.c adresy zmiennych są drukowane przy użyciu operatora jednoargumentowego adresu

Powrót

16.06.2020

scope3.c

#include < stdio.h >

int j = 42; // j is a global variable.

void func3() {

int i = 11, j = 999; // Here, j is a local variable of func3().

printf("\t\t\t[in func3] i @ 0x%08x = %d\n", &i, i);

printf("\t\t\t[in func3] j @ 0x%08x = %d\n", &j, j);

}

void func2() {

int i = 7;

printf("\t\t[in func2] i @ 0x%08x = %d\n", &i, i);

printf("\t\t[in func2] j @ 0x%08x = %d\n", &j, j);

printf("\t\t[in func2] setting j = 1337\n");

j = 1337; // Writing to j

func3();

printf("\t\t[back in func2] i @ 0x%08x = %d\n", &i, i);

printf("\t\t[back in func2] j @ 0x%08x = %d\n", &j, j);

}

void func1() {

int i = 5;

printf("\t[in func1] i @ 0x%08x = %d\n", &i, i);

printf("\t[in func1] j @ 0x%08x = %d\n", &j, j);

func2();

printf("\t[back in func1] i @ 0x%08x = %d\n", &i, i);

printf("\t[back in func1] j @ 0x%08x = %d\n", &j, j);

}

int main() {

int i = 3;

printf("[in main] i @ 0x%08x = %d\n", &i, i);

printf("[in main] j @ 0x%08x = %d\n", &j, j);

func1();

printf("[back in main] i @ 0x%08x = %d\n", &i, i);

printf("[back in main] j @ 0x%08x = %d\n", &j, j);

}

Wyniki kompilowania i wykonywania scope3.c są następujące.

reader@hacking:~/booksrc $ gcc scope3.c

reader@hacking:~/booksrc $ ./a.out

[in main] i @ 0xbffff834 = 3

[in main] j @ 0x08049988 = 42

[in func1] i @ 0xbffff814 = 5

[in func1] j @ 0x08049988 = 42

[in func2] i @ 0xbffff7f4 = 7

[in func2] j @ 0x08049988 = 42

[in func2] setting j = 1337

[in func3] i @ 0xbffff7d4 = 11

[in func3] j @ 0xbffff7d0 = 999

[back in func2] i @ 0xbffff7f4 = 7

[back in func2] j @ 0x08049988 = 1337

[back in func1] i @ 0xbffff814 = 5

[back in func1] j @ 0x08049988 = 1337

[back in main] i @ 0xbffff834 = 3

[back in main] j @ 0x08049988 = 1337

reader@hacking:~/booksrc $

W tym wyjściu jest oczywiste, że zmienna j używana przez func3 () jest inna niż j używana przez inne funkcje. J używane przez func3 () znajduje się w 0xbffff7d0, podczas gdy j używane przez inne funkcje znajduje się w 0x08049988. Zwróć też uwagę, że zmienna i jest w rzeczywistości innym adresem pamięci dla każdej funkcji. W poniższym wyniku GDB jest używany do zatrzymania wykonywania w punkcie przerwania w func3 (). Następnie polecenie backtrace pokazuje zapis każdego wywołania funkcji na stosie

reader@hacking:~/booksrc $ gcc -g scope3.c

reader@hacking:~/booksrc $ gdb -q ./a.out

Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1".

(gdb) list 1

1 #include

2

3 int j = 42; // j is a global variable.

4

5 void func3() {

6 int i = 11, j = 999; // Here, j is a local variable of func3().

7 printf("\t\t\t[in func3] i @ 0x%08x = %d\n", &i, i);

8 printf("\t\t\t[in func3] j @ 0x%08x = %d\n", &j, j);

9 }

10

(gdb) break 7

Breakpoint 1 at 0x8048388: file scope3.c, line 7.

(gdb) run

Starting program: /home/reader/booksrc/a.out

[in main] i @ 0xbffff804 = 3

[in main] j @ 0x08049988 = 42

[in func1] i @ 0xbffff7e4 = 5

[in func1] j @ 0x08049988 = 42

[in func2] i @ 0xbffff7c4 = 7

[in func2] j @ 0x08049988 = 42

[in func2] setting j = 1337

Breakpoint 1, func3 () at scope3.c:7

7 printf("\t\t\t[in func3] i @ 0x%08x = %d\n", &i, i);

(gdb) bt

#0 func3 () at scope3.c:7

#1 0x0804841d in func2 () at scope3.c:17

#2 0x0804849f in func1 () at scope3.c:26

#3 0x0804852b in main () at scope3.c:35

(gdb)

Śledzenie również pokazuje zagnieżdżone wywołania funkcji, patrząc na zapisy przechowywane na stosie. Za każdym razem, gdy wywoływana jest funkcja, na stosie umieszczany jest rekord zwany ramką stosu. Każda linia w ścieżce powrotu odpowiada ramie stosu. Każda ramka stosu zawiera również zmienne lokalne dla tego kontekstu. Zmienne lokalne zawarte w każdej ramce stosu można wyświetlić w GDB, dodając słowo pełne do komendy backtrace.

(gdb) bt full

#0 func3 () at scope3.c:7

i = 11

j = 999

#1 0x0804841d in func2 () at scope3.c:17

i = 7

#2 0x0804849f in func1 () at scope3.c:26

i = 5

#3 0x0804852b in main () at scope3.c:35

i = 3

(gdb)

Pełny backtrace wyraźnie pokazuje, że lokalna zmienna j istnieje tylko w kontekście func3 (). Globalna wersja zmiennej j jest używana w kontekście innych funkcji. Oprócz globali zmienne można również zdefiniować jako zmienne statyczne, poprzedzając słowo kluczowe static do definicji zmiennej. Podobnie jak w przypadku zmiennych globalnych, zmienna statyczna pozostaje nienaruszona między wywołaniami funkcji; zmienne statyczne są jednak zbliżone do zmiennych lokalnych, ponieważ pozostają lokalne w kontekście konkretnej funkcji. Jedną inną i unikalną cechą zmiennych statycznych jest to, że są one inicjalizowane tylko jeden raz. Kod w static.c pomoże wyjaśnić te pojęcia.

Powrót

17.06.2020

static.c

#include < stdio.h >

void function() { // An example function, with its own context

int var = 5;

static int static_var = 5; // Static variable initialization

printf("\t[in function] var = %d\n", var);

printf("\t[in function] static_var = %d\n", static_var);

var++; // Add one to var.

static_var++; // Add one to static_var.

}

int main() { // The main function, with its own context

int i;

static int static_var = 1337; // Another static, in a different context

for(i=0; i < 5; i++) { // Loop 5 times.

printf("[in main] static_var = %d\n", static_var);

function(); // Call the function.

}

}

Potocznie nazwany static_var definiuje się jako zmienną statyczną w dwóch miejscach: w kontekście main () oraz w kontekście funkcji (). Ponieważ zmienne statyczne są lokalne w określonym kontekście funkcjonalnym, zmienne te mogą mieć tę samą nazwę, ale w rzeczywistości reprezentują dwie różne lokalizacje w pamięci. Funkcja po prostu wypisuje wartości dwóch zmiennych w swoim kontekście, a następnie dodaje 1 do obu z nich. Kompilacja i wykonanie tego kodu pokaże różnicę między zmiennymi statycznymi i niestatycznymi

reader@hacking:~/booksrc $ gcc static.c

reader@hacking:~/booksrc $ ./a.out

[in main] static_var = 1337

[in function] var = 5

[in function] static_var = 5

[in main] static_var = 1337

[in function] var = 5

[in function] static_var = 6

[in main] static_var = 1337

[in function] var = 5

[in function] static_var = 7

[in main] static_var = 1337

[in function] var = 5

[in function] static_var = 8

[in main] static_var = 1337

[in function] var = 5

[in function] static_var = 9

reader@hacking:~/booksrc $

Zauważ, że static_var zachowuje swoją wartość między kolejnymi wywołaniami funkcji (). Wynika to z faktu, że zmienne statyczne zachowują swoje wartości, ale także dlatego, że są inicjowane tylko raz. Ponadto, ponieważ zmienne statyczne są lokalne dla konkretnego kontekstu funkcjonalnego, static_var w kontekście main () zachowuje wartość 1337 przez cały czas. Ponownie, drukowanie adresów tych zmiennych przez odwołanie ich do operatora jednoargumentowego adresu zapewni większą opłacalność tego, co się naprawdę dzieje. Spójrz na przykład na static2.c.

Powrót

18.06.2020

static2.c

#include < stdio.h >

void function() { // An example function, with its own context

int var = 5;

static int static_var = 5; // Static variable initialization

printf("\t[in function] var @ %p = %d\n", &var, var);

printf("\t[in function] static_var @ %p = %d\n", &static_var, static_var);

var++; // Add 1 to var.

static_var++; // Add 1 to static_var.

}

int main() { // The main function, with its own context

int i;

static int static_var = 1337; // Another static, in a different context

for(i=0; i < 5; i++) { // loop 5 times

printf("[in main] static_var @ %p = %d\n", &static_var, static_var);

function(); // Call the function.

}

}

Wyniki kompilowania i wykonywania static2.c są następujące.

reader@hacking:~/booksrc $ gcc static2.c

reader@hacking:~/booksrc $ ./a.out

[in main] static_var @ 0x804968c = 1337

[in function] var @ 0xbffff814 = 5

[in function] static_var @ 0x8049688 = 5

[in main] static_var @ 0x804968c = 1337

[in function] var @ 0xbffff814 = 5

[in function] static_var @ 0x8049688 = 6

[in main] static_var @ 0x804968c = 1337

[in function] var @ 0xbffff814 = 5

[in function] static_var @ 0x8049688 = 7

[in main] static_var @ 0x804968c = 1337

[in function] var @ 0xbffff814 = 5

[in function] static_var @ 0x8049688 = 8

[in main] static_var @ 0x804968c = 1337

[in function] var @ 0xbffff814 = 5

[in function] static_var @ 0x8049688 = 9

reader@hacking:~/booksrc $

Przy adresach wyświetlanych zmiennych widać, że static_var w main () jest inny niż ten znaleziony w funkcji (), ponieważ znajdują się one w różnych adresach pamięci (odpowiednio 0x804968c i 0x8049688). Być może zauważyłeś, że adresy zmiennych lokalnych mają bardzo wysokie adresy, takie jak 0xbffff814, podczas gdy zmienne globalne i statyczne mają bardzo niskie adresy pamięci, takie jak 0x0804968c i 0x8049688. To bardzo bystre z twojej strony - zauważając takie szczegóły i pytając, dlaczego jest jednym z kamieni węgielnych hakowania. Czytaj dalej, aby uzyskać odpowiedzi

Powrót

19.06.2020

Segmentacja pamięci

Skompilowana pamięć programu jest podzielona na pięć segmentów: tekst, dane, bss, stertę i stos. Każdy segment reprezentuje specjalną część pamięci, która jest zarezerwowana dla określonego celu. Segment tekstowy jest czasami nazywany segmentem kodu. Tutaj znajdują się złożone instrukcje dotyczące języka maszynowego programu. Wykonywanie instrukcji w tym segmencie jest nieliniowe dzięki wyżej wymienionym strukturom kontrolnym wysokiego poziomu i funkcjom, które kompilują się w instrukcje rozgałęzień, skoków i wywołań w języku asemblerowym. Gdy program jest wykonywany, EIP jest ustawiony na pierwszą instrukcję w segmencie tekstowym. Procesor wykonuje następnie pętlę wykonawczą, która wykonuje następujące czynności:

1. Czyta instrukcję, na którą wskazuje EIP

2. Dodaje długość bajtów instrukcji do EIP

3. Wykonuje instrukcję odczytaną w kroku 1

4. Powraca do kroku 1

Czasami instrukcją będzie skok lub instrukcja wywołania, która zmienia EIP na inny adres pamięci. Procesor nie dba o zmianę, ponieważ spodziewa się, że wykonanie będzie nieliniowe. Jeśli EIP zostanie zmieniony w kroku 3, procesor powróci do kroku 1 i przeczyta instrukcję znalezioną pod adresem, na którym zmienił się EIP. Uprawnienia do zapisu są wyłączone w segmencie tekstowym, ponieważ nie służą do przechowywania zmiennych, a jedynie do kodu. Zapobiega to rzeczywistej modyfikacji kodu programu; każda próba zapisu do tego segmentu pamięci spowoduje, że program powiadomi użytkownika, że stało się coś złego, a program zostanie zabity. Kolejną zaletą tego segmentu, który jest tylko do odczytu jest to, że może on być dzielony między różne kopie programu, umożliwiając jednoczesne wielokrotne wykonywanie programu bez żadnych problemów. Należy również zauważyć, że ten segment pamięci ma stały rozmiar, ponieważ nic w nim się nie zmienia. Segmenty danych i bss są używane do przechowywania globalne i statyczne zmienne programowe. Segment danych jest wypełniony z zainicjalizowanymi zmiennymi globalnymi i statycznymi, natomiast segment bss jest wypełniony niezainicjowanymi odpowiednikami. Chociaż te segmenty są zapisywalne, mają również stały rozmiar. Pamiętaj, że zmienne globalne utrzymują się pomimo kontekstu funkcjonalnego (jak zmienna j w poprzednich przykładach). Zarówno globalne, jak i statyczne zmienne mogą przetrwać, ponieważ są przechowywane we własnych segmentach pamięci. Segment sterty jest segmentem pamięci, który programista może bezpośrednio kontrolować. Bloki pamięci w tym segmencie można przydzielać i wykorzystywać na dowolne potrzeby programisty. Jedn godny uwagi punkt segmentu sterty jest taki, że nie ma stałego rozmiaru, więc w razie potrzeby może się powiększyć lub zmniejszyć. Cała pamięć w stercie jest zarządzana przez alokatory i algorytmy deallocator, które odpowiednio rezerwują region pamięci w stercie do użycia i usuwają rezerwacje, aby umożliwić wykorzystanie tej części pamięci do późniejszych rezerwacji. Sterta będzie rósł i kurczy się w zależności od ilości pamięci zarezerwowanej do użycia. Oznacza to, że programista korzystający z funkcji przydzielania sterty może rezerwować i zwalniać pamięć w locie. Wzrost sterty przesuwa się w dół w kierunku wyższych adresów pamięci. Segment stosu ma również zmienny rozmiar i jest używany jako tymczasowy pad scratch do przechowywania lokalnych zmiennych funkcji i kontekstu podczas wywołań funkcji. Oto, na co wygląda komenda Backtrace GDB. Gdy program wywoła funkcję, funkcja ta będzie miała własny zestaw przekazywanych zmiennych, a kod funkcji będzie znajdować się w innym miejscu pamięci w segmencie tekstowym (lub kodowym). Ponieważ kontekst i EIP muszą się zmieniać po wywołaniu funkcji, stos jest używany do zapamiętywania wszystkich przekazanych zmiennych, lokalizacji, do której EIP powraca po zakończeniu funkcji, oraz wszystkich lokalnych zmiennych używanych przez tę funkcję. Wszystkie te informacje są przechowywane razem na stosie w tak zwanej ramie stosu. Stos zawiera wiele ramek stosu. Mówiąc ogólnie w terminologii informatycznej, stos jest abstrakcyjną strukturą danych, która jest często używana. Ma uporządkowanie first-in, last-out (FILO), co oznacza, że pierwszy element, który jest wkładany do stosu, jest ostatnim elementem, który się z niego wychodzi. Pomyśl o tym, jak umieszczaniu koralików na kawałku sznurka, który ma węzeł na jednym końcu - nie możesz zdjąć pierwszej kulki, dopóki nie usuniesz wszystkich pozostałych koralików. Kiedy przedmiot jest układany w stos, jest znany jako push, a gdy przedmiot jest usuwany ze stosu, nazywa się pop. Jak sama nazwa wskazuje, segment stosu pamięci jest w rzeczywistości strukturą danych stosu, która zawiera ramki stosów. Rejestr ESP służy do śledzenia adresu końca stosu, który ciągle się zmienia, gdy elementy są wsuwane i wysuwane z niego. Ponieważ jest to zachowanie bardzo dynamiczne, ma sens, że stos nie ma stałego rozmiaru. W przeciwieństwie do dynamicznego wzrostu sterty, gdy stos zmienia swój rozmiar, rośnie w górę w wizualnej liście pamięci, w kierunku niższych adresów pamięci. Natura FILO stosu może wydawać się dziwna, ale ponieważ stos jest używany do przechowywania kontekstu, jest bardzo użyteczny. Kiedy funkcja jest wywoływana, kilka rzeczy jest popychanych do stosu razem w ramce stosu. Rejestr EBP - czasami nazywany wskaźnikiem ramki (FP) lub wskaźnikiem lokalnej bazy (LB) - służy do odniesienia lokalnych zmiennych funkcyjnych w bieżącej ramce stosu. Każda ramka stosu zawiera parametry do funkcji, jej zmienne lokalne i dwa wskaźniki, które są niezbędne, aby przywrócić rzeczy takie, jakimi były: zapisany wskaźnik ramki (SFP) i adres zwrotny. SFP służy do przywracania EBP do poprzedniej wartości, a adres powrotu służy do przywrócenia EIP do następnej instrukcji znalezionej po wywołaniu funkcji. Spowoduje to przywrócenie kontekstu funkcjonalnego poprzedniej ramki stosu. Poniższy kod stack_example.c ma dwie funkcje: mai () i test_function().



Powrót

20.06.2020

stack_example.c

void test_function (int, int b, int c, int d) {

flaga int;

bufor znakujący [10];

flaga = 31337;

bufor [0] = "A";

}

int main () {

test_function (1, 2, 3, 4);

}

Ten program najpierw deklaruje funkcję testu, która ma cztery argumenty, które są zadeklarowane jako liczby całkowite: a, b, c i d. Zmienne lokalne dla funkcji obejmują pojedynczy znak o nazwie flag i 10-znakowy bufor o nazwie buffer. Pamięć dla tych zmiennych znajduje się w segmencie stosu, podczas gdy instrukcje maszynowe dla kodu funkcji są przechowywane w segmencie tekstowym. Po skompilowaniu programu jego wewnętrzne działania można sprawdzić za pomocą GDB. Poniższe dane wyjściowe przedstawiają zdemontowane instrukcje maszyny dla funkcji main () i funkcji test_function(). Funkcja main () zaczyna się od 0x08048357, a funkcja test_function () zaczyna się od 0x08048344. Pierwsze kilka instrukcji każdej funkcji ustawia ramkę stosu. Te instrukcje są wspólnie nazywane prologiem procedury lub prologiem funkcji. Zapisują wskaźnik ramki na stosie i zapisują pamięć stosu dla zmiennych funkcji lokalnych. Czasami prolog funkcji obsługuje również wyrównanie stosu. Dokładne instrukcje prologu będą się znacznie różnić w zależności od opcji kompilatora i kompilatora, ale generalnie instrukcje te budują ramkę stosu.

reader@hacking:~/booksrc $ gcc -g stack_example.c

reader@hacking:~/booksrc $ gdb -q ./a.out

Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1".

(gdb) disass main

Dump of assembler code for function main():

0x08048357 < main+0 >: push ebp

0x08048358 < main+1 >: mov ebp,esp

0x0804835a < main+3 >: sub esp,0x18

0x0804835d < main+6 >: and esp,0xfffffff0

0x08048360 < main+9 >: mov eax,0x0

0x08048365 < main+14 >: sub esp,eax

0x08048367 < main+16 >: mov DWORD PTR [esp+12],0x4

0x0804836f < main+24 >: mov DWORD PTR [esp+8],0x3

0x08048377 < main+32 >: mov DWORD PTR [esp+4],0x2

0x0804837f < main+40 >: mov DWORD PTR [esp],0x1

0x08048386 < main+47 >: call 0x8048344 < test_function > 0x0804838b < main+52 >: leave

0x0804838c < main+53 >: ret

End of assembler dump

(gdb) disass test_function()

Dump of assembler code for function test_function:

0x08048344 < test_function+0 >: push ebp

0x08048345 < test_function+1 >: mov ebp,esp

0x08048347 < test_function+3 >: sub esp,0x28

0x0804834a < test_function+6 >: mov DWORD PTR [ebp-12],0x7a69

0x08048351 < test_function+13 >: mov BYTE PTR [ebp-40],0x41

0x08048355 < test_function+17 >: leave

0x08048356 < test_function+18 >: ret

End of assembler dump

(gdb)

Po uruchomieniu programu wywoływana jest funkcja main(), która po prostu wywołuje funkcję test_function (). Kiedy funkcja test_function () jest wywoływana z funkcji main (), różne wartości są przekazywane do stosu, aby utworzyć początek stosu stosu w następujący sposób. Kiedy wywoływana jest funkcja test_funtion (), argumenty funkcji są wypychane na stos w odwrotnej kolejności (ponieważ jest to FILO). Argumenty dla funkcji to 1, 2, 3 i 4, więc kolejne instrukcje wypychania przesuwają 4, 3, 2 i na końcu 1 na stos. Wartości te odpowiadają zmiennym d, c, b i a w funkcji. Instrukcje umieszczające te wartości na stosie są pogrubione w poniższej dezasemblacjimontażu funkcji main().

(gdb) disass main

Dump of assembler code for function main:

0x08048357 < main+0 >: push ebp

0x08048358 < main+1 >: mov ebp,esp

0x0804835a < main+3 >: sub esp,0x18

0x0804835d < main+6 >: and esp,0xfffffff0

0x08048360 < main+9 >: mov eax,0x0

0x08048365 < main+14 >: sub esp,eax

0x08048367 < main+16 >: mov DWORD PTR [esp+12],0x4

0x0804836f < main+24 >: mov DWORD PTR [esp+8],0x3

0x08048377 < main+32 >: mov DWORD PTR [esp+4],0x2

0x0804837f < main+40 >: mov DWORD PTR [esp],0x1

0x08048386 < main+47 >: call 0x8048344 < test_function >

0x0804838b < main+52 >: leave

0x0804838c < main+53 >: ret

End of assembler dump

(gdb)

Następnie, gdy wykonywana jest instrukcja wywołania złożenia, adres powrotu jest przesyłany na stos, a przepływ wykonania przeskakuje do początku funkcji test_function () przy 0x08048344. Wartością adresu powrotu będzie lokalizacja instrukcji następującej po aktualnym EIP-specyficznie, wartość zapisana podczas kroku 3 poprzednio wspomnianej pętli wykonawczej. W tym przypadku adres powrotu wskazywałby na instrukcję opuszczania w main () at 0x0804838b. Instrukcja wywołania przechowuje adres zwrotny na stosie i przeskakuje EIP na początek funkcji test_function(), więc instrukcje procedury prologu test_function() kończą budowanie ramki stosu. W tym kroku bieżąca wartość EBP jest przekazywana do stosu. Ta wartość nazywa się zapisanym wskaźnikiem ramki (SFP) i jest później używana do przywracania EBP do pierwotnego stanu. Aktualna wartość ESP jest następnie kopiowana do EBP, aby ustawić nowy wskaźnik ramki. Ten wskaźnik ramki służy do odniesienia lokalnych zmiennych funkcji (flaga i bufor). Pamięć jest zapisywana dla tych zmiennych przez odjęcie od TE. Możemy obserwować konstrukcję stosu na stosie za pomocą GDB. W następującym wyjściu punkt przerwania jest ustawiany w funkcji main () przed wywołaniem funkcji test_function (), a także na początku funkcji test_function (). GDB umieści pierwszy punkt przerwania, zanim argumenty funkcji zostaną wypchnięte na stos, a drugi punkt przerwania po prologu procedury test_function (). Po uruchomieniu programu wykonywanie zatrzymuje się w punkcie przerwania, gdzie Rejestr ESP (wskaźnik stosu), EBP (wskaźnik ramki) i EIP (wskaźnik wykonania) są badane.

(gdb) list main

4

5 flag = 31337;

6 buffer[0] = 'A';

7 }

8

9 int main() {

10 test_function(1, 2, 3, 4);

11 }

(gdb) break 10

Breakpoint 1 at 0x8048367: file stack_example.c, line 10.

(gdb) break test_function

Breakpoint 2 at 0x804834a: file stack_example.c, line 5.

(gdb) run

Starting program: /home/reader/booksrc/a.out

Breakpoint 1, main () at stack_example.c:10

10 test_function(1, 2, 3, 4);

(gdb) i r esp ebp eip

esp 0xbffff7f0 0xbffff7f0

ebp 0xbffff808 0xbffff808

eip 0x8048367 0x8048367 < main+16 >

(gdb) x/5i $eip

0x8048367 < main+16 >: mov DWORD PTR [esp+12],0x4

0x804836f < main+24 >: mov DWORD PTR [esp+8],0x3

0x8048377 < main+32 >: mov DWORD PTR [esp+4],0x2

0x804837f < main+40 >: mov DWORD PTR [esp],0x1

0x8048386 < main+47 >: call 0x8048344 < test_function >

(gdb)

Ten punkt przerwania znajduje się tuż przed ramką stosu dla wywołania funkcji test_function (). Oznacza to, że dno tej nowej ramki stosu ma aktualną wartość ESP, 0xbffff7f0. Następny punkt przerwania jest zaraz po procedurze prologu dla funkcji test_function (), więc kontynuowanie będzie budować ramkę stosu. Dane wyjściowe poniżej pokazują podobne informacje w drugim punkcie przerwania. Zmienne lokalne (flaga i bufor) odnoszą się do wskaźnika ramki (EBP).

(gdb) cont

Continuing.

Breakpoint 2, test_function (a=1, b=2, c=3, d=4) at stack_example.c:5

5 flag = 31337;

(gdb) i r esp ebp eip

esp 0xbffff7c0 0xbffff7c0

ebp 0xbffff7e8 0xbffff7e8

eip 0x804834a 0x804834a

(gdb) disass test_function

Dump of assembler code for function test_function:

0x08048344 < test_function+0 >: push ebp

0x08048345 < test_function+1 > : mov ebp,esp

0x08048347 < test_function+3 >: sub esp,0x28

0x0804834a < test_function+6 >: mov DWORD PTR [ebp-12],0x7a69

0x08048351 < test_function+13 >: mov BYTE PTR [ebp-40],0x41

0x08048355 < test_function+17 >: leave

0x08048356 < test_function+18 >: ret

End of assembler dump.

(gdb) print $ebp-12

$1 = (void *) 0xbffff7dc

(gdb) print $ebp-40

$2 = (void *) 0xbffff7c0

(gdb) x/16xw $esp

0xbffff7c0

Ramka stosu jest pokazana na stosie na końcu. Cztery argumenty funkcji można zobaczyć na dole ramki stosu z adresem powrotnym znajdującym się bezpośrednio na górze Powyżej, który jest zapisanym wskaźnikiem ramki 0xbffff808 , który jest tym, co EBP było w poprzednim rama stosu. Reszta pamięci jest zapisywana dla lokalnych zmiennych stosu: flaga i bufor. Obliczenie ich względnych adresów na EBP pokazuje ich dokładne położenia w ramce stosu. Pamięć dla zmiennej flagi jest pokazana i pamięć dla zmiennej buforowej jest pokazana). Dodatkowe miejsce w ramce stosu to tylko dopełnienie. Po zakończeniu wykonywania cała ramka stosu jest wyskakiwana ze stosu, a EIP jest ustawiony na adres powrotu, aby program mógł kontynuować wykonywanie. Jeśli w funkcji została wywołana inna funkcja, następna ramka stosu zostanie przesunięta na stos i tak dalej. Gdy kończy się każda funkcja, jej ramka stosu jest wyskakiwana ze stosu, więc wykonanie można przywrócić do poprzedniej funkcji. To zachowanie jest przyczyną tego, że ten segment pamięci jest zorganizowany w strukturę danych FILO. Różne segmenty pamięci są ułożone w kolejność, w jakiej zostały przedstawione, od niższych adresów pamięci do wyższych adresów pamięci. Ponieważ większość osób zna listy numerowane, które liczą się w dół, mniejsze adresy pamięci są wyświetlane u góry. Niektóre teksty mają to odwrotne, co może być bardzo mylące; więc w przypadku tej książki, mniejsze adresy pamięci są zawsze wyświetlane u góry. Większość debuggerów wyświetla także pamięć w tym stylu, z mniejszymi adresami pamięci na górze i wyższymi na dole. Ponieważ sterta i stos są jednocześnie dynamiczne, obie rosną w różnych kierunkach względem siebie. Minimalizuje to marnowanie miejsca, dzięki czemu stos może być większy, jeśli stóg jest mały i na odwrót.



Powrót

21.06.2020

Segmenty pamięci w C

W języku C, podobnie jak w innych językach kompilowanych, skompilowany kod przechodzi do segmentu tekstowego, podczas gdy zmienne znajdują się w pozostałych segmentach. Dokładnie jaki segment pamięci będzie przechowywana w zmiennej zależy od tego, jak zdefiniowana jest zmienna. Zmienne zdefiniowane poza dowolnymi funkcjami są uważane za globalne. Statyczne słowo kluczowe można również dodać do dowolnej deklaracji zmiennej, aby zmienna stała była statyczna. Jeżeli zmienne statyczne lub globalne są inicjowane danymi, są one przechowywane w segmencie pamięci danych; w przeciwnym razie zmienne te są umieszczane w segmencie pamięci bss. Pamięć w segmencie pamięci sterty musi być najpierw przydzielona przy użyciu funkcji alokacji pamięci malloc (). Zwykle wskaźniki służą do odwoływania się do pamięci na stercie. Na koniec pozostałe zmienne funkcji są przechowywane w segmencie pamięci stosu. Ponieważ stos może zawierać wiele różnych ramek stosu, zmienne stosu mogą zachować wyjątkowość w różnych kontekstach funkcjonalnych. Program memory_segments.c pomoże wyjaśnić te pojęcia w C

Powrót

22.06.2020

memory_segments.c

#include < stdio.h >

int global_var;

int global_initialized_var = 5;

void function() { // This is just a demo function.

int stack_var; // Notice this variable has the same name as the one in main().

printf("the function's stack_var is at address 0x%08x\n", &stack_var);

}

int main() {

int stack_var; // Same name as the variable in function()

static int static_initialized_var = 5;

static int static_var;

int *heap_var_ptr;

heap_var_ptr = (int *) malloc(4);

// These variables are in the data segment.

printf("global_initialized_var is at address 0x%08x\n", &global_initialized_var);

printf("static_initialized_var is at address 0x%08x\n\n", &static_initialized_var);

// These variables are in the bss segment.

printf("static_var is at address 0x%08x\n", &static_var);

printf("global_var is at address 0x%08x\n\n", &global_var);

// This variable is in the heap segment.

printf("heap_var is at address 0x%08x\n\n", heap_var_ptr);

// These variables are in the stack segment.

printf("stack_var is at address 0x%08x\n", &stack_var);

function();

}

Większość tego kodu jest dość oczywiste ze względu na opisowe nazwy zmiennych. Zmienne globalne i statyczne są zadeklarowane tak, jak to opisano wcześniej, a zadeklarowane odpowiedniki również są zadeklarowane. Zmienna stosu jest zadeklarowana zarówno w funkcji main (), jak i function (), aby pokazać efekt kontekstów funkcjonalnych. Zmienna sterty jest faktycznie zadeklarowana jako wskaźnik całkowity, który wskaże na pamięć przydzieloną w segmencie pamięci sterty. Funkcja malloc () jest wywoływana w celu przydzielenia czterech bajtów na stercie. Ponieważ nowo przydzielona pamięć może być dowolnego typu danych, funkcja malloc () zwraca wskaźnik pustego, który musi być typecast na wskaźnik całkowity. reader @ hacking: ~ / booksrc $ gcc memory_segments.c

reader@hacking:~/booksrc $ gcc memory_segments.c

reader@hacking:~/booksrc $ ./a.out

global_initialized_var is at address 0x080497ec

static_initialized_var is at address 0x080497f0

static_var is at address 0x080497f8

global_var is at address 0x080497fc

heap_var is at address 0x0804a008

stack_var is at address 0xbffff834

the function's stack_var is at address 0xbffff814

Pierwsze dwie zainicjowane zmienne mają najniższe adresy pamięci, ponieważ znajdują się w segmencie pamięci danych. Następne dwie zmienne, static_var i global_var, są przechowywane w segmencie pamięci bss, ponieważ nie są inicjowane. Te adresy pamięci są nieco większe niż adresy poprzednich zmiennych, ponieważ segment bss znajduje się poniżej segmentu danych. Ponieważ oba te segmenty pamięci mają ustalony rozmiar po kompilacji, jest mało zmarnowanego miejsca, a adresy nie są bardzo odległe. Zmienna sterty jest przechowywana w przestrzeni przydzielonej do segmentu sterty, który znajduje się tuż poniżej segmentu bss. Pamiętaj, że pamięć w tym segmencie nie jest stała, a więcej miejsca można później dynamicznie alokować. Wreszcie, ostatnie dwa pliki stack_vars mają bardzo duże adresy pamięci, ponieważ znajdują się w segmencie stosu. Pamięć w stosie też nie jest stała; jednak pamięć ta zaczyna się od dołu i rośnie wstecz w kierunku segmentu sterty. Dzięki temu oba segmenty pamięci mogą być dynamiczne bez marnowania miejsca w pamięci. Pierwszy stack_var w kontekście funkcji main () jest przechowywany w segmencie stosu w ramce stosu. Drugi stack_var w funkcji () ma własny unikalny kontekst, więc zmienna jest przechowywana w innej ramce stosu w segmencie stosu. Gdy funkcja function () zostanie wywołana w pobliżu końca programu, tworzona jest nowa ramka stosu do przechowywania (między innymi) wartości stack_var dla funkcji (). Ponieważ stos rośnie z powrotem w kierunku segmentu sterty z każdą nową ramką stosu, adres pamięci dla drugiego stosu_var (0xbffff814) jest mniejszy niż adres pierwszego stosu_var (0xbffff834) znalezionego w kontekście main().

Powrót

23.06.2020

Używanie sterty

Używanie innych segmentów pamięci jest po prostu kwestią sposobu deklarowania zmiennych. Jednak używanie sterty wymaga nieco więcej wysiłku. Jak wcześniej pokazano, przydzielanie pamięci na stercie odbywa się za pomocą funkcji malloc(). Ta funkcja przyjmuje rozmiar jako jedyny argument i rezerwuje tyle miejsca w segmencie sterty, zwracając adres na początku tej pamięci jako wskaźnik pustej przestrzeni. Jeśli funkcja malloc () nie może przydzielić pamięci z jakiegoś powodu, po prostu zwróci wskaźnik NULL z wartością 0. Odpowiednia funkcja dealokacji jest wolna (). Ta funkcja akceptuje wskaźnik jako jedyny argument i zwalnia ten obszar pamięci na stercie, aby można go było użyć później. Te względnie proste funkcje są demonstrowane w pliku heap_example.c.

heap_example.c

#include < stdio.h >

#include < stdlib.h >

#include < string.h >

int main(int argc, char *argv[]) {

char *char_ptr; // A char pointer

int *int_ptr; // An integer pointer

int mem_size;

if (argc < 2) // If there aren't command-line arguments,

mem_size = 50; // use 50 as the default value.

else

mem_size = atoi(argv[1]);

printf("\t[+] allocating %d bytes of memory on the heap for char_ptr\n", mem_size);

char_ptr = (char *) malloc(mem_size); // Allocating heap memory

if(char_ptr == NULL) { // Error checking, in case malloc() fails

fprintf(stderr, "Error: could not allocate heap memory.\n");

exit(-1);

}

strcpy(char_ptr, "This is memory is located on the heap.");

printf("char_ptr (%p) --> '%s'\n", char_ptr, char_ptr);

printf("\t[+] allocating 12 bytes of memory on the heap for int_ptr\n");

int_ptr = (int *) malloc(12); // Allocated heap memory again

if(int_ptr == NULL) { // Error checking, in case malloc() fails

fprintf(stderr, "Error: could not allocate heap memory.\n");

exit(-1);

}

*int_ptr = 31337; // Put the value of 31337 where int_ptr is pointing.

printf("int_ptr (%p) --> %d\n", int_ptr, *int_ptr);

printf("\t[-] freeing char_ptr's heap memory...\n");

free(char_ptr); // Freeing heap memory

printf("\t[+] allocating another 15 bytes for char_ptr\n");

char_ptr = (char *) malloc(15); // Allocating more heap memory

if(char_ptr == NULL) { // Error checking, in case malloc() fails

fprintf(stderr, "Error: could not allocate heap memory.\n");

exit(-1);

}

strcpy(char_ptr, "new memory");

printf("char_ptr (%p) --> '%s'\n", char_ptr, char_ptr);

printf("\t[-] freeing int_ptr's heap memory...\n");

free(int_ptr); // Freeing heap memory

printf("\t[-] freeing char_ptr's heap memory...\n");

free(char_ptr); // Freeing the other block of heap memory

}

Ten program przyjmuje argument wiersza polecenia dla wielkości pierwszej alokacji pamięci, z domyślną wartością 50. Następnie wykorzystuje funkcje malloc () i free () do alokacji i zwolnienia pamięci na stercie. Istnieje wiele instrukcji printf () do debugowania tego, co faktycznie dzieje się podczas wykonywania programu. Ponieważ malloc () nie wie, jaki typ pamięci przydziela, zwraca on wskaźnik pustej pamięci do nowo przydzielonej pamięci sterty, która musi być typecastem do odpowiedniego typu. Po każdym wywołaniu funkcji malloc () znajduje się blok sprawdzania błędów, który sprawdza, czy alokacja się nie powiodła. Jeśli alokacja nie powiedzie się, a wskaźnik ma wartość NULL, funkcja fprintf () jest używana do drukowania komunikatu o błędzie w przypadku błędu standardowego i program zostaje zamknięty. Funkcja fprintf () jest bardzo podobna do printf (); jednak jego pierwszym argumentem jest stderr, który jest standardowym strumieniem pliku przeznaczonym do wyświetlania błędów. Ta funkcja zostanie wyjaśniona bardziej później, ale na razie służy tylko jako sposób na poprawne wyświetlenie błędu. Reszta programu jest dość prosta.

reader@hacking:~/booksrc $ gcc -o heap_example heap_example.c

reader@hacking:~/booksrc $ ./heap_example

[+] allocating 50 bytes of memory on the heap for char_ptr

char_ptr (0x804a008) --> 'This is memory is located on the heap.'

[+] allocating 12 bytes of memory on the heap for int_ptr

int_ptr (0x804a040) --> 31337

[-] freeing char_ptr's heap memory…

[+] allocating another 15 bytes for char_ptr

char_ptr (0x804a050) --> 'new memory'

[-] freeing int_ptr's heap memory…

[-] freeing char_ptr's heap memory…

reader@hacking:~/booksrc $

W poprzednim wyjściu zauważ, że każdy blok pamięci ma przyrostowo wyższy adres pamięci w stercie. Mimo że pierwsze 50 bajtów zostało przydzielone, po zażądaniu dodatkowych 15 bajtów są one umieszczane po 12 bajtach przydzielonych dla int_ptr. Funkcje alokacji sterty kontrolują to zachowanie, które można zbadać, zmieniając rozmiar początkowej alokacji pamięci

reader@hacking:~/booksrc $ ./heap_example 100

[+] allocating 100 bytes of memory on the heap for char_ptr

char_ptr (0x804a008) --> 'This is memory is located on the heap.'

[+] allocating 12 bytes of memory on the heap for int_ptr

int_ptr (0x804a070) --> 31337

[-] freeing char_ptr's heap memory...

[+] allocating another 15 bytes for char_ptr

char_ptr (0x804a008) --> 'new memory'

[-] freeing int_ptr's heap memory…

[-] freeing char_ptr's heap memory…

reader@hacking:~/booksrc $



Powrót

24.06.2020

Sprawdzanie błędów malloc()

W heap_example.c, było kilka sprawdzeń błędów dla wywołań malloc(). Mimo, że wywołania malloc() nigdy nie zawiodły, ważne jest, aby obsłużyć wszystkie potencjalne przypadki podczas kodowania w C. Jednak przy wielu wywołaniach malloc () ten kod sprawdzający błędy musi pojawić się w wielu miejscach. Zwykle kod wygląda niechlujnie i jest niewygodny, jeśli trzeba wprowadzić zmiany w kodzie sprawdzania błędów lub jeśli potrzebne są nowe wywołania malloc (). Cały kod sprawdzania błędów jest zasadniczo taki sam dla każdego wywołania funkcji malloc(), jest to idealne miejsce do używania funkcji zamiast powtarzania tych samych instrukcji w wielu miejscach. Zobacz przykład błędu w pliku errorchecked_heap.c.

Powrót

25.06.2020

errorchecked_heap.c

#include < stdio.h >

#include < stdlib.h >

#include < string.h >

void * errorchecked_malloc (unsigned int); // Prototyp funkcji dla errorchecked_malloc ()

int main (int argc, char * argv []) {

char * char_ptr; // Wskaźnik char

int * int_ptr; // Wskaźnik całkowity

int mem_size;

if (argc < 2) // Jeśli nie ma argumentów wiersza poleceń,

mem_size = 50; // użyj 50 jako wartości domyślnej.

else

mem_size = atoi (argv [1]);

printf ("\ t [+] przydzielanie% d bajtów pamięci na stercie dla char_ptr \ n", mem_size);

char_ptr = (char *) errorchecked_malloc (mem_size); // Przydzielanie pamięci sterty

strcpy (char_ptr, "To jest pamięć znajduje się na stercie.");

printf ("char_ptr (% p) -> '% s' \ n", char_ptr, char_ptr);

printf ("\ t [+] przydzielanie 12 bajtów pamięci na stercie dla int_ptr \ n");

int_ptr = (int *) errorchecked_malloc (12); // Przydzieloną pamięć sterty ponownie

* int_ptr = 31337; // Wpisz wartość 31337, gdzie wskazuje int_ptr.

printf ("int_ptr (% p) ->% d \ n", int_ptr, * int_ptr);

printf ("\ t [-] zwalniając pamięć sterty char_ptr ... \ n");

free (char_ptr); // Zwolnienie pamięci sterty

printf ("\ t [+] przydzielanie kolejnych 15 bajtów dla char_ptr \ n");

char_ptr = (char *) errorchecked_malloc (15); // Przydzielanie większej ilości pamięci sterty

strcpy (char_ptr, "nowa pamięć");

printf ("char_ptr (% p) -> '% s' \ n", char_ptr, char_ptr);

printf ("\ t [-] zwalniające pamięć sterty int_ptr ... \ n");

bezpłatny (int_ptr); // Zwolnienie pamięci sterty

printf ("\ t [-] zwalniając pamięć sterty char_ptr ... \ n");

free (char_ptr); // Zwolnienie drugiego bloku pamięci sterty

}

void * errorchecked_malloc (unsigned int size) {// Funkcja malloc () sprawdzona pod kątem błędów

void * ptr;

ptr = malloc (rozmiar);

if (ptr == NULL) {

fprintf (stderr, "Błąd: nie można przydzielić pamięci sterty. \ n");

wyjście (-1);

}

return ptr;

}

Program errorchecked_heap.c jest w zasadzie równoznaczny z poprzednim kodem heap_example.c, z tym wyjątkiem, że przydział pamięci sterty i sprawdzanie błędów zostały zebrane w jedną funkcję. Pierwsza linia kodu [void * errorchecked_malloc (unsigned int);] jest prototypem funkcji. To pozwala kompilatorowi wiedzieć, że będzie funkcja o nazwie errorchecked_malloc (), która oczekuje pojedynczego, niepodpisanego argumentu całkowitego i zwraca wskaźnik pustego. Faktyczna funkcja może być wszędzie; w tym przypadku jest to po funkcji głównej (). Sama funkcja jest dość prosta; po prostu akceptuje rozmiar w bajtach, aby przydzielić i próbuje przydzielić tyle pamięci używając malloc (). Jeśli alokacja się nie powiedzie, kod sprawdzania błędów wyświetli błąd i program zostanie zamknięty; w przeciwnym razie zwraca wskaźnik do nowo przydzielonej pamięci sterty. W ten sposób niestandardowa funkcja errorchecked_malloc () może być użyta zamiast normalnego malloc (), eliminując potrzebę późniejszego powtarzania błędów. Powinno to zacząć podkreślać użyteczność programowania za pomocą funkcji.

Powrót

26.06.2020

Opierając się na podstawach

Po zrozumieniu podstawowych pojęć programowania w C, reszta jest dość łatwa. Większość mocy C pochodzi z używania innych funkcji. W rzeczywistości, jeśli funkcje zostały usunięte z któregokolwiek z programów poprzedzających, wszystkie, które pozostałyby były bardzo podstawowymi stwierdzeniami.

Powrót

27.06.2020

Dostęp do plików

Istnieją dwa podstawowe sposoby uzyskiwania dostępu do plików w C: deskryptory plików i strumienie plików. Deskryptory plików wykorzystują zestaw niskopoziomowych funkcji wejścia / wyjścia, a strumienie plików są formą wyższego poziomu buforowanych operacji we / wy, zbudowanych na funkcjach niższego poziomu. Niektórzy uważają, że funkcje strumienia plików są łatwiejsze do zaprogramowania; jednak deskryptory plików są bardziej bezpośrednie. W tej książce nacisk zostanie położony na funkcje wejścia / wyjścia niskiego poziomu, które wykorzystują deskryptory plików. Kod kreskowy reprezentuje liczbę. Ponieważ ta liczba jest unikatowa wśród innych książek w księgarni, kasjer może zeskanować numer przy kasie i użyć go do odniesienia informacji o tej książce w bazie danych sklepu. Podobnie deskryptor pliku jest numerem używanym do odwoływania się do otwartych plików. Cztery wspólne funkcje, które używają deskryptorów plików to open(), close(), read() i write(). Wszystkie te funkcje zwrócą -1, jeśli wystąpi błąd. Funkcja open() otwiera plik do odczytu i / lub zapisu i zwraca deskryptor pliku. Zwrócony deskryptor pliku jest po prostu wartością całkowitą, ale jest unikalny wśród otwartych plików. Deskryptor pliku przekazywany jest jako argument do innych funkcji, takich jak wskaźnik do otwartego pliku. Dla funkcja close(), deskryptor pliku jest jedynym argumentem. Argumenty funkcji read() i write() to deskryptor pliku, wskaźnik do danych do odczytu lub zapisu oraz liczba bajtów do odczytu lub zapisu z tej lokalizacji. Argumenty funkcji open() są wskaźnikiem do nazwy pliku do otwarcia i serią predefiniowanych flag, które określają tryb dostępu. Te flagi i ich użycie zostaną później szczegółowo wyjaśnione, ale na razie przyjrzyjmy się prostemu programowi do robienia notatek, który używa deskryptorów plików-simplenote.c. Ten program przyjmuje notatkę jako argument linii poleceń, a następnie dodaje ją do końca pliku / tmp / notes. Ten program wykorzystuje kilka funkcji, w tym dobrze znaną funkcję alokowania pamięci sterowanej przy użyciu sprawdzanych błędów. Inne funkcje służą do wyświetlania komunikatu o użytkowaniu i obsługi błędów krytycznych. Funkcja usage() jest po prostu zdefiniowana przed main(), więc nie potrzebuje prototypu funkcji.

simplenote.c

#include < stdio.h >

#include < stdlib.h >

#include < string.h >

#include < fcntl.h >

#include < sys/stat.h >

void usage(char *prog_name, char *filename) {

printf("Usage: %s \n", prog_name, filename);

exit(0);

}

void fatal(char *); // A function for fatal errors

void *ec_malloc(unsigned int); // An error-checked malloc() wrapper

int main(int argc, char *argv[]) {

int fd; // file descriptor

char *buffer, *datafile;

buffer = (char *) ec_malloc(100);

datafile = (char *) ec_malloc(20);

strcpy(datafile, "/tmp/notes");

if(argc < 2) // If there aren't command-line arguments,

usage(argv[0], datafile); // display usage message and exit.

strcpy(buffer, argv[1]); // Copy into buffer.

printf("[DEBUG] buffer @ %p: \'%s\'\n", buffer, buffer);

printf("[DEBUG] data file @ %p: \'%s\'\n", datafile, datafile);

strncat(buffer, "\n", 1); // Add a newline on the end.

// Opening file

fd = open(datafile, O_WRONLY|O_CREAT|O_APPEND, S_IRUSR|S_IWUSR);

if(fd == -1)

fatal("in main() while opening file");

printf("[DEBUG] file descriptor is %d\n", fd);

// Writing data

if(write(fd, buffer, strlen(buffer)) == -1)

fatal("in main() while writing buffer to file");

// Closing file

if(close(fd) == -1)

fatal("in main() while closing file");

printf("Note has been saved.\n");

free(buffer);

free(datafile);

}

// A function to display an error message and then exit

void fatal(char *message) {

char error_message[100];

strcpy(error_message, "[!!] Fatal Error ");

strncat(error_message, message, 83);

perror(error_message);

exit(-1);

}

// An error-checked malloc() wrapper function void *ec_malloc(unsigned int size) {

void *ptr;

ptr = malloc(size);

if(ptr == NULL)

fatal("in ec_malloc() on memory allocation");

return ptr;

}

Oprócz dziwnie wyglądających flag używanych w funkcji open(), większość tego kodu powinna być czytelna. Istnieje również kilka standardowych funkcji, z których wcześniej nie korzystaliśmy. Funkcja strlen() przyjmuje ciąg znaków i zwraca jego długość. Jest używany w połączeniu z funkcją write(), ponieważ musi wiedzieć, ile bajtów zapisać. Funkcja perror () jest krótka dla błędu drukowania i jest używana w trybie fatal(), aby wydrukować dodatkowy komunikat o błędzie (jeśli istnieje) przed wyjściem.

reader@hacking:~/booksrc $ gcc -o simplenote simplenote.c

reader@hacking:~/booksrc $ ./simplenote

Usage: ./simplenote

reader@hacking:~/booksrc $ ./simplenote "this is a test note"

[DEBUG] buffer @ 0x804a008: 'this is a test note'

[DEBUG] data file @ 0x804a070: '/tmp/notes'

[DEBUG] file descriptor is 3

Note has been saved.

reader@hacking:~/booksrc $ cat /tmp/notes

this is a test note

reader@hacking:~/booksrc $ ./simplenote "great, it works"

[DEBUG] buffer @ 0x804a008: 'great, it works'

[DEBUG] datafile @ 0x804a070: '/tmp/notes'

[DEBUG] file descriptor is 3

Note has been saved.

reader@hacking:~/booksrc $ cat /tmp/notes

this is a test note

great, it works

reader@hacking:~/booksrc $

Wynik działania programu jest dość oczywisty, ale niektóre elementy kodu źródłowego wymagają dalszych wyjaśnień. Należy uwzględnić pliki fcntl.h i sys / stat.h, ponieważ te pliki definiują flagi używane w funkcji open (). Pierwszy zestaw flag znajduje się w pliku fcntl.h i służy do ustawienia trybu dostępu. Tryb dostępu musi wykorzystywać co najmniej jedną z następujących trzech flag:

O_RDONLY

Otwórz plik, aby uzyskać dostęp tylko do odczytu.

O_WRONLY

Otwórz plik, aby uzyskać dostęp tylko do zapisu.

O_RDWR

Otwórz plik, aby uzyskać dostęp do odczytu i zapisu.

Te flagi można łączyć z kilkoma opcjonalnymi flagami za pomocą operatora OR. Oto kilka bardziej powszechnych i użytecznych obszarów flag:

O_APPEND

Zapisz dane na końcu pliku.

O_TRUNC

Jeśli plik już istnieje, skróć go do długości 0.

O_CREAT

Utwórz plik, jeśli nie istnieje.

Operacje bitowe łączą bity przy użyciu standardowych bramek logicznych, takich jak OR i AND. Gdy dwa bity wejdą do bramki OR, wynikiem jest 1, jeśli pierwszy bit lub drugi bit to 1. Jeśli dwa bity wejdą do bramki AND, wynikiem jest 1 tylko wtedy, gdy zarówno pierwszy bit, jak i drugi bit, są 1. Pełna 32-bitowe wartości mogą wykorzystywać te operatory bitowe do wykonywania operacji logicznych na każdym odpowiednim bicie. Kod źródłowy bitwise.c i wyjście programu demonstrują te operacje bitowe.

bitwise.c



#include < stdio.h >

int main () {

int i, bit_a, bit_b;

printf ("operator OR bitowy | \ n");

dla (i = 0; i <4; i ++) {

bit_a = (i & 2) / 2; // Zdobądź drugi bit.

bit_b = (i & 1); // Zdobądź pierwszy bit. printf ("% d |% d =% d \ n", bit_a, bit_b, bit_a | bit_b);

}

printf ("\ nwspolszczenie AND operator & \ n");

dla (i = 0; i <4; i ++) {

bit_a = (i & 2) / 2; // Zdobądź drugi bit.

bit_b = (i & 1); // Zdobądź pierwszy bit.

printf ("% d &% d =% d \ n", bit_a, bit_b, bit_a i bit_b);

}

}

Wyniki kompilacji i wykonywania bitwise.c są następujące.

reader @ hacking: ~ / booksrc $ gcc bitwise.c

reader @ hacking: ~ / booksrc $ ./a.out

operator bitowy OR |

0 | 0 = 0

0 | 1 = 1

1 | 0 = 1

1 | 1 = 1

operator bitowy AND

0 i 0 = 0

0 i 1 = 0

1 i 0 = 0

1 i 1 = 1

reader @ ing ~ / booksrc $

Flagi używane dla funkcji open () mają wartości odpowiadające pojedynczym bitom. W ten sposób flagi mogą być łączone za pomocą logiki OR bez niszczenia jakichkolwiek informacji. Program fcntl_flags.c i jego dane wyjściowe badają niektóre z wartości flag zdefiniowanych przez fcntl.h oraz sposób, w jaki się ze sobą łączą.

fcntl_flags.c

#include

#include

void display_flags (char *, unsigned int);

void binary_print (unsigned int);

int main (int argc, char * argv []) {

display_flags ("O_RDONLY \ t \ t", O_RDONLY);

display_flags ("O_WRONLY \ t \ t", O_WRONLY);

display_flags ("O_RDWR \ t \ t \ t", O_RDWR);

printf ("\ n");

display_flags ("O_APPEND \ t \ t", O_APPEND);

display_flags ("O_TRUNC \ t \ t \ t", O_TRUNC);

display_flags ("O_CREAT \ t \ t \ t", O_CREAT);

printf ("\ n");

display_flags ("O_WRONLY | O_APPEND | O_CREAT", O_WRONLY | O_APPEND | O_CREAT);

}

void display_flags (znak *, niepodpisana wartość int) {

printf ("% s \ t:% d \ t:", etykieta, wartość);

binary_print (wartość);

printf ("\ n");

}

void binary_print (unsigned int value {)

unsigned int mask = 0xff000000; // Rozpocznij od maski dla najwyższego bajtu.

unsigned int shift = 256 * 256 * 256; // Zacznij od przesunięcia dla najwyższego bajtu.

unsigned int byte, byte_iterator, bit_iterator;

for (byte_iterator = 0; byte_iterator <4; byte_iterator ++) {

byte = (wartość i maska) / shift; // Izoluj każdy bajt.

printf ("");

dla (bit_iterator = 0, bit_iterator <8; bit_iterator ++) {// Wydrukuj bity bajtu.

if (byte & 0x80) // Jeśli najwyższy bit w bajcie nie jest równy 0,

printf ("1"); // wydrukuj 1.

jeszcze

printf ("0"); // W przeciwnym razie wypisz 0.

bajt * = 2; // Przenieś wszystkie bity w lewo o 1.

}

maska / = 256; // Przenieś bity w masce o 8.

}

}

Wyniki kompilowania i uruchamiania fcntl_flags.c są następujące.

Widok kodu:

reader@hacking:~/booksrc $ gcc fcntl_flags.c

reader@hacking:~/booksrc $ ./a.out

O_RDONLY: 0: 00000000 00000000 00000000 00000000

O_WRONLY: 1: 00000000 00000000 00000000 00000001

O_RDWR: 2: 00000000 00000000 00000000 00000010

O_APPEND: 1024: 00000000 00000000 00000100 00000000

O_TRUNC: 512: 00000000 00000000 00000010 00000000

O_CREAT: 64: 00000000 00000000 00000000 01000000

O_WRONLY | O_APPEND | O_CREAT: 1089: 00000000 00000000 00000100 01000001

$

Używanie flag bitów w połączeniu z logiką bitową jest skuteczną i powszechnie stosowaną techniką. Dopóki każda flaga jest liczbą, która ma tylko unikatowe bity, efekt zrobienia bitowego OR na tych wartościach jest taki sam, jak dodanie ich. W fcntl_flags.c, 1 + 1024 + 64 = 1089. Ta technika działa tylko wtedy, gdy wszystkie bity są unikatowe.

Powrót

28.06.2020

Uprawnienia do plików

Jeśli flaga O_CREAT jest używana w trybie dostępu dla funkcji open (), potrzebny jest dodatkowy argument, aby zdefiniować uprawnienia do plików nowo utworzonego pliku. Ten argument używa flag bitowych zdefiniowanych w sys / stat.h, które można łączyć ze sobą za pomocą logiki bitowej OR.

S_IRUSR : Podaj uprawnienia do odczytu pliku dla użytkownika (właściciela).

S_IWUSR: Podaj uprawnienia do zapisu pliku dla użytkownika (właściciela).

S_IXUSR : Podaj uprawnienia do wykonywania pliku dla użytkownika (właściciela).

S_IRGRP : Podaj uprawnienia do odczytu pliku dla grupy.

S_IWGRP : Podaj uprawnienia do zapisu pliku dla grupy.

S_IXGRP : Daj uprawnienia do wykonywania pliku dla grupy.

S_IROTH : Nadaj uprawnienie do odczytu pliku innym (każdemu).

S_IWOTH : Nadaj uprawnienia do zapisu pliku innym (każdemu).

S_IXOTH : Nadaj uprawnienia do wykonywania pliku innym (każdemu).

Jeśli znasz już uprawnienia do plików w systemie Unix, te flagi powinny być dla Ciebie zrozumiałe. Jeśli nie mają one sensu, oto krótki kurs dotyczący uprawnień do plików Unix. Każdy plik ma właściciela i grupę. Wartości te mogą być wyświetlane za pomocą ls -l i są pokazane poniżej w następujących wynikach.

reader@hacking:~/booksrc $ ls -l /etc/passwd simplenote*

-rw-r--r-- 1 root root 1424 2007-09-06 09:45 /etc/passwd

-rwxr-xr-x 1 reader reader 8457 2007-09-07 02:51 simplenote

-rw------- 1 reader reader 1872 2007-09-07 02:51 simplenote.c

reader@hacking:~/booksrc $

W przypadku pliku / etc / passwd właścicielem jest root, a grupa jest również rootem. W przypadku pozostałych dwóch plików Simplenote właścicielem jest czytelnik, a grupą są użytkownicy. Prawa odczytu, zapisu i wykonywania można włączać i wyłączać dla trzech różnych pól: użytkownika, grupy i innych. Uprawnienia użytkowników opisują, co może zrobić właściciel pliku (czytać, pisać i / lub wykonywać), uprawnienia grupowe opisują, co mogą zrobić użytkownicy w tej grupie, a inne uprawnienia opisują, co mogą robić inni użytkownicy. Te pola są również wyświetlane na przedzie wyjścia ls -l. Najpierw wyświetlane są uprawnienia do odczytu / zapisu / wykonywania, przy użyciu r dla odczytu, w dla zapisu, x dla wykonania, oraz - dla wyłączenia. Następne trzy znaki wyświetlają uprawnienia grupowe, a ostatnie trzy znaki dla pozostałych uprawnień. . W powyższym wyjściu program simplenote ma włączone wszystkie trzy uprawnienia użytkownika (pogrubione). Każde pozwolenie odpowiada bitowej chorągiewce; odczyt to 4 (100 w binarnym), zapis to 2 (010 w binarnym), a wykonanie to 1 (001 w binarnym). Ponieważ każda wartość zawiera tylko unikalne bity, bitowa operacja OR osiąga ten sam rezultat, co dodanie razem tych liczb. Wartości te można dodać, aby zdefiniować uprawnienia dla użytkownika, grupy i innych za pomocą polecenia chmod.

reader@hacking:~/booksrc $ chmod 731 simplenote.c

reader@hacking:~/booksrc $ ls -l simplenote.c

-rwx-wx--x 1 reader reader 1826 2007-09-07 02:51 simplenote.c

reader@hacking:~/booksrc $ chmod ugo-wx simplenote.c

reader@hacking:~/booksrc $ ls -l simplenote.c

-r-------- 1 reader reader 1826 2007-09-07 02:51 simplenote.c

reader@hacking:~/booksrc $ chmod u+w simplenote.c

reader@hacking:~/booksrc $ ls -l simplenote.c-rw------- 1 reader reader 1826 2007-09-07 02:51 simplenote.c

reader@hacking:~/booksrc $

Pierwsze polecenie (chmod 721) daje uprawnienia do odczytu, zapisu i wykonywania dla użytkownika, ponieważ pierwsza liczba to 7 (4 + 2 + 1), wpisz i wykonaj uprawnienia do grupy, ponieważ druga liczba to 3 (2 + 1 ) i wykonuj uprawnienia tylko dla innych, ponieważ trzecia liczba to 1. Uprawnienia można również dodawać lub odejmować za pomocą polecenia chmod. W następnym poleceniu chmod, argument ugo-wx oznacza Odejmij prawa zapisu i wykonywania od użytkownika, grupy i innych. Komenda final chmod u + w daje użytkownikowi uprawnienia do zapisu. W programie simplenote funkcja open () używa S_IRUSR | S_IWUSR dla swojego dodatkowego argumentu zezwalającego, co oznacza, że plik / tmp / notes powinien mieć uprawnienia do odczytu i zapisu tylko podczas jego tworzenia.

reader@hacking:~/booksrc $ ls -l /tmp/notes

-rw------- 1 reader reader 36 2007-09-07 02:52 /tmp/notes

reader@hacking:~/booksrc $



Powrót

29.06.2020

ID użytkownika

Każdy użytkownik systemu uniksowego ma unikalny numer identyfikacyjny użytkownika. Ten identyfikator użytkownika można wyświetlić za pomocą polecenia id.

reader@hacking:~/booksrc $ id reader

uid=999(reader) gid=999(reader)

groups=999(reader),4(adm),20(dialout),24(cdrom),25(floppy),29(audio),30(dip),4

4(video),46(plugdev),104(scanner),112(netdev),113(lpadmin),115(powerdev),117(admin)

reader@hacking:~/booksrc $ id matrix

uid=500(matrix) gid=500(matrix) groups=500(matrix)

reader@hacking:~/booksrc $ id root

uid=0(root) gid=0(root) groups=0(root)

reader@hacking:~/booksrc $

Użytkownik root z identyfikatorem użytkownika 0 jest jak konto administratora, które ma pełny dostęp do systemu. Komenda su może zostać użyta do przełączenia się do innego użytkownika, a jeśli to polecenie jest uruchamiane jako root, można to zrobić bez hasła. Komenda sudo zezwala na uruchamianie pojedynczego polecenia jako użytkownik root. Na LiveCD, sudo zostało skonfigurowane tak, aby mogło być wykonane bez hasła, dla uproszczenia. Te polecenia zapewniają prostą metodę szybkiego przełączania się między użytkownikami.

reader@hacking:~/booksrc $ sudo su jose

jose@hacking:/home/reader/booksrc $ id

uid=501(jose) gid=501(jose) groups=501(jose)

jose@hacking:/home/reader/booksrc $

Jako użytkownik jose program simplenote będzie działał jako jose, jeśli zostanie wykonany, ale nie będzie miał dostępu do pliku / tmp / notes. Ten plik jest własnością czytelnika użytkownika i zezwala tylko na uprawnienia do odczytu i zapisu dla jego właściciela.

jose@hacking:/home/reader/booksrc $ ls -l /tmp/notes

-rw------- 1 reader reader 36 2007-09-07 05:20 /tmp/notes

jose@hacking:/home/reader/booksrc $ ./simplenote "a note for jose"

[DEBUG] buffer @ 0x804a008: 'a note for jose'

[DEBUG] datafile @ 0x804a070: '/tmp/notes'

[!!] Fatal Error in main() while opening file: Permission denied

jose@hacking:/home/reader/booksrc $ cat /tmp/notes

cat: /tmp/notes: Permission denied

jose@hacking:/home/reader/booksrc $ exit

exit

reader@hacking:~/booksrc $

Jest to w porządku, jeśli czytelnik jest jedynym użytkownikiem programu simplenote; Jednak wiele razy użytkownicy muszą mieć dostęp do niektórych fragmentów tego samego pliku. Na przykład plik / etc / passwd zawiera informacje o koncie dla każdego użytkownika w systemie, w tym domyślną powłokę logowania każdego użytkownika. Komenda chsh pozwala dowolnemu użytkownikowi zmienić własną powłokę logowania. Ten program musi mieć możliwość wprowadzania zmian w pliku / etc / passwd, ale tylko w linii odnoszącej się do konta bieżącego użytkownika. Rozwiązaniem tego problemu w systemie Unix jest uprawnienie Set ID (setuid). Jest to dodatkowy bit uprawnień do pliku, który można ustawić za pomocą polecenia chmod. Kiedy program z tą flagą jest wykonywany, działa jako identyfikator użytkownika właściciela pliku.

reader@hacking:~/booksrc $ which chsh

/usr/bin/chsh

reader@hacking:~/booksrc $ ls -l /usr/bin/chsh /etc/passwd

-rw-r--r-- 1 root root 1424 2007-09-06 21:05 /etc/passwd

-rwsr-xr-x 1 root root 23920 2006-12-19 20:35 /usr/bin/chsh

reader@hacking:~/booksrc $

Program chsh ma ustawioną flagę setuid, która jest wskazywana przez s na wyjściu ls powyżej. Ponieważ ten plik jest własnością root i ma ustawiony zestaw uprawnień, program będzie działał jako użytkownik root, gdy dowolny użytkownik uruchomi ten program. Plik / etc / passwd, do którego zapisuje chsh, również jest własnością root'a i pozwala tylko właścicielowi na zapisanie do niego. Logika programowa w chsh jest zaprojektowana tak, aby zezwalała tylko na pisanie do linii w / etc / passwd, która odpowiada użytkownikowi uruchamiającemu program, nawet jeśli program działa skutecznie jako root. Oznacza to, że uruchomiony program ma zarówno rzeczywisty identyfikator użytkownika, jak i efektywny identyfikator użytkownika. Te identyfikatory można pobrać za pomocą funkcji getuid () i geteuid (), odpowiednio, jak pokazano w uid_demo.c.

uid_demo.c

#include < stdio.h >

int main() {

printf("real uid: %d\n", getuid());

printf("effective uid: %d\n", geteuid());

}

Wyniki kompilowania i wykonywania uid_demo.c są następujące.

reader@hacking:~/booksrc $ gcc -o uid_demo uid_demo.c

reader@hacking:~/booksrc $ ls -l uid_demo

-rwxr-xr-x 1 reader reader 6825 2007-09-07 05:32 uid_demo

reader@hacking:~/booksrc $ ./uid_demo

real uid: 999

effective uid: 999

reader@hacking:~/booksrc $ sudo chown root:root ./uid_demo

reader@hacking:~/booksrc $ ls -l uid_demo

-rwxr-xr-x 1 root root 6825 2007-09-07 05:32 uid_demo

reader@hacking:~/booksrc $ ./uid_demo

real uid: 999

effective uid: 999

reader@hacking:~/booksrc $

W danych wyjściowych dla uid_demo.c pokazano, że oba identyfikatory użytkownika mają wartość 999, gdy uid_demo jest wykonywane, ponieważ 999 jest identyfikatorem użytkownika dla czytnika. Następnie polecenie sudo jest używane z poleceniem chown, aby zmienić właściciela i grupę uid_demo na root. Program może być nadal wykonywany, ponieważ ma uprawnienia do wykonywania dla innych, i pokazuje, że oba identyfikatory użytkownika pozostają 999, ponieważ to nadal jest identyfikator użytkownika.

reader@hacking:~/booksrc $ chmod u+s ./uid_demo

chmod: changing permissions of `./uid_demo': Operation not permitted

reader@hacking:~/booksrc $ sudo chmod u+s ./uid_demo

reader@hacking:~/booksrc $ ls -l uid_demo



-rwsr-xr-x 1 root root 6825 2007-09-07 05:32 uid_demo

reader@hacking:~/booksrc $ ./uid_demo

real uid: 999

effective uid: 0

reader@hacking:~/booksrc $

Ponieważ program jest teraz własnością roota, sudo musi być użyty do zmiany uprawnień do pliku na nim. Komenda chmod u + s włącza uprawnienie setuid, które można zobaczyć w następującym pliku wyjściowym ls -l. Teraz, gdy czytnik użytkownika wykonuje polecenie uid_demo, efektywny identyfikator użytkownika ma wartość 0 dla roota, co oznacza, że program może uzyskać dostęp do plików jako root. W ten sposób program chsh może zezwolić każdemu użytkownikowi na zmianę swojej powłoki logowania przechowywanej w / etc / passwd. Tę samą technikę można zastosować w programie do robienia notatek dla wielu użytkowników. Następny program będzie modyfikacją programu simplenote; zapisze także identyfikator użytkownika oryginalnego autora każdej notatki. Ponadto zostanie wprowadzona nowa składnia dla #include. Funkcje ec_malloc () i fatal () były przydatne w wielu naszych programach. Zamiast kopiować i wklejać te funkcje do każdego programu, można je umieścić w osobnym pliku włączającym. W poprzednim wyjściu program notetaker jest kompilowany i zmieniany na własność root, a ustawione jest ustawienie setuid. Teraz, gdy program jest wykonywany, program działa jako użytkownik root, więc plik / var / notes jest również własnością root, gdy jest tworzony.

reader@hacking:~/booksrc $ cat /var/notes

cat: /var/notes: Permission denied

reader@hacking:~/booksrc $ sudo cat /var/notes

?

this is a test of multiuser notes

reader@hacking:~/booksrc $ sudo hexdump -C /var/notes

00000000 e7 03 00 00 0a 74 68 69 73 20 69 73 20 61 20 74 |.....this is a t|

00000010 65 73 74 20 6f 66 20 6d 75 6c 74 69 75 73 65 72 |est of multiuser|

00000020 20 6e 6f 74 65 73 0a | notes.|

00000027

reader@hacking:~/booksrc $ pcalc 0x03e7

999 0x3e7 0y1111100111

reader@hacking:~/booksrc $

Plik / var / notes zawiera identyfikator użytkownika czytnika (999) i notatkę. Z powodu architektury mało-endianowej 4 bajty liczby całkowitej 999 są odwrócone w systemie szesnastkowym (pogrubione powyżej). Aby normalny użytkownik mógł odczytać dane notatki, potrzebny jest odpowiedni program root. Program notesearch.c odczyta dane notatki i wyświetli tylko notatki zapisane przez ten identyfikator użytkownika. Dodatkowo można podać opcjonalny argument wiersza polecenia dla ciągu wyszukiwania. Kiedy to jest używane, będą wyświetlane tylko notatki pasujące do wyszukiwanego ciągu.(…c.d.)

Powrót

30.06.2020

notesearch.c

#include < stdio.h >

#include < string.h >

#include < fcntl.h >

#include < sys/stat.h >

#include "hacking.h"

#define FILENAME "/var/notes"

int print_notes(int, int, char *); // Note printing function.

int find_user_note(int, int); // Seek in file for a note for user.

int search_note(char *, char *); // Search for keyword function.

void fatal(char *); // Fatal error handler

int main(int argc, char *argv[]) {

int userid, printing=1, fd; // File descriptor

char searchstring[100];

if(argc > 1) // If there is an arg,

strcpy(searchstring, argv[1]); // that is the search string;

else // otherwise,

searchstring[0] = 0; // search string is empty.

userid = getuid();

fd = open(FILENAME, O_RDONLY); // Open the file for read-only access.

if(fd == -1)

fatal("in main() while opening file for reading");

while(printing)

printing = print_notes(fd, userid, searchstring);

printf("-------[ end of note data ]-------\n");

close(fd);

}

// A function to print the notes for a given uid that match

// an optional search string;

// returns 0 at end of file, 1 if there are still more notes.

int print_notes(int fd, int uid, char *searchstring) {

int note_length;

char byte=0, note_buffer[100];

note_length = find_user_note(fd, uid);

if(note_length == -1) // If end of file reached,

return 0; // return 0.

read(fd, note_buffer, note_length); // Read note data.

note_buffer[note_length] = 0; // Terminate the string.

if(search_note(note_buffer, searchstring)) // If searchstring found,

printf(note_buffer); // print the note.

return 1;

}

// Funkcja znajdująca następną notatkę dla danego ID użytkownika;

// zwraca -1, jeśli osiągnięty zostanie koniec pliku;

// w przeciwnym razie zwraca długość znalezionej notatki.

int find_user_note(int fd, int user_uid) {

int note_uid=-1;

unsigned char byte;

int length;

while(note_uid != user_uid) { // Loop until a note for user_uid is found.

if(read(fd, ¬e_uid, 4) != 4) // Read the uid data.

return -1; // If 4 bytes aren't read, return end of file code.

if(read(fd, &byte, 1) != 1) // Read the newline separator.

return -1;

byte = length = 0;

while(byte != '\n') { // Figure out how many bytes to the end of line.

if(read(fd, &byte, 1) != 1) // Read a single byte.

return -1; // If byte isn't read, return end of file code.

length++;

}

}

lseek(fd, length * -1, SEEK_CUR); // Rewind file reading by length bytes.

printf("[DEBUG] found a %d byte note for user id %d\n", length, note_uid);

return length;

}



// A function to search a note for a given keyword;

// returns 1 if a match is found, 0 if there is no match.

int search_note(char *note, char *keyword) {

int i, keyword_length, match=0;

keyword_length = strlen(keyword);

if(keyword_length == 0) // If there is no search string,

return 1; // always "match".

for(i=0; i < strlen(note); i++) { // Iterate over bytes in note.

if(note[i] == keyword[match]) // If byte matches keyword,

match++; // get ready to check the next byte;

else { // otherwise,

if(note[i] == keyword[0]) // if that byte matches first keyword byte,

match = 1; // start the match count at 1.

else

match = 0; // Otherwise it is zero.

}

if(match == keyword_length) // If there is a full match,

return 1; // return matched.

}

return 0; // Return not matched.

}

Większość tego kodu powinna mieć sens, ale są pewne nowe koncepcje. Nazwa pliku jest zdefiniowana u góry, zamiast używać pamięci sterty. Ponadto, funkcja lseek () służy do przewijania pozycji odczytu w pliku. Wywołanie funkcji lseek (fd, długość * -1, SEEK_CUR); mówi programowi, aby przesunął pozycję odczytu do przodu z bieżącej pozycji w pliku o długość * -1 bajty. Ponieważ okazuje się, że jest to liczba ujemna, położenie jest cofane o długość bajtów.

reader@hacking:~/booksrc $ gcc -o notesearch notesearch.c

reader@hacking:~/booksrc $ sudo chown root:root ./notesearch

reader@hacking:~/booksrc $ sudo chmod u+s ./notesearch

reader@hacking:~/booksrc $ ./notesearch

[DEBUG] found a 34 byte note for user id 999

this is a test of multiuser notes

-------[ end of note data ]-------

reader@hacking:~/booksrc $

Podczas kompilacji i setuid root program do analizy notek działa zgodnie z oczekiwaniami. Ale to tylko pojedynczy użytkownik; co się dzieje, gdy inny użytkownik korzysta z programów Notetaker i NoteSearch?

reader@hacking:~/booksrc $ sudo su jose

jose@hacking:/home/reader/booksrc $ ./notetaker "This is a note for jose"

[DEBUG] buffer @ 0x804a008: 'This is a note for jose'

[DEBUG] datafile @ 0x804a070: '/var/notes'

[DEBUG] file descriptor is 3

Note has been saved.

jose@hacking:/home/reader/booksrc $ ./notesearch

[DEBUG] found a 24 byte note for user id 501

This is a note for jose

-------[ end of note data ]-------

jose@hacking:/home/reader/booksrc $

Gdy użytkownik jose używa tych programów, rzeczywisty identyfikator użytkownika to 501. Oznacza to, że wartość jest dodawana do wszystkich notatek zapisanych za pomocą notecakera, a tylko notatki z pasującym identyfikatorem użytkownika będą wyświetlane przez program notatek

reader@hacking:~/booksrc $ ./notetaker "This is another note for the reader user"

[DEBUG] buffer @ 0x804a008: 'This is another note for the reader user'

[DEBUG] datafile @ 0x804a070: '/var/notes'

[DEBUG] file descriptor is 3

Note has been saved.

reader@hacking:~/booksrc $ ./notesearch

[DEBUG] found a 34 byte note for user id 999

this is a test of multiuser notes

[DEBUG] found a 41 byte note for user id 999

This is another note for the reader user

-------[ end of note data ]-------

reader@hacking:~/booksrc $

Podobnie wszystkie notatki dla czytnika użytkownika mają przypisany identyfikator użytkownika 999. Chociaż zarówno programy notujące, jak i notujące są suidroot i mają pełny dostęp do odczytu / zapisu do pliku danych / var / notes, logika programu w programie notatek nie pozwala na przeglądanie uwag innych użytkowników. Jest to bardzo podobne do tego, w jaki sposób plik / etc / passwd przechowuje informacje o użytkowniku dla wszystkich użytkowników, jednak programy takie jak chsh i passwd pozwalają każdemu użytkownikowi zmienić jego własną powłokę lub hasło.



Powrót