이전 강좌에서 만들었던 CTest클래스를 그대로 사용하겠습니다.

매나지드c++로 랩퍼 DLL을 만들어서 이것을 우회적으로 경유하여 네이티브코드와 매나지드코드를 붙이는 방법입니다.

먼저 프로잭트 세팅을 다음과 같이 해줍니다.



기본적으로 매나지드 코드이기 때문에 네이티브 c++쪽 선언정보를 다음과 같이 #pragma unmanaged 키워드로 감싸서 표기 합니다.



이렇게 하면 매나지드 c++코드에서 네이티브 코드를 혼용해서 사용할수 있습니다. 단점은 CLR에서는 인탤리젼트 기능이 무력화되있기때문에 약간은 좀 불편 하실수도있습니다. 그러나 비주얼 어시스트를 쓰면 됩니다. ^^;

다음은 랩퍼 클래스입니다.



CreateTest라는 함수를 먼저 보면 인스턴스를 만들어 주는 함수인데요.




네이티브 코드의 정적 함수를 호출하여 네이티브 객체를 Wrap함수에 넘겨주는 일을 합니다.

그 다음 Wrap에서는 받아온 네이티브 포인터를 매나지드 참조형( ^  이라고 표기되는... )으로 바꿔 주는 역활을 합니다.


gcnew를 써서 랩퍼 객체를 만들어 줍니다. 생성자는 인자로 네이티브포인터를 받습니다.

생성자는 아래와 같이 구현됩니다.



가장 중요한(?) 더하기 기능을해주는 함수는 아래와 같이 네이티브 코드를 원격 호출하는것으로 구현됩니다.





이렇게 만들어진 DLL은 매나지드코드와 연결이 가능한 DLL입니다. 이걸가지고 각종 .Net40 환경의 여러 언어들과 소통이 가능합니다.

C#프로잭트에 Add Reference 해주셔야합니다.
lime.dll은 매나지드 랩퍼 dll이고
testDll.dll은 순수 네이티브 dll 입니다.



C#으로 쓰는 예는 다음과 같습니다.





소스 첨부 합니다.



닷넷4.0에서 네이티브코드와 매나지드코드의 동거 part 2-1.

CLR 2010. 8. 10. 18:00 Posted by 알 수 없는 사용자
이번에는 저번 강좌에 이어서 클래스를 다뤄 보도록 하겠습니다.

약간 강좌가  외전으로 빠지는데요. ^^;

우선 명시적인 방법으로....

네이티브 코드에서 네이티브코드DLL로 작성된 클래스를 얻는 전통적인 방법부터 알아 보도록 하겠습니다.

class __declspec(dllexport) CTest {
public:
int Add(int a,int b);

static CTest *Create();
static void Delete(CTest* pobj);
};

int CTest::Add(int a,int b)
{
return a+b;
}

CTest* CTest::Create()
{
return new CTest();
}

void CTest::Delete(CTest* pobj)
{
delete pobj;
}

이와 같이 클래스를 선언해줍니다.  외부에서 동적으로 클래스를 참조하기 위해서는 ' __declspec(dllexport) ' 가 꼭 필요합니다.


사용하는 쪽에서는 다음과 같이 클래스를 정의만 해줍니다.



암시적접근은 정적 라이브러리 쓰는것과 동일하지만 명시적으로 접근 할때는 다음과 같이 펙토리 함수등을 통해서 객체를 직접 얻어와야 합니다.



정적 함수인 Create의 함수 포인터를 이용해서 객체에 대한 인스턴스를 직접 얻어옵니다. 이것을 this포인터로 사용합니다.
외부 참조 가능한 클래스멥버함수의 호출규약은 첫번째인자에 this포인터를 넘겨주도록 정해져있습니다. 

CTest::Add 함수의 포인터를 얻어와 첫번째 인자로 방금 얻은 객체 포인터를 넘겨줍니다.

pCalAdd(pCalc,10,7);

pCalc는 Create함수를 통해 얻어진 CTest클래스의 인스턴스포인터 입니다.
pCalAdd는 CTest::Add의 함수 포인터 입니다.

여기서  GetProcAddress 로 넘겨주는 함수이름이 [?Create@CTest@@SAPAV1@XZ] 처럼 이상한데요. 이유는 C++컴파일러가 외부 참조가능하도록 전통적인 c언어형식으로 함수를 다시 만들기 때문입니다.

원래는 2강좌로 마칠려고 했는데 쓰다보니 약간 두서 없이 방향이..... ㅡ.ㅡ;;;

다음번엔 진짜로 매나지드 코드와 네이티브 클래스 연결법에 대해서 알아보도록 하겠습니다. ^^;


닷넷4.0에서 네이티브코드와 매나지드코드의 동거 part 1.

CLR 2010. 8. 2. 21:03 Posted by 알 수 없는 사용자
닷넷 4.0 환경에서 네이티브 코드 와 매나지드 코드를 연결하는 방법을 2 회정도로 나누어서 다뤄볼까합니다.

닷넷에 대한 기초적인 개념이 구조는 다른 좋은 강좌들이 많으니 넘어가기로 하겠습니다. 저는 실제로 응용사례를 통해서 방법을 소개 해볼까합니다.

기본 아이디어는 다음과 같습니다.




그림에서 처럼 네이티브로 작성된 DLL 네이티브 코드와 혼합된 DLL을 경유해서 닷넷 기반언어에서 사용하는 방법입니다.
게임개발시 이런 방법을 사용할경유 장점은 네이티브코드로된 게임엔진으로 툴제작시 mfc를 사용하지않고 좀더 다루기 쉬운 C#을 사용해서 툴을 만들수있다는 장점있습니다.
뿐만 아니라 게임에서 많이 쓰이는 루아나 파이썬대신 C#으로 스크립트언어로도 사용할수있다는 점이있습니다. C#을 스크립트언어로 사용할경우 VS강력한 IDE의 화력지원을 받아 좀더 효과적인 개발을 할수있기때문입니다.

먼저 간단하게 전역함수를 콜해보는 예제를 살펴 보도록 하겠습니다.



이런식으로 네이티브 DLL을 만든 다음 이것을 매나지드 코드 DLL로 한번더 랩핑을 해주어야합니다.


class 이름 앞에 ref라고 붙는데 이렇게 하면 닷넷 기반의 다른 언어들이 같이 쓸수잇는 클래스가 만들어 집니다. ref가 붙지 않으면 오직 네이티브에서 만 쓸 수 있는 클래스가 만들어 지고요.

 

C#쪽에서는 다음과 같이 사용을 할 수 있습니다.

다음에는 네이티브 코드쪽 class를 매나지드 코드 class로 랩핑하여 매나지드 언어에서 콜하는 방법을 알아보도록 하겠습니다.





8. System.Object (2)

CLR 2010. 3. 3. 09:00 Posted by 알 수 없는 사용자
2. GetHashCode

객체 값에 대한 HashCode를 반환해주는 Method입니다. 근데 이 Method가 반환해 주는 Hashcode는 그닥 쓸모가 없다고 합니다. 각 객체마다 유일한 Hashcode를 보장해 주어야 하지만 그렇지 못하기 때문입니다.

암튼 디자이너는 어떠한 객체라도 HashTable에 담길 수 있다면 여러모로 편리할 것이라고 판단해서 모든 Object를 상속하는 객체는 이 Method를 통해 Int32형태의 hashcode를 얻을 수 있도록 설계했다고 합니다.

하지만 결국 이 Method로 유일한 hashcode를 제대로 반환해 줄 수 있도록 사용하기 위해서는 override를 통해 재작성하여 사용해야 합니다. 그래서 CLR via C# 에서는 Object의 Method로 정의되어 있을 것이 아니라 interface로 정의해 놓는게 맞는게 아닌가 라고 말합니다.

그런데 이 GetHashCode를 재정의해서 사용하기 위해서는 기억해야 할 점이 있습니다. 지난 번에 공부했던 Object의 Method인 Equals()를 재정의 한다면 이 GetHashCode()도 재정의를 해 주어야 한다는 점입니다. 만약 Equals()만 재정의 한다면 컴파일시 warning을 발생합니다.

------ Build started: Project: Test04, Configuration: Debug x86 ------
C:\Documents and Settings\XPMUser\my documents\visual studio 2010\Projects\TestSolution\Test04\Program.cs(8,11): warning CS0659: 'Test04.Animal' overrides Object.Equals(object o) but does not override Object.GetHashCode()

Animal이라는 Class에서 Equals()만 재정의 했더니 위와 같은 경고 메시지를 발생하는 것을 볼 수 있습니다. 이유는 두 객체가 같다면 (Equals()의 값이 true라면) 두 객체의 hashcode도 같을 것이라는 가정을 System.Collections.HashTable 타입과 System.Collections.Generic.Dictionary 타입이 하고 있기 때문입니다.
결국 객체의 동질성을 판단하는 Equals()의 알고리즘에 의해 두 객체가 같다고 판명이 난다면 GetHashCode()를 구하는 알고리즘도 Equals()을 판단하는 알고리즘과 연관되어 같은 값을 반환할 수 있도록 구현해 주어야 합니다.



GetHashCode()는 객체가 속한 AppDomain수준에서 유일할 수 있는 ID를 숫자로 반환하게 되어 있으며 이 값은 객체의 일생동안 바뀌지 않는 것이 보장되지만 이 객체가 가비지 수집기에 의해 수집되면 수집된 객체 hashcode를 의미하는 ID가 다른 새로운 객체에 재활용될 수 있다고 하네요. 그래서 유일한 값을 보장 못한다고 말하는데 그렇다면 AppDomain 수준에서 객체의 유일한 값을 보장해주는 HashCode를 반환해주는 Method가 CLR에 없을까요?

아니 있습니다. System.Runtime.ComplierServices 네임 스페이스에 있는 RuntimeHelpers 클래스의 정적 Method인 GetHashCode 메서드를 제공하고 있습니다. 객체가 Object의 GetHashCode 메서드를 재정의했다고 하더라도 해당 Object를 RuntimeHelpers.GetHashCode() 메소드의 인자로 제공하면 유일한 값을 보장받을 수 있는 ID를 제공해 줍니다.

아 그럼 Object의 GetHashCode()를 쓸게 아니라 저 RuntimeHelpers.GetHashCode()를 쓰면 되겠구나.

하면 되겠죠.


그런데 이 GethashCode()가 VS2010에 포함되어 있는 4.0X 버전의 mscorlib.dll 에서 구현하고 있는 것과 이전 버전에서 구현하고 있는 것이 좀 틀립니다.

우선 기존의 CLR의 mscorlib.dll에서 제공하고 있는 GetHashCode()의 내부를 살펴보도록 하겠습니다.


내부에서 Object.InternalGetHashCode()라는 Method를 호출하고 있네요. 저 함수를 들어가 보면


위와 같은 코드를 볼 수 있습니다. 함수 선언을 보면 internalcall이라는게 붙어 있는데 이것은 unmanaged코드가 불리는 것이라고 생각하면 됩니다. CLR의 관리되는 코드가 아니라 nativecode의 영역에서 구현되어 있다는 말이지요.

그래서 실제 GetHashCode는 다음과 같이 구현이 되어 있다고 합니다.

FCIMPL1(INT32, ObjectNative::GetHashCode, Object* obj) {
    
    CONTRACTL
    {
        THROWS;
        DISABLED(GC_NOTRIGGER);
        INJECT_FAULT(FCThrow(kOutOfMemoryException););
        MODE_COOPERATIVE;
        SO_TOLERANT;
    }
    CONTRACTL_END;

    VALIDATEOBJECTREF(obj);
    
    DWORD idx = 0;
    
    if (obj == 0)
        return 0;
    
    OBJECTREF objRef(obj);

    HELPER_METHOD_FRAME_BEGIN_RET_1(objRef);

        
    idx = GetHashCodeEx(OBJECTREFToObject(objRef));

    
    HELPER_METHOD_FRAME_END();

    return idx;
}
FCIMPLEND

