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

비밀번호

댓글 작성자
 




... 46  [47]  48  49  50  51  52  53  54  55  56  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
12760정성태8/8/202112801개발 환경 구성: 594. 전용 "Command Prompt for ..." 단축 아이콘 만들기
12759정성태8/8/202117394Java: 26. IntelliJ + Spring Framework + 새로운 Controller 추가 [2]파일 다운로드1
12758정성태8/7/202116797오류 유형: 751. Error assembling WAR: webxml attribute is required (or pre-existing WEB-INF/web.xml if executing in update mode)
12757정성태8/7/202117410Java: 25. IntelliJ + Spring Framework 프로젝트 생성
12756정성태8/6/202115661.NET Framework: 1084. C# - .NET Core Web API 단위 테스트 방법 [1]파일 다운로드1
12755정성태8/5/202115665개발 환경 구성: 593. MSTest - 단위 테스트에 static/instance 유형의 private 멤버 접근 방법파일 다운로드1
12754정성태8/5/202116086오류 유형: 750. manage.py - Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
12753정성태8/5/202117046오류 유형: 749. PyCharm - Error: Django is not importable in this environment
12752정성태8/4/202113918개발 환경 구성: 592. JetBrains의 IDE(예를 들어, PyCharm)에서 Visual Studio 키보드 매핑 적용
12751정성태8/4/202116851개발 환경 구성: 591. Windows 10 WSL2 환경에서 docker-compose 빌드하는 방법
12750정성태8/3/202113862디버깅 기술: 181. windbg - 콜 스택의 "Call Site" 오프셋 값이 가리키는 위치
12749정성태8/2/202113279개발 환경 구성: 590. Visual Studio 2017부터 단위 테스트에 DataRow 특성 지원
12748정성태8/2/202114282개발 환경 구성: 589. Azure Active Directory - tenant의 관리자(admin) 계정 로그인 방법
12747정성태8/1/202114557오류 유형: 748. 오류 기록 - MICROSOFT GRAPH – HOW TO IMPLEMENT IAUTHENTICATIONPROVIDER파일 다운로드1
12746정성태7/31/202118934개발 환경 구성: 588. 네트워크 장비 환경을 시뮬레이션하는 Packet Tracer 프로그램 소개
12745정성태7/31/202114825개발 환경 구성: 587. Azure Active Directory - tenant의 관리자 계정 로그인 방법
12744정성태7/30/202115237개발 환경 구성: 586. Azure Active Directory에 연결된 App 목록을 확인하는 방법?
12743정성태7/30/202116470.NET Framework: 1083. Azure Active Directory - 외부 Token Cache 저장소를 사용하는 방법파일 다운로드1
12742정성태7/30/202114492개발 환경 구성: 585. Azure AD 인증을 위한 사용자 인증 유형
12741정성태7/29/202115995.NET Framework: 1082. Azure Active Directory - Microsoft Graph API 호출 방법파일 다운로드1
12740정성태7/29/202114453오류 유형: 747. SharePoint - InvalidOperationException 0x80131509
12739정성태7/28/202114906오류 유형: 746. Azure Active Directory - IDW10106: The 'ClientId' option must be provided.
12738정성태7/28/202115846오류 유형: 745. Azure Active Directory - Client credential flows must have a scope value with /.default suffixed to the resource identifier (application ID URI).
12737정성태7/28/202114918오류 유형: 744. Azure Active Directory - The resource principal named api://...[client_id]... was not found in the tenant
12736정성태7/28/202115189오류 유형: 743. Active Azure Directory에서 "API permissions"의 권한 설정이 "Not granted for ..."로 나오는 문제
12735정성태7/27/202115103.NET Framework: 1081. C# - Azure AD 인증을 지원하는 데스크톱 애플리케이션 예제(Windows Forms) [2]파일 다운로드1
... 46  [47]  48  49  50  51  52  53  54  55  56  57  58  59  60  ...