# 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.