'디버깅'에 해당되는 글 3건

  1. 2008.02.07 IIS 서버 콤포넌트 만들기.. (6)
  2. 2007.11.12 Dr Watson을 기본 디버거로 사용하기.. (8)
  3. 2007.09.24 프로세스 메모리 읽어내기.. (2)
몇년전에 몇번 만들어보고는 만들일이 없었는데, 최근에 ASP에서 호출하여 쓸 수 있도록 하는 콤포넌트를 만들게 되었다.. 오랜만에 새로 만들어 보려니 예전 소스를 봐도 기억도 안나고 삽질의 연속.. 아~ 이래서 기록을 남겨둬야 한다니깡.. 워드 프로세서 문서가 저장된 BLOB 필드를 읽어 웹브라우저에서 보여줄 수 있는 png 이미지 파일로 저장하는 기능을 하는 녀석이었는데, 다른 기능은 어렵지 않게 구현이 가능했는데 COM에 관련된 내용 때문에 삽질이 많았다.. 몇년전에 보고 책꽂이에 먼지만 쌓이던 책을 오랜만에 꺼내 찾기도 하고, 인터넷 검색도 하면서 찾은 내용들을 정리해보자.. VC++ 6.0 기준이라 좀 그렇긴 하다만..

아래 내용의 개발환경은 VC++ 6.0 with SP6 and Platform SDK 2001 Feb, Windows XP Professional with SP2, IIS 5.0에서 이루어졌다..



콤포넌트 만들기
IIS용 컴포넌트는 ATL COM 컴포넌트로 만든다.. 먼저 프로젝트를 생성해야 하는데, 다음과 같은 순서로 따라하면 된다.. 프로젝트 생성후 문제가 없는지 빌드해서 점검해본다..
  1. File 메뉴 > new > Projects 탭에서 ATL COM AppWizard 선택..
  2. Location에서 저장할 경로를 지정하고, Project name에 원하는 이름 입력 후 OK 클릭..
  3. 다음 화면에서 IIS의 ASP 프로그래밍에 사용할 것이므로 DLL로 선택..
  4. 3번 화면에서 MFC를 사용하고 싶다면 Support MFC를 체크한다.. UI에 관련된 클래스들은 아무런 소용이 없지만, CString 등 자료구조 및 유틸리티성 클래스들을 활용하고 싶다면 클릭해야 한다.. 나중에 프로젝트 만들고 나서 MFC를 사용할 수 있도록 만드는 방법도 있을텐데, 찾아보덜 않아 모르겠다.. 난 그냥 무조건 클릭한다.. MFC 관련 클래스들 사용하고 싶어서..
  5. 빌드는 주로 Debug와 Release MinDependency, 이렇게 2개를 주로 사용한다.. 물론, 상황에 따라서는 다른 옵션도 사용하게 되고, 유니코드를 고려한 코딩을 하게 되면 유니코드로 빌드할 수 있다.. 특히, 서버 컴포넌트의 경우 서버에서 실행되는 코드로 조금의 성능향상도 중요할 수 있기 때문에 유니코드로 빌드하면 좋다.. WinNT 계열 (NT를 포함하여 이후 버젼.. 2000, XP, 2003 서버, Vista 등)에서는 유니코드가 기본이며, 하위호환을 위해 다른 인코딩 방식을 지원하는 것이므로 아무리 작은 오버헤드라도 줄이는 것이 중요한 서버에서는 유니코드로 작성하는 것이 좋다.. MinSize와 MinDependency의 차이는 잘 모르겠다..

이렇게 만들어진 코드는 초기화 등의 경우 외에는 실제 코드가 들어가는 것은 적고, 실제 기능 및 동작을 위해서는 소스코드를 추가해야 한다.. 이 클래스가 실제 동작을 담당하는 코드이다..

  1. Insert 메뉴에서 New ATL Object를 선택한다..
  2. Category에서 Objects를 선택하고, Objects에서는 ActiveX Server Component를 선택한다..
  3. 클래스 이름을 입력한다.. Prog ID에 나타난 값이 ASP에서 Server.CreateObject로 생성시 넘기는 값이된다.. Prog ID 값을 잘 기억해 두도록 한다.. 물론, 잊어버려도 "프로젝트명.클래스명 "으로 조합하면 된다.. 최악의 경우에는 GUID를 가지고 레지스트리 편집기에서 검색을 해보면 Prog ID 문자열 값을 알아낼 수도 있다..

    사용자 삽입 이미지


  4. Attributes, ASP 탭 부분은 디폴트로 옵션을 체크한 후 마친다..

