상황별 GetFunctionPointer 반환값 정리 - x64
지난 글에서는,
상황별 GetFunctionPointer 반환값 정리 - x86
; https://www.sysnet.pe.kr/2/0/1027
x86을 대상으로만 했었는데, 이번에는 .NET 4.8 / x64 / Debug 빌드로 실행한 결과를 정리해 보겠습니다.
EXE 어셈블리의 Main 메서드
class Program
{
static void Main(string[] args)
{
ShowMainFunc();
}
static void ShowMainFunc()
{
MethodBase func = typeof(Program).GetMethod("Main", BindingFlags.Static | BindingFlags.NonPublic);
IntPtr pMain = func.MethodHandle.GetFunctionPointer();
OutputFunctionAddress("Main", pMain);
}
private static void OutputFunctionAddress(string title, IntPtr pAddr)
{
Console.WriteLine($"{title} == {pAddr.ToInt64():x}");
}
}
x86과 결과와 다르지 않습니다. 특이한 점이 있다면 "
Windbg 환경에서 확인해 본 .NET 메서드 JIT 컴파일 전과 후 - 두 번째 이야기" 글에서의 PreStub 과정 없이 곧바로 Fixup Precode에서 Native Code를 호출하는 jmp 문으로 패치된다는 점입니다. (Main 함수야말로 단 한번 실행되는 경우가 대부분일 것이므로 굳이 공을 들여 최적화를 안 해도 될 텐데 말이죠. ^^)
또한
MethodDesc 위치의 +8 바이트 위치에 저장한 기계어 코드의 주소도 GetFunctionPointer의 값과 일치합니다.
어셈블리에 정의된 메서드의 JIT 이전
class Program
{
static void Main(string[] args)
{
ShowTestFunc("Test1 - Before JITting", "Test1");
Console.ReadLine();
Program pg = new Program();
pg.Test1();
}
private void Test1()
{
Console.WriteLine("Test1 called!");
}
static void ShowTestFunc(string text, string methodName)
{
MethodBase func = typeof(Program).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic);
IntPtr pFunc = func.MethodHandle.GetFunctionPointer();
OutputFunctionAddress(text, pFunc);
}
private static void OutputFunctionAddress(string title, IntPtr pAddr)
{
Console.WriteLine($"{title} == {pAddr.ToInt64():x}");
}
}
/* 출력 결과
Test1 - Before JITting == 7ffe48310488
*/
이에 대해서도 "
Windbg 환경에서 확인해 본 .NET 메서드 JIT 컴파일 전과 후 - 두 번째 이야기" 글에서 살펴봤습니다. GetFunctionPointer가 반환한 주소는 "Fixup Precode"를 가리키고 실제 호출하려고 할 때의 call 코드의 변위도 정확히 GetFunctionPointer의 반환 주소를 가리킵니다. (따라서 x86의 call 주솟값과는 다르다는 차이점이 있습니다.)
00007ffe`48310912 e871fbffff call 00007ffe`48310488 (Program.Test1(), mdToken: 0000000006000002)
어셈블리에 정의된 메서드의 JIT 이후
static void Main(string[] args)
{
ShowTestFunc("Test1 - Before JITting", "Test1");
Program pg = new Program();
pg.Test1();
ShowTestFunc("Test1 - After JITting", "Test1");
}
/* 출력 결과
Test1 - Before JITting == 7ffe48300488
Test1 called!
Test1 - After JITting == 7ffe48300c10
*/
이번에도 "
Windbg 환경에서 확인해 본 .NET 메서드 JIT 컴파일 전과 후 - 두 번째 이야기" 글의 내용과 일치합니다.
0:000> !name2ee ConsoleApp1.exe!Program.Test1
Module: 00007ffe481f4148
Assembly: ConsoleApp1.exe
Token: 0000000006000002
MethodDesc: 00007ffe481f5a10
Name: Program.Test1()
JITTED Code Address: 00007ffe48300c10
0:000> dq 00007ffe481f5a10 L2
00007ffe`481f5a10 00080006`21020002 00007ffe`48300c10
정리해 보면, Jit 전에는 Fixup Precode의 위치를 GetFunctionPointer 메서드가 반환하지만, 일단 JIT가 되면 GetFunctionPointer는 해당 메서드의 Body가 컴파일된 메서드의 주솟값을 반환합니다.
NGen 된 BCL(Base Class Library) 메서드
public static void ShowCreateCommandAddress()
{
MethodBase func = typeof(SqlConnection).GetMethod("CreateCommand", BindingFlags.Instance | BindingFlags.Public);
IntPtr pOld = func.MethodHandle.GetFunctionPointer();
OutputFunctionAddress("CreateCommand", pOld);
}
/* 출력 결과
CreateCommand == 7ffe48320510
*/
GetFunctionPointer가 반환한 값(7ffe48320510)은 특이하게 Fixup Precode의 위치입니다. (게다가 method desc을 구하는 인덱스가 모두 0, 0입니다.)
0:000> !u 7ffe48320510
Unmanaged code
00007ffe`48320510 e80b40545f call clr!PrecodeFixupThunk (00007ffe`a7864520)
00007ffe`48320515 5e pop rsi
00007ffe`48320516 0000 add byte ptr [rax],al
00007ffe`48320518 081d9d72fe7f or byte ptr [00007ffe`c83077bb],bl
0:000> !ip2md 7ffe48320510 // 왜냐하면 fixup precode의 위치이므로.
Failed to request MethodData, not in JIT code range
하지만, 해당 메서드의 MethodDesc 값에는 정확히 기계어로 번역된 주솟값을 가지고 있습니다.
0:000> !name2ee System.Data.dll!System.Data.SqlClient.SqlConnection.CreateCommand
Module: 00007ffe72961000
Assembly: System.Data.dll
Token: 0000000006001a69
MethodDesc: 00007ffe729d1d08
Name: System.Data.SqlClient.SqlConnection.CreateCommand()
JITTED Code Address: 00007ffe72ea4410
0:000> dq 00007ffe729d1d08 L2
00007ffe`729d1d08 00006453`3b7c9a69 00000000`004d2700
0:000> ? 00007ffe`729d1d10 + 004d2700
Evaluate expression: 140730826376208 = 00007ffe`72ea4410
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
이 외에 재미있는 거 하나 더 언급해 보면 Visual Studio에서 F5 디버깅을 시작했을 때의 JIT 컴파일이 다소 특별하다는 점입니다. 실제로 그 상태에서 실행해 GetFunctionPointer로 출력된 주소의 값을 JIT 컴파일 전/후에 따라 각각 살펴보면,
// GetFunctionPointer의 반환 값 == 00007FFE48330490
[JIT 전]
00007FFE48330490 E8 8B 40 53 5F call 00007FFEA7864520
00007FFE48330495 5E pop rsi
[JIT 후]
00007FFE48330490 E9 CB 06 00 00 jmp 00007FFE48330B60
00007FFE48330495 5F pop rdi
Tiered Compilation 동작을 무시하고 Fixup Precode의 call 코드가 곧바로 Native Code로의 jump 문으로 패치되는 것을 볼 수 있습니다. 게다가 Visual Studio의 디버깅 상태가 아니라면 GetFunctionPointer는 위에서처럼 Fixup Precode의 위치를 가리키지 않고 IL 코드가 번역된 Native Code의 주소를 가리켰다는 점이 다릅니다.
한 마디로, 상황 별로 GetFunctionPointer가 반환하는 값이 달라 이에 대해 어떤 고정적인 동작을 가정하고 코딩해서는 안 됩니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]