Postanowiłem odpowiedzieć na pytanie jak wyglądają dane - wartości zmiennych - w kodzie produkowanym przez RyuJIT - system emitujący natywny kod (w moim przypadku x86_64) podczas działania aplikacji napisanej w C#.
Moja instrukcja bazuje na oficjalnej.
Pierwszą rzeczą jaką musimy zrobić to zdobyć wersję CoreCLR skompilowaną z flagami do debugowania. Nie ma takich oficjalnych obrazów (nie znalazłem), więc odpowiedzią jest zbudowanie go sobie ze źródeł.
Żeby było łatwiej to zamieszczam krótką instrukcję poniżej:
- Sklonuj repo
- Zainstaluj zależności albo użyj obrazu dockera
- Uruchom skrypt budowania
W moim przypadku
git clone --depth=1 https://github.com/dotnet/coreclr/
docker run -it \ #interactive
--rm \ #remove after use
--entrypoint "/bin/bash" \ #start shell
-v "$PWD/coreclr:/mnt/coreclr" \ #mount sources
-w /mnt/coreclr \ #workdir
microsoft/dotnet-buildtools-prereqs:ubuntu-18.04-c103199-20180628134610
# apt update && apt install libcurl4
# ./build.sh
Instrukcje budowania na inne platformy znajdziecie w repozytorium.
Note: potrzeba jakieś 9GB wolnego miejsca.
Po tym jak się zbuduje to można usunąć folder bin/obj
, który zajmuje najwięcej miejsca i zawiera tymczasowe pliki potrzebne do zlinkowania końcowych binarek.
Przygotowanie projektu
Następnie utworzyłem nowy folder i w środku wykonałem dotnet new console
i rozpisałem mój kod, który chcę wyemitować
using System;
using System.Runtime.CompilerServices;
namespace jitdumptest
{
struct Pair { public int x; public short y; public bool b; }
class PairR { public int x; public short y; public bool b; }
class Program
{
static void Main(string[] args)
{
var p1 = CreatePair();
var p2 = CreatePairR();
Console.WriteLine(p1);
Console.WriteLine(p2);
}
[MethodImpl(MethodImplOptions.NoInlining)]
static Pair CreatePair()
{
Pair p = new Pair();
p.x = 10;
p.y = 20;
p.b = true;
return p;
}
[MethodImpl(MethodImplOptions.NoInlining)]
static PairR CreatePairR()
{
PairR p = new PairR();
p.x = 10;
p.y = 20;
p.b = true;
return p;
}
}
}
Dodatkowe atrybuty na metodach nie pozwolą im się zinlinować.
W projekcie csproj
musiałem dodać linijkę
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
poniżej TargetFramework
. Potem dotnet restore
, dotnet build
i dotnet publish -c Release
.
W tym momencie mam binarki w bin/Release/linux-x64/publish
.
Podmiana CLR
Teraz trzeba wstrzyknąć nasze zbudowane CoreCLR do aplikacji oraz ustawić zmienną środowiskową, która spowoduje wypisanie emitowanych przez JIT metod.
Wchodzę do folderu z binarkami (powyżej) i wykonuję
cp -rT ~/Code/coreclr/bin/Product/Linux.x64.Debug .
Gdzie jest to ścieżka do zbudowanego wcześniej CoreCLR.
Następnie ustawiam zmienną
export COMPlus_JitDump="Main CreatePair CreatePairR"
która określa które metody JIT ma nam wypisać.
I wykonuję
./corerun jitdumptest.dll
Gdzie jitdumptest
to nazwa mojego projektu.
Następnie dostaję ogromną ścianę tekstu. Tam jest wszystko co robi JIT. Jeśli chcemy tylko kod wynikowy, to należy wyczyścić zmienną COMPlus_JitDump
(ustawiając ją na ""
) i ustawić za to zmienną COMPlus_JitDisasm
.
Wynik:
; jitdumptest.Program:Main(ref)
G_M56875_IG01:
push rbp
sub rsp, 32
lea rbp, [rsp+20H]
xor rax, rax
mov qword ptr [rbp-10H], rax
mov gword ptr [rbp-08H], rdi
G_M56875_IG02:
call jitdumptest.Program:CreatePair():struct
mov qword ptr [rbp-18H], rax
call jitdumptest.Program:CreatePairR():ref
mov gword ptr [rbp-10H], rax
lea rsi, bword ptr [rbp-18H]
mov rdi, 0x7FA58A0BA048
call CORINFO_HELP_BOX
mov rdi, rax
call System.Console:WriteLine(ref)
mov rdi, gword ptr [rbp-10H]
call System.Console:WriteLine(ref)
nop
G_M56875_IG03:
lea rsp, [rbp]
pop rbp
ret
; ==================================================
; jitdumptest.Program:CreatePair():struct
G_M64814_IG01:
push rbp
sub rsp, 16
lea rbp, [rsp+10H]
xor rax, rax
mov qword ptr [rbp-08H], rax
G_M64814_IG02:
xor rax, rax
mov qword ptr [rbp-08H], rax
mov dword ptr [rbp-08H], 10
lea rax, bword ptr [rbp-08H]
mov word ptr [rax+4], 20
lea rax, bword ptr [rbp-08H]
mov byte ptr [rax+6], 1
mov rax, qword ptr [rbp-08H]
G_M64814_IG03:
lea rsp, [rbp]
pop rbp
ret
; ==================================================
; jitdumptest.Program:CreatePairR():ref
G_M52188_IG01:
push rbp
sub rsp, 16
lea rbp, [rsp+10H]
xor rax, rax
mov qword ptr [rbp-08H], rax
G_M52188_IG02:
mov rdi, 0x7FA58A0B9EE0
call CORINFO_HELP_NEWSFAST
mov gword ptr [rbp-08H], rax
mov rdi, gword ptr [rbp-08H]
call jitdumptest.PairR:.ctor():this
mov rax, gword ptr [rbp-08H]
mov dword ptr [rax+8], 10
mov rax, gword ptr [rbp-08H]
mov word ptr [rax+12], 20
mov rax, gword ptr [rbp-08H]
mov byte ptr [rax+14], 1
mov rax, gword ptr [rbp-08H]
G_M52188_IG03:
lea rsp, [rbp]
pop rbp
ret
Interpretacja
Analizując powyższy kod zauważymy bardzo ciekawe rzeczy:
- Jeśli struktura ma conajwyżej 8 bajtów to jest przekazywana jako wartość przez rejestr.
- Obiekty są przekazywane przez wskaźnik, w dodatku ich obszar pamięci danych zaczyna się od offsetu 8 bajtów.
- Zamiast robić
mov rbp, rsp; sub rsp, X
emitowany jest kodsub rsp, X; lea rbp, [rsp+X]