nvapi에서 NvAPI_GetMemoryInfo()주소 얻어오기 – (어셈블리어 학습해야하나요?)

Voxel Horizon은 NVAPI를 사용한다. NVAPI는 GPU의 상태를 확인하거나 일부 기능을 제어할 수 있는 nvidia 그래픽 드라이버에서 제공하는 API이다.
NVAPI SDK는 nvidia 개발자 홈페이지에서 다운로드할 수 있다. C스타일의 .h / .lib / .dll로 구성된 아주 간단한 SDK다.
이걸로 게임 플레이중 GPU점유율과 GPU온도, 남은 GPU메모리의 양을 확인한다. 물론 일반 유저들을 위한 기능은 아니고 개발할때 필요한 디버깅용 기능으로 넣었다.

그런데 이 nvapi SDK와 예제를 그대로 사용할 경우 약간 문제가 있다.
당연히 nvidia 그래픽 카드가 없는 PC에선 사용할 수 없다. 사용할 수 없으면 그냥 안쓰고 게임 돌아가면 된다. GPU 점유율 표시 안되면 그만이다. 근데 크래시하면 큰 문제다.

nvapi.h에 선언된 함수들을 사용하기 위해 기본적으로 NVAPI를 사용할때 .lib를 링크한다. 그런데 이렇게 하면 implicit DLL linking이 된다.
쉽게 말해서 nvapi를 사용한 exe가 실행이 될때 exe가 있는 폴더, 혹은 windows\system32폴더 밑에 nvapi.dll이 있어야한다. 그렇지 않으면 내가 만든 exe로 제어권이 넘어오기도 전에 크래시해버린다.
nvidia 그래픽 카드 사용자라의 PC라면 Wndows\system32폴더에 nvapi.dll이 있을것이다. 하지만 nvidia 그래픽 카드가 없는 PC라면 nvapi.dll은 없을것이고 바로 크래시한다.

해결방법은 간단하다. explicit DLL linking을 사용하면 된다.
LoadLibrary()로 DLL를 로드하고 GetProcAddress()로 특정함수의 주소를 얻어와서 호출하면 된다.

그런데 또 문제가 있다.
nvapi_exports

nvapi.dll은 nvapi_QueryInterface함수 하나만 노출하고 있다. GPU온도를 얻는 NvAPI_GPU_GetThermalSettings()라든가 GPU메모리 상태를 얻기 위한 NvAPI_GetMemoryInfo()함수가 필요한데 이런 함수들은 노출되어있지 않다. 즉 GetProcAddress()로 이 함수들의 주소를 얻어올수 없다.

구글검색을 해보면 nvapi_QueryInterface()에 특정 상수를 넣어서 나머지 함수들의 주소를 얻어오는 예제가 잔뜩 나온다.
nvapi_QueryInterface가 인자로 받는 상수는 아마도 uuid 비슷한것으로 보인다. 64비트 32비트 상관없이 같은 숫자를 받으니까 상대 어드레스라든가 그런건 아닌거 같다. 좌우간 uuid든 뭐든 nvapi_QueryInterface에 전달할 ID값만 알고 있으면 필요한 함수의 주소는 다 얻어올수 있다.

검색하면 필요한 함수의 ID값은 다 찾을 수 있다. 그런데 그 중에 틀린 값도 있다.
다들 같은 코드를 복붙했는지 틀린 값도 똑같다.
예를 들면 이 함수 NvAPI_GetMemoryInfo()가 그렇다.
nvapi_gpu_memoryinfo_invalid
이거 잘못된 값이다. 어쩌면 누군가 저 코드를 작성했을 시점엔 맞는 값이었을지도 모르겠지만 좌우간 지금은 아니다. 저 값으로 NvAPI_GetMemoryInfo()의 주소를 얻어올 수 없다. 전혀 다른 함수의 주소를 돌려준다.