사용자 삽입 이미지

사용자 삽입 이미지


이렇게 추가된 클래스에 필요한 메쏘드, 프로퍼티를 추가하게 된다.. 일반적인 함수 및 멤버함수 등을 추가하면 된다.. 필요하다면 이런 클래스를 필요한 만큼 컴포넌트에 계속해 추가해 넣을 수 있다.. 이런 경우 DLL은 하나지만, Server.CreateObject를 분리하여 호출할 수 있게된다..



메쏘드와 프로퍼티 추가
ASP 페이지에서 호출할 수 있도록 메쏘드와 프로퍼티를 추가한다.. 메쏘드는 함수호출을 위해, 프로퍼티는 약속된 값에 접근하는데 편의성을 제공하기 위해 사용된다.. 특정 단위기능을 수행하기 위해서는 메쏘드 호출을, 상태값이나 특정 기능 수행후 결과값을 접근하기 위해서 프로퍼티를 사용한다.. 클래스에서의 멤버함수와 멤버변수와 비슷하다 생각할 수 있다..


메쏘드 추가/수정/삭제하는 방법
클래스 뷰 트리에서 ATL 객체를 추가해 넣을때 입력했던 클래스 이름이 C로 시작하는 것이 있을 것이고, I로 시작하는 것이 보인다.. C로 시작하는 클래스 트리를 펼쳐보면 그 안에 다시 I로 시작하는 인터페이스가 보이는데, 트리상에서 최상단에 I로 시작하는 인터페이스도 같이 보이니 그리로 접근한다.. 물론, C로 시작하는 클래스 밑에서 똑같은 작업을 해도 동일한 작업을 수행할 수 있다..
  1. 마우스 우측버튼을 클릭하여 Add Method를 클릭..
  2. 새로 나타난 다이얼로그 창에 메쏘드 이름과 파라미터 리스트를 입력..
  3. 파라미터 입력시 데이터형과 규칙에 맞게 입력을 해준다..

이 다이얼로그에서 리턴값을 선택할 수 없는 황당함을 느끼게 되는데, 이는 나중에 처리할 것이니 여기서는 일단 넘어간다.. 나도 처음에 이 다이얼로그를 보고 어찌나 황당하던지..

데이터형은 COM에서 사용할 수 있는 자료형들이 많이 있지만 long, double, BSTR, VARIANT 정도만 알고 있으면 된다.. long과 double은 C++에서와 동일하니 같은 의미로 사용하면 되고, 문자열의 경우는 BSTR을, 한 자료형의 배열이나 여러 자료형이 섞인 구조체의 경우는 VARIANT를 사용한다..

아울러 COM 인터페이스를 기술하는 방법의 특이한 점이 메쏘드 패러미터 앞에 [in], [out], [out,retval] 이라는 내용을 명시한다는 점.. 메쏘드 추가시 이 부분까지 같이 명시를 해서 표기를 해주면 더 좋다.. 물론, 여기서는 생략하고 나중에 직접 하나하나 고쳐도 된다.. 생략하면 모두 [in]으로 간주한다..

일단 만들때는 위저드를 통해 뭔가 UI가 제공되지만, 수정과 삭제를 할때는 그러한 UI가 제공되지 않는다.. VC++ 6.0의 한계중 하나인데 일관된 UI를 제공해주지 않는다.. 만들때 뭔가 UI가 나왔으니 수정이나 삭제시에도 기대를 하게 되는 것이 사람 심리인데, 이러한 당연한 기대를 완전 무시하는 것이다.. 수정과 삭제는 편집기 열고 내가 알아서 잘 해야 한다.. 만들때 내가 직접 만들지 않았으므로 어디 어디 코드들이 추가되었는지 알 길이 없는데, 이럴때에는 메쏘드 이름을 가지고 Find In Files를 잘 해서 필요한 곳 빠지지 않고 잘 수정 혹은 삭제가 되도록 신경써야 한다.. 첨 해볼때만 좀 황당하지, 한번만 해보면 별거 아니라는 생각이 든다.. VC++ 6.0에 뭔가 많이 바라지도 않아왔었고, 손으로 열심히 다듬던 코드들인데 새삼스러울 것도 없다..


