Microsoft MVP성태의 닷넷 이야기
오류 유형: 917. ClrMD - Linux 환경의 .NET 5 덤프 분석 시 hang 현상 [링크 복사], [링크+제목 복사],
조회: 7741
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
 
(연관된 글이 1개 있습니다.)

ClrMD - Linux 환경의 .NET 5 덤프 분석 시 hang 현상

간단하게 .NET 5 콘솔 프로젝트를 만들고,

using System;

namespace ConsoleApp4
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine($"PID: {Environment.ProcessId}");
            Console.WriteLine("Hello, World!");
            Console.WriteLine("Press any key to exit...");

            Console.ReadLine();
        }
    }
}

Linux 환경에서 실행한 후, Microsoft.Diagnostics.NETCore.Client(혹은 dotnet-dump)를 이용해 메모리 덤프를 떴습니다. 그다음, ClrMD를 이용해 ClrVersions까지만 구했는데 hang 현상이 발생했습니다.

// Install-Package Microsoft.Diagnostics.Runtime

using (DataTarget target = DataTarget.LoadDump(dmpFilePath))
{
    ImmutableArray<ClrInfo> clrVersions = target.ClrVersions;
}

무엇이 문제인지 추적해 보면, 한창 hang 상태일 때 Break All을 걸어 실행을 멈추니 SingleFileClrInfoProvider.ProvideClrInfoForModule의 GetExportSymbolAddress에 걸려 있고,

// .\Microsoft.Diagnostics.Runtime\SingleFileClrInfoProvider.cs

// ...[생략]...

namespace Microsoft.Diagnostics.Runtime
{
    internal class SingleFileClrInfoProvider : DotNetClrInfoProvider
    {
        public override ClrInfo? ProvideClrInfoForModule(DataTarget dataTarget, ModuleInfo module)
        {
            ulong runtimeInfo = 0;
            if ((dataTarget.DataReader.TargetPlatform != OSPlatform.Windows) || Path.GetExtension(module.FileName).Equals(".exe", StringComparison.OrdinalIgnoreCase))
                runtimeInfo = module.GetExportSymbolAddress(ClrRuntimeInfo.SymbolValue);

            if (runtimeInfo == 0)
                return null;

            ClrInfo result = CreateClrInfo(dataTarget, module, runtimeInfo, ClrFlavor.Core);
            result.IsSingleFile = true;
            return result;
        }
    }
}

이때의 module은 "/usr/share/dotnet/shared/Microsoft.NETCore.App/5.0.17/System.Private.CoreLib.dll" 또는 "/usr/share/dotnet/shared/Microsoft.NETCore.App/5.0.17/System.Console.dll" 등에서 오래 걸려 있습니다. 이후 추적을 계속하면,

// .\Microsoft.Diagnostics.Runtime\Implementation\ElfModuleInfo.cs

public override ulong GetExportSymbolAddress(string symbol)
{
    if (_elf is null || !_elf.TryGetExportSymbol(symbol, out ulong address))
        return 0;

    return ImageBase + address;
}

실질적인 문제가 되는 for 루프에 다다르게 됩니다.

// .\Microsoft.Diagnostics.Runtime\Utilities\PEImage\PEImage.cs

public bool TryGetExportSymbol(string symbolName, out ulong offset)
{
    try
    {
        ImageDataDirectory exportTableDirectory = ExportDirectory;
        if (exportTableDirectory.VirtualAddress != 0 && exportTableDirectory.Size != 0)
        {
            if (TryRead(RvaToOffset(exportTableDirectory.VirtualAddress), out ImageExportDirectory exportDirectory))
            {
                for (int nameIndex = 0; nameIndex < exportDirectory.NumberOfNames; nameIndex++)
                {
                    int namePointerRVA = Read<int>(RvaToOffset(exportDirectory.AddressOfNames + (sizeof(uint) * nameIndex)));
                    if (namePointerRVA != 0)
                    {
                        string name = ReadNullTerminatedAscii(namePointerRVA, maxLength: 4096);
                        if (name == symbolName)
                        {
                            ushort ordinalForNamedExport = Read<ushort>(RvaToOffset(exportDirectory.AddressOfNameOrdinals + (sizeof(ushort) * nameIndex)));
                            int exportRVA = Read<int>(RvaToOffset(exportDirectory.AddressOfFunctions + (sizeof(uint) * ordinalForNamedExport)));
                            offset = (uint)RvaToOffset(exportRVA);
                            return true;
                        }
                    }
                }
            }
        }
    }
    catch (IOException)
    {
    }
    catch (InvalidDataException)
    {
    }

    offset = 0;
    return false;
}

