Microsoft MVP성태의 닷넷 이야기
닷넷: 2269. C# - Win32 Resource 포맷 해석 [링크 복사], [링크+제목 복사],
조회: 7884
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)

C# - Win32 Resource 포맷 해석

RT_DIALOG와 RT_VERSION도 해석해 봤으니,

Win32 - 리소스에 포함된 대화창 Template의 2진 코드 해석 방법
; https://www.sysnet.pe.kr/2/0/13286

C# - Linux 환경에서 (Reflection 없이) DLL AssemblyFileVersion 구하는 방법
; https://www.sysnet.pe.kr/2/0/13652

하는 김에, 전체적인 Win32 Resource 포맷도 잠깐 살펴보겠습니다. 물론, 이에 대해서는 지난 글에 링크한 문서들에 자세하게 나옵니다.

Win32 Resource (RES) File Format / RESFMT.TXT
; https://bytepointer.com/resources/win32_res_format.htm

Resource File Formats
; https://learn.microsoft.com/en-us/windows/win32/menurc/resource-file-formats

An In-Depth Look into the Win32 Portable Executable File Format, Part 2
; https://learn.microsoft.com/en-us/archive/msdn-magazine/2002/march/inside-windows-an-in-depth-look-into-the-win32-portable-executable-file-format-part-2

그래도 코드로 보면서 이해하는 것이 더 빠르겠죠! ^^




자, 그럼 기왕에 하는 김에 저도 예전에 만들어 둔 WindowsPE 패키지에,

WindowsPE
; https://www.nuget.org/packages/WindowsPE

Install-Package WindowsPE

Resource를 해석하는 코드를 추가해 보겠습니다. ^^ 우선, ResourceTable Directory가 가리키는 위치는,

public IMAGE_DATA_DIRECTORY ResourceTable
{
    get
    {
        if (_is64BitHeader == true)
        {
            return _optionalHeader64.ResourceTable;
        }
        else
        {
            return _optionalHeader32.ResourceTable;
        }
    }
}

public void LoadResourceTable()
{
    if (ResourceTable.VirtualAddress == 0)
    {
        return;
    }

    IntPtr resourceTablePtr = [...VirtualAddress RVA 주소가 가리키는 파일 offset 위치...];
            
    // resourceTablePtr ==> IMAGE_RESOURCE_DIRECTORY
}

IMAGE_RESOURCE_DIRECTORY 정보를 담고 있는데요,

public struct IMAGE_RESOURCE_DIRECTORY
{
    public uint Characteristics; // unused
    public uint TimeDateStamp; // unused
    public ushort MajorVersion; // unused
    public ushort MinorVersion; // unused
    public ushort NumberOfNamedEntries;
    public ushort NumberOfIdEntries;

    // IMAGE_RESOURCE_DIRECTORY_ENTRY [] DirectoryEntries;
}

/*
// winnt.h

typedef struct _IMAGE_RESOURCE_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    WORD    NumberOfNamedEntries;
    WORD    NumberOfIdEntries;
//  IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
*/

현재 사용하고 있는 필드는 NumberOfNamedEntries, NumberOfIdEntries 2개이고 이것을 더한 숫자가 이어서 나오는 IMAGE_RESOURCE_DIRECTORY_ENTRY의 개수입니다.

[StructLayout(LayoutKind.Explicit)]
public struct IMAGE_RESOURCE_DIRECTORY_ENTRY
{
    public static int StructSize = Marshal.SizeOf(typeof(IMAGE_RESOURCE_DIRECTORY_ENTRY));

    [FieldOffset(0)]
    public IMAGE_RESOURCE_DIRECTORY_ENTRY_NAME Name;

    [FieldOffset(4)]
    public IMAGE_RESOURCE_DIRECTORY_ENTRY_OFFSET Offset;

    public bool IsDirectory
    {
        get { return Offset.DataIsDirectory; }
    }

