Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 4개 있습니다.)
디버깅 기술: 217. WinDbg - PCI 장치 열거
; https://www.sysnet.pe.kr/2/0/13873

닷넷: 2315. C# - PCI 장치 열거 (레지스트리, SetupAPI)
; https://www.sysnet.pe.kr/2/0/13877

닷넷: 2316. C# - Port I/O를 이용한 PCI Configuration Space 정보 열람
; https://www.sysnet.pe.kr/2/0/13881

닷넷: 2317. C# - Memory Mapped I/O를 이용한 PCI Configuration Space 정보 열람
; https://www.sysnet.pe.kr/2/0/13883




C# - Memory Mapped I/O를 이용한 PCI Configuration Space 정보 열람

지난 글에서,

WinDbg - PCI 장치 열거
; https://www.sysnet.pe.kr/2/0/13873

C# - Port I/O를 이용한 PCI Configuration Space 정보 열람
; https://www.sysnet.pe.kr/2/0/13881

"PCI Configuration Space"를 Port I/O 방법을 이용해 조회했는데요, 이번에는 Memory Mapped I/O로 확인해 보겠습니다. ^^ 역시 이것도 지난번에 만들어 둔 device driver의 도움을 받아야 하는데요,

커널 메모리를 읽고 쓰는 NT Legacy driver와 C# 클라이언트 프로그램
; https://www.sysnet.pe.kr/2/0/12104

아쉽게도 IOCTL_READ_MEMORY는 가상 주소를 대상으로 메모리를 읽기 때문에 물리 주소를 대상으로 읽을 수 있도록 다소 코드를 추가해야 합니다.

그런데, 여기서 재미있는 점이 하나 있는데요, ^^ MmCopyMemory 함수의 경우 물리 주소로부터도 내용을 복사할 수 있도록 MM_COPY_MEMORY_PHYSICAL 옵션을 제공하기 때문에 다음과 같이 간단하게 구현할 수 있습니다.

case IOCTL_READ_PHYSICAL_MEMORY:
    if (inBufLength != ptrSize)
    {
        ntStatus = STATUS_BUFFER_TOO_SMALL;
    }
    else 
    {
        PVOID physicalAddress = (PVOID)BytesToPtr(ioBuffer, ptrSize);

        SIZE_T numberOfBytesTransferred = 0;
        MM_COPY_ADDRESS address = { 0 };
        address.PhysicalAddress.QuadPart = (ULONGLONG)physicalAddress;

        ntStatus = MmCopyMemory(ioBuffer, address, outBufLength, MM_COPY_MEMORY_PHYSICAL, &numberOfBytesTransferred);
        pIrp->IoStatus.Information = numberOfBytesTransferred;
    }
    break;

그런데, 일반적인 물리 주소는 정상적으로 복사가 되는 반면 유독 ECAM 물리 주소에 대해서는 복사가 되지 않고 STATUS_INVALID_ADDRESS (c0000141 - "The address handle that was given to the transport was invalid.") 오류가 발생합니다.

이유를 알 수가 없군요. ^^; 분명히 WinDbg로는 해당 물리 주소 접근이 가능하다는 것을 알 수 있는데,

lkd> db /p 00000000`e0000000 L10
00000000`e0000000  86 80 14 59 06 00 90 00-08 00 00 06 00 00 00 00  ...Y............

그런 면에서 혹시나 싶어 MmMapIoSpace 함수를 이용해 가상 주소로 매핑한 후, RtlCopyMemory를 이용하는 방법으로 우회하는 코드를 추가했습니다.

ntStatus = MmCopyMemory(ioBuffer, address, outBufLength, MM_COPY_MEMORY_PHYSICAL, &numberOfBytesTransferred);

if (ntStatus != STATUS_SUCCESS)
{
    DbgPrint("KernelMemoryIO: MmCopyMemory(MM_COPY_MEMORY_PHYSICAL) failed: %x\n", ntStatus);

    PVOID ecamAddress = MmMapIoSpace(address.PhysicalAddress, outBufLength, MmNonCached);
    if (ecamAddress != NULL)
    {
        RtlCopyMemory(ioBuffer, ecamAddress, outBufLength);
        MmUnmapIoSpace(ecamAddress, outBufLength);

        pIrp->IoStatus.Information = outBufLength;
        ntStatus = STATUS_SUCCESS;
    }
}

이렇게 바꾸고 테스트하니 잘 동작하는군요. ^^; 여기서 더욱 재미있는 점은, MmMapIoSpace로 반환한 가상 주소에 대해 MmCopyMemory + MM_COPY_MEMORY_VIRTUAL 옵션으로 시도하면 다시 STATUS_INVALID_ADDRESS 오류가 발생한다는 점입니다.