nameIndex = 0으로 시작해, 이때의 NumberOfNames 값은 무려 1599230533이 나옵니다. 그러니까, 저 루프를 끝까지 도는 동안 외부에서는 hang 현상이 나온 듯 보인 것입니다.

다시 ProvideClrInfoForModule 메서드로 돌아가 이 상황을 종합해 볼까요?

// .\Microsoft.Diagnostics.Runtime\SingleFileClrInfoProvider.cs

public override ClrInfo? ProvideClrInfoForModule(DataTarget dataTarget, ModuleInfo module)
{
    ulong runtimeInfo = 0;
    if ((dataTarget.DataReader.TargetPlatform != OSPlatform.Windows) || Path.GetExtension(module.FileName).Equals(".exe", StringComparison.OrdinalIgnoreCase))
        runtimeInfo = module.GetExportSymbolAddress(ClrRuntimeInfo.SymbolValue); // SymbolValue == "DotNetRuntimeInfo"

    if (runtimeInfo == 0)
        return null;

    ClrInfo result = CreateClrInfo(dataTarget, module, runtimeInfo, ClrFlavor.Core);
    result.IsSingleFile = true;
    return result;
}

ClrInfo를 얻기 위해 Module의 ExportDirectory을 뒤져 DotNetRuntimeInfo에 해당하는 이름을 찾고 있습니다. 그래서 ExportDirectory 값을 디버거로 보니 이렇게 나오는데,

ImageDataDirectory exportTableDirectory = ExportDirectory;

// exportTableDirectory
//   .Size == 0x00000043
//   .VirtualAddress == 0x008f1d5c

제가 만든 WindowsPE 라이브러리를 사용해 저 "System.Private.CoreLib.dll" 파일을 덤프했더니 동일한 값이 나옵니다. ^^ (물론, windbg의 "!dh" 명령어로도 쉽게 확인할 수 있습니다.)

// Install-Package WindowsPE
PEImage pe = PEImage.ReadFromFile(@"c:\temp\System.Private.CoreLib.dll");
pe.ShowHeader();

/* 출력 결과:
...[생략]...
  8F1D5C [      43] address [size] of ExportTable Directory
       0 [       0] address [size] of ImportTable Directory
   42800 [     494] address [size] of ResourceTable Directory
...[생략]...
*/

그렇다면 이제 문제는 그다음 코드인,

// .\Microsoft.Diagnostics.Runtime\Utilities\PEImage\PEImage.cs

