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

비밀번호

댓글 작성자
 




... 121  122  123  124  125  126  127  [128]  129  130  131  132  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
1855정성태2/10/201520793개발 환경 구성: 256. WebDAV Redirector - Sysinternals 폴더 연결 시 "The network path was not found" 오류 해결 방법
1854정성태2/10/201521750Windows: 104. 폴더는 삭제할 수 없지만, 그 하위 폴더/파일은 생성/삭제/변경하는 보안 설정
1853정성태2/6/201552055웹: 29. 여신금융협회 웹 사이트의 "Netscape 6.0은 지원하지 않습니다." 오류 메시지 [5]
1852정성태2/5/201522458.NET Framework: 492. .NET CLR Memory 성능 카운터의 의미파일 다운로드1
1851정성태2/5/201523393VC++: 88. 하룻밤의 꿈 - 인텔 하스웰의 TSX Instruction 지원 [2]
1850정성태2/4/201544177Windows: 103. 작업 관리자에서의 "Commit size"가 가리키는 메모리의 의미 [4]
1849정성태2/4/201524171기타: 51. DropBox의 CPU 100% 현상 [1]파일 다운로드1
1848정성태2/4/201519402.NET Framework: 491. 닷넷 Generic 타입의 메타 데이터 토큰 값 알아내는 방법 [2]
1847정성태2/3/201522681기타: 50. C# - 윈도우에서 dropbox 동기화 폴더 경로 및 종료하는 방법
1846정성태2/2/201532002Windows: 102. 제어판의 프로그램 추가/삭제 항목을 수동으로 실행하고 싶다면? [1]
1845정성태1/26/201532878Windows: 101. 제어판의 "Windows 자격 증명 관리(Manage your credentials)"를 금지시키는 방법
1844정성태1/26/201530843오류 유형: 269. USB 메모리의 용량이 비정상적으로 보여진다면? [7]
1843정성태1/24/201521894VC++: 87. 무시할 수 없는 Visual C++ 런타임 함수 성능
1842정성태1/23/201544391개발 환경 구성: 255. 노트북 키보드에 없는 BREAK 키를 다른 키로 대체하는 방법
1841정성태1/21/201519366오류 유형: 268. Win32 핸들 관련 CLR4 보안 오류 사례
1840정성태1/8/201527601오류 유형: 267. Visual Studio - CodeLens 사용 시 CPU 100% 현상
1839정성태1/5/201520508디버깅 기술: 69. windbg 분석 사례 - cpu 100% 현상 (2)
1838정성태1/4/201540231기타: 49. 윈도우 내레이터(Narrator) 기능 끄는 방법(윈도우에 파란색의 굵은 테두리 선이 나타난다면?) [4]
1837정성태1/4/201526327디버깅 기술: 68. windbg 분석 사례 - 메모리 부족 [1]
1836정성태1/4/201526337디버깅 기술: 67. windbg - 덤프 파일과 handle 정보
1835정성태1/3/201526812개발 환경 구성: 254. SQL 서버 역시 SSL 3.0/TLS 1.0만을 지원하는 듯!
1834정성태1/3/201551436개발 환경 구성: 253. TLS 1.2를 적용한 IIS 웹 사이트 구성
1833정성태1/3/201527501.NET Framework: 490. System.Data.SqlClient는 SSL 3.0/TLS 1.0만 지원하는 듯! [3]
1832정성태1/2/201520611오류 유형: 266. Azure에 응용 프로그램 게시 중 로그인 오류
1831정성태1/1/201528505디버깅 기술: 66. windbg 분석 사례 - cpu 100% 현상 (1) [1]
1830정성태1/1/201527549오류 유형: 265. svchost.exe 프로세스(IP Helper: IPHLPSVC)의 CPU 100% 현상
... 121  122  123  124  125  126  127  [128]  129  130  131  132  133  134  135  ...