---
# System prepended metadata

title: Notatki z repetytorium z ASK 2024

---

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

