Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 6개 있습니다.)
개발 환경 구성: 300. C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법
; https://www.sysnet.pe.kr/2/0/11052

.NET Framework: 828. C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/11884

개발 환경 구성: 466. C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 세 번째 이야기
; https://www.sysnet.pe.kr/2/0/12118

.NET Framework: 878. C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 네 번째 이야기(IL 코드로 직접 구현)
; https://www.sysnet.pe.kr/2/0/12120

.NET Framework: 880. C# - PE 파일로부터 IMAGE_COR20_HEADER 및 VTableFixups 테이블 분석
; https://www.sysnet.pe.kr/2/0/12126

.NET Framework: 881. C# DLL에서 제공하는 Win32 export 함수의 내부 동작 방식(VT Fix up Table)
; https://www.sysnet.pe.kr/2/0/12127




C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 네 번째 이야기(IL 코드로 직접 구현)

아래의 글을 쓰다 보니,

C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법
; https://www.sysnet.pe.kr/2/0/11052

C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/11884

C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 세 번째 이야기
; https://www.sysnet.pe.kr/2/0/12118

원론적인 이야기가 궁금해졌습니다. 찾아보면, IL 코드 수준에서 export하는 방법을 다음의 글에서 소개하고 있습니다.

Unmanaged code can wrap managed methods
; https://www.codeproject.com/Articles/8124/Unmanaged-code-can-wrap-managed-methods

그리고 저 글을 쓴 사람은 Inside IL Assembler 책에서 단서를 얻었다고 하는데 실제로 그 책에 실린 "Figure 18-3. Indirect referencing of v-table entries from the EAT"가 그 구조를 담고 있습니다.

[출처: https://books.google.co.kr/books?id=Xv_0AwAAQBAJ&pg=PA353]
il_export_1.png

(0x25ff는 절대 주소로의 JMP 구문인데 그것이 v-table의 MethodDef 값을 참조한다는 것이 직관적이지는 않습니다. 그냥 전체적인 동작 의미로 유추해 해석하면 될 듯합니다.)




일단, IL assembler 2.0 규격으로 export 함수를 구현하는 방법을 먼저 살펴보겠습니다. 예를 들기 위해 C#으로 다음과 같은 소스 코드를,

using System;

public class Class1
{
    public static void Func1() // export할 함수 1
    {
        Console.WriteLine("Func1");
        return;
    }

    public static int Func2() // export할 함수 2
    {
        return 5;
    }

    public static void Func3(int n)  // export할 함수 3
    {
        Console.WriteLine(n);
    }
}

빌드해 DLL을 얻고, 그것을 다시 ildasm.exe를 통해 IL 코드로 번역합니다.

ildasm ClassLibrary1.dll /out=ClassLibrary1.il

그럼 다음과 같은 출력 결과를 얻게 되는데,


// Metadata version: v4.0.30319
.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 4:0:0:0
}
.assembly ClassLibrary1
{
  // ...[생략]...
  .hash algorithm 0x00008004
  .ver 1:0:0:0
}

.module ClassLibrary1.dll
// MVID: {16FD8023-BE13-4D5D-8491-55801E2D5C90}
.imagebase 0x10000000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003       // WINDOWS_CUI
.corflags 0x00000001    //  ILONLY
// Image base: 0x06B90000

// =============== CLASS MEMBERS DECLARATION ===================

.class public abstract auto ansi sealed beforefieldinit Class1
       extends [mscorlib]System.Object
{
  .method public hidebysig static void  Func1() cil managed
  {
    // Code size       15 (0xf)
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldstr      "Func1"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  br.s       IL_000e

    IL_000e:  ret
  } // end of method Class1::Func1

  .method public hidebysig static int32  Func2() cil managed
  {
    // Code size       7 (0x7)
    .maxstack  1
    .locals init ([0] int32 V_0)
    IL_0000:  nop
    IL_0001:  ldc.i4.5
    IL_0002:  stloc.0
    IL_0003:  br.s       IL_0005

    IL_0005:  ldloc.0
    IL_0006:  ret
  } // end of method Class1::Func2

  .method public hidebysig static void  Func3(int32 n) cil managed
  {
    // Code size       9 (0x9)
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldarg.0
    IL_0002:  call       void [mscorlib]System.Console::WriteLine(int32)
    IL_0007:  nop
    IL_0008:  ret
  } // end of method Class1::Func3

} // end of class Class1