프로퍼티 추가/수정/삭제하는 방법
프로퍼티는 메쏘드와 방식이 완전 동일하다.. 메쏘드를 추가, 수정 및 삭제를 할줄 안다면 프로퍼티도 동일한 방법으로 하면 된다..



리턴값 넘기기

리턴값을 넘기고 싶은 경우는 패러미터 리스트에서 가장 마지막 패러미터에 [out, retval]를 붙여주면 된다..
예를들면 맨 마지막 파라미터를 [out,retval] int* iResult 라고 했으면 int 값을 리턴으로 넘기는 것이며, [out,retval] BSTR* sFileName 이렇게 하면 문자열을 리턴값으로 넘긴다는 의미가 된다..

따라서, SetImagePath([in] BSTR sPath, [out,retval] int* iResult) 이렇게 메쏘드를 선언했다면 파라미터는 문자열을 넘기는 것 하나이며 정수값을 리턴하는 메쏘드가 되는 것이다..
호출할때는 대략 result = objTestCom.SetImagePath ("경로명") 이런 모습으로 사용하게 된다.. 리턴값을 파라미터 리스트에 정의한다는게 좀 어색하기는 하다.. 아무튼, COM 컴포넌트에서는 이렇게 만들어야 한다.. 이상하지만, 따르는 수 밖에 없다..



리턴값으로 Variant 넘기기
C에는 배열과 구조체라는 것이 있으며 다른 언어에도 이와 상응하는 자료구조가 있을 것이다.. 문제는 언어마다 다른 자료구조를 COM 이라는 환경에서 어떻게 동일한 인터페이스로 처리할 것인가.. 고민의 결과는 바로 VARIANT 이다.. 따라서 기본 자료구조로 해결 안되는 모든 자료구조는 바로 이 Variant로 해결을 해야 한다.. 대부분 배열을 위해 많이 사용된다.. 이를 좀 더 응용하면 여러 상황에서 활용될 수 있을 것이다.. 기본적으로 Variant는 다른 자료형들을 하나로 묶어서 관리할 수 있다..

STDMETHODIMP CTestVariant::ReturnVariant(int iValue, VARIANT *pVal)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())

    VARIANT v;
    v.vt = VT_VARIANT | VT_ARRAY;

    int iRowCount    = 3 ;
    int iColumnCount = 2 ;
   
    SAFEARRAY* pSA;
    SAFEARRAYBOUND bnd[2]={{iRowCount,1}, {iColumnCount,1}};
    pSA = SafeArrayCreate(VT_VARIANT, 2, bnd);  
   
    VARIANT* pVar;
    SafeArrayAccessData(pSA, (void**)&pVar);

    for(int i=0 ; i<iRowCount ; i++)
    {
        for(int j=0 ; j<iColumnCount ; j++)
        {
            char szPrint[1024] ;
            sprintf(szPrint, "%d, %d", i+1, j+1) ;

            int iIndex = i + j * iRowCount ;

            USES_CONVERSION ;
            pVar[iIndex].vt = VT_BSTR;
            pVar[iIndex].bstrVal = SysAllocString(T2OLE((TCHAR*)szPrint));
        }
    }    

    SafeArrayUnaccessData(pSA);
    v.parray = pSA;
   
    *pVal = v;

     return S_OK ;
}


Variant를 리턴값으로 넘기는 방법에 대해서는 kindlion님의 글을 보고 참조를 하였다.. 다만, 글에 약간 잘못된 부분이 있어 이 부분은 약간 수정을 했다.. Variant에서는 C++에서와 VB에서 서로 반대로 들어가는 것이 아니고, n*n 배열을 만드는데 있어 C++에서는 연속적인 메모리 공간이고 이를 논리적으로 분리하여 n*n 배열을 만드는 것이니 메모리에 접근할때 논리적으로 구성된 배열처럼 접근하도록 계산을 좀 해야 하는데 이 계산이 kindlion님 말처럼 항상 반대는 아니라는 것.. kindlion님의 글에서는 마침 반대로 되었을 뿐, 이 상황은 배열을 몇 바이 몇으로 만드느냐에 따라 달라질 수 있다.. 해서 n*n 크기의 일반적인 상황에서도 데이터를 제대로 넣을 수 있도록 수정을 해야했다..