    internal IntPtr GetDataPtr(IntPtr basePtr)
    {
        return IntPtr.Add(basePtr, (int)this.Offset.OffsetToDirectory);
    }
}

/*
// winnt.h

typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
    union {
        struct {
            DWORD NameOffset:31;
            DWORD NameIsString:1;
        } DUMMYSTRUCTNAME;
        DWORD   Name;
        WORD    Id;
    } DUMMYUNIONNAME;
    union {
        DWORD   OffsetToData;
        struct {
            DWORD   OffsetToDirectory:31;
            DWORD   DataIsDirectory:1;
        } DUMMYSTRUCTNAME2;
    } DUMMYUNIONNAME2;
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
*/

IMAGE_RESOURCE_DIRECTORY가 포함하는 IMAGE_RESOURCE_DIRECTORY_ENTRY는 다시 2개의 유형으로 나뉘는데요, 1) 다른 IMAGE_RESOURCE_DIRECTORY를 가리키거나, 2) 아니면 실제 리소스 위치 정보를 담고 있는 IMAGE_RESOURCE_DATA_ENTRY를 가리킵니다.

[StructLayout(LayoutKind.Sequential)]
public struct IMAGE_RESOURCE_DATA_ENTRY
{
    public uint OffsetToData;
    public uint Size;
    public uint CodePage;
    public uint Reserved;
}

그러니까, 대충 이런 식입니다.

IMAGE_RESOURCE_DIRECTORY
    [0] IMAGE_RESOURCE_DIRECTORY_ENTRY 
            Offset -> IMAGE_RESOURCE_DIRECTORY
                      [0] IMAGE_RESOURCE_DIRECTORY_ENTRY  
                          Offset -> IMAGE_RESOURCE_DATA_ENTRY
                      [1] IMAGE_RESOURCE_DIRECTORY_ENTRY
                          Offset -> IMAGE_RESOURCE_DATA_ENTRY
    [1] IMAGE_RESOURCE_DIRECTORY_ENTRY
            Offset -> IMAGE_RESOURCE_DATA_ENTRY

따라서 해석 자체는 재귀적으로 간단하게 처리할 수 있습니다.

private _IMAGE_RESOURCE_DIRECTORY BuildResourceEntries(IntPtr rootDirectoryPtr, IntPtr itemPtr)
{
    try
    {
        // .NET 4.5.1 or later, .NET Standard 1.2 or later
        // IMAGE_RESOURCE_DIRECTORY header = Marshal.PtrToStructure<IMAGE_RESOURCE_DIRECTORY>(resourceTablePtr);

        IMAGE_RESOURCE_DIRECTORY resDir = (IMAGE_RESOURCE_DIRECTORY)Marshal.PtrToStructure(itemPtr, typeof(IMAGE_RESOURCE_DIRECTORY));
        itemPtr += IMAGE_RESOURCE_DIRECTORY.StructSize;

        int numberOfEntries = resDir.NumberOfNamedEntries + resDir.NumberOfIdEntries;
        _IMAGE_RESOURCE_DIRECTORY node = new _IMAGE_RESOURCE_DIRECTORY(numberOfEntries);

        for (int i = 0; i < numberOfEntries; i++)
        {
            IMAGE_RESOURCE_DIRECTORY_ENTRY entry = (IMAGE_RESOURCE_DIRECTORY_ENTRY)Marshal.PtrToStructure(itemPtr, typeof(IMAGE_RESOURCE_DIRECTORY_ENTRY));

            _IMAGE_RESOURCE_DIRECTORY_ENTRY item = new _IMAGE_RESOURCE_DIRECTORY_ENTRY(entry, rootDirectoryPtr);
            node.Entries[i] = item;

            if (entry.IsDirectory)
            {
                IntPtr childPtr = entry.GetDataPtr(rootDirectoryPtr);
                item.Next = BuildResourceEntries(rootDirectoryPtr, childPtr);
            }
            else
            {
                IntPtr childPtr = entry.GetDataPtr(rootDirectoryPtr);
                IMAGE_RESOURCE_DATA_ENTRY data = (IMAGE_RESOURCE_DATA_ENTRY)Marshal.PtrToStructure(childPtr, typeof(IMAGE_RESOURCE_DATA_ENTRY));
                item.Data = new _IMAGE_RESOURCE_DATA_ENTRY(data);
            }

            itemPtr += IMAGE_RESOURCE_DIRECTORY_ENTRY.StructSize;
        }

        return node;

    }
    catch { }

    return null;
}

