Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 2개 있습니다.)

ClrMD를 이용해 메모리 덤프 파일로부터 특정 인스턴스를 참조하고 있는 소유자 확인

지난 글에서,

windbg - 닷넷 응용 프로그램의 메모리 누수 분석
; https://www.sysnet.pe.kr/2/0/11808

문자열이 쌓이고 있는 경우를 살펴봤는데, 분석 시 약간 아쉬운 점이 있습니다. 그 덤프의 힙 상태를 다시 보면,

0:000> !dumpheap -stat
Statistics:
              MT    Count    TotalSize Class Name
00007ff90f456748        1           24 Elasticsearch.Net.ElasticsearchDefaultSerializer
00007ff90f192f78        1           24 System.Collections.Generic.ObjectEqualityComparer`1[[System.Runtime.Caching.MemoryCache, System.Runtime.Caching]]
00007ff90f143038        1           24 System.Collections.Generic.Dictionary`2+ValueCollection[[System.String, mscorlib],[OracleInternal.SelfTuning.OracleTuner+OracleTunerInput, Oracle.ManagedDataAccess]]
00007ff90f0120b8        1           24 System.Net.SSPISecureChannelType
00007ff90f011e78        1           24 System.Net.SSPIAuthType
...[생략]...                
00007ff90d3869b8    12753      5127112 System.Int32[]
00007ff90d4ba6e0      159     14555944 System.Int64[]
00007ff90d1aba28   148673     16952928 System.Object[]
00007ff90d1fe828    75889     20114243 System.Byte[]
00007ff90ebd41e0   376168     36112128 TestApi.Data.AddrInfo
00007ff90d1f46f0  2726523    208786640 System.String
000000b69ba94870    33952    274313352      Free
Total 4063087 objects
Fragmented blocks larger than 0.5 MB:
            Addr     Size      Followed by
000000b6a40f38b0   11.4MB 000000b6a4c4d630 System.Byte[]

System.String을 소유하고 있는 객체로 (사용자가 만든 타입인) TestApi.Dat.AddrInfo 인스턴스가 많은 걸로 쉽게 짐작을 했는데요. 이런 식으로 찾아낼 수 없는 경우도 더러 있습니다. 예를 들어, 다음의 소스 코드를 덤프 떠서 살펴보면 어떨까요?

using System;
using System.Collections.Generic;

namespace ConsoleApp1
{
    class Program
    {
        static List<string> _list = new List<string>();

        static void Main(string[] args)
        {
            CreateObjects();
            GC.Collect(2);
            GC.Collect(2);

            Console.WriteLine("Dump!");
            Console.ReadKey();
        }

        private static void CreateObjects()
        {
            for (int i = 0; i < 100000; i ++)
            {
                _list.Add(Guid.NewGuid().ToString());
            }
        }
    }
}

string에 대한 소유를 1개의 List 객체가 담당하고 있으므로 heap 상태를 봐도 그 소유자를 쉽게 확인할 수 없습니다.

0:000> !dumpheap -stat
Statistics:
      MT    Count    TotalSize Class Name