INT32 ObjectNative::GetHashCodeEx(Object *objRef)
{
    CONTRACTL
    {
        MODE_COOPERATIVE;
        THROWS;
        GC_NOTRIGGER;
        SO_TOLERANT;
    }
    CONTRACTL_END

    VALIDATEOBJECTREF(objRef);

    DWORD iter = 0;
    while (true)
    {
        DWORD bits = objRef->GetHeader()->GetBits();

        if (bits & BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX)
        {
            if (bits & BIT_SBLK_IS_HASHCODE)
            {
                return  bits & MASK_HASHCODE;
            }
            else
            {
                SyncBlock *psb = objRef->GetSyncBlock();
                DWORD hashCode = psb->GetHashCode();
                if (hashCode != 0)
                    return  hashCode;

                hashCode = Object::ComputeHashCode();

                return psb->SetHashCode(hashCode);
            }
        }
        else
        {
            if ((bits & (SBLK_MASK_LOCK_THREADID | 
(SBLK_MASK_APPDOMAININDEX << SBLK_APPDOMAIN_SHIFT))) != 0)
            {
                objRef->GetSyncBlock();
            }
            else
            {
                if (bits & BIT_SBLK_SPIN_LOCK)
                {
                    iter++;
                    if ((iter % 1024) != 0 && g_SystemInfo.dwNumberOfProcessors > 1)
                    {
                        YieldProcessor();
                    }
                    else
                    {
                        __SwitchToThread(0);
                    }
                    continue;
                }

                DWORD hashCode = Object::ComputeHashCode();

                DWORD newBits = bits | BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX | 
BIT_SBLK_IS_HASHCODE | hashCode;

                if (objRef->GetHeader()->SetBits(newBits, bits) == bits)
                    return hashCode;
            }
        }
    }
}


Hashcode 소스이지만 이 소스만 읽어서는 어떤 알고리즘으로 hashcode를 생성하는 건지 정확히 파악하기가 어렵군요. :)

암튼 이런식으로 GetHashCode()가 구현되어 있었습니다.

그런데 4.0 버전에서의 구현은 다음과 같습니다.



어라 어디서 많이 보던 함수를 호출 하고 있군요. 아까 앞에서 살펴본 AppDomain상에서 객체마다 유일한 ID를 제공하는 것을 보장해준다고 하는 System.Runtime.CompilerServices.RuntimeHelpers::GetHashCode(object)를 호출하고 있습니다!


System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode()는 역시 unmanaged code를 invoke하게 되어 있는 것 같습니다. 내부 구현 코드를 찾아볼 수 있으면 좋을텐데 찾기가 힘드네요;

결국 내부 코드 구현으로 봤을 때 .NET 4.0기반의 CLR에서는 Object.GethashCode()를 사용하는 것과 System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode()를 사용하는 건 동일합니다.



GetHashCode()를 재정의 할 때 고려해야 할 점을 생각해 볼까요.

1. 무엇보다 잘 분포된 난수를 생성할 수 있는 그러니까 같은 Application 환경에서 유일한 값을 보장할 수 있는 알고리즘을 사용해야 합니다.
2. 정의하는 알고리즘 상에서 상위 클래스의 GetHashCode() 를 호출해서 자신의 Hashcode에 값을 반영하는 경우가 있겠지만 기본적으로 제공하는 Object의 GetHashCode()는 가능하면 사용하지 말아야 합니다. 빠른 성능을 고려해서 만든 알고리즘이 아니기 때문입니다.
3. 알고리즘은 적어도 하나의 인스턴스 필드는 이용해야 합니다. 미리 정의되어 있는 정적 값만을 이용하면 안된다는 말이겠지요.
4. 사용되는 필드는 불변의 속성을 가져야 합니다. 즉 필드값은 한번 생성되면 그 객체가 소멸될 때까지는 절대 값이 바뀌어서는 안됩니다.
5. 객체가 같은 값을 지니고 있다면 HashCode도 같은 값을 반환해야 합니다.

그리고 이 GetHashCode()를 사용할 떄의 주의점도 있습니다. 절대로 이 메소드를 통해 연산한 결과를 저장하여 사용하지 말아야 한다는 점인데 예를 들어 설명하자면 어떤 사이트에서 회원 정보를 받을 때 암호를 GetHashCode()를 이용해서 만들어진 hashcode를 DB에 저장해선 안된다는 것입니다.

지금까지 알아본것 처럼 CLR이 업데이트 되면서 내부 구현이 바뀌게 된다거나, 사용자가 직접 재정의한 GetHashCode()의 알고리즘이 변경된다면 저 DB에 저장된 hashcode는 모두 쓰레기가 되어 버리게 될 테니까요.



7. System.Object

CLR 2010. 2. 16. 09:00 Posted by 알 수 없는 사용자
오늘의 주제는 이겁니다.

CLR의 모든 타입은 System.Object로부터 파생된다.

자. 오늘의 공부 끝!


이라고 말하고 싶지만 그러면 안되겠죠;;

그럼 모든 타입의 기본인 System.Object에 대해서 좀 더 공부를 해 봐야겠습니다. System.Object는 Class입니다. Class니까 Method과 Property 들로 구성이 되어 있겠지요.

Object의 Public Method들에 대해서 알아보기로 하죠. IDE 편집창에서 Object를 하나 선언하고 Public Method들이 무엇이 있는지 살펴봅시다.

네 저렇게 4개의 Method가 존재합니다. 이 Method들은 Object의 public Method인 만큼 CLR의 모든 타입들은 저 4개의 Method를 기본적으로 갖고 있게 됩니다. 저 Method들을 알아보기 위해 우선 좀 말이 안되긴 하지만 다음과 같은 클래스들을 만들어 놓습니다.

    class Animal
    {
        Int32 Age;
        string Name;

        public Animal(string name, Int32 age)
        {
            Age = age;
            Name = name;
        }

        public Int32 GetAge()
        {
            return Age;
        }

        public string GetName()
        {
            return Name;
        }

        public virtual string Say()
        {
            return "Can't say";
        }

        public static Int32 GetLegs(Animal animal)
        {
            Int32 legs = 2;

            if (animal.GetType().ToString().Equals("Test04.Animal"))
                legs = 4;

            return legs;

        }
    }

    class Human : Animal
    {
        public Human(string name, Int32 age) : base(name, age)
        {
        }
        public override string Say()
        {
            return "Oh! Yeah!";
        }
    }


Animal 클래스가 있고 Animal에서 상속받은 Human 클래스가 있습니다. Animal의 class 선언에는 생략되어 있지만-

class Animal 은 class Animal : System.Object 와 같이 쓸 수 있습니다. 둘은 같은 의미입니다.


1. Equals

비교 하려는 객체가 같은 값을 가지고 있으면 true 아니면 false를 return 합니다. 같은 값을 가지고 있다는 말은 무슨 의미일까요? 테스트 해 봅시다. 다음과 같은 코드를 작성해 봅니다.

            Animal dog = new Animal("happy", 3);
Human korean = new Human("Park", 33);

            if (dog.Equals(korean))
                Console.WriteLine("dog == korean");
            else
                Console.WriteLine("dog != korean");

dog라는 Animal class의 객체를 선언하고 korean이라는 Human의 객체를 선언하고 이 둘을 비교합니다. 뭐가 찍힐까요? 당연히 dog != korean 이 찍힙니다.

            Animal cat = new Animal("nero", 1);

            if (dog.Equals(cat))
                Console.WriteLine("dog == cat");
            else
                Console.WriteLine("dog != cat");

자 이번엔 어떨까요? 같은 Animal 객체인 cat을 선언해 봅니다. 둘은 같다고 인정될까요? 아닙니다. 역시 dog != cat이 찍힙니다.

            Animal dog2 = new Animal("happy", 3);
            if (dog.Equals(dog2))
                Console.WriteLine("dog == dog2");
            else
                Console.WriteLine("dog != dog2");

아.. 그러면 이번엔 객체 이름은 다르지만 내부의 property값은 같은 dog2라는 객체를 선언하고 둘을 비교해 봅시다. 같다고 인정해 줄까요? 아.. 역시 다르다고 합니다. dog != dog2 라고 찍힙니다.

도대체 그럼 뭐를 어떻게 해야 같은거야? 라고 생각하면서 마지막으로 다음과 같이 테스트 해 봅시다.

            Animal dog3 = dog;

            if (dog.Equals(dog3))
                Console.WriteLine("dog == dog3");
            else
                Console.WriteLine("dog != dog3");

dog 객체를 새로 생성한 dog3에 할당합니다. 여기선 어떤 값이 찍힐까요? 아. 드디어 두 객체가 같다고 인정해 줍니다. dog == dog3 가 찍힙니다!

Object.Equals()의 같다는 의미는 좀 어렵네요. 두 객체가 포함하는 property등의 값이 같다고 equal이 아닙니다. 두 객체가 참조하는 것이(주소값 이라고 해야 할까요? C++적인 의미로 말입니다.) 이 완전히 일치해야지 equal입니다. 

음 개념적으로는 대충 이해가 되는데 좀 더 자세히 알아보고 싶어지는군요.  앞으로 자주 보게 될 ILDASM을 꺼내봐야겠습니다. ILDASM으로 지금까지 작성한 코드들이 어떻게 CLR에 들어가는지 살펴봅시다.


ILDASM을 이용하여 작성한 코드들을 MSIL코드로 볼 수 있습니다. 하나하나씩 따라가보면서 간단하게나마 분석을 해보려고 합니다. 일단 Main 앞 부분입니다. main()함수에서 사용하는 지역변수들을 초기화 하고 알아보기 쉽게 index를 붙여서 태깅해 놓고 시작하네요.

            Animal dog = new Animal("happy", 3);
            Human korean = new Human("Park", 33);

  IL_0001:  ldstr      "happy"
  IL_0006:  ldc.i4.3
  IL_0007:  newobj     instance void Test04.Animal::.ctor(string, int32)
  IL_000c:  stloc.0
  IL_000d:  ldstr      "Park"
  IL_0012:  ldc.i4.s   33
  IL_0014:  newobj     instance void Test04.Human::.ctor(string, int32)
  IL_0019:  stloc.1

1. [IL_0001] ldstr은 스트링 개체를 스택으로 PUSH합니다. "happy"가 스택에 PUSH됩니다. 
2. [IL_0006] ldc.i4.3 은 정수값 3을 역시 스택으로 PUSH합니다. 
3. [IL_0007] newobj는 받아야 할 argument들 갯수만큼 스택에서 POP 한 후 새로운 객체를 생성하고 그 객체를 스택에 PUSH하게 됩니다. 
4. [IL_000c] stloc.0 는 스택에서 값을 POP하여 지역변수 인덱스 0 에 넣습니다. 

IL_0001 부터 IL_000c까지의 내용입니다. 그대로 하면 어떻게 되나요. 위의 C#코드에서 처럼

Animal dog = new Animal("happy", 3); 

을 수행한 결과가 됩니다. 변수 dog는 지역변수 인덱스 0 으로 태깅되어 있지요. IL_000d 부터 IL_0019까지는 korean 객체를 생성하는 과정입니다. IL_0012의 ldc.i4.s 33은 정수값 33을 스택에 PUSH하라는 명령입니다.

            if (dog.Equals(korean))
                Console.WriteLine("dog == korean");
            else
                Console.WriteLine("dog != korean");

  IL_001a:  ldloc.0
  IL_001b:  ldloc.1
  IL_001c:  callvirt   instance bool [mscorlib]System.Object::Equals(object)
  IL_0021:  ldc.i4.0
  IL_0022:  ceq
  IL_0024:  stloc.s    CS$4$0000
  IL_0026:  ldloc.s    CS$4$0000
  IL_0028:  brtrue.s   IL_0037
  IL_002a:  ldstr      "dog == korean"
  IL_002f:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0034:  nop
  IL_0035:  br.s       IL_0042
  IL_0037:  ldstr      "dog != korean"
  IL_003c:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0041:  nop

자 다음은 두 객체를 비교하는 부분입니다. 