하지만, 트리라고 해서 말단 Leaf 노드인 IMAGE_RESOURCE_DATA_ENTRY가 리소스의 종류에 관한 모든 정보를 담고 있지는 않습니다. 이에 대한 설명을 다음의 그림 2장이 담고 있는데요,

[출처: Detailed Guide of PE Structure for Reversers]
win32_resource_structure_1.webp

win32_resource_structure_2.webp

즉, 레벨링에 따라 리소스의 위치 정보를 파악해야 합니다. 가령, AssemblyFileVersion을 담고 있는 VS_VERSION_INFO를 찾고 싶다면 다음과 같은 식으로 진행해야 합니다.

// 첫 번째 레벨의 IMAGE_RESOURCE_DIRECTORY가 소유한 Entries 중 Id가 RT_VERSION을 찾아, 그 노드의 자식으로 탐색 시작

// RT_VERSION 리소스의 경우 두 번째 레벨은 언제나 한 개이므로, 다시 그것의 자식으로 탐색 시작

// 세 번째 레벨에 Id 필드가 LCID 값을 갖는 IMAGE_RESOURCE_DIRECTORY_ENTRY를 다중으로 소유하므로 원하는 LCID를 찾아 탐색

//     세 번째 레벨에서 원하는 LCID에 해당하는 노드를 찾았다면 그것이 가리키는 IMAGE_RESOURCE_DATA_ENTRY로 이동
//         IMAGE_RESOURCE_DATA_ENTRY가 가리키는 위치에 VS_VERSION_INFO가 있으므로 그것을 해석

그러니까, Tree의 첫 번째 레벨에 따라 리소스의 종류가 달라지므로 해석 방법도 그것에 따라 별도로 구성해야 합니다. 아쉽게도 제가 이 글에서 모든 리소스 트리의 포맷을 해석할 수는 없고요, ^^ 단지 예를 들기 위해 RT_VERSION 정도만 해석해 보겠습니다.

자, 그럼 재귀 호출로 탄생한 리소스 트리의 첫 번째 레벨에서 RT_VERSION을 찾는 것은 별로 어렵지 않습니다.

public VS_VERSION_INFO FindVersionInfo(int lcid = 0)
{
    foreach (var item in this.ResourceRoot.Entries)
    {
        if (item.Id == ResourceTypeId.RT_VERSION)
        {
            var data = item.Next.Entries[0].Next.FindLcidEntry(lcid);

            IntPtr dataPtr = [data.OffsetToData RVA가 가리키는 파일 위치의 데이터를 data.Size만큼 반환];
            try
            {
                VS_VERSION_INFO versionInfo = VS_VERSION_INFO.Parse(dataPtr, data.Size);
                return versionInfo;
            }
            finally
            {
                buffer.Clear();
            }
        }
    }

    return null;
}

적절한 IMAGE_RESOURCE_DATA_ENTRY를 찾았다면 그것이 가리키는 위치에 VS_VERSION_INFO가 있으므로 그 포맷에 맞게 해석합니다.

// https://bytepointer.com/resources/win32_res_format.htm
VS_VERSION_INFO { 
    WORD wLength;             /* Length of the version resource */ 
    WORD wValueLength;        /* Length of the value field for this block */ 
    WORD wType;               /* type of information:  1==string, 0==binary */ 
    WCHAR szKey[];            /* Unicode string KEY field */ 
    [WORD Padding1;]          /* possible word of padding */ 
    VS_FIXEDFILEINFO Value;   /* Fixed File Info Structure */ 
    BYTE Children[];      /* position of VarFileInfo or StringFileInfo data */ 
}; 