어쨌든, 저렇게 해서 물리 메모리의 내용을 복사할 수 있는 IOCTL_READ_PHYSICAL_MEMORY를 추가한 KernelMemoryIO.sys 파일을 만들어 두었습니다. ^^




PCI Configuration Space를 Memory Mapped I/O로 접근하는 경우, 우선 그에 대한 물리 주소를 알아내야 합니다. 대개의 경우, 0xe0000000으로 나오겠지만 그래도 간혹 다른 물리 주소가 사용되기 때문에 이 부분은 Windbg를 이용해 우선 확인해 두는 것이 필요합니다.

그렇게 해서 ECAM 물리 주소를 알아냈다면 이제 KernelMemoryIO의 ReadPhysicalMemory 메서드를 이용해 PCIe 규격의 4,096 바이트 정보를 읽어낼 수 있는데요, ECAM 주소로부터 (256개의 버스) * (버스 하나당 32개의 장치) * (Device 하나 당 8개의 Function) * 4KB에 해당하는 물리 공간이 commit돼 있어 (지난 글에도 설명했듯이) 개별 장치의 Configuration Space 위치를 다음의 공식으로 특정할 수 있습니다.

// ECAM 시작 주소 ~ 총 256MB 공간이 commit돼 있음

Physical_Address = ECAM 주소 + ((Bus) << 20 | Device << 15 | Function << 12)

결국 이번에도 BDF 값이 필요하게 되는데요, 이에 대한 열거 정보는 지난 글에서 이미 PciHelper.EnumeratePCI 메서드를 통해 구했으므로 이를 통합해 다음과 같이 마무리할 수 있습니다.

// 현재 머신의 ECAM 주소를 0xc0000000으로 확인했다고 가정
IntPtr ecamAddress = new nint(0xc0000000);

foreach (BDF bdf in PciHelper.EnumeatePCI())
{
    PrintByMemoryMapIO(portIo, ecamAddress, bdf);
    Console.WriteLine();
}

private static void PrintByMemoryMapIO(KernelMemoryIO portIo, IntPtr ecamAddress, BDF bdf)
{
    Console.WriteLine($"Bus: {bdf.Bus}, Device: {bdf.Device}, Function: {bdf.Function}");

    byte[] buffer = new byte[4096];
    IntPtr pciConfigAddress = ecamAddress.Add((uint)(bdf.Bus << 20 | bdf.Device << 15 | bdf.Function << 12));

    int readBytes = portIo.ReadPhysicalMemory(pciConfigAddress, buffer);

    Console.WriteLine($"# of Reads: {readBytes}");

    if (readBytes == buffer.Length)
    {
        PrintPCIInfo(buffer.Take(256).ToArray());
    }
}

private static unsafe void PrintPCIInfo(byte[] bytes)
{
    for (int i = 0; i < bytes.Length; i++)
    {
        Console.Write($"{bytes[i]:x2} ");
        if (i % 16 == 15)
        {
            Console.WriteLine();
        }
    }

    Console.WriteLine();

    fixed (byte* ptr = bytes)
    {
        PCIConfigSpace pciInfo = Marshal.PtrToStructure<PCIConfigSpace>((IntPtr)ptr);
        Console.WriteLine($"VendorId: {pciInfo.VendorId:x4}");
        Console.WriteLine($"DeviceId: {pciInfo.DeviceId:x4}");
    }
}

위의 코드는 ECAM 정보가 있는 물리 머신에서 실행해 확인할 수 있습니다.

// Hyper-V VM의 경우 Gen1, Gen2 모두 MCFG 테이블이 없어 실습이 안 됩니다.

Bus: 0, Device: 26, Function: 0
# of Reads: 4096
86 80 c8 7a 06 04 10 00 11 00 04 06 10 00 81 00
00 00 00 00 00 00 00 00 00 02 02 00 f0 00 00 20
80 86 80 86 f1 ff 01 00 00 00 00 00 00 00 00 00
00 00 00 00 40 00 00 00 00 00 00 00 00 01 00 00
10 80 42 01 01 80 00 00 27 00 10 00 44 48 73 19
42 00 44 70 00 fd e4 00 00 00 40 00 08 00 00 00
00 00 00 00 37 08 b8 00 20 04 00 00 1e 00 80 01
04 00 1f 00 00 00 00 00 00 00 00 00 00 00 00 00
05 98 81 00 50 00 e0 fe 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 0d a0 00 00 43 10 94 86
01 00 03 c8 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
11 80 00 0e 42 18 01 40 08 00 1e 09 00 00 00 00
00 77 51 00 f4 09 f4 09 14 80 83 00 00 20 00 00
70 01 00 00 00 03 00 40 00 0f 11 00 00 c0 0a 01