1. [IL_001a] ldloc.0 은 0번 지역변수의 값을 스택에 PUSH합니다. 
2. [IL_001b] ldloc.1 은 1번 지역변수의 값을 스택에 PUSH하겠지요.
3. [IL_001c] callvirt 는 런타임에 바인딩된 method를 호출합니다. (call뒤에 virt가 붙은 건 호출 할 method가 virtual로 선언되어 있다는 걸 의미합니다. 실제로 Object.Equals()는 'public virtual bool Equals(object obj);' 로 선언되어 있습니다.) 호출하기 전 method와 관련된 객체와 해당 method의 인수와 관련된 값을 스택에서 POP합니다. 그리고 해당 method의 결과값이 있으면 스택에 PUSH하게 됩니다. 여기서는 equals의 값을 스택에 PUSH하게 되겠네요.
4. [IL_0021] ldc.i4.0 는 정수값 0을 스택에 PUSH하게 됩니다.
5. [IL_0022] ceq 는 비교 명령입니다. 비교에 사용될 값 value0과 value1을 스택에서 POP합니다. 여기서는 4번에서 넣었던 정수값 0과 3번 equals의 결과값이 각각 value0과 value1에 들어가게 되겠지요. 그리고 두 값을 비교해서 같으면 1이 스택으로 PUSH되며 틀리면 0이 스택으로 PUSH됩니다.
6. [IL_0024] stloc.s    CS$4$0000 스택에서 값을 꺼내어 CS$4$0000 이라는 지역변수에 넣습니다. 맨 위에서 보면 알다시피 CS$4$0000은 bool값을 가진 지역변수인데 우리가 직접 선언한 변수는 아니지요. if문에서 비교를 해서 나온 bool값을 저장하기 위해 CLR이 생성한 임시 지역 변수입니다. 결국 ceq로 비교된 값이 CS$40000 이라는 변수에 들어가게 됩니다.
7. [IL_0026] ldloc.s    CS$4$0000 은 지역변수 CS$4$0000의 값을 스택에 PUSH합니다. 아 이거 왜 뺐다가 다시 넣는거야.
8. [IL_0028] brtrue.s  IL_0037 는 스택에서 값을 하나 POP한다음에 이 값이 true거나 null이 아니거나 0이 아닌경우 IL_0037로 점프하게 됩니다. 결국 bool 형식의 CS$4$0000 값이 true면 IL_0037로 점프를 하게 되겠지요. 다시 정리하면 5번에서의 ceq의 값이 같으면 IL_0037로 점프를 하게 되는 것입니다. 또 거슬러가면 ceq의 값이 같기 위해서는 3번의 결과 값이 0이 되어야 하는데 3번의 결과 값이 0이라는 얘기는 equals()의 결과값이 0 그러니까 false라는 이야기죠. 결국 dog != cat 이면 IL_0037로 점프하는겁니다.
9. [IL_002a] ~ [IL_002f] 스택에 스트링값(dog == korean)을 넣고 Console.WriteLine()을 호출합니다.
10. [IL_0034] nop 아무일도 하지 않습니다.
11. [IL_0035] br.s       IL_0042 는 어떠한 조건에 상관없이 IL_0042로 점프하게 됩니다. [IL_0042]는 if문이 모두 끝난 다음의 명령문이 있는 주소입니다.
12. [IL_0037] ~ [IL_003C] 는 dog != korean 을 찍겠지요.

와 길고 복잡합니다. 첨부된 소스를 컴파일 하고 컴파일 된 실행 파일을 ILDASM으로 보면 계속 뒤의 코드를 볼 수 있는데 쭉- false를 리턴했던 dog2까지의 비교는 위와 동일한 패턴의 코드로 진행됩니다. 무엇보다 주시해야 될 부분은 cat이나 dog2의 객체는 쭉- newobj로 새로 생성한다는 점입니다.

            Animal dog3 = dog;

if (dog.Equals(dog3))
                Console.WriteLine("dog == dog3");
            else
                Console.WriteLine("dog != dog3");

  IL_00aa:  ldloc.0
  IL_00ab:  stloc.s    dog3
  IL_00ad:  ldloc.0
  IL_00ae:  ldloc.s    dog3
  IL_00b0:  callvirt   instance bool [mscorlib]System.Object::Equals(object)

이제 true를 반환했던 dog3입니다. IL코드를 읽어보면 알겠지만 dog3의 값은 지역변수 인덱스 0인 dog의 값(주소)를 가져와서 dog3 지역변수에 그대로 넣습니다. 이건 뭘 의미하는건가요?

Animal dog3 = dog;

를 수행한다는 것은 dog3라는 새로운 객체 공간을 생성해서 dog의 값을 복사해 오는 것이 아니라 dog라는 객체가 가리키는 값(주소)를 그대로 dog3와 연결 시킨다는 의미입니다. 결국 dog와 dog3가 가리키는 값(주소)는 같은 값(주소)입니다. equals()는 그래서 같다. 라고 결과를 반환했던 것입니다. equals()가 하는 일은 정말 간단하네요. 내용이고 자시고 간에 그냥 값이 같으면 true입니다. 

간단한 내용을 정말 길게 돌아서 확인했네요.

휴-

Object는 다음에 계속 하기로 하지요. 오늘은 이만!!

참고 소스 : 

'CLR' 카테고리의 다른 글

닷넷4.0에서 네이티브코드와 매나지드코드의 동거 part 1.  (2) 2010.08.02
8. System.Object (2)  (0) 2010.03.03
6. Assembly - GAC(Global Assembly Cache)  (2) 2010.02.02
5. Assembly - Strongly named assemblies  (0) 2010.01.26
4. Assembly  (2) 2010.01.15

6. Assembly - GAC(Global Assembly Cache)

CLR 2010. 2. 2. 09:00 Posted by 알 수 없는 사용자

오늘은 GAC (Global Assembly Cache)에 대해서 공부해 보도록 하겠습니다. GAC는 보통 윈도우 설치 폴더 아래에 위치합니다. 만약 윈도우 설치 폴더가 C:\Windows 라면

C:\Windows\Assembly

에 위치해 있습니다. 윈도우 탐색기를 이용하여 한번 확인해 봅시다.

윈도우 7에서는 이렇게 Assembly 별로 세부 정보들을 정리하여 보여주고 있네요. 지난 주에 배웠던 Strongly named assemblies의 정보들이 보입니다. 이름과 버전, culture(locale) 그리고 공개 키 토큰 정보 입니다. 실제로는 기능별/제품별로 굉장히 많은 하위 디렉토리 구조로 구성되어 있습니다. 이런 식으로요.



자 그러면 이 GAC라는 곳에 우리가 만든 Strongly named assemblies를 복사하면 우리의 Assembly를 전역적으로 사용할 수 있는 것일까요? 당연히- 아닙니다.

GAC에 넣어주는 도구가 있습니다. 바로 GACUtil.exe 라는 툴입니다. 실행시켜보면 다음과 같은 다양한 옵션에 대한 안내를 해 줍니다.


다양한 옵션 설명 중 /i 가 바로 assembly를 GAC에 인스톨하는 옵션임을 알 수 있습니다. 그리고 /u가 지정된 assembly를 언인스톨하는 옵션이군요. 이 툴을 이용하여 우리가 만든 Strongly named assembly를 GAC에 설치할 수 있습니다.

GAC라는 영역이 한번 등록시켜놓으면 편리하게 어느 어플리케이션이나 편리하게 사용할 수 있게 됨은 사실이지만 GAC에 Assembly가 설치되게 되면 단순 파일 복사, 삭제 등의 작업만으로 해당 Assembly를 관리할 수 없게 됩니다. 그래서 손쉬운 배포를 위해서는 가능하면 GAC에 등록하는 형태의 배포가 아닌 전용배포의 방식이 관리하기에 편리한 면도 있습니다.

이 GAC가 존재함으로써 시스템 전역적으로 assembly를 사용할 수 있다는 것 외에 어떤 장점이 있을까요. 우선 지난 주에 언급했던 내용 중에 DLL Hell과 관련된 이야기를 기억하시는지요? 다른 회사에서 만든 같은 이름의 DLL 모듈로 인한 충돌 문제나 같은 회사에서 만든 같은 이름의 DLL모듈이라고 해도 버전이 틀려서 발생하는 문제등을 GAC는 훌륭히 처리해 줍니다. 같은 이름을 가진 Assembly라고 하더라고 GAC의 정해진 알고리즘에 의해 제각기 분리된 영역에 설치되게 되고 각 어플리케이션이 요구하는 Assembly를 정확히 구별하여 사용할 수 있도록 구성해 주는 것입니다.


그렇다면 GAC의 내부의 구조에 대해 간략하게 살펴보도록 할까요?

우선 Assembly 디렉토리 내를 살펴보니 다음과 같이 구성되어 있네요. 여기서

GAC 는 CLR 1.0, 1.1 버전에서 생성된 Assembly들이 포함되어 있습니다. 이 곳에 있는 Assembly들은 오직 x86 그러니까 32비트 OS환경 또는 64비트 OS환경에서 WoW64기술이 적용되어 실행됩니다. 이곳에는 Native x86코드가 포함되어 있는 경우도 있습니다.

GAC_32는 CLR 2.0 버전에서 생성된 Assembly들로써 32비트 환경의 OS 또는 64비트 OS환경에서 WoW64가 적용되어 실행되는 Assembly들이 저장되어 있습니다. 여기도 역시 Native x86코드가 포함되어 있는 assembly도 있습니다.

GAC_MSIL은 CLR 2.0 버전에서 생성된 Assembly들이 저장되어 있는데 순수하게 IL코드들로만 구성되어 있는 Assembly들이 저장되어 있습니다. 이 디렉토리 하부의 Assembly들은 32비트나 64비트의 OS 환경을 가리지 않습니다.

위의 이미지에는 나와있지 않지만 GAC_64라는 디렉토리도 있습니다. GAC_32와 비슷한 이름으로 보아 유추가 가능하겠지요? CLR 2.0 버전에서 생성된 Assembly들이며 64비트 OS 환경에서 실행되는 Assembly들이 저장되어 있는 공간입니다.

그리고 NativeImages.. 로 시작되는 디렉토리도 보이네요. 이 곳에는 NGen.exe라는 CLR의 Native Code 컴파일러를 이용하여 컴파일된 Assembly들이 저장되는 곳입니다.

NGen.exe


자 이젠 좀 더 아래 디렉토리로 들어가 볼까요? GAC_MSIL 디렉토리를 들어가 봅시다.


아 딱 보니 대충 짐작이 가네요. Namespace별로 디렉토리들이 구성되어 있는 것을 알 수 있습니다. 또 들어가 봅니다.


오 이것도 대충 짐작이 갑니다. 버전과 공개키 토큰이군요. 원래는 다음과 같은 형식으로 구성되어 있습니다.

(버전)_(Culture)_(공개 키토큰)

그런데 저 디렉토리 안에 있는 Assembly는 Culture 정보가 없기 떄문에 언더바(_) 두개로 Culture 정보가 생략되어 있네요. 그럼 저런 형식의 디렉토리 안을 들어가 보면 assembly 파일이 있겠지요?

네. 존재하네요. GAC는 이런식의 구조로 Assembly를 관리하고 있습니다. :)


'CLR' 카테고리의 다른 글

8. System.Object (2)  (0) 2010.03.03
7. System.Object  (0) 2010.02.16
5. Assembly - Strongly named assemblies  (0) 2010.01.26
4. Assembly  (2) 2010.01.15
3. MSCorLib & Metadata  (4) 2010.01.06

5. Assembly - Strongly named assemblies

CLR 2010. 1. 26. 09:00 Posted by 알 수 없는 사용자
CLR에서는 Assembly를 크게 두 종류로 나눌 수 있습니다. Strongly named assemblies(강력한 이름의 어셈블리 - 뭔가 말이 좀 이상하지요?) 그렇지 않은 Assembly 입니다. 

Strongly named assemblies는 해당 assembly의 제공자를 확인할 수 있는 유일한 공개/개인키 쌍으로 서명되어 있고 이 키들은 assembly는 고유한 ID의 일부분으로 인식되어 안전하게 버전이 관리되며 이 키를 통해서 서명된 assembly는 어느곳에서라도 유일한 이름으로 인식되며 배포가 가능하게 됩니다. 

assembly는 크게 전용(Private)으로 배포하는 방식과 전역(Global)으로 배포하는 방법으로 배포 방법을 나눌 수 있습니다. 

전용 배포라는 것은 매우 일반적인 방법의 배포방식으로 배포할 프로그램의 기본 디렉토리와 그 디렉토리의 하위 디렉토리에 관련 assembly들이 설치되어 배포된 형태이며 전역 배포라는 것은 Global Assembly Cache - GAC(전역 어셈블리 캐시) 같은 잘 알려진 특정 디렉토리에 배포되는 방식을 말합니다.

Strongly named assemblies는 전용, 전역 배포가 가능하지만 그렇지 않은 assembly는 전용 배포는 가능하나 전역 배포는 안됩니다. 위에서 알았다시피 Strongly named assemblies가 아닌 경우 유일하게 해당 assembly를 인식할 수 있는 방법이 없으니 당연히 전역적인 배포가 불가능합니다.


자 그럼 Strongly named assemblies를 어떻게 만들 수 있는지 살펴보도록 하지요. 그 전에 잠깐 왜 Strongly named assemblies가 필요한지 잠깐 생각해 봅시다.