iRowCount, iColumnCount 값을 바꿔가며 배열의 생성초기화를 바꿔도 VB에서는 제대로된 값을 가져올 수 있도록 수정된 부분은 위의 샘플코드에서 색상표시가 된 부분이다..   



테스트하기
자, 그럼 이렇게 작성된 코드를 ASP에서 어떻게 활용할 수 있는가 함 보자..

<%
    set objTestCom = Server.CreateObject("Test.TestVariant")

    Dim data
    data = objTestCom.ReturnVariant (1)

    for i=1 to UBound(data,1) step 1
        for j=1 to UBound(data,2) step 1
            response.write data(i,j) & "<br><br>"
        Next
    Next
   
    set objTestCom = nothing
%>


2차원 배열을 받아 화면에 출력해주는 ASP용 코드이다.. 직접 호출을 해주면 C++ 코드에서 넣었던 인덱스 값들이 제대로 출력되는 것을 확인할 수 있을 것이다..

테스트하는 방법은 위의 코드를 ASP 파일로 저장한 후에, IIS가 설치된 사이트 혹은 가상디렉토리에 넣은 후 해당 URL을 호출하면 실행된 결과를 확인할 수 있다.. 테스트하는 PC가 컴포넌트를 빌드하는 PC와 다르다면 개발하는 PC에서 DLL 파일을 테스트용 PC에 옮긴후, 도스창을 열어서 DLL 파일이 복사된 경로로 이동한 후에 다음과 같은 명령을 실행시켜줘야 한다.. "regsvr32 DLL파일명"



디버깅하기

Visual Studio는 강력한 디버깅 환경을 제공하는 것으로 유명한데, IIS 서버 콤포넌트 개발시에는 어떻게 이 강력한 디버거를 활용할 수 있을까? 일반적인 상황처럼 사용할 수는 없다.. 조금은 번거로운 방식으로 디버깅을 해야한다..

  1. 빌드를 한다..
  2. 웹브라우저를 실행시켜 테스트용 페이지를 호출한다..
  3. 작업관리자를 실행시킨다.. 프로세스탭으로 가서 dllhost.exe 프로세스를 찾아 PID를 확인한다.. 여러개가 있는 경우에는 프로세스를 실행시킨 사용자가 인터넷 서비스 계정인 것을 찾는다..
  4. VC++에서 디버깅을 한다.. Build > Start Debug > Attacth to Process 를 클릭.. 새로 뜬 다이얼로그 창에서 Show System Processes를 클릭..  3번 과정에서 확인한 PID를 갖는 프로세스를 선택한 후 OK 버튼을 클릭하면 디버깅이 시작된다..
  5. 디버깅 하고자 하는 코드가 담긴 소스코드 파일을 열어 브레이크 포인트를 걸고 디버깅을 한다..

프로젝트의 워크스페이스 파일이 닫혀 버리니 원하는 소스코드 파일 찾기도 귀찮아지고, 디버깅을 종료했다 다시 시작할때마다 이 과정을 반복해야 하니 여간 귀찮은게 아니긴 하다.. 하지만, 그래도 웹서버와 연동이 되어 돌아가는 DLL 파일을 이렇게라도 디버깅 할 수 있다는게 어딘가.. 이런 방법이 없었다면 별 수 없다.. 천상 로그파일에 로그 찍어가며 디버깅 하는 수 밖에..

디버깅 하는 방법 자체는 VC++ 6.0의 디버거를 사용하는 것과 동일하니 이 내용은 생략한다..

여러번 반복해보면 알겠지만, 다른 프로그램이 추가로 실행되지 않는다면 3번 과정을 생략할 수 있다.. 바로 4번으로 넘어가도 우리가 원하는 프로세스가 뭔지 알 수 있다..

위와 같은 과정을 계속해서 반복해야 하는데 이를 좀 더 편하게 하는 팁이 몇개 있다.. 내가 주로 사용하는 팁인데, 우선 개발환경과 테스트 환경을 한데 붙여 한 PC에서 수행하는게 편하다.. 이를 위해서는 XP의 경우 반드시 Professional 이상을 사용해야 한다.. 그 이하에서는 IIS가 설치되지 않기 때문에..

