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

비밀번호

댓글 작성자
 




... 136  137  138  139  140  141  142  143  [144]  145  146  147  148  149  150  ...
NoWriterDateCnt.TitleFile(s)
1487정성태8/27/201332771개발 환경 구성: 195. 로컬 PC에서의 WCF 통신을 Fiddler로 보는 방법 [1]
1486정성태8/27/201330322.NET Framework: 381. SqlCommand를 이용해 Microsoft SQL 서버의 쿼리 실행 계획을 구하는 방법파일 다운로드1
1485정성태8/26/201333802.NET Framework: 380. 프로세스 스스로 풀 덤프 남기는 방법 [3]파일 다운로드1
1484정성태8/23/201328148제니퍼 .NET: 24. 제니퍼 닷넷 적용 사례 (4) - GZIP 인코딩으로 인한 성능 하락
1483정성태8/23/201328233.NET Framework: 379. System.IO.MemoryStream, ArraySegment&lt;T&gt; 의 효율적인 사용법 [1]
1482정성태8/23/201321699.NET Framework: 378. Java / C# - 정수의 부호 유무에 따른 16진수 문자열 변환
1481정성태8/22/201322637오류 유형: 184. PaaS 유형(Cloud Services)의 Azure VM에 연결할 때 계정 만료 에러가 발생하는 경우
1480정성태8/22/201338902개발 환경 구성: 194. 윈도우 서버의 80 포트에 대한 port forwarding 설정 방법파일 다운로드1
1479정성태8/14/201326479오류 유형: 183. IIS - 바인딩 추가 시 Object reference not set to an instance of an object 오류 [5]
1478정성태8/14/201329769오류 유형: 182. 윈도우 정품 활성화 오류 - 0x80070426
1477정성태8/14/201328683VC++: 71. codeplex의 Project Austin - 실감나게 책장 넘기는 표현
1476정성태8/13/201337075디버깅 기술: 55. Windbg - 윈도우 핸들 테이블 (2)
1475정성태8/12/201335569.NET Framework: 377. 프로세스가 종료된 후에도 소켓이 살아있다면?파일 다운로드1
1474정성태8/10/201332406오류 유형: 181. 윈도우 8 - WmiPrvSE.exe 프로세스가 CPU 소비하는 현상
1473정성태8/8/201329141VC++: 70. Win32 socket이 Thread-safe할까? [1]파일 다운로드1
1472정성태8/7/201327579.NET Framework: 376. .NET 2.0의 유니코드 관련 문자열 비교 오류
1471정성태8/7/201331699개발 환경 구성: 193. .aspx 확장자 대신 .html 확장자를 사용하는 방법
1470정성태8/6/201328519오류 유형: 180. DISM.exe 0xc1510111 실행 오류
1469정성태8/6/201325414.NET Framework: 375. System.Net.Sockets.Socket이 Thread-safe할까? [2]파일 다운로드1
1468정성태8/6/201323413오류 유형: 179. IIS - No connection could be made because the target machine actively refused it 127.0.0.1:80
1467정성태8/5/201326877Java: 16. IE에 로드된 Java Applet의 다운로드 위치를 확인하는 방법
1466정성태7/27/201332492.NET Framework: 374. C#과 비교한 C++ STL vector 성능 [7]파일 다운로드1
1465정성태7/18/201335816기타: 33. C:\Windows\Installer 폴더의 용량 줄이기 [3]
1464정성태7/15/201323966오류 유형: 178. Visual Studio 2012 Express - ImportCardinalityMismatchException
1463정성태7/15/201324738오류 유형: 177. [DBNETLIB][ConnectionOpen (Connect()).]SQL Server does not exist or access denied.
1462정성태7/5/201328157VC++: 69. geek스러운 C/C++ 퀴즈 문제 [2]
... 136  137  138  139  140  141  142  143  [144]  145  146  147  148  149  150  ...