위의 문서에 따라 VS_VERSION_INFO를 펼치면 대략 이런 구조가 나옵니다.

VS_VERSION_INFO
    VS_FIXEDFILEINFO

    [VarFileInfo | StringFileInfo | None]

VarFileInfo
    Var[]

StringFileInfo
    StringTable[]
        String[]

그리고 문서에는 나오지 않지만 말단 Var와 String을 제외하고는 공통적으로 VS_VERSION_INFO가 초기에 갖는 4개의 필드를 가지고 있습니다.

WORD wLength;             /* Length of the version resource */ 
WORD wValueLength;        /* Length of the value field for this block */ 
WORD wType;               /* type of information:  1==string, 0==binary */ 
WCHAR szKey[];            /* Unicode string KEY field */ 

또한, 위에서 szKey의 경우 "Unicode string"이라고 나오는데요, 이것은 BSTR처럼 문자열의 수를 기록하고 있지만 아쉽게도 그 영역이 2바이트라서 BSTR을 그대로 쓸 수는 없습니다.

* BSTR 형식 - SysAllocString으로 Unicode 문자열 할당

0                     2                      4                          (문자열 끝)
|-------------[4byte: Length]----------------|---[Unicode String]-------|[null 2bytes]
* RESOURCE 영역의 Unicode 형식

0                      2
|---[2byte: Length]----|-----------------[Unicode String]---------------|[null 2bytes]

Resource Directory String
; https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#resource-directory-string

또한, 저렇게 4개의 필드까지 합쳐서 4바이트 정렬로 다음 데이터가 나와야 합니다. 그래서 szKey가 홀수 바이트가 아니라면 항상 2바이트 padding이 필요합니다. 위와 같은 점을 고려해 편의상 VERSION_INFO_HEADER라는 구조체를 별도로 만들어 관리할 수 있습니다.

public class VERSION_INFO_HEADER
{
    public ushort wLength;
    public ushort wValueLength;
    public ushort wType;
    public string szKey;

    public static unsafe VERSION_INFO_HEADER Parse(IntPtr ptr)
    {
        VERSION_INFO_HEADER item = new VERSION_INFO_HEADER();

        item.wLength = ptr.ReadUInt16(0);
        item.wValueLength = ptr.ReadUInt16(2);
        item.wType = ptr.ReadUInt16(4);
        item.szKey = ptr.ReadString(6);

        int length = sizeof(ushort) // wLength
            + sizeof(ushort) // wValueLength
            + sizeof(ushort) // wType
            + item.szKey.Length * 2 + 2; // szKey with null

        IntPtr nextPtr = IntPtr.Add(ptr, length);
        if (nextPtr.ToInt64() % 4 != 0)
        {
            length += 2;
        }

        item._size = length;
        return item;
    }

    int _size = 0;
    public int Size => _size;
}

자, 그래서 이것들을 종합하면 VS_VERSION_INFO 해석을 다음과 같이 할 수 있습니다. ^^

public class VS_VERSION_INFO
{
    public VERSION_INFO_HEADER Header;
    public VS_FIXEDFILEINFO FileInfo;
    public VarFileInfo FileInfoVar;
    public StringFileInfo FileInfoString;

    public int TotalLength
    {
        get { return Header.wLength; }
    }