디스어셈블리 창을 띄워놓고 nvapi.h에 선언된 함수들을 조금 따라가보면 내부적으로 nvapi_QueryInterface()함수를 호출하여 해당 기능을 수행하는 함수의 어드레스를 얻어오도록 되어있다. 그걸 호출해서 실제 기능을 수행한다.
즉 nvapi.lib를 링크했을때(implicit linking으로 nvapi.dll을 로딩) nvapi.h에 선언된 NvAPI_GetMemoryInfo()를 호출하면 nvapi_QueryInterface()에 어떤 ID값을 넣고 그걸로 진짜 NvAPI_GetMemoryInfo()의 주소를 받아와서 그걸 실행한다는 것이다.

그럼 간단하다. nvapi.lib를 링크한 상태로 NvAPI_GetMemoryInfo()의 어셈블리 코드를 쫓아가보면 어떤 ID값을 nvapi_QueryInterface()함수에 전달하는지 알 수 있을 것이다.

먼저 nvapi_QueryInterface()의 주소를 얻는다. 이건 그냥 API호출로 간단하게 얻을수 있다.

nvapi_gpu_memoryinfo_00

nvapi_QueryInterface의 주소는 0x00007ffc22365e00이다. 일단 이걸 확인해둔다.
nvapi_queryinterface_addr

이제 nvapi.lib의 NvAPI_GetMemoryInfo()를 따라가본다.

nvapi_gpu_memoryinfo_01

여기부터 nvapi.lib에 들어있는 NvAPI_GetMemoryInfo()함수다. 실질적으로 이 녀석이 메모리 정보를 주진 않는다. nvapi_QueryInterface()에 ID값을 넣고 얻은 함수가 실제 메모리 정보를 준다. 이 녀석은 중계를 할 뿐이다.

nvapi_gpu_memoryinfo_05.PNG

디스어셈블리 창에서 F10을 눌러서 몇줄 따라내려가보면
이와 같이 nvAPI_QueryInterface함수의 포인터를 로드하는 코드가 보인다.
mov         rax,qword ptr [g_nvapi_lpNvAPI_gpuQueryInterface (07FF7B0EBCB98h)]

조금 더 내려가면. call rax명령이 보인다. 함수 포인터를 호출하는 흔하디 흔한 간접호출 코드다.
호출 직전 rax레지스터의 값을 확인해보면 0x00007ffc22365e00로 앞에서 확인한 nvAPI_QueryInterface의 주소와 같다. 이제 nvAPI_QueryInterface()를 호출하는것이 확실하다.

그럼 파라미터로 어떤 값을 전달할까? x64에선 x64용 fastcall을 사용하므로 파라미터가 4개 이하려면 rcx,rdx,r8,r9 레지스터로 전달한다. call 명령 전에 ecx레지스터(rcx레지스터의 하위 32비트에 해당)에 7F9B368h를 넣는게 보인다.OK. 

NvAPI_GetMemoryInfo()의 ID값은 7F9B368h이다.

이제 진짜 NvAPI_GPU_GetMemoryInfo()함수의 주소를 얻을 수 있다.
nvapi_gpu_memoryinfo_call

그리고 이렇게 잘 사용중.
voxel_horizon_gpu_info_full
voxel_horizon_gpu_info

nvapi의 함수 주소를 얻어온 방법 자체는 사실 아무것도 아니다. 그래서 포스팅할 생각은 없었다.
그런데 최근 어셈블리 학습이 필요하냐는 질문을 받았다. 각잡고 학습할 필요는 없다고 답변했다. 물론 학습할 필요가 없다는 뜻이 절대 아니다. 책 펴놓고 학습할 필요까진 없고 효율적으로 학습하라는 뜻이다.
Visual Studio에서 Disasembly 창을 항상 띄워놓고 C/C++코드가 어떻게 어셈블리 코드로 바뀌었는지 확인하는 습관을 들이라는 얘기다.
그러면 이런 때에도 써먹을 수 있다는 얘길 하고 싶었다.

그러니까 이번 포스팅의 핵심 내용은 “어셈블리 학습을 하면 이런데 쓸 수 있습니다” 라는 사례 소개인 것이다.


답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중