C# 9.0의 Function pointer를 이용한 함수 주소 구하는 방법
그러고 보니,
C# 9.0 - (6) Function pointers
; https://www.sysnet.pe.kr/2/0/12374
신규 문법은 unmanaged, managed 메서드에 대해 모두 함수 포인터를 지원하는데, 그것 자체가 포인터이므로 사용하기도 간단합니다. 살짝 테스트를 해볼까요? ^^
using System;
using System.Reflection;
class Program
{
static void Main(string[] args)
{
// JIT 완료를 위해!
Program.CallMyMethod();
unsafe
{
delegate*<void> func = &Program.CallMyMethod;
Console.WriteLine("function pointer: " + new IntPtr(func).ToInt64().ToString("x"));
}
}
public static void CallMyMethod()
{
Console.WriteLine("CallMyMethod");
}
}
위의 프로그램을 비주얼 스튜디오에서 F5 디버깅으로 확인하면, func 주소에 해당하는 코드가 다음과 같이 jmp를 가리키고 있는 것을 볼 수 있습니다.
01210448 E9 23 06 00 00 jmp Program.CallMyMethod() (01210A70h)
0121044D 5F pop edi
재미있는 것은, 위의 프로그램을 F5 디버깅이 아닌 (Ctrl + F5로) 그냥 실행했을 경우엔
_PrecodeFixupThunk의 호출 코드를 가리킨다는 점입니다.
02A80460 E8 AB EC 4B 6F call _PrecodeFixupThunk@0 (71F3F110h)
02A80465 5E pop esi
02A80466 00 00 add byte ptr [eax],al
02A80468 6C ins byte ptr es:[edi],dx
02A80469 4D dec ebp
02A8046A 09 01 or dword ptr [ecx],eax
02A8046C 00 00 add byte ptr [eax],al
02A8046E 00 00 add byte ptr [eax],al
예전에 위의
thunk 호출에 대해 확인하는 방법을 살펴본 적이 있었는데요, 그것에 따라 계산해,
rax == 02A80465
r10 == 0, r11 == 0
MethodDesc == [rax + r10 * 4 + 3] == [02A80468] == 01094d6c
얻은 값(01094d6c)을
windbg의 dumpmd로 보면 Program.CallMyMethod를 가리킵니다.
0:000> !DumpMD 01094d6c
Method Name: Program.CallMyMethod()
Class: 010912f8
MethodTable: 01094d80
mdToken: 06000003
Module: 01094044
IsJitted: yes
CodeAddr: 02a80a50
Transparency: Critical
당연히 (이전에 Program.CallMyMethod 호출을 했으므로) CodeAddr의 위치는 기계어로 번역된 함수의 처음 부분에 해당합니다.
59: public static void CallMyMethod()
60: {
02A80A50 55 push ebp
02A80A51 8B EC mov ebp,esp
02A80A53 83 3D F0 42 09 01 00 cmp dword ptr ds:[10942F0h],0
02A80A5A 74 05 je Program.CallMyMethod()+011h (02A80A61h)
02A80A5C E8 1F F3 85 6F call JIT_DbgIsJustMyCode (722DFD80h)
02A80A61 90 nop
61: Console.WriteLine("CallMyMethod");
02A80A62 8B 0D 44 23 BA 03 mov ecx,dword ptr ds:[3BA2344h]
02A80A68 E8 F7 44 D2 6D call System.Console.WriteLine(System.String) (707A4F64h)
02A80A6D 90 nop
62: }
02A80A6E 90 nop
02A80A6F 5D pop ebp
02A80A70 C3 ret
그러니까, 함수 포인터의 호출은 일반 메서드의 접근과는 별도로 또 다른 call site의 역할을 하도록 처리한 것 같습니다. 따라서 precode fixup을 호출한다고 해서 함수 포인터의 속도에 영향이 있는 것은 아닙니다. 왜냐하면 마찬가지로 함수 포인터의 호출 역시 처음 한 번만 precode fixup 단계를 거칠 뿐 이후에는 jmp로 바뀌기 때문입니다.
정리해 보면, 함수 포인터를 통한 닷넷 메서드의 주소를 (F5, Ctrl+F5 실행 방식에 상관없이) 가져오고 싶다면 아쉽게도 함수 포인터가 한 번은 실행되었어야 가능합니다. 그런 경우라면, 다음과 같은 식의 코드로 간단하게 처리할 수 있습니다.
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
class Program
{
static void Main(string[] args)
{
// MethodHandle.GetFunctionPointer() 값과 비교를 위해!
MethodInfo mi = typeof(Program).GetMethod("CallMyMethod",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
RuntimeHelpers.PrepareMethod(mi.MethodHandle);
unsafe
{
delegate*<void> func = &Program.CallMyMethod;
func();
IntPtr ptrFunc = ReadJmpPointer32(func, 0xe9);
Console.WriteLine("function pointer: " + new IntPtr(func).ToInt64().ToString("x"));
Console.WriteLine("function pointer target: " + ptrFunc.ToInt64().ToString("x"));
Console.WriteLine("MethodInfo: " + mi.MethodHandle.GetFunctionPointer().ToInt64().ToString("x"));
Console.ReadLine();
}
}
private static unsafe IntPtr ReadJmpPointer32(delegate*<void> func, byte jmpCode)
{
IntPtr ptr = new IntPtr(func);
byte* pBuf = (byte*)ptr;
if (*pBuf != jmpCode)
{
return IntPtr.Zero;
}
if (IntPtr.Size == 4)
{
int pos = pBuf[1] | (pBuf[2] << 8) | (pBuf[3] << 16) | pBuf[4] << 24;
return IntPtr.Add(ptr, pos + /* jmp 5bytes */ 5);
}
throw new ApplicationException("x64 - Not supported yet");
}
public static void CallMyMethod()
{
Console.WriteLine("CallMyMethod");
}
}
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
시간 되시면 다음의 글도 한 번 보시고. ^^
상황별 GetFunctionPointer 반환값 정리
; https://www.sysnet.pe.kr/2/0/1027
상황별 GetFunctionPointer 반환값 정리 - x64
; https://www.sysnet.pe.kr/2/0/12143
C++의 inline asm 사용을 .NET으로 포팅하는 방법 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/10889
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]