두번째로는 VC++을 하나는 소스코드 수정 및 콤포넌트 빌드용, 다른 하나는 디버깅용 이렇게 2개를 동시에 띄워놓고 사용하는 것이 편하다.. 일반적인 디버깅 환경과 달리 Attach Process를 통한 디버깅은 소스코드 워크스페이스가 닫혀 버리기 때문이다.. 매번 열고 닫고 하기가 귀찮으니 하나는 코드수정 및 빌드용으로, 다른 하나는 디버깅 전용으로 2개를 동시에 실행시켜두고 사용하는 것이 편하다..

마지막으로, 정말 귀찮은 작업중에 하나가 웹서버를 계속 내렸다 올렸다 하는 것이다.. IIS는 서버 컴포넌트를 한번 올리면 메모리에 올려두기 때문에 코드를 수정한 경우 수정된 코드가 반영되지 않는다.. IIS가 파일을 물고 있기 때문에 VC++이 새로운 DLL을 덮어쓰지 못하기 때문이다.. 따라서 IIS를 재기동 시키는 일이 자주 반복되는데 이거 여간 귀찮은게 아니라서 마우스 더블클릭 혹은 커맨드라인에서 쉽게 실행시킬 수 있도록 bat 파일을 하나 만들었다..

net stop W3SVC
net stop IISADMIN
net start IISADMIN
net start W3SVC


위의 내용을 담은 텍스트 파일을 하나 만들고, 파일명을 ReStartIIS.bat 으로 바꾸면 된다.. 명령어는 XP에 IIS 5.0을 사용했을때 기준이며, Windows 2003의 경우에는 명령어가 약간 다를 수도 있다.. 이 bat 파일을 VC++의 Tools 메뉴에 등록시켜두면 단축키로 쉽게 웹서버를 내렸다 올렸다 할 수 있다..



Posted by 미친병아리

데스크탑 어플리케이션 개발시 잡기 힘든 버그 중 하나가 가끔씩 일어나는 크래쉬.. 언제 죽는다, 재현을 해보기 힘든 비정상 종료상황이 발생하는 경우는 죽는 상황을 어떻게 하면 재현할 수 있다는 정보를 수집하기가 상당히 어려운 부분이 있다..

다행히 윈2000 이상의 윈도우 시스템에서는 Dr Watson 이라는 꽤 괜찮은 기본 디버거가 윈도우 시스템에 기본 탑재가 되어 있다.. 메트 피에트릭이 소개한 SEH 처리용 클래스를 활용하는 방법 보다는 훨씬 더 많은 디버깅 관련 자료를 제공해주는 덤프를 작성해 줄 수 있기 때문에 Dr Watson을 활용하는 방법이 좋다고 생각한다.. (SEH 처리용 클래스에 관련된 보다 더 많은 정보 참고)

사용자 삽입 이미지

실행시킨 모습은 위와 같은데, 로그파일과 크래쉬 덤프를 위와 같이 세팅한 대로 남겨준다.. 이렇게 남은 정보는 위에서 이야기한 기사에 소개된 SEH 처리용 클래스가 남기는 정보보다 훨씬 더 유용한 정보들을 많이 남겨준다.. 하지만, 위 그림과 같이 저장되는 경로만 내가 원하는 곳으로 저장해둔다고 동작하는 것은 아니다.. 잊지말고 반드시 해줘야 하는 작업, 하지만 막상 하려면 명령어가 생각나지 않는 작업을 해줘야 한다..

명령행에서 "drwtsn32 -i" 라고 쳐줘야 Dr Watson이 기본 디버거로 설정이 되면서 정보를 남겨주게 된다..

그럼 이 정보를 제대로 활용하는 방법은? 인터넷에서 검색을 통해 더 좋은 자료를 찾을 수도 있겠지만, 지금까지 내가 알기로는 Debugging Applications라는 책이 가장 좋은 활용법을 알려준다..

언제 죽는지 모르는 버그에 시달리고 있다면, 개발자 자신의 PC의 세팅을 이렇게 남겨두면 어떨까?