    public static unsafe VS_VERSION_INFO Parse(IntPtr ptr, uint length)
    {
        VS_VERSION_INFO item = new VS_VERSION_INFO();

        item.Header = VERSION_INFO_HEADER.Parse(ptr);
        Trace.Assert(item.Header.szKey == "VS_VERSION_INFO");

        int pos = item.Header.Size;

        IntPtr fileInfoPtr = IntPtr.Add(ptr, pos);
        item.FileInfo = (VS_FIXEDFILEINFO)Marshal.PtrToStructure(fileInfoPtr, typeof(VS_FIXEDFILEINFO));

        Trace.Assert(item.FileInfo.dwSignature == 0xFEEF04BD);

        pos += VS_FIXEDFILEINFO.StructSize;
        IntPtr childrenPtr = IntPtr.Add(ptr, pos);

        while (pos < item.TotalLength)
        {
            VERSION_INFO_HEADER header = VERSION_INFO_HEADER.Parse(childrenPtr);
            pos += header.Size;
            IntPtr infoPtr = IntPtr.Add(ptr, pos);

            int bodyLength = header.wLength - header.Size;

            switch (header.szKey)
            {
                case "VarFileInfo":
                    item.FileInfoVar = VarFileInfo.Parse(header.szKey, infoPtr, bodyLength);
                    pos += item.FileInfoVar.Size;
                    break;

                case "StringFileInfo":
                    item.FileInfoString = StringFileInfo.Parse(header.szKey, infoPtr);
                    pos += item.FileInfoString.Size;
                    break;
            }

            if (header.szKey == null)
            {
                break;
            }

            childrenPtr = IntPtr.Add(ptr, pos);
        }

        return item;
    }
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct VS_FIXEDFILEINFO
{
    public static int StructSize = Marshal.SizeOf(typeof(VS_FIXEDFILEINFO));

    public uint dwSignature;        /* signature - always 0xfeef04bd */
    public uint dwStrucVersion;     /* structure version - currently 0 */
    public uint dwFileVersionMS;    /* Most Significant file version dword */
    public uint dwFileVersionLS;    /* Least Significant file version dword */
    public uint dwProductVersionMS; /* Most Significant product version */
    public uint dwProductVersionLS; /* Least Significant product version */
    public uint dwFileFlagMask;     /* file flag mask */
    public uint dwFileFlags;        /*  debug/retail/prerelease/... */
    public uint dwFileOS;           /* OS type.  Will always be Windows32 value */
    public uint dwFileType;         /* Type of file (dll/exe/drv/... )*/
    public uint dwFileSubtype;      /* file subtype */
    public uint dwFileDateMS;       /* Most Significant part of date */
    public uint dwFileDateLS;       /* Least Significant part of date */
}

뭐 그래도 이 정도면 간단한 편이군요. ^^ 이후의 "VarFileInfo"와 "StringFielInfo" 설명은 생략할 텐데요, 사실 그 정보를 조회할 일은 거의 없을 것이므로 대부분의 경우 해석하지 않고 넘어가도 무방할 것입니다. (궁금하신 분은 링크의 소스코드를 참고하세요.)





코드 구현을 하면서, 리소스의 포맷을 다시 정리해 보면 이렇게 됩니다.

INFO_HEADER {
    WORD wLength;             /* Length of the version resource */ 
    WORD wValueLength;        /* Length of the value field for this block */ 
    WORD wType;               /* type of information:  1==string, 0==binary */ 
    WCHAR szKey[];            /* Unicode string KEY field */ 
    [WORD Padding1;]          /* possible word of padding */ 
}

VS_VERSION_INFO { 
    INFO_HEADER Header;
    VS_FIXEDFILEINFO Value;   /* Fixed File Info Structure */ 
    StringFileInfo_OR_VarFileInfo Children[];  /* VarFileInfo or StringFileInfo data */ 
}; 

StringFileInfo_OR_VarFileInfo {
    INFO_HEADER Header;
    (StringFileInfo | VarFileInfo) Children;
}

StringFileInfo { 
    INFO_HEADER Header;
    StringTable Children[]; 
}; 
 
StringTable { 
    WORD wLength;             /* Length of the version resource */ 
    WORD wValueLength;        /* Length of the value field for this block */ 
    WORD wType;               /* type of information:  1==string, 0==binary */ 
    String Children[];    /* array of children String structures */ 
} 
 
String { 
    WCHAR   szKey[];          /* arbitrary Unicode encoded KEY string */ 
                         /* note that there is a list of pre-defined keys */ 
    [WORD   padding;]         /* possible padding */ 
    WCHAR Value[];            /* Unicode-encoded value for KEY */ 
    [WORD   padding;]         /* possible padding */ 
} String; 

VarFileInfo { 
    WORD wLength;             /* Length of the version resource */ 
    WORD wValueLength;        /* Length of the value field for this block */ 
    WORD wType;               /* type of information:  1==string, 0==binary */ 
    Var        Children[];    /* children array */ 
}; 
 
Var { 
    WCHAR szKey[];       /* Unicode "Translation" (or other user key) */ 
    [WORD padding;]      /* possible padding */ 
    WORD  Value[];       /* one or more values, normally language id's */ 
}; 




한 가지 주의할 것은, Padding을 할 때 해당 구조체 내에서의 위치를 가지고 추가 여부를 결정해서는 안 된다는 점입니다. 이것이 문제가 되는 예로 위에서 StringTable과 String을 들 수 있습니다.

StringTable { 
    WORD wLength;             /* Length of the version resource */ 
    WORD wValueLength;        /* Length of the value field for this block */ 
    WORD wType;               /* type of information:  1==string, 0==binary */ 
    String Children[];    /* array of children String structures */ 
} 
 
String { 
    WCHAR   szKey[];          /* arbitrary Unicode encoded KEY string */ 
                         /* note that there is a list of pre-defined keys */ 
    [WORD   padding;]         /* possible padding */ 
    WCHAR Value[];            /* Unicode-encoded value for KEY */ 
    [WORD   padding;]         /* possible padding */ 
} String; 

StringTable의 경우 Padding 없이 곧바로 String이 나오는데요, 문제는 그 자체로 WORD 3개라서 원래는 Padding이 필요한 상태입니다. 그런데 그 상태에서 하위 String 측에서 szKey 단독으로 Padding 여부를 정하면 메모리 주솟값 상태의 4바이트 정렬이 제대로 반영되지 않을 수 있습니다.

그런 탓에, 제가 구현한 코드의 경우 단순히 구조체 스스로의 내부에서 position에 따른 패딩 여부를 결정하지 않고, 굳이 메모리 포인터를 기준으로 패딩 여부를 결정하도록 한 것입니다.

item.szKey = ptr.ReadString(0);
ptr = IntPtr.Add(ptr, item.szKey.Length * 2 + 2); // with null

if (ptr.ToInt64() % 4 != 0)
{
    ptr = IntPtr.Add(ptr, 2); // 32bit align padding
}

제가 소개했던 Workshell.PE는 전체 데이터를 읽어들여 Stream 형식으로 쭈욱 읽어들이는 방식이라 패딩이 자연스러웠지만 제 경우에는 구조체 스스로가 전달받은 메모리 포인터를 기준으로 역직렬화하는 것이라 저런 식으로 처리하게 됐습니다.




마지막으로, 제가 만든 WindowsPE는 필요할 때마다 매번 파일로부터 일정 영역을 읽어들이는 방식이라 메모리 관리 방식이 효율적이지 않습니다. 따라서 가볍게 쓰는 프로젝트라면 사용해도 좋지만 기왕이면 Workshell.PE를 사용하는 것을 더 권장합니다. 제 코드는 그냥 직관적인 해석 용도로 보시는 것이 좋습니다.




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 7/17/2024]

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