다양한 프로그램들이 자신들이 사용하는 assembly를 사용하려고 하는 상황입니다. 일반적으로 그렇듯이 여러 프로그램에서 자주 사용하는 assembly는 잘 알려진 디렉터리에 위치해 있어야 다양한 프로그램들이 사용하기 용이할 것입니다. 

CLR 프로그램 이전의 배포 방식을 되새겨 볼까요.

Visual Studio 6, 7 버전의 Visual C++ 프로그램을 살펴보면 Visual C++ 컴파일러로 생성된 프로그램들은 공통적으로 msvcp71.dll이라던가 msvcp60.dll 등의 공용 모듈을 사용하게 됩니다. 그래서 일반적으로 프로그램 설치시 위의 모듈들을 설치되는 프로그램과 같은 경로에 넣기도 하지만 windows 폴더의 system32 디렉터리에 넣어놓곤 하지요. 일반적으로 MS관련 프로그램을 설치하게 되면 위의 ms..로 시작하는 dll들은 system32 디렉터리에 설치가 되기 때문에 응당 위의 모듈이 모든 PC의 system32 폴더에 있겠거니 착각하고 배포본에 넣어놓지 않았다가 낭패를 보는 경우도 적지 않게 있었지요. 

MS관련의 공통모듈 뿐만이 아니라 다양한 프로그램에서 자주 사용하는 모듈들은 Windows가 설치된 PC라면 설치된 프로그램이 어느 디렉터리에 설치가 되던지 참조가 가능한 system32 폴더에 설치해 놓는 경우가 많습니다. 이러면서 문제가 발생하게 되었는데 다른 회사에서 같은 파일 이름을 가진 모듈을 system32 폴더에 설치를 하게 되었다던가, 같은 이름을 가지지만 버전이 틀린 파일을 설치하게 되어 특정 프로그램은 정확히 자신이 참조해야 하는 모듈을 찾을 수 없어 프로그램이 정상적으로 실행되지 않는 이른바 DLL Hell과 관련된 문제가 생기가 된 것입니다. 무엇보다 별 생각없이 system32같은 중요한 디렉터리에 자신들이 배포하는 프로그램과 관련한 모듈들을 무작정 설치하는 것도 DLL Hell을 만들게 된 중요한 원인 중 하나였지요.

최근에도 그런 문제가 있었습니다. 인터넷 익스플로러 8과 아래아 한글과의 충돌 문제였지요,

인터넷 익스플로러 8이 설치된 PC에 아래아 한글이 설치되면 인터넷 익스플로러 8이 시작하자마자 다운되는 문제가 발생했는데 그 원인은 이미 인터넷 익스플로러 8 이 사용하는 system32폴더에 설치된 최신 버전의 jscript.dll 모듈을 아래아 한글이 사용자 PC에 설치되면서 구 버전의 jscript.dll모듈로 교체 설치를 함으로써(같은 파일명이니 덮어씌워지는 형태로 설치가 되어버린 겁니다.) 인터넷 익스플로러 8은 자신이 사용하는 jscript.dll 파일을 찾지 못해 뻗어버리게 된 것이지요.

단순히 파일 이름만으로 프로그램 자신이 참조해야 하는 모듈 또는 assembly를 찾는 건 좋은 방법이 아니라는 것이 증명된 것입니다. 그래서 CLR의 경우 정확히 자신이 참조해야 하는 assembly를 식별하기 위해서 다음과 같은 4가지 요소를 이용하여 assembly를 식별하게 됩니다.

1. 파일 이름 (확장자를 제외한)
2. 버전 번호
3. 컬쳐 (로케일)
4. 공개키 (또는 공개키를 이용하여 생성한 작은 해시값)

어떤 프로그램이 자신이 원하는 assembly를 찾을 때 저 4가지 요소가 정확히 맞아야지 해당 assembly를 자신이 찾는 assembly라고 확신하고 사용하게 된다는 말이지요. Strongly named assemblies에 해당하는 이야기입니다.

저 4가지 요소중 1, 2, 3번 요소는 굉장히 드물기는 하겠지만 다른 회사와 동일 할수도 있습니다. 하지만 4번 공개키는 공개/개인키 쌍을 만들어서 사용하는 것이니만큼 해당 assembly의 제작자가 틀리다면 같을 수가 없을 것입니다.

Strongly named assemblies가 아닌 경우는 공개키를 제외한 요소들을 manifest metadata로 가질 수 있지만 assembly를 찾을 때는 단순히 파일 이름만을 이용하여 찾게 됩니다.

자 그럼 이젠 Strongly named assemblies를 만드는 방법을 살펴보도록 합시다.

우선은 키를 생성해야 합니다. 키를 생성하기 위해 .NET Framework에서는 SN.exe 라는 툴을 제공합니다. Visual Studio Command Prompt 2010을 실행하여 다음과 같이 실행해 보면-

SN -k VSTSTeam.keys

VSTSTeam.keys 라는 파일명의 공개/개인키의 쌍을 생성하게 됩니다. 이 파일은 바이너리의 형태로 공개/개인키의 쌍을 갖고 있습니다. 우리는 공개키를 사용해야 합니다. 공개키를 확인하기 위해서는 만들어진 키 파일을 이용하여 다음과 같이 SN.exe 를 한번더 실행해야 합니다.

SN -p VSTSTeam.keys VSTSTeam.publickey

결과로 VSTSTeam.publickey 라는 파일이 생성됩니다. 이 파일은 공개키를 포함하고 있는 바이너리 파일입니다. 이 파일의 공개키를 한번 확인해 봅시다.

SN -tp VSTSTeam.publickey

그럼 다음과 같이 굉장히 긴- 공개키 값을 확인할 수 있습니다.


이렇게 공개키는 확인할 수 있지만 개인키를 확인할 수 있는 방법은 제공하지 않고 있습니다.

이렇게 생성한 키를 이용하여 assembly를 컴파일 할 때 다음과 같은 Argument를 이용하여 assembly에 공개키를 포함시키게 됩니다.

csc /keyfile:VSTSTeam.keys hello.cs

당연히 Visual Studio IDE에서도 이런 작업이 가능합니다.



Sign the assembly 라는 체크박스에 체크를 한 후 새로 key를 생성해도 되고 이미 생성된 key 파일이 있다면 해당 key파일을 이용하여 사용하면 됩니다.



참고 자료
CLR Via C# 2nd. Ed.

'CLR' 카테고리의 다른 글

7. System.Object  (0) 2010.02.16
6. Assembly - GAC(Global Assembly Cache)  (2) 2010.02.02
4. Assembly  (2) 2010.01.15
3. MSCorLib & Metadata  (4) 2010.01.06
2. CLR! CLR! CLR!  (3) 2009.12.30

4. Assembly

CLR 2010. 1. 15. 09:00 Posted by 알 수 없는 사용자
Assembly 라고 하면 맨 처음 생각 나는게 뭔가요? 저는 Assembly 언어가 생각이 납니다. 기계어와 Assembly 언어. 요사이는 Assembly 언어로 프로그램을 작성하는 분이 거의 없을 거라고 보지만 예전에 PC 사양이 많이 안 좋았던 시절에는 컴파일러의 최적화 성능도 그리 좋지 않았기 때문에 최적화된 코드로 빠른 속도를 내기 위해서 많은 부분을 직접 Assembly 언어로 구현했다는 이야기를 많이 들었었지요. 참 대단합니다. ㅎㅎ

아.. 이야기가 딴 길로 샙니다. 지난 주에 Metadata를 까보면서 넘어갔던 부분이 몇 개 있었는데요. 그 중의 하나가 Assembly 였지요. 오늘은 이 Assembly에 대해 공부해 보려고 합니다. 

Assembly?

Assembly 는 뭔가요? 지난 주에 저희가 만들었던 hi.exe 도 어셈블리입니다. 그럼 어셈블리는 실행 파일인가요? 틀린 말은 아니지만 좀 더 정확히 정의하자면 Assembly는 타입이나 리소스를 포함한 하나 이상의 파일들이 모인 파일의 컬렉션 이라고 CLR Via c# 2nd 한글판에서는 이야기하고 있습니다. 물론 일반적으론 하나의 파일로 구성되는 경우가 대부분입니다. Assembly를 논리적인 DLL 또는 EXE 파일로 이해하면 될 것 같습니다.

이 Assembly 파일이 갖고 있는 정보 중의 하나는 Manifest라고 하는데 이 Manifest에는 Assembly의 버전과 컬처, 배포자, public 형태의 타입, Assembly를 구성하는 모든 파일의 정보를 갖고 있습니다. CLR은 이 Assembly를 로드하여 지난주에 살펴본 Metadata 테이블을 읽은 후, Manifest에 있는 다른 파일들의 정보를 읽게 됩니다.

좀 어렵네요. 왜 이런 Assembly를 사용하는가. 를 먼저 살펴봅시다. Assembly는 다음과 같은 특징을 갖습니다.

* 개발된 코드를 여러 파일로 분산하여 실행 시 최소한의 파일로 구성한 다음, 필요한 파일이 생길 경우 인터넷 상으로 다운로드하는 식의 효과적인 배포가 가능합니다. (어플리케이션 설정 파일의 codebase 항목에서 설정된 곳을 통해 필요한 Assembly를 다운로드 할 수 있도록 설정이 가능합니다.)

* Assembly에 리소스나 데이터 파일을 추가할 수 있습니다. 이 데이터 파일의 형식은 어플리케이션이 파싱하여 이해할 수 있는 파일이라면 제약이 없습니다.(예 : 오피스 워드 파일, 엑셀 파일 등등)

* 서로 다른 언어로 구현된 타입(클래스)의 구성으로 Assembly를 생성하는 것이 가능합니다. 전체 모듈 중 일부는 C#으로 다른 일부는 Visual Basic으로 구현하는 것이 가능합니다. 어떤 언어로 작성되었던 컴파일 하게 되면 같은 IL코드로 생성이 되어 있으며 Assembly 테이블을 참조하면 해당 모듈을 재사용하는데 필요한 모든 정보를 얻을 수 있기 때문입니다.


Assembly는 Manifest Metadata 테이블이라는 것을 가지고 있습니다. 테이블 리스트는 다음과 같습니다.

 테이블 이름 설명 
 Assembly Assembly를 식별하는 단일 엔트리를 가집니다. 이 엔트리에는 어셈블리의 이름, 버전, 컬처, 플래그, 해시 알고리즘, 발행자의 공용키를 포함합니다. 
 File Assembly의 부분인 각 PE 파일과 리소스 파일을 위한 항목을 갖고 있습니다. 엔트리에는 파일의 이름, 확장자, 해시, 플래그를 포함합니다. 만일 Assembly가 오직 자신 하나의 파일만으로 구성된다면 이 테이블이 갖고 있는 엔트리는 없습니다. 
 ManifestResource 각 리소스를 위한 항목을 가집니다. 엔트리는 리소스 이름, 플래그(public, private)를 포함합니다.  
 ExportedTypes Assembly의 PE 모듈로부터 노출된 각 공용 타입을 위한 항목을 가집니다. 엔트리는 타입 이름, File 테이블의 인덱스,  그리고 TypeDef 테이블의 인덱스를 포함합니다.

이러한 manifest를 정의함으로써 Assembly의 파일 구성에 대한 정보를 제공하는 것이 가능하고 어셈블리 스스로 설명하는 것이 가능해집니다.

컴파일러 옵션에서 /t 의 값으로 /t[arget]:exe, /t[arget]:winexe, /t[arget]:library 를 주게 되면 컴파일러는 manifest정보를 포함한 단일 파일의 Assembly를 생성하게 됩니다. 이는 각각 CUI 실행 파일, GUI 실행 파일, DLL 라이브러리 입니다.

이외에도 /t[arget]:module 이라는 옵션도 있습니다. 이 옵션을 사용하게 되면 컴파일러는 manifest와 metadata를 포함하지 않은 PE파일을 생성하게 됩니다. 이 파일은 기본적으로 .netmodule 의 확장자를 가지게 되며 다중 파일로 구성된 Assembly를 구성할 때 사용하게 됩니다. 이 타입의 모듈을 CLR이 이용하기 위해서는 항상 이 해당 모듈이 Assembly에 포함되어 있어야 합니다. 이렇게 .netmodule 형식의 파일을 생성해 놓음으로써 자주 사용하는 메소드와 그렇지 않은 메소드를 구분하여 분리해서 파일 구성을 할 수도 있겠군요.

다음과 같은 코드를 컴파일 하려고 합니다.