export할 메서드에 대해 단순히 다음과 같이 IL 언어로 표시만 해주면 됩니다.

.method public hidebysig static void  Func1() cil managed
  {
    // Code size       15 (0xf)
    .export [1] // 1 == ordinal number
    .maxstack  8
    IL_0000:  nop
    ...[생략]...

    IL_000e:  ret
  } // end of method Class1::Func1

  .method public hidebysig static int32  Func2() cil managed
  {
    // Code size       7 (0x7)
    .export [2] // 2 == ordinal number
    .maxstack  1
    .locals init ([0] int32 V_0)
    IL_0000:  nop
    ...[생략]...
    IL_0006:  ret
  } // end of method Class1::Func2

  .method public hidebysig static void  Func3(int32 n) cil managed
  {
    // Code size       9 (0x9)
    .export [3] // 3 == ordinal number
    .maxstack  8
    IL_0000:  nop
    ...[생략]...
    IL_0008:  ret
  } // end of method Class1::Func3

그다음 IL 언어로만 구성되었다는 표시를 제거하는데,

.corflags 0x00000001    //  ILONLY

32비트 assembly를 생성하는 경우에는 2로, 64비트 assembly를 생성하는 경우에는 0으로 지정합니다.

// 32bit
.corflags 0x00000002

// 64bit
.corflags 0x00000000

끝입니다. 이렇게만 바꾸고 ilasm.exe로 빌드하면,

// 32bit
ilasm ClassLibrary1.il /RESOURCE=ClassLibrary1.res /DLL

// 64 bit
ilasm ClassLibrary1.il /RESOURCE=ClassLibrary1.res /DLL /X64

"Figure 18-3. Indirect referencing of v-table entries from the EAT"에서 보여준 V-Table, VT Fixup Table은 모두 자동으로 생성되고 ".export [<ordinal>] as <export_name>"으로 명시한 부분이 PE 포맷의 IMAGE_EXPORT_DIRECTORY에 기록됩니다. 자, 그럼 마저 확인을 해봐야겠지요. ^^ ilasm.exe로 다시 빌드하고,

C:\temp> ilasm ClassLibrary1.il /RESOURCE=ClassLibrary1.res /DLL /X64

64 bit target must be specified for machine type /ITANIUM or /X64. Target set to 64 bit.

Microsoft (R) .NET Framework IL Assembler.  Version 4.8.3752.0
Copyright (c) Microsoft Corporation.  All rights reserved.
Assembling 'ClassLibrary1.il'  to DLL --> 'ClassLibrary1.dll'
Source file is ANSI

Assembled method Class1::Func1
Assembled method Class1::Func2
Assembled method Class1::Func3
Creating PE file

Emitting classes:
Class 1:        Class1

Emitting fields and methods:
Global
Class 1 Methods: 3;

Emitting events and properties:
Global
Class 1
Writing PE file
Operation completed successfully

dumpbin.exe 도구를 이용해 ilasm.exe가 출력한 DLL을 조사해 보면,

C:\temp> dumpbin /EXPORTS ClassLibrary1.dll
Microsoft (R) COFF/PE Dumper Version 14.24.28314.0
Copyright (C) Microsoft Corporation.  All rights reserved.

Dump of file ClassLibrary1.dll

File Type: DLL

  Section contains the following exports for ClassLibrary1.dll

    00000000 characteristics
    5E23CEF5 time date stamp Sun Jan 19 12:37:25 2020
        0.00 version
           1 ordinal base
           3 number of functions
           3 number of names

    ordinal hint RVA      name

          1    0 00002622 Func1
          2    1 0000262E Func2
          3    2 0000263A Func3

  Summary

        2000 .reloc
        2000 .rsrc
        2000 .sdata
        2000 .text

Func1, Func2, Func3이 export되었고 이와 함께 전에는 없던 (_MEM_WRITE 속성이 있는) ".sdata" 섹션이 생긴 것을 확인할 수 있습니다. (.export에 의해 생성되는 EAT와 자동으로 구성되는 V-Table이 .sdata 섹션에 위치하고, VT Fix up Table은 .text 섹션에 위치한다고 합니다.)

export한 함수가 정상적으로 동작하는지, 또 다른 Console 응용 프로그램으로 DllImport를 이용해 호출까지 하는 것으로 확인을 완료할 수 있습니다.

using System;
using System.Runtime.InteropServices;