비밀번호

댓글 작성자
 




... 46  47  48  49  50  51  52  53  54  [55]  56  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
12562정성태3/15/202117931개발 환경 구성: 550. C# - JIRA REST API 사용 정리 (2) JIRA OAuth 토큰으로 API 사용하는 방법파일 다운로드1
12561정성태3/12/202116677VS.NET IDE: 159. Visual Studio에서 개행(\n, \r) 등의 제어 문자를 치환하는 방법 - 정규 표현식 사용
12560정성태3/11/202117735개발 환경 구성: 549. ssh-keygen으로 생성한 PKCS#1 개인키/공개키 파일을 각각 PKCS8/PEM 형식으로 변환하는 방법
12559정성태3/11/202117976.NET Framework: 1028. 닷넷 5 환경의 Web API에 OpenAPI 적용을 위한 NSwag 또는 Swashbuckle 패키지 사용 [2]파일 다운로드1
12558정성태3/10/202117059Windows: 192. Power Automate Desktop (Preview) 소개 - Bitvise SSH Client 제어 [1]
12557정성태3/10/202115277Windows: 191. 탐색기의 보안 탭에 있는 "Object name" 경로에 LEFT-TO-RIGHT EMBEDDING 제어 문자가 포함되는 문제
12556정성태3/9/202113552오류 유형: 703. PowerShell ISE의 Debug / Toggle Breakpoint 메뉴가 비활성 상태인 경우
12555정성태3/8/202116849Windows: 190. C# - 레지스트리에 등록된 DigitalProductId로부터 라이선스 키(Product Key)를 알아내는 방법파일 다운로드2
12554정성태3/8/202116409.NET Framework: 1027. 닷넷 응용 프로그램을 위한 PDB 옵션 - full, pdbonly, portable, embedded
12553정성태3/5/202116407개발 환경 구성: 548. 기존 .NET Framework 프로젝트를 .NET Core/5+ 용으로 변환해 주는 upgrade-assistant, try-convert 도구 소개 [4]
12552정성태3/5/202115861개발 환경 구성: 547. github workflow/actions에서 Visual Studio Marketplace 패키지 등록하는 방법
12551정성태3/5/202114220오류 유형: 702. 비주얼 스튜디오 - The 'CascadePackage' package did not load correctly. (2)
12550정성태3/5/202113966오류 유형: 701. Live Share 1.0.3713.0 버전을 1.0.3884.0으로 업데이트 이후 ContactServiceModelPackage 오류 발생하는 문제
12549정성태3/4/202115255오류 유형: 700. VsixPublisher를 이용한 등록 시 다양한 오류 유형 해결책
12548정성태3/4/202116380개발 환경 구성: 546. github workflow/actions에서 nuget 패키지 등록하는 방법
12547정성태3/3/202117037오류 유형: 699. 비주얼 스튜디오 - The 'CascadePackage' package did not load correctly.
12546정성태3/3/202116869개발 환경 구성: 545. github workflow/actions에서 빌드시 snk 파일 다루는 방법 - Encrypted secrets
12545정성태3/2/202119734.NET Framework: 1026. 닷넷 5에 추가된 POH (Pinned Object Heap) [10]
12544정성태2/26/202119943.NET Framework: 1025. C# - Control의 Invalidate, Update, Refresh 차이점 [2]
12543정성태2/26/202117926VS.NET IDE: 158. C# - 디자인 타임(design-time)과 런타임(runtime)의 코드 실행 구분
12542정성태2/20/202119586개발 환경 구성: 544. github repo의 Release 활성화 및 Actions를 이용한 자동화 방법 [1]
12541정성태2/18/202117156개발 환경 구성: 543. 애저듣보잡 - Github Workflow/Actions 소개
12540정성태2/17/202118274.NET Framework: 1024. C# - Win32 API에 대한 P/Invoke를 대신하는 Microsoft.Windows.CsWin32 패키지
12539정성태2/16/202118190Windows: 189. WM_TIMER의 동작 방식 개요파일 다운로드1
12538정성태2/15/202118659.NET Framework: 1023. C# - GC 힙이 아닌 Native 힙에 인스턴스 생성 - 0SuperComicLib.LowLevel 라이브러리 소개 [2]
12537정성태2/11/202119353.NET Framework: 1022. UI 요소의 접근은 반드시 그 UI를 만든 스레드에서! - 두 번째 이야기 [2]
... 46  47  48  49  50  51  52  53  54  [55]  56  57  58  59  60  ...