Języki programowania na platformie .NET / Programowanie .NET
Rok akademicki: 2026/27
Instrukcja laboratoryjna nr 1: MSIL, Zarządzanie Pamięcią i Ekosystem Narzędzi .NET (CLI, NuGet, Cake, Docker)
Prowadzący: Tomasz Goluch
Wersja: 1.2
1. Wprowadzenie
Cel: Zrozumienie procesu kompilacji kodu w ekosystemie .NET, architektury Common Intermediate Language (CIL/MSIL), mechanizmów środowiska uruchomieniowego (CLR) oraz ewolucji od klasycznego .NET Framework do wieloplatformowego .NET 8/9+.
W klasycznych językach kompilowanych bezpośrednio do kodu maszynowego (np. C, C++) wynikiem kompilacji jest plik binarny ściśle powiązany z architekturą procesora (np. x86, x64, ARM64) oraz interfejsem jądra konkretnego systemu operacyjnego. Platforma .NET wykorzystuje architekturę dwuetapowej kompilacji:
- Kompilacja statyczna do kodu pośredniego: Kod
źródłowy (C#, F#, VB.NET) jest tłumaczony przez kompilator języka
(kompilator
Roslyn–csc.exedla C#) na uniwersalny kod pośredni – CIL (ang. Common Intermediate Language, historycznie znany jako MSIL) oraz metadane precyzyjnie opisujące typy, składowe klas i atrybuty. Wynikiem kompilacji jest zestaw (ang. Assembly, plik.dlllub.exe). - Kompilacja w locie (JIT – Just-In-Time): Podczas uruchamiania aplikacji maszyna wirtualna – środowisko uruchomieniowe CLR (ang. Common Language Runtime, we współczesnym .NET: CoreCLR) – ładuje zestaw, weryfikuje poprawność typów, a następnie za pomocą kompilatora JIT kompiluje instrukcje MSIL na natywny kod maszynowy procesora bezpośrednio przed wykonaniem danej metody.
[ Kod źródłowy C# (.cs) ]
│
▼ (Roslyn / csc.exe / dotnet build)
[ Zestaw .NET Assembly (.dll / .exe) ]
├── Metadane (Manifest, Typy, Sygnatury)
└── Kod CIL / MSIL (Instrukcje oparte na stosie ewaluacyjnym)
│
▼ (Środowisko uruchomieniowe CoreCLR)
[ Kompilator JIT (Just-In-Time) ] ──► [ Natywny kod maszynowy (x64 / ARM64) ]
🧠 Zrozumieć różnicę pokoleniową: .NET Framework vs .NET 8+
W klasycznym .NET Framework (wersje 1.0 – 4.8.1) środowisko było nierozerwalnie związane z systemem Windows, a wszystkie biblioteki systemowe znajdowały się w jednym, monolitycznym miejscu (mscorlib.dll, globalna pamięć podręczna GAC).
Od wersji .NET Core, a obecnie w .NET 8 (LTS) i .NET 9, platforma jest w pełni modułowa, otwartoźródłowa i wieloplatformowa. Zrezygnowano z globalnych rejestrów systemowych na rzecz izolowanych pakietów NuGet oraz zestawów referencyjnych (Ref Packs), eliminując problem konfliktów wersji (tzw. DLL Hell).
2. Środowisko Visual Studio Developer Command Prompt & PowerShell
Cel: Opanowanie niskopoziomowych narzędzi deweloperskich .NET, konfiguracja zmiennych środowiskowych i kompilacja manualna.
Standardowy wiersz poleceń systemu Windows (cmd.exe lub
powershell.exe) nie ma w domyślnej zmiennej
PATH dostępu do specjalistycznych kompilatorów i
dezasemblerów dostarczanych wraz z Visual Studio. Developer
Command Prompt / PowerShell to powłoka z zainicjalizowanymi
zmiennymi środowiskowymi (INCLUDE, LIB,
PATH), umożliwiającymi bezpośrednie wywoływanie narzędzi
SDK. Wersja PowerShell pozwala dodatkowo na przekazywanie obiektów i
danych pomiędzy kolejnymi poleceniami (cmdlets).
1. Uruchamianie środowiska deweloperskiego
Z menu Start:
Visual Studio 2022→Developer Command Prompt for VS 2022lubDeveloper PowerShell for VS 2022.Z menu Visual Studio: Menu górne →
Narzędzia (Tools)→Wiersz polecenia (Command Line)→Wiersz polecenia dla deweloperówlubProgram PowerShell dla deweloperów.Ręczna inicjalizacja w dowolnym terminalu: Wywołanie odpowiedniego skryptu wsadowego z katalogu instalacyjnego Visual Studio (ścieżka dla VS 2022 Enterprise):
cd "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build"Dostępne konfiguracje środowiska docelowego:
vcvars64.bat– narzędzia natywne dla architektury 64-bitowej (x64) tworzące kod 64-bitowy.vcvars32.bat– narzędzia natywne dla architektury 32-bitowej (x86) tworzące kod 32-bitowy.vcvarsamd64_x86.bat– kompilacja krzyżowa (host 64-bit, target 32-bit).vcvarsx86_amd64.bat– kompilacja krzyżowa (host 32-bit, target 64-bit).
💡 Wyjaśnienie: Czym są skrypty vcvars*.bat, czym różni się architektura gospodarza (Host) od docelowej (Target) i kiedy ich używać? (Kliknij, aby rozwinąć/zwinąć)
1. Rola skryptów
vcvars: Izolacja środowiskaFirma Microsoft nie dodaje ścieżek do kompilatorów i linkerów do globalnej zmiennej systemowej
PATH. Gdyby tak zrobiono, instalacja kilku wersji Visual Studio (np. VS 2019 i VS 2022) doprowadziłaby do konfliktów wersji narzędzi.Zamiast tego Visual Studio dostarcza skrypty wsadowe (
.bat), które konfigurują zmienne środowiskowe wyłącznie dla bieżącej, otwartej sesji terminala:PATH– dopisuje ścieżki do kompilatorów (cl.exe,csc.exe), dezasemblerów (ildasm.exe), narzędzi inspekcji binarnej (dumpbin.exe) i konsolidatora (link.exe).INCLUDE– wskazuje ścieżki do nagłówków biblioteki C Runtime (CRT) oraz Windows SDK.LIB/LIBPATH– wskazuje katalogi z bibliotekami importowymi (.lib) dla konsolidatora.
2. Architektura gospodarza (Host) vs Architektura docelowa (Target)
Kompilator to również zwykły program wykonywalny (
.exe). Musi na jakimś procesorze działać (to jest Host) i dla jakiegoś procesora generować kod binarny (to jest Target):- Host (Gospodarz): Architektura maszyny, na której aktualnie siedzisz i uruchamiasz proces kompilacji (dzisiaj w 99% przypadków jest to 64-bitowy procesor x64/AMD64).
- Target (Cel): Architektura maszyny, na której ma zostać uruchomiony wynikowy program (np. serwer 64-bitowy x64, stary terminal 32-bitowy x86, procesor ARM w smartfonie).
Skrypt Host (Gospodarz) Target (Cel) Typ kompilacji Kiedy stosować? vcvars64.batx64 (64-bit) x64 (64-bit) Natywna Zalecany standard. Używasz 64-bitowego systemu Windows i tworzysz 64-bitowe aplikacje. Sam proces kompilatora może zaadresować więcej niż 4 GB pamięci RAM, co zapobiega błędom Out of Memory przy potężnych projektach. vcvarsamd64_x86.batx64 (64-bit) x86 (32-bit) Krzyżowa (Cross) Twój komputer ma system 64-bitowy, ale musisz wygenerować plik wykonywalny ściśle 32-bitowy (np. praca z legacy 32-bitowymi bibliotekami DLL, sterownikami przemysłowymi lub komponentami COM). vcvars32.batx86 (32-bit) x86 (32-bit) Natywna Używany wyłącznie wtedy, gdy sam system operacyjny programisty jest 32-bitowy (rozwiązanie historyczne). vcvarsx86_amd64.batx86 (32-bit) x64 (64-bit) Krzyżowa (Cross) Uruchomienie 32-bitowego kompilatora na 32-bitowym systemie w celu wygenerowania kodu 64-bitowego (rozwiązanie historyczne).
3. Czy student musi ręcznie uruchamiać te pliki
.bat?- NIE, jeśli korzystasz z menu Start:
Skrót w menu Windows o nazwieDeveloper Command Prompt for VS 2022to nic innego jak zwykły terminalcmd.exe, który na starcie automatycznie wykonuje skrypt konfigurujący środowisko. - TAK, w codziennej pracy programistycznej, gdy:
- Otwierasz zwykły terminal w Visual Studio Code,
Windows Terminal lub PowerShell i chcesz w nim
skompilować kod natywny/CIL bez restartowania całego edytora. Wystarczy
wpisać w konsoli pełną ścieżkę do skryptu:
"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" - Tworzysz automatyczne skrypty budowania CI/CD (np. potoki PowerShell, GitHub Actions, GitLab CI), w których środowisko deweloperskie musi zostać zainicjalizowane przed wywołaniem narzędzi MSBuild/Roslyn.
4. Jak to się ma do platformy .NET i kodu C#?
Większość kodu w C# kompilowana jest domyślnie do konfiguracji
AnyCPU– co oznacza, że ten sam plik.dllz kodem MSIL może zostać uruchomiony jako 32-bitowy lub 64-bitowy w zależności od środowiska CLR.Wybór architektury narzędzi staje się jednak kluczowy, gdy:
- Twoja aplikacja .NET korzysta z mechanizmu P/Invoke
(odwołania do natywnych bibliotek C/C++ z użyciem
[DllImport]). Próba załadowania 32-bitowej biblioteki.dlldo 64-bitowego procesu .NET natychmiast zakończy się wyjątkiemBadImageFormatException. - Używasz narzędzia
dumpbin.exedo analizy nagłówków PE (Portable Executable) lub manipulujesz flagami bitowymi pliku wykonywalnego za pomocącorflags.exe.
⚠️ Uprawnienia administratora:
Zaleca się uruchamianie wiersza poleceń dewelopera z uprawnieniami administratora (Uruchom jako administrator). Będą one wymagane przez niektóre narzędzia systemowe (np.gacutil.exeprzy obsłudze klasycznych aplikacji).
3. Przegląd kluczowych narzędzi inżynierii wstecznej i kompilacji
1.
Kompilator języka C# (csc.exe) i silnik MSBuild
(msbuild.exe)
csc.exe: Odpowiada za bezpośrednią kompilację plików źródłowych.csdo postaci zestawu binarnego (.dlllub.exe).msbuild.exe: Narzędzie orkiestrujące proces budowania w oparciu o deklaratywne pliki projektów XML (.csproj). Automatycznie rozwiązuje drzewo zależności, odwołuje się do pakietów NuGet, zarządza zasobami i wywołujecsc.exez właściwymi parametrami.
Anatomia kompilacji: .NET Framework vs .NET 5+
W przypadku klasycznego .NET Framework kompilacja z wiersza poleceń była prosta, ponieważ kompilator domyślnie odwoływał się do monolitycznych bibliotek systemowych zainstalowanych w systemie operacyjnym:
csc.exe -out:JP_NET.Lab1a.dll Program.cs
W .NET 5+ zrezygnowano z jednej biblioteki systemowej na rzecz modularności. Funkcjonalności są podzielone na setki mniejszych zestawów (tzw. Contract Assemblies lub Ref Packs), co pozwala aplikacjom być lżejszymi i działać wieloplatformowo.
Ręczne wywołanie csc.exe dla aplikacji konsolowej .NET
5+ wymaga odcięcia bibliotek domyślnych (-noconfig,
-nostdlib) oraz jawnego wskazania wszystkich referencji
bibliotek bazowych za pomocą parametru -r (w wierszu
poleceń CMD znak karetki ^ pozwala przenieść długie
polecenie do kolejnej linii):
"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\Roslyn\csc.exe" ^
-noconfig ^
-nostdlib ^
-t:exe ^
-r:"C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.21\ref\net8.0\System.Runtime.dll" ^
-r:"C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.21\ref\net8.0\netstandard.dll" ^
-r:"C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.21\ref\net8.0\System.Console.dll" ^
-r:"C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.21\ref\net8.0\System.Runtime.InteropServices.dll" ^
-r:"C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.21\ref\net8.0\System.Reflection.dll" ^
-r:"C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.21\ref\net8.0\System.Diagnostics.FileVersionInfo.dll" ^
-out:Program.exe Program.cs
(Uwaga: Ścieżka wersji pakietu 8.0.21 zależy od
aktualnie zainstalowanego SDK na Twojej stacji roboczej – sprawdź
dokładny podkatalog w
C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\).
Ponadto, aby środowisko CoreCLR potrafiło uruchomić plik
Program.exe, w tym samym folderze musi znaleźć się plik
konfiguracyjny Program.runtimeconfig.json:
{
"runtimeOptions": {
"tfm": "net8.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "8.0.0"
}
}
}
Uruchomienie następuje poprzez hosta poleceniem:
dotnet Program.exe
Powyższy przykład dobitnie obrazuje, jak ogromną pracę orkiestracyjną automatyzuje za programistę silnik MSBuild oraz CLI!
2. Baza symboli
debugowania (*.pdb) i instrukcje nop
Podczas kompilacji w konfiguracji Debug (oraz
Release) powstają pliki Program Database
(*.pdb). Są to bazy danych zawierające informacje
niezbędne do efektywnego debugowania oraz diagnostyki awarii
aplikacji:
- Mapowanie instrukcji MSIL na konkretne wiersze i kolumny plików kodu
źródłowego (
.cs). - Lokalizację pułapek debugera (breakpoints, klawisz
F9w Visual Studio). - Nazwy oraz zakresy widoczności zmiennych lokalnych (w samym kodzie
CIL zmienne w dyrektywie
.locals initsą jedynie anonimowymi indeksami liczbowymi).
Dzięki plikom *.pdb, w przypadku wystąpienia
nieobsłużonego wyjątku, ślad stosu (stack trace) zawiera
precyzyjną nazwę pliku, numer wiersza oraz kolumnę, w której wystąpił
błąd:
- Bez pliku
*.pdbobok pliku wykonywalnego środowisko nie ma dostępu do metadanych kodu źródłowego i wyświetla jedynie przesunięcie binarne instrukcji IL:at JP_NET.Lab1.Program.Main() +0x001f - Gdy plik
*.pdbjest obecny, CLR tłumaczy przesunięcie IL i wyświetla pełną ścieżkę:at JP_NET.Lab1.Program.Main() in C:\Projects\Lab1\ProgA.cs:line 12, col 9
Rola instrukcji
nopw trybie Debug:
W kodzie CIL zestawu skompilowanego w konfiguracjiDebugkompilator celowo generuje nadmiarowe instrukcjenop(no operation). Umożliwiają one debugerowi stawianie pułapek (breakpoints, klawiszF9) na pustych liniach lub klamrach{}oraz ułatwiają krokowe przechodzenie kodu (F10/F11) bez zakłóceń ze strony mechanizmów optymalizacji. W konfiguracjiReleasekompilator usuwa instrukcjenopi optymalizuje operacje stosu.
3. IL
Disassembler (ildasm.exe) i IL Assembler
(ilasm.exe)
- IL Disassembler (
ildasm.exe): Narzędzie inżynierii wstecznej umożliwiające przeglądanie kodu pośredniego MSIL oraz metadanych (klas, interfejsów, metod, atrybutów) zawartych w skompilowanych zestawach (.dll,.exe).- Uruchomienie z interfejsem graficznym (GUI):
ildasm MyAssembly.dll - Zrzut kodu pośredniego i metadanych do pliku tekstowego:
ildasm MyAssembly.dll /out=MyAssembly.il
- Uruchomienie z interfejsem graficznym (GUI):
- IL Assembler (
ilasm.exe): Narzędzie odwrotne. Przyjmuje plik tekstowy zawierający instrukcje MSIL (.il) i asembluje go z powrotem do postaci działającego zestawu.NET:
(Dla aplikacji wykonywalnych stosujemy przełącznikilasm MyAssembly.il /dll /out=MyAssemblyRebuilt.dll/exe).
Zastosowanie: Badanie optymalizacji kompilatora, inżynieria wsteczna, manualna optymalizacja opkodów stosu, usuwanie zabezpieczeń (binary patching) lub tworzenie kompilatorów własnych języków.
4.
Konsolidator zestawów (al.exe) i moduły wieloplikowe –
Kontekst historyczny
W klasycznym .NET kod źródłowy mógł być kompilowany do postaci
modułów (.netmodule), które zawierały skompilowany kod CIL
oraz definicje typów, lecz były całkowicie pozbawione własnego manifestu
zestawu.
Konsolidator zestawów al.exe
(Assembly Linker) pozwala połączyć wiele niezależnych modułów w
jeden zestaw wieloplikowy (multi-file assembly) ze wspólnym
manifestem oraz dołączać do niego zasoby satelickie (lokalizacyjne) lub
komponenty COM:
- Tworzenie biblioteki współdzielonej
(
.dll):al /out:MyAssembly.dll /target:library module1.netmodule module2.netmodule - Tworzenie aplikacji wykonywalnej (
.exe) ze wskazaniem punktu wejścia:
(Opcjaal /out:App.exe /target:exe /main:MyApp.Program.Main module1.netmodule module2.netmodule/target:exejest tu konieczna, ponieważ konsolidatoral.exe– w przeciwieństwie docsc.exe– domyślnie generuje bibliotekęlibrary).
⚠️ Uwaga architektoniczna:
Współczesny .NET (.NET 5+) odszedł od zestawów wieloplikowych na rzecz pojedynczych, autonomicznych bibliotek DLL. Narzędzieal.exepozostaje jednak kluczowe w utrzymaniu systemów legacy .NET Framework oraz unaocznia fundamentalną regułę CLR: zestaw (assembly) to jednostka logiczna i granica wersjonowania, która fizycznie może składać się z wielu odrębnych plików modułów.
5.
Narzędzie silnych nazw (sn.exe) vs Podpisywanie
cyfrowe
Klucze o silnej nazwie (*.snk) służyły w .NET Framework
do kryptograficznego podpisywania zestawów wyjściowych w celu
weryfikacji tożsamości wydawcy i unikalności nazwy:
- Generowanie nowej pary kluczy:
sn -k KeyPair.snk - Wyodrębnienie klucza publicznego:
sn -p KeyPair.snk PublicKey.pub
⚠️ Bezpieczeństwo kluczy:
Pliki kluczy prywatnych*.snknigdy nie powinny trafiać do repozytoriów kodu Git. We współczesnym .NET podpisywanie silną nazwą nie jest już traktowane jako bariera bezpieczeństwa (zostało zastąpione podpisywaniem pakietów certyfikatami X.509 za pomocądotnet nuget sign).
6. Global Assembly Cache
(gacutil.exe)
GAC to centralny magazyn współdzielonych bibliotek w systemie Windows
(C:\Windows\Microsoft.NET\assembly\GAC_MSIL). Wymagał
bibliotek z silną nazwą. Pozwalał na współistnienie wielu różnych wersji
tej samej biblioteki (np. 1.0.0.0 i 1.0.0.1) w jednym systemie:
gacutil /i MyLibrary.dll
gacutil /l MyLibrary
gacutil /u MyLibrary
4.
Zarządzanie Pamięcią: Stos, Sterta, Boksowanie i Typy
Span<T>
Cel: Zrozumienie kosztu alokacji pamięci na stercie
zarządzanej, zjawiska boksowania w CIL oraz opanowanie wzorca
zero-allocation za pomocą Span<T> i
ReadOnlySpan<T>.
1. Stos (Stack) vs Sterta Zarządzana (Managed Heap)
- Stos: Pamięć bardzo szybka, alokowana automatycznie
przy wejściu do metody i zwalniana przy wyjściu (LIFO). Trafiają na nią
typy wartościowe (
int,double,char,struct). - Sterta Zarządzana: Pamięć dynamiczna dla typów
referencyjnych (
class,string, tablice). Każda alokacja (operatornew) obciąża odśmiecacz pamięci (Garbage Collector – GC). GC musi okresowo wstrzymywać wątki aplikacji (Stop-the-World), skanować grafy referencji i kompaktować pamięć w generacjach (Gen 0, Gen 1, Gen 2, LOH).
2. Boksowanie (Boxing) w kodzie CIL
Jeśli przekażesz typ wartościowy (int) w miejsce
oczekujące typu referencyjnego (object), kompilator
wygeneruje instrukcję MSIL box:
ldc.i4.1
box [System.Runtime]System.Int32
call instance void MyClass::Process(object)
Instrukcja box powoduje alokację nowego obiektu
na stercie zarządzanej i skopiowanie wartości ze stosu do
sterty. Jest to ukryty koszt wydajnościowy, który w pętlach obciąża
procesor i Garbage Collector (GC). Na architekturze 64-bitowej 4-bajtowa
wartość int staje się wówczas 24-bajtowym obiektem na
stercie (16 bajtów nagłówka: MethodTable Pointer i
SyncBlockIndex + 4 bajty danych + 4 bajty wyrównania
pamięci).
💡 Dlaczego środowisko CLR na to zezwala i jak unikać boksowania? (Kliknij, aby rozwinąć/zwinąć)
Dlaczego środowisko CLR w ogóle na to zezwala?
- Ujednolicony system typów (Unified Type System): W
platformie .NET każdy typ – w tym typy proste i struktury – dziedziczy
po
System.Object. Umożliwia to tworzenie uniwersalnych algorytmów (np. serializatory, loggery, silniki refleksji) bez konieczności utrzymywania dwóch odrębnych światów typów. - Polimorfizm struktur i obsługa interfejsów: Typy
wartościowe mogą implementować interfejsy (np.
IComparable). Wywołanie metody przez referencję do interfejsu wymaga wskaźnika na tabelę metod wirtualnych (MethodTable Pointer). Ponieważ surowe dane na stosie go nie posiadają, CLR automatycznie opakowuje strukturę w pełnoprawny obiekt na stercie. - Kompatybilność wsteczna: W .NET 1.0/1.1 brakowało
typów generycznych, a kolekcje (
ArrayList,Hashtable) operowały wyłącznie na typieobject. Boksowanie było jedynym sposobem na przechowywanie w nich typów wartościowych.
Kompletne techniki eliminacji boksowania podczas przekazywania parametrów:
Metody generyczne ze specjalizacją JIT (
<T>): Zamiast parametruobject, metoda przyjmuje parametr generycznyT. Środowisko wykonawcze (JIT) generuje w pamięci osobną kopię kodu maszynowego dla każdego typu wartościowego (Monomorphization), a parametry są przekazywane bezpośrednio przez rejestry CPU lub stos:// ŹLE: Boksowanie do referencji public void Log(object value); // DOBRZE: Zero alokacji na stercie public void Log<T>(T value);Ograniczenia generyczne z prefiksem IL
constrained.: Przekazanie struktury do metody przyjmującej interfejs (np.void Draw(IDrawable d)) wymusza boksowanie. Zastosowanie ograniczenia generycznegowhere T : struct, IDrawablerozwiązuje ten problem:// DOBRZE: JIT wywoła Draw() bezpośrednio na wartości ze stosu public void Render<T>(T shape) where T : struct, IDrawable { shape.Draw(); }Pod maską w CIL: Kompilator emituje instrukcję
constrained. !!Tprzedcallvirt. Widząc, żeTjest strukturą, silnik JIT dewirtualizuje wywołanie i pomija tworzenie instancji na stercie.Dedykowane przeciążenia metod (Method Overloading): Wzorzec stosowany w bibliotece standardowej (np.
StringBuilder.AppendlubConsole.WriteLine). Zamiast jednej metody przyjmującejobject, definiuje się wyspecjalizowane warianty dla popularnych typów wartościowych:public void Append(int value); public void Append(double value); public void Append(bool value); public void Append(ReadOnlySpan<char> value);Przekazywanie przez referencję tylko do odczytu (
in,ref readonly): Przekazanie przez wartość zawsze oznacza pełną kopię wszystkich bajtów struktury. O ile małe struktury (do 8–16 bajtów) procesor może skopiować błyskawicznie w rejestrach, o tyle w przypadku większych struktur konieczne jest kopiowanie całego bloku pamięci na stosie. Słowo kluczoweinprzekazuje wskaźnik zarządzany na stosie bez kopiowania i bez ucieczki na stertę:public void Transform(in Matrix4x4 matrix);Własna implementacja metod
ToString()iGetHashCode()w strukturach: WywołaniemyStruct.ToString()lubmyStruct.GetHashCode()na strukturze, która nie nadpisuje tych metod, zboksuje ją na stercie, ponieważ domyślna implementacja wSystem.ValueTypewykorzystuje refleksję. Zawsze dodawaj jawnyoverride:public struct Point { public int X, Y; public override string ToString() => $"({X}, {Y})"; // Brak boksowania }Handlery ciągów interpolowanych (
InterpolatedStringHandler, C# 10+): Dawniej zapis$"Wartość: {42}"lubstring.Formatrzutował argumenty do tablicyobject[]. Nowoczesny C# tłumaczy interpolację na wywołania handlera z generyczną metodąAppendFormatted<T>(T value), co eliminuje alokację przy formatowaniu liczb i struktur.Kolekcje generyczne: Bezwzględne zastąpienie przestarzałych kolekcji nietypowanych (
ArrayList,HashtablezSystem.Collections) ich generycznymi odpowiednikami zSystem.Collections.Generic(List<T>,Dictionary<TKey, TValue>).Modyfikator
ref struct(C# 7.2+): Architektoniczny bezpiecznik dla typów wskaźnikowych (np.Span<T>). Wymusza na poziomie kompilatora istnienie instancji wyłącznie na stosie — próba rzutowania jej naobject, interfejs czy użycia w domknięciu asynchronicznym kończy się błędem kompilacji, co całkowicie wyklucza ucieczkę na stertę.
3.
Rewolucja bezalokacyjna: Span<T>,
ReadOnlySpan<T> i modyfikator
ref struct
Przez lata operacje na łańcuchach znaków i tablicach (np. parsowanie
ramek sieciowych, podział tekstu string.Substring(),
string.Split()) nieuchronnie alokowały dziesiątki obiektów
pośrednich na stercie. Wprowadzenie typów Span<T> i
ReadOnlySpan<T> w połączeniu z modyfikatorem
ref struct umożliwiło przetwarzanie
dowolnych ciągłych obszarów pamięci przy zerowej alokacji
(Zero-Allocation Overhead).
Wymuszenie alokacji wyłącznie na stosie (
ref struct):
Span<T>przechowuje tzw. wskaźnik zarządzany (interior pointer) do pamięci oraz długość bufora. Aby zagwarantować, że wskaźnik ten nigdy nie odwoła się do zwolnionej pamięci, kompilator nakłada twarde reguły:- Typu
ref structnie da się zboksować – próba przypisania go doobjectlub interfejsu powoduje błąd kompilacji. - Nie może być polem w klasie ani w standardowej strukturze.
- Nie może być elementem tablicy ani parametrem w metodach
async/awaitoraz wyrażeniach lambda (gdzie zmienne uciekają na stertę w domknięciach).
- Typu
Praktyczny zysk wydajnościowy:
string date = "2026-09-09"; // Podejście klasyczne: Substring() alokuje nowy obiekt string na stercie (GC musi go posprzątać) string yearStr = date.Substring(0, 4); int yearClassic = int.Parse(yearStr); // Podejście bezalokacyjne: AsSpan() tworzy lekki widok na istniejącą pamięć ReadOnlySpan<char> span = date.AsSpan(); ReadOnlySpan<char> yearSpan = span.Slice(0, 4); // 0 bajtów alokacji na stercie! int yearZeroAlloc = int.Parse(yearSpan); // int.Parse operuje bezpośrednio na Span
5. Interfejs wiersza poleceń: .NET CLI
Cel: Poznanie możliwości interfejsu wiersza poleceń .NET, diagnostyka środowiska, zarządzanie cyklem życia aplikacji i publikacja natywna.
Interfejs .NET CLI (dotnet) stanowi zunifikowany punkt
wejścia dla programistów na systemach Windows, Linux i macOS. Pozwala na
pełną obsługę cyklu życia oprogramowania – od inicjalizacji szablonów,
przez konfigurację kompilatora, aż po dystrybucję produkcyjną.
1. Diagnostyka środowiska
Sprawdzenie szczegółowych informacji o zainstalowanych pakietach SDK, środowiskach uruchomieniowych (runtimes) oraz architekturze maszyny:
dotnet --info
dotnet --list-sdks
dotnet --list-runtimes
2. Tworzenie i struktura nowego projektu
Wylistowanie dostępnych szablonów aplikacji:
dotnet new list
Utworzenie nowej aplikacji konsolowej opartej na .NET 8:
dotnet new console -f net8.0 --name MyConsoleApp
cd MyConsoleApp
Zawartość wygenerowanego pliku projektu
MyConsoleApp.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
3. Wielo-platformowość (Multi-Targeting) i wersje języka
Zamieniając element <TargetFramework> na liczbę
mnogą <TargetFrameworks>, możemy zdefiniować
kompilację biblioteki dla wielu wersji środowiska jednocześnie:
<TargetFrameworks>net8.0;net9.0;net48</TargetFrameworks>
W przypadku konieczności użycia cech nowszej wersji języka C# (np.
cech C# 12/13), możemy jawnie wymusić wersję kompilatora wewnątrz sekcji
<PropertyGroup>:
<LangVersion>latest</LangVersion>
(Dopuszczalne wartości to m.in.: latest,
preview, latestMajor lub konkretna wersja
numeryczna, np. 12.0).
4. Uruchamianie i kompilacja
Kompilacja i uruchomienie aplikacji:
dotnet run -c Release
- Domyślnie zestaw kompilowany jest do konfiguracji
Debugi trafia do katalogubin\Debug\<SDK version>\. - Z flagą
-c Releasezoptymalizowany zestaw powstaje w katalogubin\Release\<SDK version>\. - Bezpośrednie uruchomienie wygenerowanego zestawu z folderu
wyjściowego:
dotnet bin/Release/net8.0/MyConsoleApp.dll
5. Strategie publikacji i wdrożenia aplikacji
Polecenie dotnet publish przygotowuje kompletną,
zoptymalizowaną paczkę produkcyjną aplikacji. Platforma .NET 8/9
udostępnia cztery zasadnicze strategie dystrybucji kodu:
Wdrożenie zależne od środowiska (Framework-Dependent Deployment – FDD):
Domyślny i najlżejszy tryb publikacji. Wynikowy katalog zawiera wyłącznie skompilowane biblioteki aplikacji (.dll), wymagając obecności zainstalowanego środowiska uruchomieniowego .NET o zgodnej wersji na maszynie docelowej:dotnet publish -c Release -f net8.0Wdrożenie samowystarczalne (Self-Contained Deployment – SCD):
Paczka zawiera aplikację wraz z kompletną maszyną wirtualną CoreCLR oraz wszystkimi wymaganymi bibliotekami bazowymi dla wskazanego identyfikatora platformy docelowej – RID (ang. Runtime Identifier, np.win-x64,linux-x64,osx-arm64). Aplikacja może zostać uruchomiona na systemie, na którym w ogóle nie zainstalowano platformy .NET:dotnet publish -c Release -f net8.0 -r linux-x64 --self-contained trueKompilacja hybrydowa Ahead-of-Time: ReadyToRun (R2R):
Współczesny, wieloplatformowy odpowiednik mechanizmu NGEN z klasycznego .NET Framework. Jest to forma kompilacji wyprzedzającej (AOT), w której kompilatorcrossgen2wstrzykuje prekompilowany kod maszynowy procesora bezpośrednio do zestawu obok standardowego kodu pośredniego CIL:- Zaleta: Drastycznie skraca czas rozruchu aplikacji (cold start) i redukuje opóźnienia, odciążając kompilator JIT podczas startu.
- Koszt: Pliki binarne są 2–3 razy większe ze względu na obecność zarówno kodu CIL, jak i instrukcji maszynowych.
- Tryb R2R można łączyć zarówno z wdrożeniem FDD, jak i SCD:
dotnet publish -c Release -r win-x64 --no-self-contained -p:PublishReadyToRun=true
Pełna kompilacja natywna (Native AOT):
Zaawansowany mechanizm kompilacji bezpośrednio do monolitycznego, natywnego pliku wykonywalnego systemu operacyjnego (format PE/ELF/Mach-O). Wynikowy plik binarny jest całkowicie pozbawiony kodu MSIL oraz maszyny wirtualnej JIT:- Zapewnia natychmiastowy start, minimalne zużycie pamięci RAM oraz
brak możliwości inżynierii wstecznej za pomocą
ildasm. - Ograniczenia: Wymaga statycznej analizy kodu w
czasie kompilacji; nie obsługuje dynamicznego ładowania zestawów przez
Assembly.LoadFile, ogranicza klasyczną refleksję (Reflection) i wymaga odpowiednich adnotacji przycinania kodu (Trimming annotations).
dotnet publish -c Release -r linux-x64 -p:PublishAot=true- Zapewnia natychmiastowy start, minimalne zużycie pamięci RAM oraz
brak możliwości inżynierii wstecznej za pomocą
6. Zarządzanie plikiem rozwiązania (Solution) i repozytorium Git
Inicjalizacja pliku .gitignore dla ekosystemu
.NET/Visual Studio:
dotnet new gitignore
Utworzenie pliku rozwiązania (.sln) i dołączenie do
niego projektu:
dotnet new sln --name MyCompanySolution
dotnet sln MyCompanySolution.sln add MyConsoleApp/MyConsoleApp.csproj
dotnet sln MyCompanySolution.sln list
6. Konteneryzacja aplikacji .NET w środowisku Docker
Cel: Praktyczne wdrożenie aplikacji .NET w odizolowanym kontenerze linuksowym przy użyciu wieloetapowego procesu budowania (Multi-stage build).
Znajomość CLI jest niezbędna podczas konteneryzacji. Nowoczesne podejście polega na rozdzieleniu etapu kompilacji (z użyciem ciężkiego obrazu SDK) od obrazu produkcyjnego (z użyciem odchudzonego obrazu samego runtime'u).
Wzorcowy plik
Dockerfile:
# ETAP 1: Budowanie i publikacja aplikacji (Build Stage)
FROM [mcr.microsoft.com/dotnet/sdk:8.0](https://mcr.microsoft.com/dotnet/sdk:8.0) AS build
WORKDIR /src
# Kopiowanie pliku csproj i przywracanie pakietów (warstwa buforowana przez Dockera)
COPY ["MyConsoleApp.csproj", "./"]
RUN dotnet restore "MyConsoleApp.csproj"
# Kopiowanie reszty kodu i publikacja w wersji Release
COPY . .
RUN dotnet publish "MyConsoleApp.csproj" -c Release -o /app/publish --no-restore
# ETAP 2: Minimalistyczne środowisko uruchomieniowe (Runtime Stage)
FROM [mcr.microsoft.com/dotnet/runtime:8.0](https://mcr.microsoft.com/dotnet/runtime:8.0) AS final
WORKDIR /app
# Skopiowanie wyłącznie skompilowanych artefaktów z etapu 1
COPY --from=build /app/publish .
# Uruchomienie aplikacji
ENTRYPOINT ["dotnet", "MyConsoleApp.dll"]
Budowanie i testowe uruchomienie kontenera:
docker build -t my-console-app:1.0 .
docker run --rm my-console-app:1.0
7. Zarządzanie zależnościami (NuGet) i tworzenie własnych narzędzi CLI
Cel: Automatyzacja bibliotek oraz dystrybucja własnych programów jako narzędzi wiersza poleceń (.NET Global Tools).
1. Zarządzanie zależnościami NuGet w .NET CLI
W .NET 5+ zarządzanie zależnościami opiera się na deklaracjach
<PackageReference> w plikach .csproj
oraz zestawie poleceń CLI.
Podstawowe polecenia
- Dodawanie nowej zależności:
Z określeniem konkretnej wersji:dotnet add package Newtonsoft.Jsondotnet add package Serilog --version 3.1.1 - Usuwanie zależności:
dotnet remove package Newtonsoft.Json - Weryfikacja zainstalowanych pakietów:
dotnet list package - Weryfikacja nieaktualnych zależności (Outdated):
dotnet list package --outdated - Audyt bezpieczeństwa (wykrywanie znanych luk CVE):
dotnet list package --vulnerable --include-transitive - Przywracanie pakietów (Restore):
dotnet restore - Pakowanie własnej biblioteki do formatu
.nupkg:dotnet pack -c Release
Deterministyczne
odtwarzanie zależności (packages.lock.json)
Aby zagwarantować, że kompilacja na serwerze CI użyje dokładnie tych samych wersji i sum kontrolnych pakietów co środowisko deweloperskie:
<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<RestoreLockedMode Condition="'$(ContinuousIntegrationBuild)' == 'true'">true</RestoreLockedMode>
</PropertyGroup>
Central Package Management (CPM)
W projektach wielomodułowych wersje bibliotek definiuje się
centralnie w pliku Directory.Packages.props w katalogu
głównym:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3"/>
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0"/>
</ItemGroup>
</Project>
Dzięki temu w plikach .csproj podaje się wyłącznie
referencję:
<ItemGroup>
<PackageReference Include="Newtonsoft.Json"/>
</ItemGroup>
2. Tworzenie i instalacja własnego narzędzia (.NET Global Tool)
Platforma .NET pozwala w prosty sposób przekształcić zwykłą aplikację konsolową w narzędzie systemowe wywoływane wprost z poziomu terminala.
Do pliku projektu MyConsoleApp.csproj w elemencie
<PropertyGroup> dodajemy wpisy:
<PackAsTool>true</PackAsTool>
<ToolCommandName>my-cli-tool</ToolCommandName>
<PackageOutputPath>./nupkg</PackageOutputPath>
PackAsTool– flaga informująca kompilator, że wynikowa paczka NuGet ma być traktowana jako narzędzie wykonywalne CLI.ToolCommandName– opcjonalna unikalna nazwa polecenia, którym wywołamy narzędzie w terminalu (domyślnie jest to nazwa pliku projektu).PackageOutputPath– katalog wyjściowy dla paczki.nupkg.
Budowanie pakietu narzędzia:
dotnet pack
Instalacja narzędzia w systemie jako polecenia globalnego z lokalnego źródła:
dotnet tool install --global --add-source ./nupkg MyConsoleApp
Od tego momentu narzędzie można wywołać w dowolnym terminalu komendą:
my-cli-tool
Aktualizacja narzędzia po wprowadzeniu zmian w kodzie i podbiciu wersji:
dotnet tool update --global --add-source ./nupkg MyConsoleApp
Odinstalowanie narzędzia:
dotnet tool uninstall --global MyConsoleApp
💡 Narzędzia lokalne i plik manifestu dotnet-tools.json (Kliknij, aby rozwinąć/zwinąć)
Jeśli narzędzie ma być powiązane wyłącznie z konkretnym projektem/repozytorium (a nie instalowane globalnie w całym systemie operacyjnym):
Utwórz plik manifestu w katalogu głównym projektu:
dotnet new tool-manifestPolecenie utworzy plik konfiguracyjny
.config/dotnet-tools.json.Zainstaluj narzędzie w kontekście lokalnym:
dotnet tool install Cake.ToolUruchom narzędzie lokalne:
dotnet tool run dotnet-cake # lub składnia skrócona: dotnet cakePrzywracanie narzędzi na innej maszynie / na serwerze CI:
Po sklonowaniu repozytorium przez innego programistę wystarczy wykonać polecenie:dotnet tool restoreWszystkie narzędzia zdefiniowane w manifeście zostaną pobrane i będą natychmiast gotowe do użycia w kontekście repozytorium.
8. Automatyzacja potoków budowania: Cake (C# Make)
Cel: Poznanie podstawowych możliwości narzędzia Cake i pisanie scenariuszy CI/CD w czystym języku C#.
Cake (C# Make) to system wspomagający proces ciągłej integracji (CI/CD), który pozwala na orkiestrację kompilacji, czyszczenia folderów, uruchamiania testów jednostkowych oraz tworzenia pakietów instalacyjnych za pomocą silnika Roslyn i składni C#.
1. Podejście skryptowe:
build.cake
Instalacja Cake jako narzędzia w systemie (globalnie lub lokalnie):
dotnet tool install --global Cake.Tool
Skrypt automatyzacji build.cake:
var target = Argument("target", "Build");
var configuration = Argument("configuration", "Release");
var solutionFile = "./MyCompanySolution.sln";
Task("Clean")
.WithCriteria(c => HasArgument("rebuild"))
.Does(() =>
{
Information("Czyszczenie katalogów wyjściowych dla konfiguracji: {0}", configuration);
CleanDirectories($"./**/bin/{configuration}");
CleanDirectories($"./**/obj/{configuration}");
});
Task("Restore")
.IsDependentOn("Clean")
.Does(() =>
{
Information("Przywracanie pakietów NuGet...");
DotNetRestore(solutionFile);
});
Task("Build")
.IsDependentOn("Restore")
.Does(() =>
{
Information("Kompilacja rozwiązania...");
DotNetBuild(solutionFile, new DotNetBuildSettings
{
Configuration = configuration,
NoRestore = true,
});
});
Task("Test")
.IsDependentOn("Build")
.Does(() =>
{
Information("Uruchamianie testów jednostkowych...");
DotNetTest(solutionFile, new DotNetTestSettings
{
Configuration = configuration,
NoBuild = true,
ArgumentCustomization = args => args.Append("--collect:\"XPlat Code Coverage\"")
});
});
RunTarget(target);
Uruchomienie procesu budowania:
# Uruchomienie domyślnego celu (Build wraz z zależnościami Clean i Restore):
dotnet cake
# Wymuszenie przebudowy z argumentem niestandardowym oraz wskazaniem innego celu:
dotnet cake --target=Test --configuration=Release --rebuild
2. Nowoczesny standard inżynieryjny: Cake Frosting
W środowiskach korporacyjnych tradycyjne pliki .cake
zastępuje się Cake Frosting – potok budowania tworzy
się jako standardowy, w pełni typowany projekt konsolowy C#.
Zalety Cake Frosting:
- Nawigacja po kodzie
F12: Zadania i konfiguracje narzędzi to standardowe typy C#, więcF12natychmiast przenosi do definicji zadania lub pozwala podejrzeć dostępne właściwości SDK bez otwierania dokumentacji w przeglądarce. - IntelliSense i bezpieczna refaktoryzacja: Pełne podpowiadanie kodu w
edytorze oraz automatyczna zmiana nazw zadań czy właściwości w całym
projekcie
F2bez ryzyka pozostawienia literówki. - Walidacja w czasie kompilacji: Błędy typowania i brakujące parametry są natychmiast podkreślane przez edytor, dzięki czemu pomyłki wychodzą lokalnie w ułamku sekundy, a nie po uruchomieniu na serwerze CI.
- Debugowanie krokowe potoku budowania bezpośrednio pod klawiszem
F5. - Zarządzanie wtyczkami przez zwykłe referencje
<PackageReference>bez konieczności stosowania dyrektyw#addin.
Plik projektu
Build/Build.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RunWorkingDirectory>$(MSBuildProjectDirectory)/..</RunWorkingDirectory>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Cake.Frosting" Version="4.0.0"/>
</ItemGroup>
</Project>
Kod orkiestracji w C#
(Build/Program.cs)
using Cake.Common.IO;
using Cake.Common.Tools.DotNet;
using Cake.Common.Tools.DotNet.Build;
using Cake.Common.Tools.DotNet.Pack;
using Cake.Common.Tools.DotNet.Test;
using Cake.Core;
using Cake.Core.Diagnostics;
using Cake.Frosting;
public static class Program
{
public static int Main(string[] args)
{
return new CakeHost()
.UseContext<BuildContext>()
.Run(args);
}
}
public class BuildContext : FrostingContext
{
public string Configuration { get; set; }
public string SolutionPath { get; set; }
public BuildContext(ICakeContext context) : base(context)
{
Configuration = context.Arguments.GetArgument("configuration") ?? "Release";
SolutionPath = "./MyCompanySolution.sln";
}
}
[TaskName("Clean")]
public sealed class CleanTask : FrostingTask<BuildContext>
{
public override void Run(BuildContext context)
{
context.CleanDirectories($"./**/bin/{context.Configuration}");
context.CleanDirectories($"./**/obj/{context.Configuration}");
}
}
[TaskName("Build")]
[IsDependentOn(typeof(CleanTask))]
public sealed class BuildTask : FrostingTask<BuildContext>
{
public override void Run(BuildContext context)
{
context.DotNetBuild(context.SolutionPath, new DotNetBuildSettings
{
Configuration = context.Configuration
});
}
}
[TaskName("Test")]
[IsDependentOn(typeof(BuildTask))]
public sealed class TestTask : FrostingTask<BuildContext>
{
public override void Run(BuildContext context)
{
context.DotNetTest(context.SolutionPath, new DotNetTestSettings
{
Configuration = context.Configuration,
NoBuild = true
});
}
}
[TaskName("Default")]
[IsDependentOn(typeof(TestTask))]
public class DefaultTask : FrostingTask
{
}
Uruchomienie potoku Cake Frosting:
dotnet run --project ./Build -- --target=Test --configuration=Release
3. Zestawienie podejść do orkiestracji CI/CD
| Cecha | Skrypty Shell / PowerShell | Tradycyjny Cake (.cake) |
Cake Frosting (C#) |
|---|---|---|---|
| Przenośność międzyplatformowa | Niska (różnice bash / pwsh) | Wysoka (działa na każdym środowisku z .NET) | Wysoka (działa na każdym środowisku z .NET) |
| Wsparcie IDE / IntelliSense | Zmienne | Ograniczone (wtyczki Roslyn) | Pełne (standardowy kod C#) |
| Debugowanie krokowe (F5) | Złożone | Trudne / ograniczone | Pełne (jak w standardowej aplikacji C#) |
| Zarządzanie wtyczkami | Zewnętrzne moduły powłoki | Dyrektywy #addin |
Standardowy
<PackageReference> |
| Bariera wejścia dla programisty .NET | Wymaga nauki bash/pwsh | Średnia (specyficzny DSL) | Zerowa (czysty C# i klasy OOP) |
Zadanie Laboratoryjne do wykonania (100 pkt)
Organizacja pracy: Każdy podpunkt można realizować w osobnym podkatalogu roboczym, kopiując wymagane pliki z poprzednich zadań. Wszelkie operacje kompilacji manualnej (
csc,ildasm,ilasm,sn,gacutil,al) wykonujemy w oknie Developer Command Prompt for VS 2022.
Część 1: Kompilacja manualna, optymalizacje i boksowanie w CIL (15 pkt)
- Utwórz plik
ClassA.cso poniższej zawartości:using System; using System.Diagnostics; using System.Reflection; namespace JP_NET.Lab1 { public partial class ClassA { private int _a; public int A { get => _a; set => _a = value; } public int b; public const char nameConst = 'A'; public readonly char nameReadonly = 'A'; public void MethodOfClassA() { Assembly asm = Assembly.GetExecutingAssembly(); FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(asm.Location); var x = String.Format("{0}.{1}", fvi.ProductMajorPart, fvi.ProductMinorPart); PartialMethodOfClassA(x); } partial void PartialMethodOfClassA(object x); partial void PartialMethodOfClassA(object x) { Console.WriteLine("Wydruk z" + " " + "klasy A. {0}", x); } } } - Skompiluj plik
ClassA.csza pomocą kompilatoracsc.exedo biblioteki.dllo nazwieJP_NET.Lab1a.dll(opcja-t:library). - Skompiluj plik w trybie debugowania (
-debug) do plikuJP_NET.Lab1a.DEB.dll. Zauważ, że powstał plik bazy symboli*.pdb. - Skompiluj plik z włączoną optymalizacją (
-o) do plikuJP_NET.Lab1a.O.dll. - Dokonaj dezasemblacji obu bibliotek narzędziem
ildasm.exe /out=<nazwa>.il. Porównaj oba pliki.ilw edytorze tekstu:- Wskaż obecność nadmiarowych instrukcji
nopw wersji Debug i ich brak w wersji zoptymalizowanej. - Zauważ, że kompilator już na etapie budowania połączył literały
"Wydruk z" + " " + "klasy A. {0}"w jeden ciąg znaków. - Zlokalizuj w kodzie metody instrukcję
box [mscorlib]System.Int32(lub odpowiednikSystem.Runtime) – wyjaśnij prowadzącemu, dlaczego nastąpiła alokacja na stercie zarządzanej.
- Wskaż obecność nadmiarowych instrukcji
- Zlokalizuj w CIL 4 instrukcje wywołania metody
PartialMethodOfClassA():ldarg.0 ldc.i4.1 box [mscorlib]System.Int32 call instance void JP_NET.Lab1.ClassA::PartialMethodOfClassA(object) - Zakomentuj w pliku
ClassA.cssamą definicję metody częściowej (zostawiając jej deklarację nagłówkowąpartial void PartialMethodOfClassA(object x);). Skompiluj bibliotekę ponownie i sprawdź jej kod wildasm. Zauważ, że kompilator całkowicie wyciął wywołanie tej metody z kodu binarnego. - Odkomentuj metodę i skompiluj bibliotekę do stanu pierwotnego.
Część 2: Referencje, wiązanie stałych i wersjonowanie (15 pkt)
- Utwórz plik
ProgA.cs:using System; using JP_NET.Lab1; public class Program { public static void Main(string[] args) { ClassA a = new ClassA(); a.MethodOfClassA(); a.A = 1; a.b = 2; Console.WriteLine("Const {0}", ClassA.nameConst); Console.WriteLine("Readonly {0}", a.nameReadonly); } } - Skompiluj
ProgA.csz referencją do biblioteki (-r:JP_NET.Lab1a.dll) do plikuProgA.exe. Uruchom program. - Otwórz
ProgA.exewildasmi porównaj sposób odwołania do właściwościA(call instance void set_A(int32)) oraz pola publicznegob(stfld int32 b). - Utwórz plik
AssemblyInfo.cs:using System.Reflection; [assembly: AssemblyVersion("1.0.0.0")] - W pliku
ClassA.cszmień wartości pól na:public const char nameConst = 'B'; public readonly char nameReadonly = 'B'; - Przekompiluj wyłącznie samą bibliotekę
JP_NET.Lab1a.dll(dołączającAssemblyInfo.cs). - Bez ponownej kompilacji uruchom stary plik
ProgA.exe. - Zaobserwuj wynik w konsoli: pole
readonlyprzyjęło nową wartość'B', natomiastconstzachowało wartość'A'.
Wyjaśnij na obronie na podstawie kodu MSIL, dlaczego wartość stałejconstzostała trwale wstrzyknięta (inlined) do wnętrza zestawu wywołującego.
Część 3: Inżynieria wsteczna i CIL Patching (10 pkt)
- Zdezasembluj bibliotekę
JP_NET.Lab1a.dlldo pliku CIL:ildasm JP_NET.Lab1a.dll /out=JP_NET.Lab1a.il - Otwórz plik
JP_NET.Lab1a.ilw edytorze tekstowym. - Zlokalizuj ciało metody
PartialMethodOfClassAi zmień wyświetlany literał tekstowy z"Wydruk z klasy A. {0}"na:
"Wydruk z klasy A po spatchowaniu CIL. {0}". - Zrekompiluj zmodyfikowany plik
.ilnarzędziemilasm.exe:ilasm JP_NET.Lab1a.il /dll /out=JP_NET.Lab1a.dll - Uruchom
ProgA.exei udowodnij, że aplikacja wykonała zmodyfikowany kod binarny. Zwróć uwagę, że brak podpisu cyfrowego umożliwia dokonywanie niewykrywalnych zmian w plikach binarnych.
Część
4: Moduły wieloplikowe i konsolidator al.exe (10 pkt)
(Część demonstrująca architekturę modułów wieloplikowych CLR)
- Utwórz plik
ClassB.cs:namespace JP_NET.Lab1 { public class ClassB { public void MethodOfClassB() => System.Console.WriteLine("Wydruk z klasy B."); } } - Skompiluj
ClassA.csdo modułu .NET (opcja-t:module) o nazwieClassA.netmodule. To samo wykonaj dlaClassB.cs(ClassB.netmodule). - Skompiluj
ProgA.csdo aplikacjiProgABmod.exe, dołączając oba moduły opcją-addmodule:ClassA.netmodule,ClassB.netmodule. - Zmień nazwę pliku
ClassB.netmodulena dysku i uruchom program – program zadziała. Następnie zmień nazwęClassA.netmodule– aplikacja rzuci wyjątekFileNotFoundException. Świadczy to o leniwym ładowaniu modułów do pamięci. - Skompiluj
ProgA.csjako modułProgA.netmodule. - Za pomocą konsolidatora
al.exepołącz trzy moduły w jeden plik wykonywalny ze wskazanym punktem wejścia:al /out:ProgLinked.exe /target:exe /main:Program.Main ClassA.netmodule ClassB.netmodule ProgA.netmodule - Porównaj w
ildasmmanifest plikuProgLinked.exei zaobserwuj deklaracje.file ClassA.netmoduleoraz.class extern.
Część 5: Podpisywanie silną nazwą i integralność zestawów (10 pkt)
- Wygeneruj parę kluczy kryptograficznych:
sn -k paraKluczy.snk - Dodaj do pliku
AssemblyInfo.csatrybut:[assembly: AssemblyKeyFile("paraKluczy.snk")] - Skompiluj bibliotekę
JP_NET.Lab1a.dllz plikiem klucza. Sprawdź poprawność podpisu poleceniem:sn -v JP_NET.Lab1a.dll - Uruchom aplikację
ProgA.exeskompilowaną uprzednio z niepodpisaną biblioteką. Zaobserwuj błąd niezgodności manifestu:System.IO.FileLoadException/0x80131040. - Skompiluj aplikację konsolową ponownie z podpisaną biblioteką – zweryfikuj poprawne uruchomienie.
Część 6: Global Assembly Cache (GAC) i współdzielenie bibliotek (10 pkt)
(Uwaga: Wymaga uprawnień administratora w terminalu).
- Zainstaluj podpisaną bibliotekę w GAC:
gacutil /i JP_NET.Lab1a.dll - Zmień w
AssemblyInfo.cswersję na[assembly: AssemblyVersion("1.0.0.1")], a wClassA.cszmień komunikat na"Wydruk z nowej wersji 1.0.0.1". - Skompiluj nową wersję biblioteki i zainstaluj ją w GAC obok poprzedniej.
- Skompiluj nową aplikację konsolową
ProgA_v2.exeodwołującą się do nowej wersji biblioteki. - Usuń lokalne pliki
.dllz bieżącego folderu roboczego. - Uruchom kolejno
ProgA.exeorazProgA_v2.exe. Zaobserwuj, że każda aplikacja ładuje z GAC właściwą dla siebie wersję biblioteki. Sprawdź listę w GAC poleceniemgacutil /l JP_NET.Lab1a. - Na koniec odinstaluj biblioteki z GAC
(
gacutil /u JP_NET.Lab1a).
Część
7: Wydajność: ReadyToRun (R2R) oraz Bezalokacyjny Parsing
(Span<T>) (10 pkt)
Krok A: Publikacja ReadyToRun (5 pkt)
- Przygotuj aplikację konsolową zawierającą metodę intensywną
obliczeniowo:
static void HeavyMethod() { double result = 0; for (int i = 0; i < 50000; i++) result += Math.Sqrt(i) * Math.Sin(i); Console.WriteLine(result); } - Opublikuj projekt w wariancie standardowym oraz ReadyToRun dla
architektury 64-bitowej:
dotnet publish -c Release -r win-x64 --no-self-contained dotnet publish -c Release -r win-x64 --no-self-contained -p:PublishReadyToRun=true -o ./pub-r2r - Porównaj rozmiar pliku
.dll/.exew obu folderach i wyjaśnij na obronie obecność prekompilowanego kodu maszynowego.
Krok B:
Bezalokacyjny parsing ze Span<T> (5 pkt)
- W aplikacji zaimplementuj metodę dekodującą ciąg tekstowy z
parametrami:
string sample = "STATUS=200;ID=49201;LINE=12;DELAY=00:04:15"; - Przygotuj dwie wersje metody wyciągającej wartość
LINEorazDELAY:- Wersja tradycyjna: używająca
sample.Split(';')oraz.Substring()(generująca alokacje na stercie). - Wersja wydajna: operująca wyłącznie na
ReadOnlySpan<char>i metodzie.Slice()(zero alokacji na stercie).
- Wersja tradycyjna: używająca
- Wykaż w kodzie brak alokacji nowych stringów podczas ekstrakcji wartości.
Część 8: Narzędzie .NET Global Tool (Otwarte Dane ZTM Gdańsk) (10 pkt)
- Utwórz aplikację konsolową .NET 8/9 pobierającą w czasie
rzeczywistym odjazdy autobusów i tramwajów z API Otwartych Danych ZTM w
Gdańsku (serwis TRISTAR/CKAN):
- URL zapytania dla przystanku Miszewskiego 01 (ID:
2019):
https://ckan2.multimediagdansk.pl/departures?stopId=2019
- URL zapytania dla przystanku Miszewskiego 01 (ID:
2019):
- Narzędzie powinno przyjmować parametr wejściowy z linii komend (np.
identyfikator przystanku
stopIdlub numer linii) i wyświetlać sformatowaną listę odjazdów w oknie konsoli. - Skonfiguruj plik
.csprojjako instalowalne narzędzie (.NET Global Tool):<PropertyGroup> <PackAsTool>true</PackAsTool> <ToolCommandName>ztm-gdansk</ToolCommandName> <PackageOutputPath>./nupkg</PackageOutputPath> </PropertyGroup> - Zbuduj pakiet poleceniem
dotnet pack -c Release, a następnie zainstaluj narzędzie lokalnie w systemie:dotnet tool install --global --add-source ./nupkg <NazwaProjektu> - Zademonstruj wywołanie narzędzia wprost z konsoli poleceniem
ztm-gdansk 2019.
Część 9: Automatyzacja potoku CI/CD w Cake oraz Testy (10 pkt)
- Dodaj do solucji projekt z testami jednostkowymi (np. xUnit lub MSTest) weryfikujący poprawność działania metody filtrującej dane odjazdów.
- Przygotuj w głównym katalogu potok budowania wykorzystując
do wyboru: tradycyjny skrypt
build.cakeLUB projekt konsolowy Cake Frosting (Build/Build.csproj). Potok musi realizować następujące zadania:Clean– czyszczenie katalogów wyjściowychbin/iobj/oraz folderu artefaktów.Restore– przywrócenie zależności NuGet (DotNetRestore).Build– kompilacja całego rozwiązania w trybie Release (DotNetBuild).Test– automatyczne wykonanie testów jednostkowych (DotNetTest).Pack– wygenerowanie pakietu.nupkgnarzędzia CLI do katalogu./artifacts(DotNetPack).
- Uruchom proces automatyzacji odpowiednim poleceniem:
- Dla skryptu
build.cake:dotnet cake --target=Pack - Dla projektu Cake Frosting (rozwiązanie
alternatywne):
dotnet run --project ./Build -- --target=Pack
- Dla skryptu
- Zaprezentuj działanie potoku prowadzącemu.
Zestawienie Oceny (Suma: 100 pkt)
| Nr | Zakres modułu | Weryfikacja | Punkty |
|---|---|---|---|
| 1 | Kompilacja, CIL i alokacja (nop/box) | Flagi csc, porównanie w
ildasm, usunięcie metody partial. |
15 pkt |
| 2 | Referencje i stałe (const vs readonly) | Różnica w kodzie MSIL, inlining stałych w kompilacji. | 15 pkt |
| 3 | Inżynieria wsteczna (CIL Patching) | Zmiana literału w .il,
rekompilacja asemblerem ilasm. |
10 pkt |
| 4 | Moduły wieloplikowe i al.exe | Flaga -t:module, konsolidator
al.exe, analiza manifestu. |
10 pkt |
| 5 | Podpisywanie silną nazwą (sn.exe) | Wygenerowanie pary kluczy, błąd manifestu
0x80131040. |
10 pkt |
| 6 | Global Assembly Cache (GAC) | Instalacja w GAC dwóch wersji biblioteki
przez gacutil.exe. |
10 pkt |
| 7 | Optymalizacja: R2R i
Span<T> |
Narzut binarny R2R oraz bezalokacyjny
parsing na ReadOnlySpan. |
10 pkt |
| 8 | Narzędzie .NET Global Tool (ZTM) | Pobieranie danych ZTM Gdańsk, instalacja narzędzia w systemie. | 10 pkt |
| 9 | Potok automatyzacji w Cake & Testy | Skrypt build.cake (lub
projekt Cake Frosting) z zadaniami Clean → Restore →
Build → Test → Pack. |
10 pkt |
| SUMA | 100 pkt |
Przykładowe pytania na obronie ustnej:
- Dlaczego instrukcja
boxpowoduje alokację na stercie i jak nowoczesny C# pozwala jej unikać (np. za pomocą typów generycznych lubSpan<T>)? - Dlaczego podmiana wartości stałej
constw bibliotece DLL nie zadziała w programie klienckim bez jego rekompilacji, a podmiana wartościreadonlyzadziała? - Czym różni się instrukcja maszynowa wygenerowana w technologii ReadyToRun od natywnego pliku wykonywalnego wygenerowanego przez Native AOT?
- Dlaczego struktura oznaczona jako
ref struct(np.ReadOnlySpan<char>) nie może być polem wewnątrz zwykłej klasyclass?