class Program
{
    [DllImport("ClassLibrary1.dll")]
    public static extern void Func1();

    [DllImport("ClassLibrary1.dll")]
    public static extern int Func2();

    [DllImport("ClassLibrary1.dll")]
    public static extern void Func3(int n);

    static void Main(string[] args)
    {
        Func1();
        Console.WriteLine(Func2());
        Func3(3);
    }
}

/* 출력 결과
Func1
5
3
*/




그러니까, 결국 특정 DLL에 정의된 메서드를 export하고 싶다면 1) 그 메서드를 식별할 수 있어야 하고 2) IL disasm의 결과에서 .corflags와 .export를 재정의/삽입한 다음 3) 다시 ilasm시키면 됩니다.

그래서 다음의 글에 실린 ExportDll.exe 도구는,

How to Automate Exporting .NET Function to Unmanaged Programs
; https://www.codeproject.com/Articles/16310/How-to-Automate-Exporting-NET-Function-to-Unmanage

ildasm.exe로 얻은 il 코드에서 ExportDllAttribute가 설정된 메서드를 "텍스트"로써 찾은 다음 "// Code"라는 주석을 기준으로 ".export ..." 항목을 심어 다시 ilasm.exe하는 단순한 역할을 합니다. 즉, 복잡한 것은 ildasm.exe/ilasm.exe가 모두 대신하고 있는 것입니다.

정리하면, ExportDLL.exe의 IL 코드 변환 부분만 아주 쉽게 재작성해 보면 다음과 같은 정도로 나타낼 수 있는 것입니다.

using System.IO;
using System.Text;

namespace MakeExport
{
    class Program
    {
        static void Main(string[] args)
        {
            string filePath = args[0];
            string targetPlatform = args[1];

            int index = 1;
            StringBuilder sb = new StringBuilder();

            foreach (string line in File.ReadAllLines(filePath))
            {
                if (line.IndexOf("// Code size") == -1)
                {
                    sb.AppendLine(line);
                }
                else if (line.IndexOf(".corflags") != -1)
                {
                    if (targetPlatform == "x64")
                    {
                        sb.AppendLine(".corflags 0x00000000");
                    }
                    else
                    {
                        sb.AppendLine(".corflags 0x00000002");
                    }
                }
                else
                {
                    sb.AppendLine(line);
                    sb.AppendLine($".export[{index}]");
                    index++;
                }
            }

            File.WriteAllText(filePath, sb.ToString());
        }
    }
}

그래서 이것을 적절하게 batch 스크립트와 함께 작성해 주면,

SET BUILDCONFIG=Debug
SET DLLNAME=ClassLibrary1
SET TARGETPLATFORM=x64

ildasm %DLLNAME%.dll /out=%DLLNAME%.il

MakeExport.exe %DLLNAME%.il %TARGETPLATFORM%

if '%TARGETPLATFORM%' == 'x64' (
    ilasm %DLLNAME%.il /RESOURCE=%DLLNAME%.res /DLL /X64
) else (
    ilasm %DLLNAME%.il /RESOURCE=%DLLNAME%.res /DLL
)

나름의 ExportDll.exe 역할을 하게 됩니다. ^^

(첨부 파일은 위의 예제를 모아두었고, 빌드한 다음 ClassLibrary1.csproj가 있는 폴더의 il_export.bat를 "Developer Command Prompt for VS 2019" 명령행에서 실행하면 됩니다.)




기왕 알아본 김에 조금 더 내려가보겠습니다. IL assembler 2.0 규격으로 ".export"를 지정하면 V-Table, VT Fixup Table을 자동으로 구성해 준다고 했는데요. 당연히 수작업으로 구성할 수도 있습니다.

VT Fixup Table은 ".vtfixup" 코드로 구성되는데,

.vtfixup [<num_slots>] <flags> at <data_label>

예)
.vtfixup [1] int32 fromunmanaged at VT_01
.data VT_01 = int32(0x0600001A)

".vtfixup"은 다중으로 정의할 수 있고 개별적으로 "테이블"이 구성됩니다. 해당 테이블에 담을 항목 수는 "[num_slots]"에 지정된 숫자가 결정하며 그 테이블에 들어갈 entry의 규격은 "data_label"로 연결된 항목으로 결정됩니다. 즉, 위와 같은 경우에는 int32로 지정되었기 때문에 slot 1개를 가진 테이블 하나를 정의한 것입니다.