VendorId: 8086
DeviceId: 7ac8

Bus: 0, Device: 31, Function: 4
# of Reads: 4096
86 80 a3 7a 03 00 80 02 11 00 05 0c 00 00 00 00
04 40 22 15 60 00 00 00 00 00 00 00 00 00 00 00
a1 ef 00 00 00 00 00 00 00 00 00 00 43 10 94 86
00 00 00 00 00 00 00 00 00 00 00 00 ff 03 00 00
11 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
01 04 00 00 01 01 00 00 00 00 00 00 00 00 00 00
04 05 05 00 00 00 0a 0a 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
24 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 21 01 00 0c 00 00 1f 00 20 01 00 0e 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 11 00 00 00 00 00

VendorId: 8086
DeviceId: 7aa3

...[생략]...

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 2/12/2025]

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/201719333.NET Framework: 674. Thread 타입의 Suspend/Resume/Join 사용 관련 예외 처리
11273정성태8/22/201721624오류 유형: 415. 윈도우 업데이트 에러 Error 0x80070643
11272정성태8/21/201724756VS.NET IDE: 120. 비주얼 스튜디오 2017 버전 15.3.1 - C# 7.1 공개 [2]
11271정성태8/19/201719188VS.NET IDE: 119. Visual Studio 2017에서 .NET Core 2.0 프로젝트 환경 구성하는 방법
11270정성태8/17/201730648.NET Framework: 673. C#에서 enum을 boxing 없이 int로 변환하기 [2]
11269정성태8/17/201721437디버깅 기술: 93. windbg - 풀 덤프에서 .NET 스레드의 상태를 알아내는 방법
11268정성태8/14/201721015디버깅 기술: 92. windbg - C# Monitor Lock을 획득하고 있는 스레드 찾는 방법
11267정성태8/10/201725087.NET Framework: 672. 모노 개발 환경
11266정성태8/10/201724893.NET Framework: 671. C# 6.0 이상의 소스 코드를 Visual Studio 설치 없이 명령행에서 컴파일하는 방법
11265정성태8/10/201753128기타: 66. 도서: 시작하세요! C# 7.1 프로그래밍: 기본 문법부터 실전 예제까지 [11]
11264정성태8/9/201724023오류 유형: 414. UWP app을 signtool.exe로 서명 시 0x8007000b 오류 발생
11263정성태8/9/201719498오류 유형: 413. The C# project "..." is targeting ".NETFramework, Version=v4.0", which is not installed on this machine. [3]
11262정성태8/5/201718211오류 유형: 412. windbg - SOS does not support the current target architecture. [3]
11261정성태8/4/201720795디버깅 기술: 91. windbg - 풀 덤프 파일로부터 강력한 이름의 어셈블리 추출 후 사용하는 방법
11260정성태8/3/201718882.NET Framework: 670. C# - 실행 파일로부터 공개키를 추출하는 방법
11259정성태8/2/201718129.NET Framework: 669. 지연 서명된 어셈블리를 sn.exe -Vr 등록 없이 사용하는 방법
11258정성태8/1/201718906.NET Framework: 668. 지연 서명된 DLL과 서명된 DLL의 차이점파일 다운로드1
11257정성태7/31/201719134.NET Framework: 667. bypassTrustedAppStrongNames 옵션 설명파일 다운로드1
11256정성태7/25/201720603디버깅 기술: 90. windbg의 lm 명령으로 보이지 않는 .NET 4.0 ClassLibrary를 명시적으로 로드하는 방법 [1]
11255정성태7/18/201723172디버깅 기술: 89. Win32 Debug CRT Heap Internals의 0xBAADF00D 표시 재현 [1]파일 다운로드3
11254정성태7/17/201719495개발 환경 구성: 322. "Visual Studio Emulator for Android" 에뮬레이터를 "Android Studio"와 함께 쓰는 방법
11253정성태7/17/201719790Math: 21. "Coding the Matrix" 문제 2.5.1 풀이 [1]파일 다운로드1
11252정성태7/13/201718419오류 유형: 411. RTVS 또는 PTVS 실행 시 Could not load type 'Microsoft.VisualStudio.InteractiveWindow.Shell.IVsInteractiveWindowFactory2'
11251정성태7/13/201717078디버깅 기술: 88. windbg 분석 - webengine4.dll의 MgdExplicitFlush에서 발생한 System.AccessViolationException의 crash 문제 (2)
11250정성태7/13/201720664디버깅 기술: 87. windbg 분석 - webengine4.dll의 MgdExplicitFlush에서 발생한 System.AccessViolationException의 crash 문제 [1]
11249정성태7/12/201718473오류 유형: 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  ...