# Repetytorium z ASK 2024 Notatki są na razie w szczątkowej wersji. Wszelkie uwagi proszę przekazywać do Artura Kraski. ## Repetytorium 1, 20.02.2024 Link do Godbolt: https://godbolt.org/ Godbolt jest to kompilator online, który upraszcza nam generowanie kodu w asemblerze na potrzeby zajęć. Przykładowo, zbadajmy działanie słowa kluczowego `volatile`. Weźmy funkcję: ```c= int fun() { int volatile x = 0; for(int i=0; i<1000; i++) x += 3; return x; } ``` Kompilując go z flagą `-O2` zauważymy, że kod będzie wykonywał pętlę (skok `jne`) 1000 razy. Jednakże po usunięciu słowa kluczowego volatile możemy zauważyć, że kod upraszcza się do jednej instrukcji. ## Repetytorium 2, 27.02.2024 Aby uzyskać zapis bitowy liczby zmiennopozycyjnej w zmiennej całkowitoliczbowej, możemy na przykład zrzutować sobie tą zmienną jako wskaźnik: ```c= #include <stdio.h> int main() { float f = 15213; int i = *(int *)&f; printf("0x%X\n", i); printf("0b%032b\n", i); int x = 0xC0A00000; float f2 = *(float *)&x; printf("%f\n", f2); return 0; } ``` ## Repetytorium 3, 05.03.2024 Weźmy dwa poniższe pliki. `main.c`: ```c= #include <stdio.h> int fun(int a, int b); int main() { int x, y; scanf("%d %d", &x, &y); printf("%d\n", fun(x, y)); return 0; } ``` `plik.c`: ```c= int fun(int a, int b) { return a + b; } ``` Oczywiście, możemy je razem skompilować poleceniem `gcc main.c plik.c`. Wygeneruje nam to plik wykonywalny `a.out`, który dodaje dwie podane na wejściu liczby. Jednakże kompilator potrafi też wygenerować kod asemblera powyższych plików. Możemy na przykład wykonać komendę `gcc plik.c -S -O2` (flagę `-O2` dodano, aby kod był ładniejszy). Komenda ta wygeneruje nam plik `plik.s`, który zawiera kod w asemblerze. Po wyrzuceniu z niego mniej lub bardziej zbędnych rzeczy wygląda on mniej więcej tak: ```asm= .globl fun .type fun, @function fun: leal (%rdi,%rsi), %eax ret .size fun, .-fun ``` Kod ten możemy modyfikować, a nawet uruchomić go łącznie z plikiem `main.c`, napisanym w języku `c`. Podmieńmy ten kod na poniższy: ```asm= .globl fun .type fun, @function fun: mov %rdi, %rax sub %rsi, %rax ret .size fun, .-fun ``` Kod ten możemy skompilować poleceniem: `gcc plik.s main.c` W efekcie otrzymaliśmy kod, który zamiast dodawać, to odejmuje. ## Repetytorium 4, 12.03.2024 `Application Binary Interface` - dokument, w którym znajdują się wszystkie konwencje dotyczące programowania w assemblerze. Można tam znaleźć między innymi informację jak przekazywać argumenty do funkcji, albo jak zwracać z niej wartość. Link do ABI: https://github.com/hjl-tools/x86-psABI/wiki/x86-64-psABI-1.0.pdf Dokumentacja instrukcji asemblerowych: https://www.felixcloutier.com/x86/ (Uwaga: powyższa dokumentacja jest w formacie intelowym, co oznacza, że w formacie AT&T argumenty powinny być podawane w odwrotnej kolejności). Aby najłatwiej podejrzeć tablicę skoków wygenerowaną przez instrukcję switch, możemy użyć godbolta. Przykładowo, biorąc funkcję: ```c= int fun(int x) { int y = 0; switch(x) { case 0: case 4: y += 7; case 3: y += 100; break; case 6: y += 20; case 5: y += 1000; default: y += 6; } return y; } ``` możemy zobaczyć jak wygląda przykładowa tablica skoków. ## Repetytorium 5, 19.03.2024 `gdb` jest konsolowym debugerem do asemblera. Przed pierwszym użyciem zalecamy instalację nakładki `gdb dashbord` albo podobnej. Link: https://github.com/cyrus-and/gdb-dashboard Skrótowy opis instrukcji znajduje się w pliku: https://skos.ii.uni.wroc.pl/mod/resource/view.php?id=39437 Aby podejrzeć co siędzieje na stosie podczas wywołań rekurencyjnych, możemy wziąć sobie kod: ```c= #include <stdio.h> long long fib(long long n) { if (n < 2) return 1; return fib(n - 1) + fib(n - 2); } long long silnia(long long n) { if (n == 0) return 1; return n * silnia(n - 1); } int main() { long long n; scanf("%lld", &n); long long r = fib(n); printf("%lld\n", r); return 0; } ``` Po skompilowaniu `gcc fib.c -o fib -Og -g` (flaga `-g` dodaje przydatne informacje dla debugera, między innymi pozwala śledzić jednocześnie kod w c i asemblerze) możemy go uruchomić poleceniem `gdb fib`. Polecenie `break fib` ustawi nam breakpoint na początku funkcji `fib`. Teraz możemy instrukcją `run` uruchomić program. Na początku zapyta nas on o liczbę -- argument funkcji. Po jego podaniu pokaże się widok dashboard, gdzie będziemy mogli zobaczyć w jakiej jesteśmy aktualnie linijce albo jakie są wartości rejestrów. Instrukcja `continue` pozwala nam przejść do kolejnego uruchomienia funkcji `fib`. Możemy odpalić ją kilka/kilkanaście razy. Będą wtedy widoczne aktualne rekurencyjne wywołania funkcji na stosie. Sam stos można też podejrzeć instrukcją `x/20gx $rsp`. Będziemy mogli tam zobaczyć między innymi odłożone argumenty, wyniki częściowe albo adresy powrotu. `gdb` posiada też wiele innych przydatnych instrukcji pozwalających stawiać breakpointy w dowolnej linijce, posuwać się po jednej instrukcji c/asemblera albo podglądać dowolne miejsce w pamięci. ## Repetytorium 6, 26.03.2024 ## Repetytorium 7, 09.04.2024 Poruszna tematyka: 1. Ułożenie tablic (wielowymiarowych) w pamięci 2. Ułożenie struktur w pamięci 3. Instrukcje zmiennopozycyjne w procesorze Przykładowy plik pozwalający nam podejrzeć jak w pamięci ułożone są tablice albo struktury. ```c= #include <stdio.h> #include <stddef.h> int A[3][4][5]; struct B { int a; int b; char c; short d; }; int main() { printf("%lld\n", (long long)&A[1] - (long long)&A[0][0][0]); A[0][1][2] = 7; int *A2 = (int *)A; for (int i = 0; i < 10; i++) { printf("%d: %d\n", i, A2[i]); } struct B b; printf("size: %d\n", (int)sizeof(b)); printf("align a: %d\n", (int)alignof(b.a)); printf("align b: %d\n", (int)alignof(b.b)); printf("align c: %d\n", (int)alignof(b.c)); printf("align d: %d\n", (int)alignof(b.d)); printf("offset a: %d\n", (int)offsetof(struct B, a)); printf("offset b: %d\n", (int)offsetof(struct B, b)); printf("offset c: %d\n", (int)offsetof(struct B, c)); printf("offset d: %d\n", (int)offsetof(struct B, d)); printf("%d\n", (long long)&b.d - (long long)&b); return 0; } ``` ## Repetytorium 8, 23.04.2024 Poruszana tematyka: 1. Relokacje 2. Czym różnią się pliki relokowalne i wykonywalne 3. Format plików ELF, co jest w środku. 4. Sekcje, symbole (silne i słabe), rekordy relokacji 5. Biblioteki Plik `plik.c`: ```c= int var = 1000; int tab[1000]; int fun(int a, int b) { static int x = 7; x++; return a + b + var + x; } ``` Plik `main.c` ```c= #include <stdio.h> extern int var; int fun(int a, int b); int main() { int x, y; scanf("%d %d", &x, &y); x += var; y += var; printf("%d\n", fun(x, y)); return 0; } ``` Żaden z powyższych plików nie da się skompilować samodzielnie. Jednakże z obu można stworzyć pliki relokowalne za pomocą flagi `-c`. Przydatne komendy: * `gcc main.c plik.c -O2 -fno-pic -no-pie` - kompilacja plików (dwie ostatnie flagi sprawiają, że adresy w pliku wykonywalnym są już ustalone), generuje plik `a.out` * `gcc plik.c -O2 -c` - tworzenie pliku relokowalnego, generuje plik `plik.o` * `objdump -d plik.o` - podejrzenie kodu w pliku relokowalnym * `objdump -d a.out` - podejrzenie kodu pliku wykonywalnego * `readelf -a plik.o` - podejrzenie podstawowych informacji o pliku * `objdump -x plik.o` - podejrzenie podstawowych informacji o pliku * `objdump -r main.o` - podejrzenie rekordów relokacji w pliku (można połączyć z flagą `-d`) * `readelf -S plik.o` - podejrzenie sekcji (i ich numerów) * `readelf -s plik.o` - podejrzenie informacji o symbolach * `readelf -x 3 plik.o` - podejrzenie zawartości konkretnej sekcji ## Repetytorium 9, 30.04.2024 #### Podatność kodu na ataki ## Repetytorium 10, 07.05.2024 #### Pamięć operacyjna i dyskowa ## Repetytorium 11, 14.05.2024 #### Pamięć podręczna Parametry dotyczące rozmiarów cache na własnym komputerze można sprawdzić za pomocą polecenia: `getconf -a | grep CACHE` Przykładowo, na moim komputerze można odczytać parametry L1: ``` LEVEL1_DCACHE_SIZE 32768 LEVEL1_DCACHE_ASSOC 8 LEVEL1_DCACHE_LINESIZE 64 ``` Oznacza to, że mój cache ma rozmiar $2^{15}$ bajtów, każda linia ma $2^6$ bajtów, a w każdym zbiorze jest $2^3$ linii. Mozna z tego wyliczyć, że cache zawiera $2^6$ zbiorów. Parametry cache można też wywnioskować analizując czas działąnia programów robiących odczyty z pamięci. Przykładowo, weźmy poniższy program. ```c= #include <stdio.h> int mem[2200007]; const int skok = (1 << 12); const int maks = (1 << 16); const int ile = 1000000000; int main(void) { int i = 0; int res = 0; while (i < ile) { for (int j = 0; j < maks; j += skok, i++) mem[j]++; } return 0; } ``` Program ten wykonuje w kółko kolejne odwołania do pamięci, skacząc o zadaną ilość komórek w tablicy. Można zauważyć, że na przykład po zmianie wartości zmiennej `skok` na `(1 << 12)+16`, nasz program przyspieszy kilkukrotnie, ze względu, że w tym wypadku program dużo bardziej optymalnie korzysta z wielu zbiorów w pamięci podręcznej.