가령 메서드 3개를 export하고 싶다면 다음과 같은 식으로 정의할 수 있습니다.

// 32비트인 경우
.vtfixup [1] int32 fromunmanaged at VT_01
.data VT_01 = int32(0x0600001A)

.vtfixup [1] int32 fromunmanaged at VT_02
.data VT_02 = int32(0x0600001B)

.vtfixup [1] int32 fromunmanaged at VT_03
.data VT_03 = int32(0x0600001C)

// 64비트인 경우
.vtfixup [1] int64 fromunmanaged at VT_01
.data VT_01 = int64(0x0600001A)

.vtfixup [1] int64 fromunmanaged at VT_02
.data VT_02 = int64(0x0600001B)

.vtfixup [1] int64 fromunmanaged at VT_03
.data VT_03 = int64(0x0600001C)

여기서 0x0600001A, 0x0600001B, 0x0600001C 값은 EAT와 연결될 메서드의 MethodToken 값이기 때문에 이것을 일일이 알아내서 연결하는 것은 불편하므로 ilasm.exe로 하여금 자동으로 연결하도록 0을 지정하는 것이 좋습니다.

.vtfixup [1] int32 fromunmanaged at VT_01
.data VT_01 = int32(0)

.vtfixup [1] int32 fromunmanaged at VT_02
.data VT_02 = int32(0)

.vtfixup [1] int32 fromunmanaged at VT_03
.data VT_03 = int32(0)

그리고 어차피 저렇게 생성된 3개의 테이블은 내부 slot의 규격이 같기 때문에 편의상 다음과 같이 1개로 정의하는 것도 가능합니다.

// 32비트인 경우
.vtfixup [3] int32 fromunmanaged at VT_01
.data VT_01 = int32(0)[3]

// 64비트인 경우
.vtfixup [3] int64 fromunmanaged at VT_01
.data VT_01 = int64(0)[3]

마지막으로, 이렇게 정의된 VT Fixup Table과 EAT와 연결해야 하는데 그 역할을 각 메서드의 ".vtentry"로 할 수 있습니다.

.vtentry <entry_number> : <slot_number>

첫 번째 entry_number는 VT Fixup Table을 지정하고, 두 번째 slot_number는 해당 Table 내의 slot 인덱스를 지정합니다. 따라서 3개의 vtfixup으로 정의한 경우라면 이렇게 .vtentry를 사용하고,

.method public hidebysig static void  Func1() cil managed
  {
    .vtentry 1:1
    .export [1]
    ...[생략]...
  } // end of method Class1::Func1

  .method public hidebysig static int32  Func2() cil managed
  {
    .vtentry 2:1
    .export [2]
    ...[생략]...
  } // end of method Class1::Func2

  .method public hidebysig static void  Func3(int32 n) cil managed
  {
    .vtentry 3:1
    .export [3]
    ...[생략]...
  } // end of method Class1::Func3

3개의 slot을 가진 한 개의 vtfixup으로 정의한 경우라면 다음과 같이 지정하면 됩니다.

.method public hidebysig static void  Func1() cil managed
  {
    .vtentry 1:1
    .export [1]
    ...[생략]...
  } // end of method Class1::Func1

  .method public hidebysig static int32  Func2() cil managed
  {
    .vtentry 1:2
    .export [2]
    ...[생략]...
  } // end of method Class1::Func2

  .method public hidebysig static void  Func3(int32 n) cil managed
  {
    .vtentry 1:3
    .export [3]
    ...[생략]...
  } // end of method Class1::Func3

다행히 여기까지가 IL 수준에서 들어갈 수 있는 최대 깊이입니다.




"Unmanaged code can wrap managed methods" 글에 실린 IL 코드를 ilasm.exe로 실행한다면 다음과 같은 오류가 발생합니다.

C:\temp> ilasm test.il

Microsoft (R) .NET Framework IL Assembler.  Version 4.8.3752.0
Copyright (c) Microsoft Corporation.  All rights reserved.
Assembling 'test.il'  to EXE --> 'test.exe'
Source file is ANSI

test.il(43) : error : syntax error at token 'from' in: .vtfixup [1] int32 from unmanaged at VT_01

***** FAILURE *****

(그 당시의 상황에서는 잘 되었을지도 모르지만) 43라인의 코드가,

.vtfixup [1] int32 from unmanaged at VT_01

이렇게 바뀌어야 합니다.