코드 1) Hellovb.vb
NameSpace Hello
Public Class HelloWorld
Overloads Shared Sub PrintHello()
System.Console.WriteLine("HELLO!!")
End Sub
End Class
End NameSpace 

코드 2) Test.cs
using Hello;
public sealed class Program
{
public static void Main()
{
System.Console.WriteLine("Hi");
HelloWorld.PrintHello();
}

코드 1은 VB.NET용 소스 파일이며 코드 2는 C#용 소스 파일입니다.

코드 1은 .netModule로 컴파일을 해 봅시다.


코드 2 는 코드 1에서 정의된 HelloWorld 클래스의 PrintHello 메소드를 이용하고 있습니다. 방금 생성한 hellovb.netModule을 끌어와야 정상적으로 컴파일이 되겠지요. 생성하려는 Assembly에 모듈을 추가하려고 할 때는 /addmodule 이라는 옵션을 사용하면 됩니다.


test.exe라는 Assembly를 만들고 실행을 시키자 코드대로 잘 실행이 됩니다. Assembly의 특징 중 하나인 서로 다른 언어로 구현된 타입(클래스)의 구성으로 Assembly를 생성하는 것이 정말 가능하네요. :)

자 이제 우리는 생성된 Hellovb.netmodule 과 test.exe 파일을 열어봐서 앞에서 살펴본 내용을 확인해 보면 되겠습니다.

먼저 Hellovb.netmodule의 Metadata 엔트리를 봅니다.


앞에서 /t[arget]:module 이라는 옵션을 이용하여 PE파일을 생성하게 되면 컴파일러는 Manifest 정보를 포함하지 않는 PE 파일을 생성한다고 했습니다. 정말 manifest 정보가 없네요.

아 여기에 보이는 AssemblyRef 테이블은 현재 파일이 참조한 모든 참조 어셈블리 파일의 목록을 갖고 있습니다. 이 목록을 이용하여 Assembly의 다른 파일들을 열지 않고도 참조된 어셈블리가 어떤 것들인지 확인 할 수가 있는 것입니다. 이 파일은 기본 모듈인 mscorlib과 Visual Basic으로 만들어진 모듈이기에 Microsoft.VisualBasic이라는 Assembly를 참조하고 있다고 표현하고 있네요.

자 이제는 test.exe을 살펴봅시다.


테이블이 많이 보이네요. 앞에서 언급한 Assembly, FIle, ExportedTypes 테이블이 보입니다. ManifestResource는 없네요. 이 파일엔 리소스가 없거든요 :(
하나하나 살펴봅니다. 먼저 Assembly 테이블을 보니 이름(Name), 버전(Major, Minor, Build, Revision), Culture, Flag, 해시 알고리즘(hashAlgId), 발행자의 공용키(PublicKey) 값이 보입니다.


이번에는 File 테이블입니다. Test.exe Assembly의 부분인 다른 PE 파일의 정보가 들어있네요. 이름(Name : Hellovb.netmodule), 플래그(Flags : 이 파일은 리소스 파일이 아님을 알 수 있습니다.) 등을 확인할 수 있습니다.

마지막으로 ExportedType 테이블입니다. 타입의 이름(TypeName : HelloWorld), File테이블의 인덱스(Implementation)등을 확인 할 수 있습니다. TypeDefID 값이 0200002라고 적혀 있네요. Ildsam.exe 를 이용해서 hellovb.netmodule을 찾아보니(TypeName이 HelloWorld고 Namespace가 Hello니까요) 다음과 같이 ID를 확인할 수가 있군요.

아 복잡하네요. 간단히 말하자면 컴파일러는 컴파일을 하면서 Assembly를 생성하게 되며 이 Assembly들의 내부 Manifest정보와 Metadata정보 덕분에 Assembly 스스로 자신에 대한 설명이 가능하므로 어디서든지 쉽게 재 사용이 가능하구나! 라고 이해하면 될 것 같습니다.

Assembly 구성에 대해 좀 더 알아봅시다. 다음의 코드를 컴파일하여 Assembly 구성을 하되 DLL 라이브러리로 구성합니다.

코드 3) Test2.cs

using Hello;

public class GoodbyeWorld
{
public static void PrintGoodbye()
{
System.Console.WriteLine("Goodbye World");
}
}

public sealed class Program
{
public static void Main()
{
System.Console.WriteLine("Hi");
HelloWorld.PrintHello();
GoodbyeWorld.PrintGoodbye();
}

}

컴파일을 하니 test2.dll 이라는 라이브러리 모듈이 생성되었네요. 

이 라이브러리는 그렇다면 다른 곳에서 어떻게 사용하면 될까요? 물론 지금 해왔던 것 처럼 컴파일 시mscorlib.dll을 /r[eference] 옵션을 이용하여 추가해 컴파일 했던 것 처럼 /r[eference] 옵션을 이용하여 이 모듈을 사용할 수 있을 것입니다.

앞으로 우리가 개발할 때 사용할 툴인 Visual Studio IDE에서는 어떻게 사용하나요? 물론 다 알고 계시겠지만 프로젝트에서 참조 추가(Add Referece) 메뉴를 이용하면 쉽게 이 라이브러리를 사용할 수 있습니다.
우리가 구현했던 메소드들이 잘 보이네요 :)

컴파일러말고도 Assembly를 생성할 수 있는 방법이 또 있습니다. Assembly Linker(AL.exe)를 이용해서 생성할 수 있다고 합니다. Assembly Linker는 서로 다른 컴파일러에 의해서 빌드된 모듈들로 구성된 어셈블리를 만들 때 사용됩니다.

test3.cs 에서 Hellovb에서 사용되었던 코드를 제외하여 test4.cs를 만들고 이걸 netmodule로 컴파일을 하겠습니다.
public class GoodbyeWorld
{
public static void PrintGoodbye()
{
System.Console.WriteLine("Goodbye World");
}
}

public sealed class Program
{
public static void Main()
{
System.Console.WriteLine("Hi");
GoodbyeWorld.PrintGoodbye();
}

}

그리고 앞에서 생성했던 VB.NET의 Hellovb.netmodule과 지금 생성한 C#으로 만든 test3.netmodule을 AL.exe를 이용하여 Assembly로 만들어 봅니다.

잘 생성되었습니다. :)

AL.exe를 이용해서 Assembly에 리소스를 추가할 수가 있습니다. 리소스를 추가하게 되면 앞에서 살펴본 ManifestResource 테이블이 생성되겠지요.

다음과 같이 AL.exe를 이용하여 DLL을 생성합니다.
그리고 테이블을 살펴보니

ManifestResource 테이블에 kara.jpg 이미지 리소스가 추가되어 있네요!! 물론 csc.exe나 vbc.exe 같은 각 언어의 컴파일러에서도 /resource 옵션을 이용하여 리소스 추가가 가능합니다.



휴- 정신없네요. 한숨 돌립시다. @_@




우리가 생성한 test.dll 파일을 보니까 뭔가 썰렁합니다. 파일 정보를 보니 버전 리소스 정보가 하나도 없네요!
물론 Visual Studio IDE를 이용하여 프로젝트를 생성하면 C#의 경우는 AssemblyInfo.cs 라는 파일이 자동으로 추가되면서 이 파일을 통해 생성하려는 Assembly의 파일 정보를 구성할 수 있습니다. 그렇다면 AL.exe나 csc.exe, vbs.exe의 컴파일러를 이용하여 Assembly를 생성하려는 경우 파일 정보는 어떻게 추가 할 수 있을까요?

using System.Reflection;

[assembly: AssemblyTitle("Test Library")]
[assembly: AssemblyDescription("Test")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("VSTS2010")]
[assembly: AssemblyProduct("Test Library")]
[assembly: AssemblyCopyright("Copyleft ©  2010")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]


public class GoodbyeWorld
{
public static void PrintGoodbye()
{
System.Console.WriteLine("Goodbye World");
}
}

public sealed class Program
{
public static void Main()
{
System.Console.WriteLine("Hi");
GoodbyeWorld.PrintGoodbye();
}

}

위의 코드를 다음과 같이 컴파일 합니다.

그러면 생성된 test.dll 에는 파일 정보가 포함되게 됩니다.


오늘 너무 많은 것을 하네요. 하지만 다음 주 진도를 위해 몇 가지를 더 짚고 넘어가야 합니다. 앞에서 버전 번호를 보면 알겠지만 버전 번호는 다음과 같은 형식을 갖고 있습니다.

주 번호(Major) / 부 버전 번호(Minor) / 빌드 번호(Build) / 리비전 번호(Revision)

Test.dll의 버전은 1.0.0.0 이라고 되어 있습니다. 앞의 1.0은 공식적으로 사용되는 버전의 숫자입니다. 보통 .NET FrameWork 2.0 , 3.5 이런 식으로 이야기 할 때의 버전 번호입니다.

세번 째의 빌드 번호는 말 그대로 빌드 할 때마다 증가하는 번호를 의미합니다. 만약 Test.dll이 32번째 빌드된 버전이라면 2.0.32.0 으로 버전 번호가 매겨지겠지요. 마지막 번호는 빌드의 수정을 의미합니다. 만약 32번째 빌드된 버전에 문제가 있어서 그날 다시 빌드를 해야 한다면 마지막 번호가 증가를 하게 됩니다.

이 버전 구성 방식은 꼭 지켜야 하는 규칙은 아닙니다. 하지만 Microsoft를 포함한 일반적인 소프트웨어 회사에서는 위와 같은 버전 구성 방식을 사용하고 있습니다.

그런데 위의 소스에서 보면 Assembly와 관련된 버전 정보가 하나가 아님을 알 수 있습니다.

[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

이 두 개 말고도 [assembly:AssemblyInformationVersion] 이라는 항목도 있습니다.  결국 Version과 관련된 항목이 3개가 있는데 CLR에서 사용하는 버전 정보는 [assembly: AssemblyVersion] 입니다. 이 버전 정보는 CLR에서 굉장히 중요합니다. 뒤에서 배우게 될 Strongly named assemblies를 바인딩 할 때 사용되는 버전 정보입니다. 이 버전 번호가 Assembly의 metadata 테이블 정보 중 AssemblyRef 테이블 항목에 기록이 됩니다.

버전 정보와 함께 Assembly는 Culture(언어) 라는 항목을 가집니다. 이 항목의 값에 따라 사용자는 영어 버전, 일본어 버전, 한국어 버전등의 Assembly를 가질 수 있게 됩니다. Culture에 들어가는 문자열은 주태그 - 부가태그의 형식으로(표준 RFC1766) 표현됩니다.

de-AT Austrian German
en-GB British English
en-US U.S. English

일반적으로는 코드가 포함된 Assembly에는 Culture를 지정하지 않습니다. 코드가 언어에 종속적으로 실행되어야 할 이유가 없기 때문이지요. 대신에 다국어를 지원하기 위한 어플리케이션을 만들 경우 Culture와 관련된 전용 리소스만을 포함한 Assembly를 만들어서 빌드를 하게 됩니다.(이를 satellite assembly - 위성 어셈블리 라고 합니다.) 이렇게 포함된 리소스는 System.Resources.ResourceManager 클래스를 이용하여 사용하게 됩니다.



오늘은 여기까지! 다음주에 계속됩니다!! :)

참고 자료
CLR Via C# 2nd.

'CLR' 카테고리의 다른 글

6. Assembly - GAC(Global Assembly Cache)  (2) 2010.02.02
5. Assembly - Strongly named assemblies  (0) 2010.01.26
3. MSCorLib & Metadata  (4) 2010.01.06
2. CLR! CLR! CLR!  (3) 2009.12.30
1. Hello 世界  (0) 2009.12.23

3. MSCorLib & Metadata

CLR 2010. 1. 6. 09:00 Posted by 알 수 없는 사용자
어서 빨리 System.Object로 시작해서 CLR을 배우고 싶지만 .NET Framework 환경의 CLR의 기반에 대해 좀 더 자세히 짚고 넘어가야 할 것 같습니다. 그래서 이번엔 이 부분에 대해서 정리를 하면서 공부를 해보려고 합니다.

MSCorLib.dll

public sealed class Program
{
  public static void Main()
  {
    System.Console.WriteLine("Hi");
   }
}