719025c4        1           12 System.Collections.Generic.GenericEqualityComparer`1[[System.String, mscorlib]]
71900414        1           12 System.Collections.Generic.ObjectEqualityComparer`1[[System.Type, mscorlib]]
718fde9c        1           16 System.Security.Policy.AssemblyEvidenceFactory
7190be18        1           20 System.IO.Stream+NullStream
71901888        1           20 Microsoft.Win32.SafeHandles.SafeFileHandle
718fdde8        1           20 Microsoft.Win32.SafeHandles.SafePEFileHandle
7190c3dc        1           24 System.IO.TextWriter+SyncTextWriter
71902298        1           24 System.Version
71900994        2           24 System.Int32
71461e00        1           24 System.Collections.Generic.List`1[[System.String, mscorlib]]
7190c2e4        1           28 System.IO.__ConsoleStream
7190c29c        1           28 Microsoft.Win32.Win32Native+InputRecord
...[생략]...
71900958        6          452 System.Int32[]
718fff54       22          616 System.RuntimeType
718ff54c        6          626 System.Char[]
7190316c        1          783 System.Byte[]
71901e50        3          924 System.Globalization.CultureData
718fef34        4        17604 System.Object[]
718ff698       19       524868 System.String[]
00f2b548       24       525636      Free
718feb40   100154      8604850 System.String
Total 100299 objects

물론, "!dumpheap -mt 718feb40" 명령어를 이용해 문자열 인스턴스를 나열할 수 있지만 windbg에서 이 명령어를 수행하면 10만 개가 넘는 인스턴스를 출력하느라 한 세월이 걸립니다. 게다가 그중에서 몇 개를 골라 "!gcroot"로 찾아볼 수 있겠지만 무작위로 선정하는 것이므로 운이 좋아야 List를 가리키는 객체를 알아낼 수 있습니다. 또한, 그 객체가 문자열을 보관하는 (Leak이 없는) 또 다른 List 객체일 수도 있다는 점을 감안하면 찾기가 그리 쉽지 않습니다.




이런 상황을 해결하려면, windbg에서 스크립트를 이용해 10만 개의 System.String에 대한 참조를 유지하고 있는 객체 리스트를 작성하는 것입니다. 대표적으로 pykd를 이용하면 될 텐데요,

windbg에서 python 스크립트 실행하는 방법 - pykd
; https://www.sysnet.pe.kr/2/0/11227

아쉽게도 실제로 수행해보면 출력 결과에 대해 일일이 sos.dll의 명령어로 다시 수행하는 방식이어서 전체 과정이 꽤나 긴 시간을 요구하게 됩니다. 그래서 좀 더 빠른 방법을 찾게 되는데, 바로 그 해답이 ClrMD입니다.

Microsoft.Diagnostics.Runtime

Install-Package Microsoft.Diagnostics.Runtime -Version 1.0.2 

아울러 이미 제공되고 있는 풍부한 예제 코드에 따라,

Tutorials - 1. Getting Started - A brief introduction to the API and how to create a CLRRuntime instance.
; https://github.com/Microsoft/clrmd/blob/master/Documentation/GettingStarted.md

Tutorials - 3. Walking objects on the GC heap, working with types in CLR MD.
; https://github.com/Microsoft/clrmd/blob/master/Documentation/WalkingTheHeap.md

이거저거 짜깁기하면 우리가 원하는 목적의 코드를 다음과 같이 간단하게 만들 수 있습니다.

using Microsoft.Diagnostics.Runtime;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace RefOwner
{
    class Program
    {
        static string _platformPostfix = (IntPtr.Size == 4) ? "32" : "64";

        static void Main(string[] args)
        {
            if (args.Length < 2)
            {
                Console.WriteLine("RefOwner" + _platformPostfix + " [/d] [dumppath] [typename | address]");
                Console.WriteLine("[Sample]");
                Console.WriteLine("\tRefOwner" + _platformPostfix + " c:\\temp\\test.dmp System.String");
                Console.WriteLine("\tRefOwner" + _platformPostfix + "  /d c:\\temp\\test.dmp System.String");
                return;
            }

            string filePath = (args.Length == 3) ? args[1] : args[0];
            if (File.Exists(filePath) == false)
            {
                Console.WriteLine("FILE-NOT-FOUND: " + filePath);
                return;
            }

            string basePath = Path.GetDirectoryName(filePath);
            string dmpPath = filePath;
            string dacPath = Path.Combine(basePath, "mscordacwks.dll");

            string typeNameOrAddress = (args.Length == 3) ? args[2] : args[1];

            bool detailed = args.Length == 3 && args[0] == "/d";

            using (DataTarget dataTarget = DataTarget.LoadCrashDump(dmpPath))
            {
                foreach (ClrInfo version in dataTarget.ClrVersions)
                {
                    Console.WriteLine("Found CLR Version: " + version.Version);

                    // This is the data needed to request the dac from the symbol server:
                    ModuleInfo dacInfo = version.DacInfo;
                    Console.WriteLine("Filesize:  {0:X}", dacInfo.FileSize);
                    Console.WriteLine("Timestamp: {0:X}", dacInfo.TimeStamp);
                    Console.WriteLine("Dac File:  {0}", dacInfo.FileName);

                    string dacLocation = version.LocalMatchingDac;
                    if (!string.IsNullOrEmpty(dacLocation))
                        Console.WriteLine("Local dac location: " + dacLocation);

                    Console.WriteLine();

                    ClrRuntime runtime;

                    if (File.Exists(dacPath) == false)
                    {
                        runtime = version.CreateRuntime();
                    }
                    else
                    {
                        runtime = version.CreateRuntime(dacPath);
                    }

                    // DumpHeapStat(runtime);
                    // Console.WriteLine();
                    DumpHeapRefHierachy(runtime, typeNameOrAddress, detailed, 0);
                }
            }
        }

        private static void DumpHeapRefHierachy(ClrRuntime runtime, string typeNameOrAddress, bool detailed, int depth)
        {
            HashSet<ulong> instances = GetObjectListByName(runtime, typeNameOrAddress);
            Console.WriteLine(typeNameOrAddress + ", # of instances: " + instances.Count);

            Dictionary<string, HeapObjectCounter> owners = GetObjectOwners(runtime, instances);
            List<ObjectHistogramItem> histogram = new List<ObjectHistogramItem>();

            foreach (var owner in owners)
            {
                ObjectHistogramItem item = new ObjectHistogramItem { Key = owner.Key, Counter = owner.Value };
                histogram.Add(item);
            }

            histogram.Sort();

            int count = 0;

            foreach (var item in histogram)
            {
                Console.WriteLine($"{item.Counter.Total} {item.Key}({item.Counter.OwnerCount})");

                if (detailed == true)
                {
                    foreach (var instance in item.Counter)
                    {
                        Console.WriteLine($"\t[{instance.Key:x}, {instance.Value}]");
                    }
                }

                count += item.Counter.Total;
            }

            Console.WriteLine("Total: " + count);
        }

        private static ulong GetFieldAddressValue(ClrRuntime runtime, ClrInstanceField field, ulong addr)
        {
            ulong fieldAddress = field.GetAddress(addr);
            return ReadMemory(runtime, fieldAddress);
        }

        private static ulong ReadMemory(ClrRuntime runtime, ulong address)
        {
            bool x86 = IntPtr.Size == 4;

            unsafe
            {
                byte[] refAddr = null;

                if (x86 == true)
                {
                    refAddr = new byte[4];
                }
                else
                {
                    refAddr = new byte[8];
                }

                runtime.Heap.ReadMemory(address, refAddr, 0, IntPtr.Size);

                if (x86 == true)
                {
                    return BitConverter.ToUInt32(refAddr, 0);
                }
                else
                {
                    return BitConverter.ToUInt64(refAddr, 0);
                }                    
            }
        }

        private static Dictionary<string, HeapObjectCounter> GetObjectOwners(ClrRuntime runtime, HashSet<ulong> instances)
        {
            Dictionary<string, HeapObjectCounter> dict = new Dictionary<string, HeapObjectCounter>();

            if (!runtime.Heap.CanWalkHeap)
            {
                Console.WriteLine("Cannot walk the heap!");
            }
            else
            {
                foreach (ClrSegment seg in runtime.Heap.Segments)
                {
                    for (ulong obj = seg.FirstObject; obj != 0; obj = seg.NextObject(obj))
                    {
                        ClrType type = runtime.Heap.GetObjectType(obj);

                        if (type == null || type.IsFree == true)
                        {
                            continue;
                        }

                        string typeName = type.ToString();

                        int gen = runtime.Heap.GetGeneration(obj);
                        if (gen != 2)
                        {
                            continue;
                        }

                        if (type.IsArray == true)
                        {
                            int arrayLength = type.GetArrayLength(obj);
                            for (int i = 0; i < arrayLength; i++)
                            {
                                ulong elemAddress = type.GetArrayElementAddress(obj, i);

                                ClrType elemType = type.ComponentType;
                                if (elemType.IsValueClass == false)
                                {
                                    ulong elemRef = ReadMemory(runtime, elemAddress);
                                    if (instances.Contains(elemRef) == true)
                                    {
                                        if (dict.ContainsKey(typeName) == true)
                                        {
                                            dict[typeName].AddCount(obj);
                                        }
                                        else
                                        {
                                            dict.Add(typeName, new HeapObjectCounter(obj));
                                        }
                                    }
                                }
                                else
                                {
                                    AddItem(type, typeName, obj);
                                }
                            }
                        }
                        else
                        {
                            AddItem(type, typeName, obj);
                        }
                    }
                }
            }

            return dict;

            void AddItem(ClrType type, string typeName, ulong objAddress)
            {
                foreach (ClrInstanceField field in type.Fields)
                {
                    ulong fieldAddress = GetFieldAddressValue(runtime, field, objAddress);

                    if (instances.Contains(fieldAddress) == true)
                    {
                        if (dict.ContainsKey(typeName) == true)
                        {
                            dict[typeName].AddCount(objAddress);
                        }
                        else
                        {
                            dict.Add(typeName, new HeapObjectCounter(objAddress));
                        }
                    }
                }
            }
        }

        private static HashSet<ulong> GetObjectListByName(ClrRuntime runtime, string typeNameOrAddress)
        {
            HashSet<ulong> list = new HashSet<ulong>();

            ulong result;
            if (UInt64.TryParse(typeNameOrAddress, System.Globalization.NumberStyles.AllowHexSpecifier, null, out result) == true)
            {
                list.Add(result);
                return list;
            }

            if (!runtime.Heap.CanWalkHeap)
            {
                Console.WriteLine("Cannot walk the heap!");
            }
            else
            {
                foreach (ClrSegment seg in runtime.Heap.Segments)
                {
                    for (ulong obj = seg.FirstObject; obj != 0; obj = seg.NextObject(obj))
                    {
                        ClrType type = runtime.Heap.GetObjectType(obj);

                        if (type == null)
                        {
                            continue;
                        }

                        int gen = runtime.Heap.GetGeneration(obj);
                        if (gen != 2)
                        {
                            continue;
                        }

                        if (type.ToString() != typeNameOrAddress)
                        {
                            continue;
                        }

                        list.Add(obj);
                    }
                }
            }

            return list;
        }

        private static void DumpHeapObject(ClrRuntime runtime)
        {
            if (!runtime.Heap.CanWalkHeap)
            {
                Console.WriteLine("Cannot walk the heap!");
            }
            else
            {
                foreach (ClrSegment seg in runtime.Heap.Segments)
                {
                    for (ulong obj = seg.FirstObject; obj != 0; obj = seg.NextObject(obj))
                    {
                        ClrType type = runtime.Heap.GetObjectType(obj);

                        if (type == null)
                        {
                            continue;
                        }

                        ulong size = type.GetSize(obj);
                        Console.WriteLine("{0,12:X} {1,8:n0} {2,1:n0} {3}", obj, size, seg.GetGeneration(obj), type.Name);
                    }
                }
            }
        }

        private static void DumpHeapStat(ClrRuntime runtime)
        {
            Console.WriteLine("{0,12} {1,12} {2,12} {3,12} {4,4} {5}", "Start", "End", "CommittedEnd", "ReservedEnd", "Heap", "Type");

            foreach (ClrSegment segment in runtime.Heap.Segments)
            {
                string type;
                if (segment.IsEphemeral)
                    type = "Ephemeral";
                else if (segment.IsLarge)
                    type = "Large";
                else
                    type = "Gen2";

                Console.WriteLine("{0,12:X} {1,12:X} {2,12:X} {3,12:X} {4,4} {5}", segment.Start, segment.End, segment.CommittedEnd, segment.ReservedEnd, segment.ProcessorAffinity, type);
            }

            ClrHeap heap = runtime.Heap;
            foreach (var item in (from seg in heap.Segments
                                  group seg by seg.ProcessorAffinity into g
                                  orderby g.Key
                                  select new
                                  {
                                      Heap = g.Key,
                                      Size = g.Sum(p => (uint)p.Length)
                                  }))
            {
                Console.WriteLine("Heap {0,2}: {1:n0} bytes", item.Heap, item.Size);
            }
        }
    }

    public class ObjectHistogramItem : IComparable<ObjectHistogramItem>
    {
        public string Key;
        public HeapObjectCounter Counter;

        public int CompareTo(ObjectHistogramItem other)
        {
            return Counter.Total.CompareTo(other.Counter.Total);
        }
    }

    public class HeapObjectCounter : IEnumerable<KeyValuePair<ulong, int>>
    {
        Dictionary<ulong, int> _owners = new Dictionary<ulong, int>();
        int _total;

        public HeapObjectCounter(ulong address)
        {
            AddCount(address);
        }

        public int OwnerCount
        {
            get
            {
                return _owners.Keys.Count;
            }
        }

        public int Total
        {
            get
            {
                return _total; // _owners.Values.Sum();
            }
        }

        public IEnumerator<KeyValuePair<ulong, int>> GetEnumerator()
        {
            return this._owners.GetEnumerator();
        }

        internal void AddCount(ulong objAddress)
        {
            if (_owners.ContainsKey(objAddress) == true)
            {
                _owners[objAddress]++;
            }
            else
            {
                _owners[objAddress] = 1;
            }

            _total++;
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this._owners.GetEnumerator();
        }
    }
}

그래서 위의 프로그램을 이 글에서 만든 예제 프로젝트의 덤프에 대해 실행하면,

c:\temp\RefOwner\bin> RefOwner32.exe c:\tmp\ConsoleApp1.dmp System.String
Found CLR Version: v4.7.3260.00
Filesize:  6EF000
Timestamp: 5BB7BCB7
Dac File:  mscordacwks_X86_X86_4.7.3260.00.dll
Local dac location: C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscordacwks.dll

System.String, # of instances: 100034
1 System.AppDomainSetup(1)
31 System.Object[](3)
100003 System.String[](2)
Total: 100035

총 100,034개의 문자열 인스턴스 중에 무려 100,003개가 단 2개의 System.String[] 객체에 참조되고 있는 것을 볼 수 있습니다. 여기서 "/d" 옵션을 주면 2개의 System.String[]에 대한 인스턴스 주소와 그것들이 각각 참조하고 있는 System.String 객체의 수를 알 수 있습니다.

c:\temp\RefOwner\bin> RefOwner32.exe /d c:\tmp\ConsoleApp1.dmp System.String
...[생략]...
System.String, # of instances: 100034
System.String, # of instances: 100034
1 System.AppDomainSetup(1)
        [2b714e0, 1]
31 System.Object[](3)
        [3b71020, 1]
        [3b72300, 2]
        [3b72520, 28]
100003 System.String[](2)
        [2b7156c, 3]
        [3bd5570, 100000]
Total: 100035

보는 바와 같이, 두 번째 인스턴스인 3bd5570 객체가 100,000개의 String을 담고 있는데, 테스트 코드에서 100,000번의 루프로 Guid 문자열을 담은 것과 일치하는군요. ^^

windbg에서 위의 주소에 따라 dump를 해보면,

0:000> !do 3bd5570
Name:        System.String[]
MethodTable: 718ff698
EEClass:     714d4b80
Size:        524300(0x8000c) bytes
Array:       Rank 1, Number of elements 131072, Type CLASS (Print Array)
Fields:
None

배열의 크기 자체는 131,072라고 합니다. 즉, 그 배열 중에 String이 할당된 것은 100,000개이고 이후의 요소들은 모두 null입니다. 그럼, 여기서 다시 3bd5570 객체의 소유주를 보면,

c:\temp\RefOwner\bin\x86> RefOwner32.exe /d c:\tmp\ConsoleApp1.dmp 3bd5570
Found CLR Version: v4.7.3260.00
Filesize:  6EF000
Timestamp: 5BB7BCB7
Dac File:  mscordacwks_X86_X86_4.7.3260.00.dll
Local dac location: C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscordacwks.dll

3bd5570, # of instances: 1
1 System.Collections.Generic.List<System.String>(1)
        [2b720a4, 1]
Total: 1

답이 나왔습니다. (만들어 놓고 보니 그런대로 쓸만한 도구가 된 듯합니다. ^^)




참고로, ClrMD 라이브러리 사용 시 다음과 같은 오류가 발생할 수 있습니다.

System.InvalidOperationException
  HResult=0x80131509
  Message=Mismatched architecture between this process and the dac.
  Source=Microsoft.Diagnostics.Runtime
  StackTrace:
   at Microsoft.Diagnostics.Runtime.DataTarget.ConstructRuntime(ClrInfo clrInfo, String dac) in /_/src/Microsoft.Diagnostics.Runtime/src/DataTargets/DataTarget.cs:line 326
   at Microsoft.Diagnostics.Runtime.DataTarget.CreateRuntime(ClrInfo clrInfo, String dacFilename, Boolean ignoreMismatch) in /_/src/Microsoft.Diagnostics.Runtime/src/DataTargets/DataTarget.cs:line 320
   at Microsoft.Diagnostics.Runtime.ClrInfo.CreateRuntime(String dacFilename, Boolean ignoreMismatch) in /_/src/Microsoft.Diagnostics.Runtime/src/DataTargets/ClrInfo.cs:line 115
   at RefOwner.Program.Main(String[] args) in c:\temp\RefOwner\RefOwner\RefOwner\Program.cs:line 39

이유는? 현재 프로세스는 x86이면서, 분석 대상이 되는 덤프는 x64 프로세스를 뜬 경우에 해당합니다. 따라서 분석하려는 프로세스를 x64로 빌드해 다시 실행하면 정상적으로 동작합니다.




이 글의 소스 코드는 https://github.com/stjeong/RefOwner/에도 있습니다.




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 2/11/2019]

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

비밀번호

댓글 작성자
 




... 31  32  33  34  35  36  37  38  39  40  41  [42]  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12575정성태3/24/20217950개발 환경 구성: 557. Docker Desktop for Windows에서 DockerDesktopVM 기반의 Kubernetes 구성
12574정성태3/23/202111883.NET Framework: 1030. C# Socket의 Close/Shutdown 동작 (동기 모드)
12573정성태3/22/20219724개발 환경 구성: 556. WSL 인스턴스 초기 설정 명령어 [1]
12572정성태3/22/20219259.NET Framework: 1029. C# - GC 호출로 인한 메모리 압축(Compaction)을 확인하는 방법파일 다운로드1
12571정성태3/21/20218439오류 유형: 706. WSL 2 기반으로 "Enable Kubernetes" 활성화 시 초기화 실패 [1]
12570정성태3/19/202112749개발 환경 구성: 555. openssl - CA로부터 인증받은 새로운 인증서를 생성하는 방법
12569정성태3/18/202111582개발 환경 구성: 554. WSL 인스턴스 export/import 방법 및 단축 아이콘 설정 방법
12568정성태3/18/20217276오류 유형: 705. C# 빌드 - Couldn't process file ... due to its being in the Internet or Restricted zone or having the mark of the web on the file.
12567정성태3/17/20218613개발 환경 구성: 553. Docker Desktop for Windows를 위한 k8s 대시보드 활성화 [1]
12566정성태3/17/20218957개발 환경 구성: 552. Kubernetes - kube-apiserver와 REST API 통신하는 방법 (Docker Desktop for Windows 환경)
12565정성태3/17/20216465오류 유형: 704. curl.exe 실행 시 dll not found 오류
12564정성태3/16/20216960VS.NET IDE: 160. 새 프로젝트 창에 C++/CLI 프로젝트 템플릿이 없는 경우
12563정성태3/16/20218916개발 환경 구성: 551. C# - JIRA REST API 사용 정리 (3) jira-oauth-cli 도구를 이용한 키 관리
12562정성태3/15/202110020개발 환경 구성: 550. C# - JIRA REST API 사용 정리 (2) JIRA OAuth 토큰으로 API 사용하는 방법파일 다운로드1
12561정성태3/12/20218648VS.NET IDE: 159. Visual Studio에서 개행(\n, \r) 등의 제어 문자를 치환하는 방법 - 정규 표현식 사용
12560정성태3/11/20219975개발 환경 구성: 549. ssh-keygen으로 생성한 개인키/공개키 파일을 각각 PKCS8/PEM 형식으로 변환하는 방법
12559정성태3/11/20219370.NET Framework: 1028. 닷넷 5 환경의 Web API에 OpenAPI 적용을 위한 NSwag 또는 Swashbuckle 패키지 사용 [2]파일 다운로드1
12558정성태3/10/20218893Windows: 192. Power Automate Desktop (Preview) 소개 - Bitvise SSH Client 제어 [1]
12557정성태3/10/20217540Windows: 191. 탐색기의 보안 탭에 있는 "Object name" 경로에 LEFT-TO-RIGHT EMBEDDING 제어 문자가 포함되는 문제
12556정성태3/9/20216819오류 유형: 703. PowerShell ISE의 Debug / Toggle Breakpoint 메뉴가 비활성 상태인 경우
12555정성태3/8/20218823Windows: 190. C# - 레지스트리에 등록된 DigitalProductId로부터 라이선스 키(Product Key)를 알아내는 방법파일 다운로드2
12554정성태3/8/20218652.NET Framework: 1027. 닷넷 응용 프로그램을 위한 PDB 옵션 - full, pdbonly, portable, embedded
12553정성태3/5/20219134개발 환경 구성: 548. 기존 .NET Framework 프로젝트를 .NET Core/5+ 용으로 변환해 주는 upgrade-assistant, try-convert 도구 소개 [4]
12552정성태3/5/20218393개발 환경 구성: 547. github workflow/actions에서 Visual Studio Marketplace 패키지 등록하는 방법
12551정성태3/5/20217294오류 유형: 702. 비주얼 스튜디오 - The 'CascadePackage' package did not load correctly. (2)
12550정성태3/5/20217008오류 유형: 701. Live Share 1.0.3713.0 버전을 1.0.3884.0으로 업데이트 이후 ContactServiceModelPackage 오류 발생하는 문제
... 31  32  33  34  35  36  37  38  39  40  41  [42]  43  44  45  ...