더 나아가 우리가 배포하는 사용자 PC의 환경도 이렇게 바꿔두면 어떨까? 사실 사용자의 PC 환경을 우리가 만든 어플리케이션이 맘대로 조작한다는건 상당히 기분 나쁜 일일 수도 있다.. 더구나 내가 만든 프로그램의 사용자 역시 프로그래머다.. 그런데 그 사람이 사용하는 기본 디버거가 따로 있는데, 이게 Dr Watson으로 어느날 갑자기 바뀌었다.. 이건 좀 아니긴 하다.. 하지만, 많은 경우에 기본 디버거를 바꿔버리고, 우리가 원하는 위치에 크래쉬 덤프를 남기고 그것을 우리가 수집할 수 있다면 여러 사용자 환경에서 발생하는 오류에 대한 대처에 더욱 능동적일 수 있지 않을까?

최소한 사용자 동의를 얻어 진행할 수 있는 클로즈 베타, 혹은 베타 서비스 기간에는 이를 적용하여 크래쉬에 대한 정보를 적극적으로 수집하는 것도 좋은 방법일 것이다..


추가내용 : jrogue님이 알려주신 주옥같은 정보..

1. Dr Watson은 윈도우 설치시 사용할 수 있는 기본 디버거.. 별도로 세팅하지 않아도 크래쉬 정보를 수집할 수 있다.. 난, 개발툴을 설치했기 때문에 Dr Watson이 기본 디버거가 아니었던 것이다..

2. Dr Watson에 관련된 좋은 정보

3. Dr Watson의 로그파일을 점검하는 방법에 관한 정보

음.. 이쯤되면, 무식한 덕에 제목을 완전 잘 못 달았음을 알 수 있다.. Dr Watson은 원래 윈도우즈의 기본 디버거였던 것이다.. 이름은 아마도 셜록홈즈 소설에 나오는 바로 그 왓슨 박사에서 따온 듯 하다..

Posted by 미친병아리
국내 인터넷 뱅킹에 메모리 해킹이 적용되면 큰일이라는 이야기들이 나오던데.. 정말 그런 일들이 대규모로 일어난다면 속수무책인 금융사건이 발생할지도 모른다.. 하지만, 내가 걱정하고 있는다고 해서 상황이 바뀌는 것도 아니고, 이런 문제점을 해결하기 위한 난세의 영웅이 될 수 있는 것도 아니니 뒤로 미뤄두기로 하고.. 관심을 다른데로 돌려보자..

어떻게 이런 것이 가능할까? 게임에서는 이미 많이 사용되던 기법이라고 하는데, 컴퓨터 상에서 실행되는 프로그램은 컴퓨터의 구조상 필연적으로 메모리에 올라가게 되어 있으며 이 메모리를 읽어내는 기술을 아는 사람에게는 그 어떤 데이터도 보호할 수 없는 상황인 것이 문제다.. 이를 위한 해결책은 논하는 것은 내 능력 밖의 일인 것 같고..

메모리를 읽어내는 좋은 샘플이 하나 있다.. Minesweeper, Behind the scenes, Enhancement for the cool Minesweeper Memory Reader 라는 글에서 소개된 윈도우즈의 지뢰찾기 프로그램의 메모리를 읽어 지뢰가 있는 위치를 알려주는 프로그램이다.. 이 처럼 내 컴퓨터의 메모리에 올라가 있는 데이터는 전문지식만 갖추면 쉽게 꺼내 볼 수 있다..
사용자 삽입 이미지

문제는 어느 메모리에 어떤 값들이 있는지를 알 수 없다는 것인데.. 리버스 엔지니어링을 통하면 시간이 걸리는 것이 문제지, 마음만 먹으면 알아낼 수 있다.. (예 : Olly Debugger 사용해보기에 리버스 엔지니어링에 관한 몇개의 글이 링크되어 있다..)

결국 아직까지는 메모리 속의 데이터까지 암호화를 하거나 보호하지는 않기 때문에 S/W들의 크랙버젼이 나오고, Microsoft가 온라인인증 기능을 넣어 윈도우를 출시한지 하루만에 이를 무력화 시키는 불법 윈도우 설치본이 나오기도 하는 것이다..

리버스 엔지니어링.. 나중에 은퇴해서 좀 한가해지면 심심풀이로 공부해볼라고 마음먹고 있는 분야다.. 메모리 속을 들여다 보고 내 맘대로 제어할 수 있다면 정말 흥미로울 것 같다..
Posted by 미친병아리