이런 코드를 하나 만들어 봅시다. C# 코드입니다. 우리는 CLR을 공부하려고 하는 것이지 C#을 공부하려는 건 아니지만 앞으로 C#을 이용하여 많은 코드를 작성할 예정입니다. 암튼 빌드 해봐야겠죠. 워워- VS 2010 IDE 아직 띄우지 마세요. 훌륭한 개발툴 깔아놓고서도 쓰지 않는게 억울하긴 하지만 지난번과 같이 Visual Studio Command Prompt를 실행해서 빌드를 해 봅시다.

csc.exe /out:hi.exe /t:exe /r:MSCorLib.dll hi.cs


.exe의 실행파일이 생성되고 실행 해 보면 Hi 라고 문자열을 출력합니다.

csc는 C#컴파일러 입니다. 인자를 살펴보면 /out 은 생성될 실행파일명을 지정하는 것이고 /t는 Target의 약자로 exe로 지정되었다는 것은 Win32 어플리케이션을 생성하겠다는 의미입니다. 그리고 /r은 Reference의 약자인데 코드에서 사용된 외부에서 정의된 타입의 참조를 어떤 모듈을 이용하여 해결하겠다는 의미입니다. 마지막으로 hi.cs는 컴파일할 소스 파일이지요.

여기서 우리가 자세히 살펴봐야 할 부분은 /r의 인자인 MSCorLib.dll 입니다.

우선 소스를 다시 한번 살펴 봅시다. 소스에서는 System.Console.WriteLine이라는 메서드를 사용하고 있습니다. 여기서 System.Console 이라는 타입은 C# 컴파일러에서는 정의되어 있는 타입이 아니기 때문에 위에서 보면 MSCorLib.dll이라는 어셈블리 모듈을 참조하여 컴파일을 하고 있습니다.

MSCorLib.dll은 특별한 파일로서 모든 기본 타입을 제공하고 있습니다. Byte, Char등의 타입부터 그 외에 기본적으로 사용하는 많은 타입들을 포함한 어셈블리입니다. 그야말로 핵심 모듈이지요. 그렇기 때문에 C#등의 컴파일러에서는 /r의 인자로 MSCorLib.dll 만큼은 생략해도 기본적으로 이 모듈은 참조하여 컴파일을 하게 됩니다.
결국

csc.exe /out:hi.exe /t:exe hi.cs

라고 해도 정상적으로 컴파일은 된다는 말이지요. /out 옵션과 /t옵션의 기본값도 원래는 소스파일명(확장자 제외한).exe 이고 /t도 기본이 exe이므로 본래는

csc.exe hi.cs

라고 해도 컴파일은 문제없이 잘 됩니다. csc 컴파일러의 인자 중에서는 기본 라이브러리 모듈은 MSCorLib.dll을 참조하지 말라고 지시하는 인자가 있는데 바로 /nostdlib 입니다.

만약에 hi.cs 를 /nostdlib 인자를 주어서 컴파일을 하면 어떻게 될까요.


당연히 컴파일이 실패합니다. 'System.Object' 타입을 찾을 수 없다고 하면서 말이지요. System.Object는 모든 타입의 기본입니다. 모든 타입은 System.Object로부터 파생이 됩니다. 기본 라이브러리 모듈을 참조하지 않았으니 모든 타입의 기본이 되는 System.Object 타입을 찾지 못하는 건 당연한 결과지요. 다시 말하지만 MSCorLib.dll 은 .NET Framework의 핵심 모듈입니다.

Metadata

자 우리는 hi.exe 라는 작은 프로그램을 생성하였습니다. 이제 이 생성된 파일을 좀 까봐야겠습니다. 지난번에 실행파일은 IL코드와 메타데이터가 포함되어 있다고 배웠습니다. 좀 더 정확히 말하자면 PE32(또는 PE32+)헤더와 CLR헤더 IL코드 그리고 메타데이터가 포함되어 있습니다. 한번 살펴봐야죠.

.NET에서 생성하는 PE32 포맷의 실행파일은 기존의 PE32 형식의 실행 파일과 거의 동일합니다. 거기에 .NET Metadata 관련 섹션 정보가 추가되어 있는 형식입니다.


생성된 파일의 Metadata 정보를 확인하기 전에 Metadata의 구조는 어떤식으로 되어 있는지 확인해 봐야겠습니다. Metadata의 구조는 Microsoft SDK에 포함되어 있는 CorHdr.h 라는 헤더 파일에 IMAGE_COR20_HEADER 라는 구조체로 정의가 되어 있습니다.

typedef struct IMAGE_COR20_HEADER
{
    // Header versioning
    DWORD                   cb;             
    WORD                    MajorRuntimeVersion;
    WORD                    MinorRuntimeVersion;
   
    // Symbol table and startup information
    IMAGE_DATA_DIRECTORY    MetaData;       
    DWORD                   Flags;          
 
 // The main program if it is an EXE (not used if a DLL?)
    // If COMIMAGE_FLAGS_NATIVE_ENTRYPOINT is not set, EntryPointToken represents a managed entrypoint.
 // If COMIMAGE_FLAGS_NATIVE_ENTRYPOINT is set, EntryPointRVA represents an RVA to a native entrypoint
 // (depricated for DLLs, use modules constructors intead).
    union {
        DWORD               EntryPointToken;
        DWORD               EntryPointRVA;
    };
   
    // This is the blob of managed resources. Fetched using code:AssemblyNative.GetResource and
    // code:PEFile.GetResource and accessible from managed code from
 // System.Assembly.GetManifestResourceStream.  The meta data has a table that maps names to offsets into
 // this blob, so logically the blob is a set of resources.
    IMAGE_DATA_DIRECTORY    Resources;
 // IL assemblies can be signed with a public-private key to validate who created it.  The signature goes
 // here if this feature is used.
    IMAGE_DATA_DIRECTORY    StrongNameSignature;
    IMAGE_DATA_DIRECTORY    CodeManagerTable;   // Depricated, not used
 // Used for manged codee that has unmaanaged code inside it (or exports methods as unmanaged entry points)
    IMAGE_DATA_DIRECTORY    VTableFixups;
    IMAGE_DATA_DIRECTORY    ExportAddressTableJumps;
 // null for ordinary IL images.  NGEN images it points at a code:CORCOMPILE_HEADER structure
    IMAGE_DATA_DIRECTORY    ManagedNativeHeader;
   
} IMAGE_COR20_HEADER, *PIMAGE_COR20_HEADER;


좀 보기 힘들긴 하지만 위에서부터 하나하나 확인해보죠. 우리가 위에서 생성한 hi.exe 파일을 대상으로 확인하려고 합니다. 직접 헤더를 분석하는 툴을 만들어도 좋겠지만(^-^;;) CFF Explorer 라는 툴을 이용하여 헤더를 확인해 보기로 합니다.


CFF Explorer로 확인해 보면 .NET Directory 섹션부터 위의 헤더 정보와 동일한 이름의 정보들이 순차적으로 기록되어 있는 것을 확인할 수 있습니다. 굉장히 많은 정보 중에서 우리가 짚고 넘어가야 할 정보들은 메타데이터 정의 테이블과 메타데이터 참조 테이블입니다.

Corhdr.h 파일에서 메타 데이터 정의 테이블과 참조 테이블 리스트들의 이름들을 확인해 볼 수 있습니다.   

// Token  definitions

typedef mdToken mdModule;               // Module token (roughly, a scope)
typedef mdToken mdTypeRef;              // TypeRef reference (this or other scope)
typedef mdToken mdTypeDef;              // TypeDef in this scope
typedef mdToken mdFieldDef;             // Field in this scope
typedef mdToken mdMethodDef;            // Method in this scope
typedef mdToken mdParamDef;             // param token
typedef mdToken mdInterfaceImpl;        // interface implementation token

typedef mdToken mdMemberRef;            // MemberRef (this or other scope)
typedef mdToken mdCustomAttribute;      // attribute token
typedef mdToken mdPermission;           // DeclSecurity

typedef mdToken mdSignature;            // Signature object
typedef mdToken mdEvent;                // event token
typedef mdToken mdProperty;             // property token

typedef mdToken mdModuleRef;            // Module reference (for the imported modules)

여기서 확인해 봐야 할 테이블들은 다음과 같습니다.

메타 데이터 정의 테이블 리스트
 메타 데이터 정의 테이블 이름 설명 
 ModuleDef 모듈을 식별하기 위한 항목
 TypeDef 모듈 내의 정의된 각 타입을 위한 항목
 ModuleDef 모듈 내에 정의된 각 메서드를 위한 항목
 FieldDef 모듈 내에 정의된 모든 필드를 위한 항목
 ParamDef 모듈 내에 정의된 각 파라미터를 위한 항목  
 PropertyDef 모듈 내에 정의된 각 속성을 위한 항목 
 EventDef 모듈 내에 정의된 각 이벤트를 위한 항목 

메타 데이터 참조 테이블 리스트
 메타 데이터 참조 테이블 이름 설명
 AssemblyRef 모듈이 참조하는 각 어셈블리를 위한 항목 
 ModuleRef 모듈이 참조하는 타입을 구현하는 각 PE 모듈을 위한 항목 
 TypeRef 모듈이 참조하는 각 타입을 위한 항목 
 MemberRef 모듈이 참조하는 각 멤버(필드 메서드 속성 이벤트 등)를 위한 항목 

위의 두 테이블을 참조하여 파일의 메타 데이터 항목들을 살펴 봅니다.

ModuleDef
모듈의 이름 버전 ID (GUID의 형태로 생성됩니다.) 등이 기록되어 있는 것을 확인 할 수 있습니다.

TypeRef
4개의 타입 참조가 보입니다. 그 중 Console Type에 대한 정보를 살펴봅시다. 타입의 이름(Name)과 이 타입이 어디에 있는지(Namespace)에 대한 정보를 포함하고 있네요.

TypeDef
모듈 내에 정의된 각 타입을 위한 항목을 포함한다고 했습니다. 각 엔트리는 타입의 이름(Name), 상위 타입(Namespace), 플래그(TypeDef Flags)등을 갖고 있습니다. Extends에 TypeRef Table Index 1이라고 되어 있네요. TypeRef의 1번째 인덱스는 위의 화면에서 보면 알겠지만 Object입니다. Object 클래스로부터 상속되어 확장되었다는 걸 알 수 있겠네요.

Method
위의 테이블에선 없던 항목이지만 Method라는 항목도 보이네요. 보시면 알겠지만 hi.exe 내의 메서드 정보를 포함하고 있습니다.  .ctor은 뭔가요? 생성자입니다. .ctor이 호출되어야지 해당 객체가 메모리에서 생성된 위치를 가리키는 포인터 this라는 값을 갖게 됩니다.

MemberRef
모듈이 참조하는 각 멤버들의 참조 정보를 갖고 있네요. 3번째는 WriteLine입니다. Class정보를 보면 TypeRef Table Index 4 라고 되어 있네요. 위에서 확인해 보면 System.Console을 가리키는 것임을 알 수 있습니다. 다른 멤버들 역시 TypeRef의 다른 멤버들을 가리키고 있습니다.

그리고 밑에 보이는 CustomAttribute는 나중에 Attribute를 공부할 때 다시 한번 언급해 보기로 하구요. Assembly는 다음에 공부해야 할 Assembly에서 다시 한번 보면 될 것 같네요.

대략이나마 Metadata 정보를 확인해 보니까 정말 다양한 정보들이 체계적으로 기록되어 있다는 것을 확인해 볼 수 있었습니다.

생각보다 진도가 늦어지고 있네요. 그래도 이왕 공부하는 것 아주 완벽히는 아니더라도 이렇게 하나하나씩 살펴봐야지 공부하면 공부할수록 좀 더 깊이있는 이해가 필요한 부분에서 도움이 될 것이라고 생각합니다.

이 글을 보시는 분들 시간을 내서 직접 맨 위의 코드를 컴파일 해보시고 이런저런 메타데이터 정보를 한번 확인해 보세요~ CFF Explorer 라는 툴도 있지만 기본적로 지원하는 IL 역어셈블러 툴인 ILDasm.exe를 이용하셔도 위에서 언급한 메타데이터 정보들을 확인할 수 있습니다.



메타데이터 정보가 보이지요?


함수의 IL코드도 확인할 수 있습니다.

위에서 언급한 .ctor 코드도 볼 수 있습니다. System.Object의 생성자를 부르고 있네요. 이외에도 다양한 정보들을 확인해 볼 수 있으니까 꼭 한번씩 살펴보세요. :)


참고 자료
CLR via C# 2nd Ed.



'CLR' 카테고리의 다른 글

