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

비밀번호

댓글 작성자
 




1  2  [3]  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13558정성태2/18/20242150닷넷: 2217. C# - 최댓값이 1인 SemaphoreSlim 보다 Mutex 또는 lock(obj)를 선택하는 것이 나은 이유
13557정성태2/18/20241902Windows: 258. Task Scheduler의 Author 속성 값을 변경하는 방법
13556정성태2/17/20241947Windows: 257. Windows - Symbolic (hard/soft) Link 및 Junction 차이점
13555정성태2/15/20242101닷넷: 2216. C# - SemaphoreSlim 사용 시 주의점
13554정성태2/15/20241849VS.NET IDE: 189. Visual Studio - 닷넷 소스코드 디컴파일 찾기가 안 될 때
13553정성태2/14/20241932닷넷: 2215. windbg - thin/fat lock 없이 동작하는 Monitor.Wait + Pulse
13552정성태2/13/20241881닷넷: 2214. windbg - Monitor.Enter의 thin lock과 fat lock
13551정성태2/12/20242077닷넷: 2213. ASP.NET/Core 웹 응용 프로그램 - 2차 스레드의 예외로 인한 비정상 종료
13550정성태2/11/20242162Windows: 256. C# - Server socket이 닫히면 Accept 시켰던 자식 소켓이 닫힐까요?
13549정성태2/3/20242491개발 환경 구성: 706. C# - 컨테이너에서 실행하기 위한 (소켓) 콘솔 프로젝트 구성
13548정성태2/1/20242319개발 환경 구성: 705. "Docker Desktop for Windows" - ASP.NET Core 응용 프로그램의 소켓 주소 바인딩(IPv4/IPv6 loopback, Any)
13547정성태1/31/20242067개발 환경 구성: 704. Visual Studio - .NET 8 프로젝트부터 dockerfile에 추가된 "USER app" 설정
13546정성태1/30/20241922Windows: 255. (디버거의 영향 등으로) 대상 프로세스가 멈추면 Socket KeepAlive로 연결이 끊길까요?
13545정성태1/30/20241842닷넷: 2212. ASP.NET Core - 우선순위에 따른 HTTP/HTTPS 호스트:포트 바인딩 방법
13544정성태1/30/20241854오류 유형: 894. Microsoft.Data.SqlClient - Could not load file or assembly 'System.Security.Permissions, ...'
13543정성태1/30/20241850Windows: 254. Windows - 기본 사용 중인 5357 포트 비활성화는 방법
13542정성태1/30/20241884오류 유형: 893. Visual Studio - Web Application을 실행하지 못하는 IISExpress - 두 번째 이야기
13541정성태1/29/20241929VS.NET IDE: 188. launchSettings.json의 useSSL 옵션
13540정성태1/29/20242058Linux: 69. 리눅스 - "Docker Desktop for Windows" Container 환경에서 IPv6 Loopback Address 바인딩 오류
13539정성태1/26/20242155개발 환경 구성: 703. Visual Studio - launchSettings.json을 이용한 HTTP/HTTPS 포트 바인딩
13538정성태1/25/20242236닷넷: 2211. C# - NonGC(FOH) 영역에 .NET 개체를 생성파일 다운로드1
13537정성태1/24/20242294닷넷: 2210. C# - Native 메모리에 .NET 개체를 생성파일 다운로드1
13536정성태1/23/20242458닷넷: 2209. .NET 8 - NonGC Heap / FOH (Frozen Object Heap) [1]
13535정성태1/22/20242272닷넷: 2208. C# - GCHandle 구조체의 메모리 분석
13534정성태1/21/20242089닷넷: 2207. C# - SQL Server DB를 bacpac으로 Export/Import파일 다운로드1
13533정성태1/18/20242288닷넷: 2206. C# - TCP KeepAlive의 서버 측 구현파일 다운로드1
1  2  [3]  4  5  6  7  8  9  10  11  12  13  14  15  ...