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

비밀번호

댓글 작성자
 




... 151  152  153  [154]  155  156  157  158  159  160  161  162  163  164  165  ...
NoWriterDateCnt.TitleFile(s)
1202정성태12/21/201125995오류 유형: 144. The database '...' cannot be opened because it is version 661.
1201정성태12/14/201141090디버깅 기술: 47. .NET Reflector를 이용한 "소스 코드가 없는" 어셈블리 디버깅 [4]
1200정성태12/11/201126945디버깅 기술: 46. Windbg 확장 DLL 만들기 (2) - Debugger Extension API 사용파일 다운로드1
1199정성태12/11/201128347VC++: 55. JNI DLL 컴파일 시 x86과 x64의 Export된 함수의 이름이 왜 다를까요? [2]파일 다운로드1
1198정성태12/9/201132162디버깅 기술: 45. Windbg 확장 DLL 만들기 (1) - 스레드를 강제 종료시키는 명령어 [2]파일 다운로드1
1197정성태12/9/201129904.NET Framework: 282. Shader 강좌와 함께 배워보는 XNA Framework (2) - RenderMonkey의 Shader/Model 파일 연동파일 다운로드2
1196정성태12/9/201133117.NET Framework: 281. Shader 강좌와 함께 배워보는 XNA Framework (1) - 기초 프로그램 구조 [3]파일 다운로드2
1195정성태12/8/201147772오류 유형: 143. DXSDK_Jun10.exe 설치 시 "Error Code: S1023" 오류 해결하는 방법 [4]
1194정성태12/8/201135529개발 환경 구성: 137. Visual C++ 런타임 구성요소에 대한 디버그 버전 설치하는 방법
1193정성태12/8/201122580오류 유형: 142. Windows Phone SDK 7.1 설치 시 Expression Blend 제거를 요구하는 경우
1192정성태12/8/201125594개발 환경 구성: 136. Windows 7 SP1의 IIS에서 사용자 프로파일을 로드하는 방법
1191정성태12/6/201126714.NET Framework: 280. MVC3에서 JavaScriptSerializer 재정의하는 방법파일 다운로드1
1190정성태12/6/201129886오류 유형: 141. Visual C++ 컴파일 오류 - error C2275: 'xxxxx' : illegal use of this type as an expression [1]
1189정성태12/6/201136985VS.NET IDE: 70. Visual Studio에서 프로젝트 로드가 안된다면?
1188정성태12/3/201126088개발 환경 구성: 135. 마이크로소프트 TFS 호스팅 서비스 - Preview [3]
1187정성태12/2/201130737개발 환경 구성: 134. Robocopy 오류 및 종료 코드
1186정성태12/1/201132582.NET Framework: 279. WPF - 그리기 성능 및 Blurring 문제파일 다운로드1
1185정성태11/29/201123376.NET Framework: 278. WPF - Content의 Changed 이벤트에 해당하는게 뭔가요?파일 다운로드1
1184정성태11/29/201126161.NET Framework: 277. F#과 WPF가 어울리지 못하는 근본적인 이유 [2]
1183정성태11/26/201121636오류 유형: 140. Visual Studio 2010 - Floating된 에디트 윈도우가 사라지지 않는 경우 [2]
1182정성태11/25/201157390.NET Framework: 276. 중복 없는 숫자를 랜덤으로 배열하는 방법 [5]파일 다운로드1
1181정성태11/24/201127844디버깅 기술: 44. windbg의 mscordacwks DLL 로드 문제
1180정성태11/23/201137659.NET Framework: 275. 레지스트리 등록 및 Interop DLL 없이 COM 개체 사용하는 방법 [2]파일 다운로드1
1179정성태11/22/201128275.NET Framework: 274. ReaderWriterLockSlim은 언제 쓰는 걸까요? [4]파일 다운로드1
1178정성태11/19/201124739.NET Framework: 273. 설치된 .NET 버전에 민감한 코드를 포함하는 경우, 다중으로 어셈블리를 만들어야 할까요?파일 다운로드1
1177정성태11/18/201130001.NET Framework: 272. 소켓 연결 시간 제한 - 두 번째 이야기 [1]파일 다운로드1
... 151  152  153  [154]  155  156  157  158  159  160  161  162  163  164  165  ...