6. Assembly - GAC(Global Assembly Cache)  (2) 2010.02.02
5. Assembly - Strongly named assemblies  (0) 2010.01.26
4. Assembly  (2) 2010.01.15
2. CLR! CLR! CLR!  (3) 2009.12.30
1. Hello 世界  (0) 2009.12.23

2. CLR! CLR! CLR!

CLR 2009. 12. 30. 09:00 Posted by 알 수 없는 사용자
CLR의 장점은 무엇일까요? 비교 대상은 CLR을 통해 생성되는 관리되는 코드(managed code)와 기존의 컴파일러로 실행 파일이 생성되는 관리되지 않는 코드(unmanaged code) 입니다.


CLR 이래서 좋다!

1. 성능 향상

CLR의 JIT 컴파일러를 통해 만들어진 코드는 unmanaged code에 비해 성능이 우수하다고 합니다. 처음 듣기에는 좀 의아한데 성능이 향상된다고 주장하는 근거는 다음과 같습니다.



a. 컴파일 시에 실행 환경에 대한 정보를 알 수 있고 이에 최적화된 지시어를 사용하여 코드를 생성합니다. (예 : 실행 환경이 펜티엄 4라고 한다면 펜티엄 4에 최적화된 지시어를 이용하여 코드를 생성하게 됩니다.)

b. 특정 상황의 테스트 값 또는 논리 연산의 결과를 실행 전에 이미 정확하게 예측할 수 있습니다.

if (numberOfCPUs > 1)
{
...
}

위와 같은 코드가 있을 경우 현재 JIT컴파일러를 통해 컴파일 될 시스템의 CPU가 하나라면 CLR의 컴파일러는 해당 if 블록이 항상 실행되지 않을 것을 알기 때문에 이 코드를 네이티브 코드로 변환하지 않습니다. 이 경우 이 실행코드는 호스팅 하는 운영체제에 최적화된 코드로 컴파일이 될 것이며 좀 더 작고 빠른 코드를 생성하게 됩니다.

c. CLR은 어플리케이션의 실행 패턴을 profile 할 수 있으며 이에 따라 실행 중에 IL코드를 네이티브 코드로 다시 컴파일 할 수 있습니다. 재 컴파일된 코드는 profile된 실행 패턴을 바탕으로 코드의 분기를 최적화하기 위해 재정렬 됩니다.

d. 일반적으로 unmanaged code는 최저사양에 맞춰 컴파일이 됩니다. 하지만 managed 코드는 실행 환경에 최적화된 코드를 생성할 수 있습니다.

저 근거들을 보면 정말 managed code가 성능면에서 봤을 때 우월하겠다는 생각이 들기도 합니다. 그런데 곰곰히 생각해보면 실제 Native Code로 컴파일하는 과정에 있어서 컴파일러가 얼마나 최적으로 컴파일을 해 내느냐가 관건일 텐데 CLR의 JIT컴파일러와 기존 비관리되는 코드를 생성해 내는 컴파일러들의 성능 비교를 해봐야 할 것입니다.

하지만 평균적으로 봤을 때 실제 실행되는 환경에 최적화된 코드를 즉시에서 뽑아내는 방식의 managed code가 좀 더 나은 코드를 만들어 낼 수 있을거라는 생각이 들기는 합니다.

2. 다른 언어로 개발된 구성 요소를 쉽게 사용할 수 있는 기능

managed code는 IL 코드와 함께 메타데이터 정보가 들어있습니다. CLR은 이 메타데이터 정보를 이용하여 클래스를 찾고 로드하며, 메모리에 인스턴스를 배치하고, 메서드 호출을 확인하고, 네이티브 코드를 생성하고, 보안을 강화하며, 런타임 컨텍스트 경계를 설정합니다. CLR을 지원하는 어떤 언어든지 컴파일을 하게 되면 동일한 IL을 생성하게 됩니다. 이러한 이유로 인해 다른 언어로 개발되었다 하더라도 구성 요소를 쉽게 사용할 수 있게 됩니다.

3. 클래스 라이브러리에서 제공하는 확장 가능한 형식

구성 요소 및 응용 프로그램에 속한 개체가 여러 언어를 통해 상호 작용하는 경우 이를 쉽게 디자인할 수 있습니다. 다른 언어로 작성된 개체들이 서로 통신할 수 있고 해당 동작들이 완벽하게 통합될 수 있습니다. 예를 들어, 클래스를 정의한 다음 다른 언어를 사용하여 원본 클래스에서 클래스를 파생시키거나 원본 클래스의 메서드를 호출할 수 있습니다. 또한 클래스의 인스턴스를 다른 언어로 작성된 클래스의 메서드로 전달할 수 있습니다. 런타임을 대상으로 하는 언어 컴파일러 및 도구에서 런타임에서 정의한 공용 형식 시스템을 사용하고, 형식의 생성, 사용, 유지 및 바인딩 뿐만 아니라 새 형식을 정의할 때도 런타임 규칙을 따르기 때문에 이러한 언어 간 통합이 가능합니다.

4. 상속, 인터페이스 및 개체 지향적인 프로그래밍을 위한 오버로딩 등과 같은 새로운 언어 기능. 확장 가능한 다중 스레드 응용 프로그램을 만들 수 있도록 해주는 명시적 자유 스레딩에 대한 지원. 구조적 예외 처리 및 사용자 지정 특성에 대한 지원.

이 부분은 이전의 unmanaged code와 비교한 장점이라기 보다는 .NET Framework 차원에서 지원하는 플랫폼 상의 새로운 기능에 대한 이야기로 보입니다. 말이 참 길고 어렵습니다. 이 부분에 대해서는 공부하면서 차차 알아가게 될 것으로 보입니다.

5. 횟수 계산이 필요 없도록 개체 수명을 관리하는 가비지 수집

기존 C/C++ 환경에서의 프로그래밍을 하던 분이시라면 가장 편리하게 생각할 것이라고 생각합니다. 예전 JAVA가 나왔을 때도 장점으로 내세웠던 부분 중 하나였지요. 이건 CLR의 장점이라기 보다는 .NET Framework 차원에서 지원하는 장점이라고 봐야 할 것 같네요. 가비지 수집기는 메타데이터를 통해 객체의 수명 상태를 조사해서 수집 대상을 확인할 수 있으며 런타임시에도 해당 객체가 포함하고 있는 멤버의 파악은 물론 이들 중 어떤 멤버가 다른 객체에 의해서 참조되고 있는지도 파악이 가능합니다.  이런 이유로 메모리 누수로 인한 문제점을 원천적으로 해결할 수 있기 때문에 프로그래밍 환경이 매우 깔끔하며 메모리 누수로 인해 시스템에 악영향을 미치지 않습니다.

7. IDL(인터페이스 정의 언어)의 필요성을 없앤 자체 설명 개체

CLR환경의 파일들은 실행파일 자체내에 모든 정의된 타입과 참조된 타입의 정보가 자세하게 기록되어 있습니다. MIDL의 경우에는 사용하기가 복잡하고 어려웠던 것이 사실이지요.

8. 한 번 컴파일하여 런타임을 지원하는 모든 CPU와 운영 체제에서 실행할 수 있는 기능

지금까지 거론된 장점들도 중요한 장점들이겠지만 이만큼 한눈에 확- 들어오는 장점이 있을까요. 한번 작성된 프로그램은 어디에서나 수정 없이 실행이 가능합니다. 다만 이 플랫폼 독립성이 윈도우 환경에만 국한된다는 점이 아쉽습니다.

9. 손쉬운 IL 컴파일러 제작 및 코드 검증

IL코드는 스택기반이며 레지스트리를 직접 제어하는 지시어를 포함하고 있지 않습니다. 그리고 IL의 모든 지시어들은 특별히 타입에 의존적이지 않습니다. (예 : IL의 ADD 지시어의 경우 32비트 또는 64비트의 경우의 ADD가 따로 존재하지 않습니다.) 그렇기 때문에 개발자는 레지스트리나 CPU에 대한 관리를 생각할 필요가 없이 비교적 손쉽게 IL코드를 생성하는 컴파일러를 만들 수 있습니다.

그리고 이 IL코드가 CPU종속적인 네이티브 코드로 전환 될 때 CLR은 검증 작업이라는 것을 수행하게 됩니다. 수행되는 메서드의 인자의 타입과 개수 체크부터 시작하여 메모리 관련 오류나 (잘못된 메모리를 읽고 쓰는 등의) 타 프로세스의 메모리에 불법적으로 접근하는 등의 문제도 검증할 수 있기 때문에 부적절한 코드의 실행을 원천적으로 차단할 수 있습니다.(이를 코드 기반 보안이라고 합니다.) CLR은 각각의 관리되는 어플리케이션을 AppDomain이라는 것으로 안전하게 관리하게 됩니다. 이로써 프로그램을 안전하고 견고하게 작성하는 것이 가능합니다.

10. 손쉬운 응용프로그램 배포

단순히 파일을 복사하는 것만으로도 설치가 가능합니다. 이전의 레지스트리에 의존하는 COM이나 DLL처럼 버전차이로 인한 문제도 없습니다.


 
CLR이 과연 좋기만 할까?

1. IL 코드 보안

IL코드는 역어셈블을 하게 되면 소스코드를 얻어내는 것이 매우 쉽습니다. 이 말은 소스코드 레벨의 보안이 어렵다는 이야기입니다. 물론 third-party 벤더에서 판매하는 암호화 처리 유틸리티등으로 좀 더 역어셈블을 어렵게 만들 수는 있습니다.

2. 다양한 언어를 지원한다는 것이 마냥 좋을 것일까?

다양한 언어를 지원하여 언어 독립성을 지켜준다는게 대규모 프로젝트 시 장점으로만 작용하는 것은 아닐거라고 생각합니다. 물론 한 프로젝트에서 3~4가지 이상의 언어가 동시에 사용되는 경우가 드물기는 하겠지만 만에 하나 사용된다고 하더라도 언어별 차이로 인한 골치 아픈 관리문제가 발생할 가능성도 큽니다. 전체 프로젝트를 관리하는 입장에서 해당 프로젝트를 후에 유지보수하게 되는 개발자가 있다면 각 언어에 대해 어느정도의 지식이 있어야 전체 프로젝트를 관리할 수 있을 것입니다. 그런데 이렇게 써놓고 보니까 좀 억지스럽기도 하네요. ㅎㅎ 프로젝트 개발 시작 시 이런 문제는 정책적으로 해결할 수 있을테니까요.

3. Native Code보다 빠릅니까?

성능 향상에 대한 근거를 앞에서 살펴보았습니다. 그런데 메모리 관리나 명령어 최적화는 납득이 가는데 정말 C/C++을 이용하여 생성된 Native Code보다 빠르게 작동할 수 있겠느냐. 라는 문제는 회의적인 입장을 가진 시각이 있습니다.

4. Framework가 깔려야 합니다.

CLR을 이용하기 위해서는 .NET Framework가 설치되어야 합니다. 요사이는 윈도우의 경우 기본적으로 .NET Framework가 설치되어 있기 때문에 크게 문제될 것은 없습니다. 하지만 계속해서 새로운 버전의 .NET Framework가 출시되고 있는터라 개발 시 그리고 배포 시 버전에 신경을 써야 합니다.



지금까지 CLR에 대해 알아보았습니다. CLR은 .NET Framework의 구성요소 중 하나입니다. 그렇다면 우리가 배워야 할 CLR을 포함하고 있는 .NET Framework에 대해서도 간략하게 살펴보아야 하겠네요.

.NET Framework는 크게 다음과 같은 요소로 구성되어 있습니다.

1)CLR(Common Language Runtime) 공통 언어 런타임

우리가 배울 놈이지요. 자바에서는 JVM이 있고 .NET에서는 CLR이 있습니다. .NET의 핵심 커널로써 .NET 프로그램 실행을 위한 모든 서비스를 제공하는 주체라고 할 수 있습니다.

2) CTS(Common Type System) 공통 타입 시스템

.NET 언어 간에 공통으로 사용되는 표준 데이터 타입입니다. 각 언어마다의 변수, 메서드, 클래스, 구조체, 인터페이스 등의 타입이 다를수 있는데, 이렇게 다른 타입을 .NET 기본 클래스에 정의한 타입으로 관리할 때 참조하는 명세서와 같습니다. 이는 어떤 언어로 프로그래밍을 하던 .NET 기본 클래스에 정의된 타입으로 매치가 가능하다는것을 나타내고 있습니다. 다시 말해, 서로 다른 언어로 작성되었다 하더라도 .Net에서 이해할 수 있는 데이터 형식인지 아닌지를 테스트하고 검증 관리하는 역할을 담당합니다. 그러므로 .Net 내에서 동작할 수 있는 데이터 형에 대한 관리를 해준다고 말할 수 있습니다.