.vtfixup [1] int32 fromunmanaged at VT_01




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 1/20/2020]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 121  122  123  124  125  126  127  128  129  130  [131]  132  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
1780정성태10/15/201424225오류 유형: 249. The application-specific permission settings do not grant Local Activation permission for the COM Server application with CLSID
1779정성태10/15/201419755오류 유형: 248. Active Directory에서 OU가 지워지지 않는 경우
1778정성태10/10/201418208오류 유형: 247. The Netlogon service could not create server share C:\Windows\SYSVOL\sysvol\[도메인명]\SCRIPTS.
1777정성태10/10/201421321오류 유형: 246. The processing of Group Policy failed. Windows attempted to read the file \\[도메인]\sysvol\[도메인]\Policies\{...GUID...}\gpt.ini
1776정성태10/10/201418335오류 유형: 245. 이벤트 로그 - Name resolution for the name _ldap._tcp.dc._msdcs.[도메인명]. timed out after none of the configured DNS servers responded.
1775정성태10/9/201419457오류 유형: 244. Visual Studio 디버깅 (2) - Unable to break execution. This process is not currently executing the type of code that you selected to debug.
1774정성태10/9/201426649개발 환경 구성: 246. IIS 작업자 프로세스의 20분 자동 재생(Recycle)을 끄는 방법
1773정성태10/8/201429801.NET Framework: 471. 웹 브라우저로 다운로드가 되는 파일을 왜 C# 코드로 하면 안되는 걸까요? [1]
1772정성태10/3/201418613.NET Framework: 470. C# 3.0의 기본 인자(default parameter)가 .NET 1.1/2.0에서도 실행될까? [3]
1771정성태10/2/201428113개발 환경 구성: 245. 실행된 프로세스(EXE)의 명령행 인자를 확인하고 싶다면 - Sysmon [4]
1770정성태10/2/201421715개발 환경 구성: 244. 매크로 정의를 이용해 파일 하나로 C++과 C#에서 공유하는 방법 [1]파일 다운로드1
1769정성태10/1/201424136개발 환경 구성: 243. Scala 개발 환경 구성(JVM, 닷넷) [1]
1768정성태10/1/201419559개발 환경 구성: 242. 배치 파일에서 Thread.Sleep 효과를 주는 방법 [5]
1767정성태10/1/201424650VS.NET IDE: 94. Visual Studio 2012/2013에서의 매크로 구현 - Visual Commander [2]
1766정성태10/1/201422516개발 환경 구성: 241. 책 "프로그래밍 클로저: Lisp"을 읽고 나서. [1]
1765정성태9/30/201426063.NET Framework: 469. Unity3d에서 transform을 변수에 할당해 사용하는 특별한 이유가 있을까요?
1764정성태9/30/201422308오류 유형: 243. 파일 삭제가 안 되는 경우 - The action can't be comleted because the file is open in System
1763정성태9/30/201423878.NET Framework: 468. PDB 파일을 연동해 소스 코드 라인 정보를 알아내는 방법파일 다운로드1
1762정성태9/30/201424556.NET Framework: 467. 닷넷에서 EIP/RIP 레지스터 값을 구하는 방법 [1]파일 다운로드1
1761정성태9/29/201421613.NET Framework: 466. 윈도우 운영체제의 보안 그룹 이름 및 설명 문자열을 바꾸는 방법파일 다운로드1
1760정성태9/28/201419892.NET Framework: 465. ICorProfilerInfo::GetILToNativeMapping 메서드가 0x80131358을 반환하는 경우
1759정성태9/27/201430997개발 환경 구성: 240. Visual C++ / x64 환경에서 inline-assembly를 매크로 어셈블리로 대체하는 방법파일 다운로드1
1758정성태9/23/201437897개발 환경 구성: 239. 원격 데스크톱 접속(RDP)을 기존의 콘솔 모드처럼 사용하는 방법 [1]
1757정성태9/23/201418438오류 유형: 242. Lync로 모임 참여 시 소리만 들리지 않는 경우 - 두 번째 이야기
1756정성태9/23/201427469기타: 48. NVidia 제품의 과다한 디스크 사용 [2]
1755정성태9/22/201434252오류 유형: 241. Unity Web Player를 설치해도 여전히 설치하라는 화면이 나오는 경우 [4]
... 121  122  123  124  125  126  127  128  129  130  [131]  132  133  134  135  ...