public bool TryGetExportSymbol(string symbolName, out ulong offset)
{
    int nameIndex = 0;
    try
    {
        ImageDataDirectory exportTableDirectory = ExportDirectory;
        if (exportTableDirectory.VirtualAddress != 0 && exportTableDirectory.Size != 0)
        {
            if (TryRead(RvaToOffset(exportTableDirectory.VirtualAddress), out ImageExportDirectory exportDirectory))
            {
                // ...[생략]...
}

TryRead에서 반환한 ImageExportDirectory 구조체의 값으로 넘어갑니다. 실제로 저 메서드에서 반환한 exportDirectory의 값은 그냥 봐도,

AddressOfFunctions  0x190a00ee  int
AddressOfNameOrdinals   0x1d526a09  int
AddressOfNames  0x23421009  int
Base    0x2651320b  int
Characteristics 0x2603043b  int
MajorVersion    0x5133  short
MinorVersion    0x4686  short
Name    0x51335b26  int
NumberOfFunctions   0xb3513183  int
NumberOfNames   0x035d5130  int
TimeDateStamp   0x2b5a2f63  int

문제가 있어 보입니다. 가령, .NET DLL의 경우 AddressOfFunctions는 대개 0인데 0x190a00ee와 같이 매우 큰 값이 나온 것입니다.




왜 저런 이상한 것이 나왔는지에 대한 이유는, TryRead를 하기 전 RvaToOffset을 호출해 RVA로부터 실제 주소를 구하는 단계에서 찾을 수 있습니다. 바로 여기에 문제가 있었는데요,

// .\Microsoft.Diagnostics.Runtime\Utilities\PEImage\PEImage.cs

public int RvaToOffset(int virtualAddress)
{
    if (virtualAddress < 4096)
        return virtualAddress;

    // 실행 상태 또는 메모리 덤프에는 아래의 코드에서 반환
    if (_isVirtual)
        return virtualAddress;

    // 파일 상태의 PE 데이터에서는 아래의 코드에서 반환
    ImageSectionHeader[] sections = ReadSections();
    for (int i = 0; i < sections.Length; i++)
    {
        ref ImageSectionHeader section = ref sections[i];
        if (section.VirtualAddress <= virtualAddress && virtualAddress < section.VirtualAddress + section.VirtualSize)
            return (int)(section.PointerToRawData + ((uint)virtualAddress - section.VirtualAddress));
    }

    return -1;
}

PE 파일은, 파일에 존재할 때는 Section으로부터 RVA를 이용한 file offset 위치를 구해야 하지만, 실행 시점에는, 즉 "_isVirtual" 필드가 "true"여야 해서 그 값 그대로를 반환해야 합니다. 그런데, 메모리 덤프를 분석하고 있는 ClrMD는 저 상태에서 _isVirtual이 false를 갖고 있습니다.

원인이 그것인지 간단하게 확인을 할 수 있는데요, 그냥 virtualAddress를 바로 반환하는 것입니다.

// .\Microsoft.Diagnostics.Runtime\Utilities\PEImage\PEImage.cs

public int RvaToOffset(int virtualAddress)
{
    return virtualAddress;
}

이후, 다시 .NET 5 덤프를 열어 TryRead가 반환한 ImageExportDirectory exportDirectory를 보면 정상적으로 값이 채워져 있습니다.

AddressOfFunctions	0x00000000	int
AddressOfNameOrdinals	0x00000000	int
AddressOfNames	0x00000000	int
Base	0x00000000	int
Characteristics	0x00000000	int
MajorVersion	0x0000	short
MinorVersion	0x0000	short
Name	0x008f1d84	int
NumberOfFunctions	0x00000000	int
NumberOfNames	0x00000000	int
TimeDateStamp	0x00000000	int

그렇다면, 왜 저런 문제가 .NET 6 이상의 메모리 덤프 분석에서는 발생하지 않는 걸까요? 이유는, .NET 6부터 포함된 DLL들은 아예 Export Data Directory가 없어 아래의 if 문 자체를 진입하지 않기 때문입니다.

public bool TryGetExportSymbol(string symbolName, out ulong offset)
{
    int nameIndex = 0;
    try
    {
        ImageDataDirectory exportTableDirectory = ExportDirectory;
        if (exportTableDirectory.VirtualAddress != 0 && exportTableDirectory.Size != 0)
        {




자, 원인을 알았으니 이제 해결책을 찾아보겠습니다. 우선, _isVirtual 필드를 설정하는 곳은 PEImage의 생성자인데,

// .\Microsoft.Diagnostics.Runtime\Utilities\PEImage\PEImage.cs

private PEImage(Stream stream, bool leaveOpen, bool isVirtual, ulong loadedImageBase)
{
    _isVirtual = isVirtual;

    // ...[생략]...
}

public PEImage(ReadVirtualStream stream, bool leaveOpen, bool isVirtual)
    : this(stream, leaveOpen, isVirtual, 0)
{
}

저 생성자는 PEModuleInfo에서 GetPEImage를 통해 호출되는데,

// .\Microsoft.Diagnostics.Runtime\Implementation\PEModuleInfo.cs

internal PEImage? GetPEImage()
{
    if (_peImage is not null || _loaded)
        return _peImage;

    try
    {
        PEImage image = new(new ReadVirtualStream(_dataReader, (long)ImageBase, int.MaxValue), leaveOpen: false, isVirtual: _isVirtual);
                
    // ...[생략]...
}

public PEModuleInfo(IDataReader dataReader, ulong imageBase, string fileName, bool isVirtualHint)
    : base(imageBase, fileName)
{
    if (dataReader is null)
        throw new ArgumentNullException(nameof(dataReader));

    if (fileName is null)
        throw new ArgumentNullException(nameof(fileName));

    _dataReader = dataReader;
    _isVirtual = isVirtualHint;
}

이상하게도, PEModuleInfo 생성자를 호출하는 CoreDumpReader 타입에서는 무조건 virtualHint를 false로 설정하고 있습니다.

// .\Microsoft.Diagnostics.Runtime\DataReaders\Core\CoreDumpReader.cs

private ModuleInfo CreateModuleInfo(ElfLoadedImage image)
{
    using ElfFile? file = image.Open();

    // We suppress the warning because the function it wants us to use is not available on all ClrMD platforms

    // This substitution is for unloaded modules for which Linux appends " (deleted)" to the module name.
    string path = image.FileName.Replace(" (deleted)", "");
    if (file is not null)
    {
        long size = image.Size > long.MaxValue ? long.MaxValue : unchecked((long)image.Size);
        return new ElfModuleInfo(this, file, image.BaseAddress, size, path);
    }

    return new PEModuleInfo(this, image.BaseAddress, path, false);
}

이름이 CoreDumpReader인 점을 감안하면, File로부터 직접 PE 이미지를 읽어들이는 것이 아니므로 virtualHint는 기본적으로 true여야 합니다.




혹시, 윈도우에서 뜬 .NET 5 덤프는 어떻게 분석하고 있을까요? 비교를 위해 보면 좋을 텐데, 바로 MinidumpReader.cs 파일이 그 역할을 담당하고 있습니다.

// .\Microsoft.Diagnostics.Runtime\DataReaders\Minidump\MinidumpReader.cs

public IEnumerable<ModuleInfo> EnumerateModules()
{
    // We set buildId to "Empty" since only PEImages exist where minidumps are created, and we do not
    // want to try to lazily evaluate the buildId later
    return from module in _minidump.EnumerateModuleInfo()
            select new PEModuleInfo(this, module.BaseOfImage, module.ModuleName ?? "", true, module.DateTimeStamp, module.SizeOfImage);
}

보는 바와 같이 저렇게 virtualHint를 고정으로 true를 주고 있으니... CoreDumpReader에서도 당연히 true로 설정하는 것이 맞습니다.

일단, 관련해서 이슈 제기와

_isVirtual has to be set as true in the context of DataTarget.LoadDump. #1279
; https://github.com/microsoft/clrmd/issues/1279

PR을 날렸는데 어떻게 될지 기다려야 할 듯합니다. ^^ (2024-08-14 업데이트: merge가 되었지만 nuget 릴리스는 아직 안 된 상태입니다.)




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 8/14/2024]

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

비밀번호

댓글 작성자
 




... [106]  107  108  109  110  111  112  113  114  115  116  117  118  119  120  ...
NoWriterDateCnt.TitleFile(s)
11274정성태8/22/201719369.NET Framework: 674. Thread 타입의 Suspend/Resume/Join 사용 관련 예외 처리
11273정성태8/22/201721633오류 유형: 415. 윈도우 업데이트 에러 Error 0x80070643
11272정성태8/21/201724784VS.NET IDE: 120. 비주얼 스튜디오 2017 버전 15.3.1 - C# 7.1 공개 [2]
11271정성태8/19/201719217VS.NET IDE: 119. Visual Studio 2017에서 .NET Core 2.0 프로젝트 환경 구성하는 방법
11270정성태8/17/201730703.NET Framework: 673. C#에서 enum을 boxing 없이 int로 변환하기 [2]
11269정성태8/17/201721485디버깅 기술: 93. windbg - 풀 덤프에서 .NET 스레드의 상태를 알아내는 방법
11268정성태8/14/201721050디버깅 기술: 92. windbg - C# Monitor Lock을 획득하고 있는 스레드 찾는 방법
11267정성태8/10/201725108.NET Framework: 672. 모노 개발 환경
11266정성태8/10/201724911.NET Framework: 671. C# 6.0 이상의 소스 코드를 Visual Studio 설치 없이 명령행에서 컴파일하는 방법
11265정성태8/10/201753152기타: 66. 도서: 시작하세요! C# 7.1 프로그래밍: 기본 문법부터 실전 예제까지 [11]
11264정성태8/9/201724067오류 유형: 414. UWP app을 signtool.exe로 서명 시 0x8007000b 오류 발생
11263정성태8/9/201719545오류 유형: 413. The C# project "..." is targeting ".NETFramework, Version=v4.0", which is not installed on this machine. [3]
11262정성태8/5/201718246오류 유형: 412. windbg - SOS does not support the current target architecture. [3]
11261정성태8/4/201720832디버깅 기술: 91. windbg - 풀 덤프 파일로부터 강력한 이름의 어셈블리 추출 후 사용하는 방법
11260정성태8/3/201718931.NET Framework: 670. C# - 실행 파일로부터 공개키를 추출하는 방법
11259정성태8/2/201718167.NET Framework: 669. 지연 서명된 어셈블리를 sn.exe -Vr 등록 없이 사용하는 방법
11258정성태8/1/201718960.NET Framework: 668. 지연 서명된 DLL과 서명된 DLL의 차이점파일 다운로드1
11257정성태7/31/201719157.NET Framework: 667. bypassTrustedAppStrongNames 옵션 설명파일 다운로드1
11256정성태7/25/201720649디버깅 기술: 90. windbg의 lm 명령으로 보이지 않는 .NET 4.0 ClassLibrary를 명시적으로 로드하는 방법 [1]
11255정성태7/18/201723206디버깅 기술: 89. Win32 Debug CRT Heap Internals의 0xBAADF00D 표시 재현 [1]파일 다운로드3
11254정성태7/17/201719558개발 환경 구성: 322. "Visual Studio Emulator for Android" 에뮬레이터를 "Android Studio"와 함께 쓰는 방법
11253정성태7/17/201719886Math: 21. "Coding the Matrix" 문제 2.5.1 풀이 [1]파일 다운로드1
11252정성태7/13/201718444오류 유형: 411. RTVS 또는 PTVS 실행 시 Could not load type 'Microsoft.VisualStudio.InteractiveWindow.Shell.IVsInteractiveWindowFactory2'
11251정성태7/13/201717121디버깅 기술: 88. windbg 분석 - webengine4.dll의 MgdExplicitFlush에서 발생한 System.AccessViolationException의 crash 문제 (2)
11250정성태7/13/201720690디버깅 기술: 87. windbg 분석 - webengine4.dll의 MgdExplicitFlush에서 발생한 System.AccessViolationException의 crash 문제 [1]
11249정성태7/12/201718517오류 유형: 410. LoadLibrary("[...].dll") failed - The specified procedure could not be found.
... [106]  107  108  109  110  111  112  113  114  115  116  117  118  119  120  ...