CTS의 멤버에는 필드(Field), 메서드(Method), 속성(Property), 이벤트(Event) 가 있으며 각 타입들의 접근성에 대해서도 명시하고 있습니다. 접근성의 종류에는 Private, Family(C++이나 C#에서는 Protected로 표현됩니다.), Family & Assmebly(C#등이나 Visual Basic .NET등에서는 지원하고 있지 않습니다.), Assembly(보통 Internal이라는 키워드로 표현됩니다.), Public 이 있습니다.

3) CLS(Common Language Specification) 공통 언어 사양

.NET 용 프로그래밍 언어가 따라야 하는 공통된 최소한의 표준 규약으로, 적어도 .Net에서 동작하기 위해서는 반드시 지켜야하는 언어 스펙입니다. 모든 언어들이 다 중간언어로 변환이 가능한 것은 아닙니다. 언어마다 특성이 있어서(예를 들자면 어떤 언어는 대소문자를 구분하지 않거나, unsigned integer 같은 형식은 지원하지 않습니다. 또는 어떤 언어는 가변 인자를 지원하지만 어떤 언어는 그렇지 않습니다.), 변환이 가능한 프로그램을 작성하기 위해서는 지켜져야 할 최소한의 규칙이 있는데, 이 규칙을 모아 놓은 것이 공통 언어 사양(Common Language Specification: CLS) 입니다.



그림을 보면 CLS는 다양한 언어들 사이에서 최소한의 교집합을 가지고 있습니다. 그리고 CLR을 지원하는 언어들은 CLR/CTS환경에 속해 있는데 이를 보면 알 수 있듯이 C#이나 Visual Basic 그리고 기타 등등의 CLR을 지원하는 언어들은 100% CLR/CTS의 기능을 지원해 주지 않는 것을 알 수 있습니다. 100% 활용을 위해서는 개발자가 IL 어셈블러 언어로 코드를 작성해야 합니다.

4) BCL(Base Class Library) 기본 클래스 라이브러리
모든 언어에서 공통적으로 사용할 수 있는 방대한 클래스 라이브러리입니다. 객체 지향적이며 문서화도 잘 되어 있기 때문에 쓰기가 매우 편리합니다. 대략 다음과 같은 것들이 있습니다.

* 기본 타입 관련
* 배열, 연결 리스트, 큐 같은 컬렉션
* 그래픽 라이브러리 (GDI+)
* 스레드와 동기화 클래스
* 각종 컨트롤
* 파일 IO
* 네트워크 / 인터넷
* 데이터 베이스

FCL(Framework Class Library)라는 명칭도 있습니다. BCL(최소한의 기본적인 라이브러리 모음) + 추가 클래스 라이브러리 모음으로 BCL보다 훨씬 큰 집합입니다.


자. 지금까지 CLR의 특징부터 .NET Framework의 구조에 대해 대략 알아봤습니다. 이제 배경에 대해 어느정도 살펴 봤으니 다음부턴 본격적으로 CLR에 대해 들어가보려고 합니다. :)

현재 .NET Framework는 3.5까지 공개가 되어 있고 Visual Studio 2010에서는 .NET Framework 4.0이 포함이 되어 있습니다. 우리가 공부할 CLR의 버전도 .NET Framework의 버전을 따라 업그레이드 되고 있지요.

.NET Framework 4.0의 특징은 .NET Framework 섹션에서 아주 자세히 잘 설명이 되어 있습니다.(http://vsts2010.net/6) 꼭 읽어보세요!!!




'CLR' 카테고리의 다른 글

6. Assembly - GAC(Global Assembly Cache)  (2) 2010.02.02
5. Assembly - Strongly named assemblies  (0) 2010.01.26
4. Assembly  (2) 2010.01.15
3. MSCorLib & Metadata  (4) 2010.01.06
1. Hello 世界  (0) 2009.12.23

1. Hello 世界

CLR 2009. 12. 23. 09:00 Posted by 알 수 없는 사용자
다음의 소스코드로 시작해 봅시다.

.assembly PrintString {}

/*
    Console.WriteLine("Hello, 世界)"
*/

.method static public void main() il managed
{
    .entrypoint            // 프로그램의 진입점. 곧 Entrypoint입니다.
    .maxstack 8


    // *****************************************************
    // Console.WriteLine("Hello, 世界");
    // *****************************************************
    ldstr "Hello, 世界"        // 스택에 스트링을 PUSH 합니다.

    // System.Console.Writeline 함수 호출합니다.
    // 이 함수는 스택에 있는 스트링을 인자로 POP합니다.
    call   void [mscorlib]System.Console::WriteLine
                                 (class System.String)

    // *****************************************************
    ldstr "Press Enter to continue"
    call   void [mscorlib]System.Console::WriteLine
                                 (class System.String)

    // System.Console.Read 함수를 호출합니다.
    // 키입력을 기다리게 되겠네요.
    call int32 [mscorlib]System.Console::Read()

    // Read()함수 호출로 인한 키 입력이 스택에 PUSH되었을 테니까
    // 이 함수가 끝나기 전에 POP하여 스택을 비웁니다.
    pop
    // *****************************************************

    ret
}

어 이거 뭔가요. 뭔가 익숙한 것 같으면서도 이상한 코드입니다.

대충 보아하니 화면에 "Hello, 世界" 를 출력하는 프로그램 같은데 Console::WriteLine() 같은 걸 보자니 Visual C++ 에서 Managed Code 프로그래밍 하는 것 같기도 하고, call, pop 이런 것들을 보니까 어셈블리어 같기도 합니다. 야~~ 21세기에 컴퓨터 개론에서나 보던 스택 PUSH, POP 신경쓰면서 화면에 저런거 찍어야 겠나 싶기도 하네요.

그런데 또 다시 생각해보면 사람이 보긴 답답해도 속도만 빨라졌지 여전히 좀 멍청한 CPU가 알아먹기는 좀 쉬울 것 같다는 생각이 들기도 합니다. 

네 저 코드는 .NET하면 누구나 들어봤을만한 IL (Intermediate Language) 코드라고 하는 것이라고 하네요. .NET Framework 기반의 환경에서 프로그래밍을 하게 되면 어떤 언어로 프로그래밍을 하던 .NET을 지원하는 모든 컴파일러는 지금까지 많이 봐오던 기계어로 컴파일을 하는 것이 아니라 MSIL이라는 코드로 컴파일을 합니다.

자. 일단 코드를 만들어 봤으니 실행을 시켜봐야죠. IL코드는 뭘로 컴파일을 해야 하나요. MSDN을 디벼보니 ilasm.exe 라는 .NET Framework의 도구를 이용해 컴파일을 할 수 있다고 합니다.

그러면 Visual Studio 2010에서 제공하는 CMD를 열고 해당 소스를 컴파일 해봅니다. (시작 -> 프로그램 -> Microsoft Visual Studio 2010 -> Visual Studio Tools -> Visual Studio Command Prompt (2010) 을 이용하면 됩니다.)



컴파일 하면 exe 실행파일이 생성이 되고 그 실행파일을 실행하니까 Hello, 世界 찍힙니다! 코드대로 Press Enter to continue 나오고 거기서 아무 키나 누르면 프로그램 실행이 종료됩니다.

(당연한거지만) 유니코드 지원 잘 되는군요. 보통 Hello World를 찍는데 Hello 世界 라고 글로벌하게(?) 찍은 이유는 얼마전 구글에서 공개한 Go 언어의 소개 프로그램이 Hello 世界 였거든요. 그게 괜히 멋져보여서 따라해 봤습니다. ㅎㅎ


명색이 프로그래머인데 첫인사는 코드로 해야 하지 않겠나. 싶어서 이렇게 너저분하게 인사를 해봤네요. 앞으로 CLR에 대해서 공부하면서 공부한 내용을 나름대로 정리해서 이런식으로 공유를 해 볼 생각입니다. 같이 공부하는 입장이라 이래저래 시행착오도 많을테고 실수도 분명히 있을테지만 앞으로 잘 부탁드립니다. _(_ _)_

CLR은 Common Language Runtime의 약자로 우리말로 번역하니 [공용 언어 런타임] 이라고 번역이 되네요. [공용 언어]는 우리말인데 [런타임]은 왜 런타임인가. 도대체 런타임이 뭔가요?

IT용어사전을 찾아보니 런타임에 대한 내용이 다음과 같이 나와 있습니다.

런타임 【run-time】

읽기 : 런타임

어플리케이션 소프트를 실하는데에 필요한 소프트웨어 모듈(부품)을 말한다. Windows의 경우 DLL파일의 형태로 제공된다. 실시에 런타임이 필요한지는 어플리케이션 소프트의 개발에 쓰여진 개발 툴에 의존한다. 런타임은 어플리케이션 소프트에 같이 들어있는 경우도 있지만, Microsoft사의 Visual Basic으로 개발 된 어플리케이션 소프트를 실하기 위해서는 [MSVBVMxx.DLL](xx는 버전 번호)라는 파일이 필요하지만, Borland Software사의 C++ Builder로 개발된 어플리케이션 소프트는 런타임 없이 동작 시킬수 있다.


출처 : http://www.chinatotheworld.net/w/C3ABC29FC2B0C3ADC283C280C3ACC29EC284.html


아. 프로그램을 실행하는데 필요한 소프트웨어 모듈을 말한답니다. 그렇다면 CLR은 공용 언어를 실행하는데 필요한 소프트웨어 모듈. 정도가 되는건가요?

위키도 한번 뒤져봤습니다.

공통 언어 런타임

위키백과 ― 우리 모두의 백과사전.

공통 언어 런타임(Common Language Runtime, CLR)은 마이크로소프트 닷넷 이니셰이티브의 가상 머신 구성 요소이다. 프로그램 코드를 위한 실행 환경을 정의하는 마이크로소프트의 공통 언어 기반 (CLI) 표준의 기능이다. 공통 언어 런타임은 공통 중간 언어(CIL, 이전에는 MSIL로 알려져 있었음)라고 불리는 바이트코드의 형태를 실행한다.

공통 언어 런타임을 사용하는 개발자들은 C#이나 VB 닷넷과 같은 언어의 코드를 기록한다. 컴파일 시간에 닷넷 컴파일러는 이러한 코드를 공통 중간 언어의 코드로 변환한다. 런타임할 때 공통 언어 런타임의 JIT 컴파일러(JIT 컴파일러)는 공통 중간 언어 코드를 운영 체제의 네이티브 코드로 변환한다. 아니면 중간 언어 코드는 런타임 이전에 개별단계에서 네이티브 코드로 컴파일될 수도 있다. 이로써 공통 중간 언어를 네이티브 코드로 변환하는 컴파일이 더 이상 필요하지 않기때문에 나중에 실행되는 모든 소프트웨어가 빠르게 실행되도록 도와 준다.

공통 언어 기반의 몇 가지 다른 기능이 윈도가 아닌 운영 체제에서 실행되는 반면, 공통 언어 런타임은 마이크로소프트 윈도 운영 체제에서만 동작한다.

출처 : http://ko.wikipedia.org/wiki/%EA%B3%B5%ED%86%B5_%EC%96%B8%EC%96%B4_%EB%9F%B0%ED%83%80%EC%9E%84


아 맞네요. 우리가 오늘부터 공부하고자 하는 CLR이라는 놈은 바로 IL을 실행가능하게 해주는 놈입니다. .NET Framework 기반의 모든 언어들은 언어를 가리지 않고 각자의 컴파일러에 의해 IL이라는 언어로 컴파일이 되고 CLR에 의해 IL기반의 코드들이 실제로 실행이 되게 되는 것이네요.

대충 CLR이 어떤 일을 한다는 것을 알게 되었습니다. 그렇다면 왜 CLR이라는 걸 만들었는지 CLR의 주변엔 또 어떤 것들이 있는지 주욱- 한번 훑어봐야지 앞으로 진도 나가는데 어려움이 없겠지요.

후아-
오늘은 여기까지 하죠!

'CLR' 카테고리의 다른 글

6. Assembly - GAC(Global Assembly Cache)  (2) 2010.02.02
5. Assembly - Strongly named assemblies  (0) 2010.01.26
4. Assembly  (2) 2010.01.15
3. MSCorLib & Metadata  (4) 2010.01.06
2. CLR! CLR! CLR!  (3) 2009.12.30