딥웹 .onion 익명 사이트 개설(호스팅)하기 (Tor를 사용한 .onion 주소 할당 방법)
일반적인 검색엔진(구글, 네이버, 다음 등)으로 검색되지 않는 웹 사이트들이 존재하는 통신망을 딥웹(Deep Web), 심층 웹, 깊은 웹 등이라 부른다.
Tor 네트워크는 딥 웹의 한 종류이며 전용 브라우저인 Tor Browser를 사용하여 접속할 경우 여러 프록시 서버들을 거치며 그 내용 또한 암호화되어 전송되기 때문에 익명으로 인터넷을 이용할 수 있고, warning.or.kr처럼 특정 국가에서 차단해 놓은 사이트들을 접근할 수도 있다. Tor 네트워크에 존재하는 웹 사이트들은 통상적인 도메인이 아닌 .onion으로 끝나는 해쉬hash 형태의 주소를 가지는데, 이번 포스팅에서는 Windows 상에서 간단하게 웹 서버를 구축하고 .onion 주소를 할당받는 방법을 소개하겠다.
1 단계. 웹 서버 구성하기
포트가 충돌할 우려가 있으니 기존 사이트를 운영하던 서버에 Deep Web 사이트를 설정하는 것보다는 Deep Web 사이트만을 위한 서버를 따로 마련하는 것이 권장된다. 그리고 이 항목은 Apache나 IIS 등의 서버를 설치하고 구성한 경우라면 건너뛰어도 된다. 여기에서는 XAMPP(Apache + MySQL + PHP + Perl)을 이용해 서버를 구성해보겠다.
Onion 사이트의 페이지는 80번 포트를 통해 Tor 네트워크로 나갈 것이다. 이는 Apache가 내보내는 포트의 기본값과 같으므로 충돌을 방지하기 위해 Apache가 사이트를 내보내는 포트를 다른 번호로 바꿔줘야 하다. 다음과 같이 XAMPP 제어판이 실행되었다면, Apache에서 [Config] 버튼 -> [Apache (httpd.conf)]를 클릭한다.
다음과 같이 XAMPP 제어판에서 8080번 포트로 웹 사이트가 게시되고 있음을 확인할 수 있다.
xampp\htdocs 폴더에 웹 사이트로 게시되는 파일들을 저장한다. 기본 제공되는 테스트 파일들은 모두 제거한 후 다음과 같은 내용의 html 파일을 작성하여 index.html로 저장한다.
구 버전의 Tor에서는 Tor Browser라는 Tor 네트워크 브라우저와 Vidalia라는 Tor 네트워크 제어판이 분리되었는데, 현재 나오는 버전의 Tor는 Tor Browser로 일원화되어있다. 따라서 Tor Browser만 종료하지 않고 실행해 두면 항상 Tor 네트워크 접속 상태가 유지될 것이다. 서버의 유동 IP/고정 IP 여부도 염려할 필요가 없다. 외부에서 접속할 때는 IP가 아닌 onion 주소를 사용하며, IP 주소와 onion 주소의 대응은 Tor 네트워크에서 알아서 해 줄 것이다.
Tor Browser가 설치된 경로(\Tor Browser\Browser\TorBrowser\Data\Tor)에 들어가면 torrc라는 0 바이트짜리 파일이 보일 것이다. Tor Browser를 처음으로 실행한 후 이 파일을 다시 찾을 것이다.
설치된 경로(\Tor Browser\Start Tor Browser.lnk)를 실행한다.
3 단계. 포트 설정하기
앞서 확인했던 경로(\Tor Browser\Browser\TorBrowser\Data\Tor)를 다시 열면 0 바이트였던 torrc 파일의 크기가 늘어난 것을 확인할 수 있다. 텍스트 편집기로 이를 열어서 내용을 확인한다.
아래의 두 줄을 새로 추가한다. 첫 번째 줄은 xampp로 게시되는 로컬 경로를 Tor 네트워크에게 알려주는 것이고, 두 번째 줄은 서버의 아이피와 xampp로 게시되는 일반 웹의 포트번호(여기서는 8080)를 딥 웹으로는 80번 포트로 재게시 하는 것이다.
HiddenServiceDir 사이트의로컬경로
HiddenServicePort 80 서버의아이피:서버의포트번호
Tor Browser를 다시 실행한다. 이 때부터는 htdocs의 로컬 경로가 하나의 Deep Web 사이트로서 Tor 네트워크에 내보내지고 있으므로 닫으면 외부에서 접속이 안 된다.
4 단계. onion 주소 확인하기
torrc 파일에서 HiddenServiceDir 항목으로 지정한 로컬 경로로 들어가보면 몇 개의 파일이 생성된 것을 볼 수 있다. 이 중 hostname 파일을 열어보면 이 사이트의 onion 주소를 확인할 수 있다.
Tor Browser를 실행하고 약 1분이 지난 후 이 주소를 접속하면 일반 웹 브라우저에서 http://localhost:8080로 보았던 것과 동일한 웹 페이지를 볼 수 있다. 즉, 외부 사용자도 이 onion 주소를 통해 같은 페이지를 보고 있다는 뜻이다. 이렇게 해서 하나의 Deep Web 사이트 구성이 완료되었다.
마무리
이번 포스팅에서는 Tor Browser와 xampp를 활용하여 딥 웹 사이트를 운영하는 서버를 구축하여 보았다. 다음 포스팅에서는 커스텀 onion 주소를 할당받는 방법에 대해 설명하겠다.
WM_PAINT(MFC: OnPaint) 이벤트는 창의 내용을 그릴 때 호출되는 이벤트이다. 이 이벤트에 DirectX 9.0을 적용하여 보겠다.
IDirect3DDevice9::TestCooperativeLevel
로스트 상태는, DirectX 객체와 실제 그래픽 어댑터 사이의 연결이 끊어진 상태를 뜻한다. DirectX 객체가 생성된 시점과 이 객체를 이용하여 실제로 그리는 시각 시점의 상대적으로 긴 시간이 있는 경우 이 사이에 여러가지 이유(예: 로그오프, 절전모드 등)로 객체와 장치간 연결이 끊어질 수 있는데 이는 IDirect3DDevice9::TestCooperativeLevel함수로 체크할 수 있다.
만일 DirectX와 장치간 연결이 정상이 아닐 경우 그 원인에 따라 D3DERR_DRIVERINTERNALERROR, D3DERR_DEVICELOST, D3DERR_DEVICENOTRESET 중 하나를 반환하는데, 이 때는 IDirect3DDevice9::CreateDevice에 사용되었던 D3DPRESENT_PARAMETERS 구조체를 다시 사용하여 IDirect3DDevice9::Reset 함수를 실행하여 장치와 객체의 연결을 리셋한다.
/* C++ Source */
IDirect3DDevice * pDirect3DDevice;
D3DPRESENT_PARAMETERS d3dPresentParameters;
HRESULT hResult;
// ...
hResult = pDirect3DDevice->TestCooperativeLevel();
switch (hResult)
{
case D3DERR_DRIVERINTERNALERROR:
pDirect3DDevice->Reset(&d3dPresentParameters); // D3DPRESENT_PARAMETERS *
// Reset 실패 시 각종 조치
break;
case D3DERR_DEVICELOST:
// Reset 실패 시 각종 조치
pDirect3DDevice->Reset(&d3dPresentParameters); // D3DPRESENT_PARAMETERS *
break;
case D3DERR_DEVICENOTRESET:
// Reset 실패 시 각종 조치
pDirect3DDevice->Reset(&d3dPresentParameters); // D3DPRESENT_PARAMETERS *
break;
}
IDirect3DDevice9::Clear
장치의 로스트 상태 확인 및 조치를 완료하였다면 지정된 위치의 버퍼를 특정 색상으로 지운다.
/* C++ Source */
IDirect3DDevice * pDirect3DDevice;
D3DPRESENT_PARAMETERS d3dPresentParameters;
DWORD dwRects;
D3DRECT * pRects;
HRESULT hResult;
// ...
hResult = pDirect3DDevice->Clear
(
0, // DWORD Count : pRects 배열의 원소 수
NULL, // const D3DRECT * pRects
D3DCLEAR_TARGET, //
D3DCOLOR_XRGB(0x00, 0xFF, 0xFF), // D3DCOLOR Color : 리셋 후 버퍼의 색상
0.0f, // float Z
0 // DWORD Stencil
);
if (FAILED(hResult))
{
// Clear 실패 시 각종 조치
}
DirectX 9.0을 시작하기 위해 객체의 생성을 하고 창을 닫을 때의 해제는 다음과 같이 한다. DirectX 객체는 COMComponent Object Model으로 제공되므로 C++ 언어를 사용한다.
Direct3DCreate9, IDirect3D9::Release
DirectX 9.0의 객체 생성은 Direct3DCreate9 함수를 실행한다. 성공하면 LPDIRECT3D9(struct IDirect3D9 *) 형식의 객체를 반환하고, 실패하면 NULL을 반환한다.
/* C++ Source */
#pragma comment(lib, "d3d9.lib") // DirectX 9.0 라이브러리를 링크
#include <Windows.h> // Windows API에서 제공되는 기본 자료형들을 불러오기
#include <d3d9.h> // DirectX 9.0 API를 불러오기
LPDIRECT3D9 lpDirect3D = (LPDIRECT3D9)NULL; // DirectX 9.0 객체
/* 창을 생성하거나 그래픽을 리셋하는 단계에서 */
lpDirect3D = Direct3DCreate9(D3D_SDK_VERSION); // DirectX 9.0 버전으로 객체를 생성한다.
if (!lpDirect3D)
{
// 생성에 실패한 경우 실행 작업
}
/* 생성에 성공한 경우 실행 작업 */
DirectX 9.0 객체의 해제는 IDirect3D9::Release를 호출한다.
/* C++ Source */
#pragma comment(lib, "d3d9.lib") // DirectX 9.0 라이브러리를 링크
#include <Windows.h> // Windows API에서 제공되는 기본 자료형들을 불러오기
#include <d3d9.h> // DirectX 9.0 API를 불러오기
LPDIRECT3D9 lpDirect3D = (LPDIRECT3D9)NULL; // DirectX 9.0 객체
/* 창을 닫거나 그래픽을 종료하는 단계에서 */
lpDirect3D->Release(); // 객체 해제
/* 이후 작업 */
실제 코드에 적용
다음은 MFC에서 DirectX 객체를 생성하고 해제하는 예이다. CTestFrame은 CWndFrame에서 상속된 클래스이고 m_pDirect3D는 사전에 클래스 헤더에 선언된 멤버 변수이다.
/* MFC Source */
int CTestFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
HRESULT hResult = NULL;
int ret = 0;
TRACE(TEXT("[SUCCESS] CTestFrame::OnCreate called.\n"));
ret = CFrameWnd::OnCreate(lpCreateStruct);
if (ret != 0)
{
TRACE(TEXT("[FAILURE] CTestFrame::OnCreate.\n"));
return ret;
}
this->m_pDirect3D = Direct3DCreate9(D3D_SDK_VERSION);
if (this->m_pDirect3D == NULL)
{
TRACE(TEXT("[FAILURE] Direct3DCreate9.\n"));
return ret;
}
return ret;
}
void CTestFrame::OnDestroy()
{
TRACE(TEXT("[SUCCESS] CTestFrame::OnDestroy called.\n"));
if (this->m_pDirect3D != NULL)
{
this->m_pDirect3D->Release();
}
CFrameWnd::OnDestroy();
}
어댑터 종류 열거하기
DirectX를 사용하기 위해 컴퓨터에서 지원 가능한 그래픽 어댑터의 종류를 모두 가져오려면 CreateDXGIFactory 함수로 생성된 IDXGIFactory형 객체를 사용한다.
CreateDXGIFactory
/* C++ Source */
#pragma comment(lib, "d3d9.lib")
#pragma comment(lib, "dxgi.lib")
#include <d3d9.h>
#include <dxgi.h>
IDXGIFactory * pDXGIFactory = NULL;
IDXGIAdapter * pDXGIAdapter = NULL;
SIZE_T nDXGIAdapter = 0;
/* 창을 생성하거나 그래픽을 리셋하는 단계에서 */
hResult = CreateDXGIFactory
(
__uuidof(IDXGIFactory), // REFIID riid
reinterpret_cast<void **>(&pDXGIFactory) // void ** ppFactory
);
// 해당 인덱스 번호에 해당하는 어댑터가 없다면 DXGI_ERROR_NOT_FOUND를 반환하기 시작한다.
// 0번 어댑터부터 검색을 시작한다.
nDXGIAdapter = 0;
while (pDXGIFactory->EnumAdapter(nDXGIAdapter, &pDXGIAdapter) != DXGI_ERROR_NOT_FOUND)
{
nDXGIAdapter++; // 다음 번호의 어댑터를 확인한다.
}
실제 코드에 적용
그래픽 어댑터의 종류를 열거하는 위 코드를 응용하여 디버그 문자열로 그래픽 어댑터의 이름을 출력해보겠다.
/* MFC Source */
int CTestFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
HRESULT hResult = NULL;
int ret = 0;
TRACE(TEXT("[SUCCESS] CTestFrame::OnCreate called.\n"));
ret = CFrameWnd::OnCreate(lpCreateStruct);
if (ret != 0)
{
TRACE(TEXT("[FAILURE] OnCreate::OnCreate.\n"));
return ret;
}
// DirectX 객체를 생성
this->m_pDirect3D = Direct3DCreate9(D3D_SDK_VERSION);
if (this->m_pDirect3D == NULL)
{
TRACE(TEXT("[FAILURE] Direct3DCreate9.\n"));
return ret;
}
// 어댑터를 열거할 수 있는 IDXGIFactory 객체 생성
IDXGIFactory * pDXGIFactory = NULL;
hResult = CreateDXGIFactory
(
__uuidof(IDXGIFactory), // REFIID riid
reinterpret_cast<void **>(&pDXGIFactory) // void ** ppFactory
);
if (FAILED(hResult))
{
TRACE(TEXT("[FAILURE] CreateDXGIFactory.\n"));
return ret;
}
// 열거된 어댑터를 하나씩 배열에 보관
CArray<IDXGIAdapter *> dxgiAdapters;
IDXGIAdapter * pDXGIAdapter = NULL;
UINT i = 0;
while (TRUE)
{
hResult = pDXGIFactory->EnumAdapters
(
i++, // UINT Adapter
&pDXGIAdapter // IDXGIAdapter ** ppAdapter
);
if (hResult == DXGI_ERROR_NOT_FOUND) break;
dxgiAdapters.Add(pDXGIAdapter);
pDXGIAdapter = NULL;
}
// 배열에 보관된 어댑터의 이름을 디버그 문자열로 출력
for (INT_PTR i = 0; i < dxgiAdapters.GetCount(); i++)
{
DXGI_ADAPTER_DESC dxgiAdapterDesc;
TCHAR szBuffer[256];
dxgiAdapters[i]->GetDesc(&dxgiAdapterDesc);
TRACE(TEXT("[%2d] DXGI_ADAPTER_DESC.Description = %s\n"), i, dxgiAdapterDesc.Description);
}
// 사용이 끝난 어댑터 객체와 Factory 객체 해제
for (INT_PTR i = 0; i < dxgiAdapters.GetCount(); i++)
{
dxgiAdapters[i]->Release();
}
dxgiAdapters.RemoveAll();
pDXGIFactory->Release();
return ret;
}
본 컴퓨터에 장착된 그래픽 어댑터의 종류가 열거되는 것을 확인할 수 있다. 다만 [장치 관리자]에는 없는 Microsoft Basic Render Driver가 마지막에 있을 수도 있다.
그래픽 어댑터의 색상 포맷의 지원 여부 확인
ARGB, RGB, HSV 등등의 색상 포맷을 사용하고자 할 때 그래픽 어댑터가 이를 지원하는지 여부를 확인한다. 여기서 XRGB는 RGB와 같은 것을 의미한다.
IDirect3D9::EnumAdapterModes
/* C++ Source */
#pragma comment(lib, "d3d9.lib")
#pragma comment(lib, "dxgi.lib")
#include <d3d9.h>
#include <dxgi.h>
LPDIRECT3D9 lpDirect3D = (LPDIRECT3D9)NULL; // DirectX 9.0 객체
UINT uAdapterMode = 0;
/* 창 또는 그래픽을 리셋하는 단계에서 */
uAdapterMode = lpDirect3D->GetAdapterModeCount
(
D3DADAPTER_DEFAULT, // UINT Adapter : 시스템 기본 어댑터를 사용하겠다
D3DFMT_X8R8G8B8 // D3DFORMAT Format : 픽셀당 32비트 색상 포맷을 쓴다. 단, 실제 사용되는 비트는 R, G, B 24비트이다.
);
IDirect3D9::EnumAdapterModes 함수는 그래픽 어댑터가 지원 가능한 모드를 상세히 열거하는 함수이다. 지원 모드는 D3DDISPLAYMODE 구조체를 통해 반환된다.
/* C++ Source */
UINT uAdapterMode = this->m_pDirect3D->GetAdapterModeCount
(
D3DADAPTER_DEFAULT, // UINT Adapter
D3DFMT_X8R8G8B8 // D3DFORMAT Format
);
for (UINT i = 0; i < uAdapterMode; i++)
{
D3DDISPLAYMODE d3dDisplayMode;
hResult = this->m_pDirect3D->EnumAdapterModes
(
D3DADAPTER_DEFAULT, // UINT Adapter
D3DFMT_X8R8G8B8, // D3DFORMAT Format
i, // UINT Mode : 어댑터에 대해 얻고자 하는 모드의 번호
&d3dDisplayMode // D3DDISPLAYMODE * pMode : 반환 정보
);
}
실제 코드에 적용
기본 어댑터(D3DADAPTER_DEFAULT)의 RGB 컬러(D3DFMT_X8R8G8B8)로 몇 가지의 어댑터 모드가 지원 가능한지 조회해 보겠다. 실행 결과는 컴퓨터마다 상이하다.
/* MFC Source */
int CTestFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
HRESULT hResult = NULL;
int ret = 0;
TRACE(TEXT("[SUCCESS] CTestFrame::OnCreate called.\n"));
ret = CFrameWnd::OnCreate(lpCreateStruct);
if (ret != 0)
{
TRACE(TEXT("[FAILURE] OnCreate::OnCreate.\n"));
return ret;
}
this->m_pDirect3D = Direct3DCreate9(D3D_SDK_VERSION);
if (this->m_pDirect3D == NULL)
{
TRACE(TEXT("[FAILURE] Direct3DCreate9.\n"));
return ret;
}
IDXGIFactory * pDXGIFactory = NULL;
hResult = CreateDXGIFactory
(
__uuidof(IDXGIFactory), // REFIID riid
reinterpret_cast(&pDXGIFactory) // void ** ppFactory
);
if (FAILED(hResult))
{
TRACE(TEXT("[FAILURE] CreateDXGIFactory.\n"));
return ret;
}
UINT uAdapterMode = this->m_pDirect3D->GetAdapterModeCount
(
D3DADAPTER_DEFAULT, // UINT Adapter
D3DFMT_X8R8G8B8 // D3DFORMAT Format
);
TRACE(TEXT("[SUCCESS] GetAdapterModeCount = %u\n"), uAdapterMode);
for (UINT i = 0; i < uAdapterMode; i++)
{
D3DDISPLAYMODE d3dDisplayMode;
hResult = this->m_pDirect3D->EnumAdapterModes
(
D3DADAPTER_DEFAULT, // UINT Adapter
D3DFMT_X8R8G8B8, // D3DFORMAT Format
i, // UINT Mode : 어댑터에 대해 얻고자 하는 모드의 번호
&d3dDisplayMode // D3DDISPLAYMODE * pMode : 반환 정보
);
if (SUCCEEDED(hResult))
{
TRACE(TEXT("[%2u] EnumAdapterModes\n"), i);
TRACE(TEXT(" D3DDISPLAYMODE.Format = %x\n"), d3dDisplayMode.Format);
TRACE(TEXT(" D3DDISPLAYMODE.RefreshRate = %u\n"), d3dDisplayMode.RefreshRate);
TRACE(TEXT(" D3DDISPLAYMODE.Width = %u\n"), d3dDisplayMode.Width);
TRACE(TEXT(" D3DDISPLAYMODE.Height = %u\n"), d3dDisplayMode.Height);
}
}
return ret;
}
어댑터 장치의 성능 확인하기
장치의 성능 중에서 특히 자주하게 확인할 것은 하드웨어로 버텍스 처리가 가능한지 여부이다. 하드웨어에서 버텍스 프로세싱이 가능하면 그대로 이용하고, 그렇지 않을 경우 소프트웨어(CPU)로 버텍스를 연산해야 한다. 이 경우 체감 속도가 다소 느릴 수 있다.
IDirect3D9::GetDeviceCaps
IDirect3D9::GetDeviceCaps 함수가 반환하는 구조체인 D3DCAPS9의 DevCaps 멤버를 통해 확인이 가능하며 D3DDEVCAPS_HWTRANSFORMANDLIGHT 비트의 설정 여부로 하드웨어의 버텍스 처리 성능을 판단할 수 있다.
/* C++ Source */
D3DCAPS9 d3dCaps;
ZeroMemory(&d3dCaps, sizeof(D3DCAPS9));
hResult = this->m_pDirect3D->GetDeviceCaps
(
D3DADAPTER_DEFAULT, // UINT Adapter
D3DDEVTYPE_HAL, // D3DDEVTYPE DeviceType
&d3dCaps // D3DCAPS9 * pCaps
);
if (FAILED(hResult))
{
// 장치 정보 얻기에 실패시 조치
}
if ((d3dCaps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT) != 0)
{
// 하드웨어로 버텍스 처리가 가능하다.
}
else
{
// 소프트웨어로 버텍스 처리가 가능하다. (속도 느림)
}
실제 코드에 적용
실제 코드에 적용하여 보겠다.
/* MFC Source */
int CTestFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
HRESULT hResult = NULL;
int ret = 0;
TRACE(TEXT("[SUCCESS] CTestFrame::OnCreate called.\n"));
ret = CFrameWnd::OnCreate(lpCreateStruct);
if (ret != 0)
{
TRACE(TEXT("[FAILURE] OnCreate::OnCreate.\n"));
return ret;
}
this->m_pDirect3D = Direct3DCreate9(D3D_SDK_VERSION);
if (this->m_pDirect3D == NULL)
{
TRACE(TEXT("[FAILURE] Direct3DCreate9.\n"));
return ret;
}
D3DCAPS9 d3dCaps;
ZeroMemory(&d3dCaps, sizeof(D3DCAPS9));
hResult = this->m_pDirect3D->GetDeviceCaps
(
D3DADAPTER_DEFAULT, // UINT Adapter
D3DDEVTYPE_HAL, // D3DDEVTYPE DeviceType
&d3dCaps // D3DCAPS9 * pCaps
);
if (FAILED(hResult))
{
TRACE(TEXT("[FAILURE] IDirect3D9::GetDeviceCaps = %u"), hResult);
return ret;
}
if ((d3dCaps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT) != 0)
{
TRACE(TEXT("[DEVCAPS] D3DCAPS9.DevCaps has D3DDEVCAPS_HWTRANSFORMANDLIGHT\n"));
}
else
{
TRACE(TEXT("[DEVCAPS] D3DCAPS9.DevCaps not D3DDEVCAPS_HWTRANSFORMANDLIGHT\n"));
}
return ret;
}
IDirect3DDevice9 객체 생성
그래픽 어댑터를 호출할 수 있는 장치 인터페이스인 IDirect3DDevice9 객체를 생성한다. 앞서 확인한 각종 하드웨어 사항과 본인이 선정한 색상 포맷, 기타 필요한 사항들을 조합하여 하나의 컨텍스트를 만든 다음 이를 IDirect3D9::CreateDevice함수에 D3DPRESENT_PARAMETERS 구조체로 전달한다.
전통적인 배열 반복문은 역시 인덱스 변수를 선언하고 그 변수를 증감하면서 배열의 원소에 순차적으로 접근하는 방식이다.
/* C++ source */
#include <array> // std::array
#include <iostream> // std::cout
using namespace std;
int main(int argc, char * argv[])
{
std::array<int, 10> integers = { 2, 4, 6, 8, 0, 9, 7, 5, 3, 1 };
for (size_t i = 0; i < integers.size(); i++)
{
cout<<integers[i]<<'\t';
}
return 0;
}
2. 반복자 사용하기
STL Container 클래스내의 반복자(iterator)를 자동변수로 선언하여 배열의 원소에 순차적으로 접근할 수도 있다. 여기에서 반복자의 자료형은 std::array<int, 10>::iterator이지만 작성의 편의를 위해 auto 키워드로 대체하여 반복자를 선언하는 것이 보통이다.
OpenCL은 실행을 위한 세팅 과정이 너무 많다. 따라서 본 포스팅을 통해 OpenCL을 실행하는 예시 코드를 정리해 둔다.
0 단계. 사용 가능한 플랫폼 개수 확인하기.
OpenCL을 사용할 수 있는 플랫폼(CPU와 GPU 등 디바이스 조합)이 총 몇 개인지를 확인하기 위해 clGetPlatformIDs 함수를 사용한다. 2 번째 매개변수인 cl_platform_id * platforms에 NULL을 대입하면 플랫폼의 총 개수가 반환된다. 이 때 플랫폼의 개수는 1 번째 매개변수인 cl_uint num_entries 이하로 반환한다. 그러므로 최대의 개수를 얻고자 하면 1 번째 매개변수에 -1을 대입하면 된다. 이 매개변수의 자료형은 부호 없는 정수(cl_uint)이기 때문에 -1은 0xFFFF, 0xFFFFFFFF, 0xFFFFFFFFFFFFFFFF 중에 하나이고 부호 없는 정수의 최댓값과 같다.
사용 가능한 플랫폼의 수는 3 번째 매개변수인 cl_uint * num_platforms으로 반환된다. 함수 자체가 반환하는 값은 함수의 실행 결과로서, 정상 작동했다면 CL_SUCCESS를 반환한다.
/* C source */
// 새로 추가된 변수
cl_int errNo;
cl_uint nPlatformId = 0;
cl_platform_id * lpPlatformId = NULL;
/* ... */
// 0 단계. 시스템으로부터 OpenCL이 사용 가능한 플랫폼의 수를 확인한다.
errNo = clGetPlatformIDs
(
(cl_uint)(-1), // cl_uint num_entries
NULL, // cl_platform_id * platforms
&nPlatformId // cl_uint * num_platforms
);
if (errNo != CL_SUCCESS)
{
printf("[FAILURE] clGetPlatformIDs = %d\n", errNo);
// 실패 시 작업
}
else if (nPlatformId <= 0)
{
printf("[FAILURE] clGetPlatformIDs: nPlatformId = %u", nPlatformId);
// 실패 시 작업
}
else
{
printf("[SUCCEED] clGetPlatformIDs: nPlatformId = %u\n", nPlatformId);
}
/* ... */
1 단계. 사용 가능한 플랫폼 목록 가져오기.
앞서 0 단계에서 플랫폼 수를 확인하였다면, 메모리를 동적할당하여 플랫폼 목록을 가져온다. 마찬가지로 clGetPlatformIDs를 사용한다.
컨텍스트에는 CPU, GPU를 포함한 가속 장치들이 포함되어 있다. 현재 컨텍스트에 몇 개의 장치들이 있고, 각각의 장치가 무엇인지 확인하기 위해 다음과 같이 코드를 작성한다.
3-1. 장치 정보를 얻기 위한 버퍼의 크기 구하기
컨텍스트에 포함된 장치들의 목록을 얻기 위해 총 몇 바이트의 버퍼가 필요한지 확인하는 과정이다. clGetContextInfo를 사용하며 매개변수에 param_value_size = 0, param_value = NULL을 하고 param_value_size_ret =&변수를 하면 변수를 통해 메모리의 크기가 반환된다.
/* C source */
cl_int errNo;
cl_uint nPlatformId = 0;
cl_platform_id * lpPlatformId = NULL;
size_t i, j;
cl_uint nContextProperties = 0;
cl_context_properties * lpContextProperties = NULL;
cl_context context;
size_t cbDeviceBuffer = 0;
size_t nDeviceId = 0;
cl_device_id * lpDeviceId = NULL;
size_t nCommandQueue = 0;
cl_command_queue * lpCommandQueue = NULL;
cl_program program;
/* ... */
errNo = clBuildProgram
(
program, // cl_program program : 컴파일하고자 하는 프로그램 객체
0, // cl_uint num_devices : 컴파일 대상이 될 장치의 개수 (0개 = 컨텍스트에 있는 기본 장치들)
NULL, // const cl_device_id * device_list : 컴파일 대상이 될 장치 (NULL = 컨텍스트에 있는 기본 장치들)
NULL, // const char * options : 빌드 옵션
NULL, // void (*pfn_notify)(cl_program, void * user_data) : 콜백 함수
NULL // void * user_data : 콜백함수에 전달할 사용자 데이터
);
if (errNo != CL_SUCCESS)
{
printf("[FAILURE] clBuildProgram == %d\n", errNo);
// 실패 시 작업
}
else
{
printf("[SUCCESS] clBuildProgram\n");
}
/* ... */
clReleaseProgram(program); // 프로그램 객체 해제
free(lpDeviceId); // 장치 버퍼 메모리 해제
clReleaseContext(context); // 컨텍스트를 해제한다.
free(lpContextProperties); // 컨텍스트 속성 배열을 해제한다.
free(lpPlatformId); // 플랫폼 구조체 동적할당을 해제한다.
5 단계. OpenCL에서 선언한 함수 실행하기
커널kernel은 OpenCL 소스에서 선언한 각 함수들의 진입점에 해당하는 개념이다. 앞서 컴파일한 OpenCL 소스에서 특정 함수의 이름을 통해 그 커널을 가져와 호출한다. 함수가 매개변수를 필요로 한다면 메모리 객체를 별도로 구성해야 한다.
5-1 단계. 커널 얻기
hello.cl 소스 파일에서 선언한 한 함수 hello를 호출해보도록 하겠다. 이를 위해 커널 객체를 생성한다.
/* C source */
cl_int errNo;
cl_uint nPlatformId = 0;
cl_platform_id * lpPlatformId = NULL;
size_t i, j;
cl_uint nContextProperties = 0;
cl_context_properties * lpContextProperties = NULL;
cl_context context;
size_t cbDeviceBuffer = 0;
size_t nDeviceId = 0;
cl_device_id * lpDeviceId = NULL;
size_t nCommandQueue = 0;
cl_command_queue * lpCommandQueue = NULL;
cl_program program;
// 새로 추가된 변수
cl_kernel kernel;
/* ... */
kernel = clCreateKernel
(
program, // cl_program program
"hello", // const char * kernel_name
&errNo // cl_int * errcode_ret
);
if (errNo != CL_SUCCESS)
{
printf("[FAILURE] clCreateKernel = %d\n", errNo);
// 실패 시 작업
}
else if (kernel == NULL)
{
printf("[FAILURE] clCreateKernel: kernel = %p\n", kernel);
// 실패 시 작업
}
else
{
printf("[SUCCESS] clCreateKernel\n");
}
/* ... */
clReleaseKernel(kernel); // 커널 객체 해제
clReleaseProgram(program); // 프로그램 객체 해제
free(lpDeviceId); // 장치 버퍼 메모리 해제
clReleaseContext(context); // 컨텍스트를 해제한다.
free(lpContextProperties); // 컨텍스트 속성 배열을 해제한다.
free(lpPlatformId); // 플랫폼 구조체 동적할당을 해제한다.
5-2 단계. 매개변수 구성하기
OpenCL 소스에서 선언한 hello 함수에서는 float * 형식의 3개의 매개변수(out, ptr1, ptr2)가 필요하다. 이를 구성해보겠다.
먼저 다음과 같이 테스트 배열을 마련한다. 하나는 1부터 50까지이고 다른 하나는 51부터 100까지의 각 50개의 실수로 구성된 배열이다.
/* C source */
float out[50] = { 0.0f, };
float ptr1[50] = { 0.0f, };
float ptr2[50] = { 0.0f, };
size_t i = 0;
for (i = 0; i < 50; i++)
{
ptr1[i] = (1.0f + (float)i);
ptr2[i] = (51.0f + (float)i);
}
C의 배열을 OpenCL 소스에서 작성한 함수로 보내기 위해서는 메모리 객체를 생성한다.
/* C source */
cl_int errNo;
cl_uint nPlatformId = 0;
cl_platform_id * lpPlatformId = NULL;
size_t i, j;
cl_uint nContextProperties = 0;
cl_context_properties * lpContextProperties = NULL;
cl_context context;
size_t cbDeviceBuffer = 0;
size_t nDeviceId = 0;
cl_device_id * lpDeviceId = NULL;
size_t nCommandQueue = 0;
cl_command_queue * lpCommandQueue = NULL;
cl_program program;
cl_kernel kernel;
// 새로 추가된 변수
cl_mem memOut;
cl_mem memPtr1;
cl_mem memPtr2;
/* ... */
memOut = clCreateBuffer
(
context, // cl_context context : 배열을 사용할 컨텍스트
CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, // cl_mem_flags flags : 각종 속성
sizeof(out), // size_t size : 배열의 크기 (단위 = 바이트)
out, // void * host_ptr : 배열
&errNo // cl_int * errcode_ret : 오류 코드
);
if (errNo != CL_SUCCESS)
{
printf("[FAILURE] clCreateBuffer = %d", errNo);
// 실패 시 작업
}
else if (memOut == NULL)
{
printf("[FAILURE] clCreateBuffer: memOut = %p", memOut);
// 실패 시 작업
}
else
{
printf("[SUCCESS] clCreateBuffer\n");
}
memOut = clCreateBuffer
(
context, // cl_context context
CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, // cl_mem_flags flags
sizeof(ptr1), // size_t size
ptr1, // void * host_ptr
&errNo // cl_int * errcode_ret
);
if (errNo != CL_SUCCESS)
{
printf("[FAILURE] clCreateBuffer = %d", errNo);
// 실패 시 작업
}
else if (memOut == NULL)
{
printf("[FAILURE] clCreateBuffer: memOut = %p", memOut);
// 실패 시 작업
}
else
{
printf("[SUCCESS] clCreateBuffer\n");
}
memOut = clCreateBuffer
(
context, // cl_context context
CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, // cl_mem_flags flags
sizeof(ptr2), // size_t size
ptr2, // void * host_ptr
&errNo // cl_int * errcode_ret
);
if (errNo != CL_SUCCESS)
{
printf("[FAILURE] clCreateBuffer = %d", errNo);
// 실패 시 작업
}
else if (memOut == NULL)
{
printf("[FAILURE] clCreateBuffer: memOut = %p", memOut);
// 실패 시 작업
}
else
{
printf("[SUCCESS] clCreateBuffer\n");
}
/* ... */
clReleaseMemObject(memOut); // float out[50]에 대한 메모리 객체 해제
clReleaseMemObject(memPtr1); // float ptr1[50]에 대한 메모리 객체 해제
clReleaseMemObject(memPtr2); // float ptr2[50]에 대한 메모리 객체 해제
clReleaseKernel(kernel); // 커널 객체 해제
clReleaseProgram(program); // 프로그램 객체 해제
free(lpDeviceId); // 장치 버퍼 메모리 해제
clReleaseContext(context); // 컨텍스트를 해제한다.
free(lpContextProperties); // 컨텍스트 속성 배열을 해제한다.
free(lpPlatformId); // 플랫폼 구조체 동적할당을 해제한다.
5-3 단계. 커널에 매개변수 전달하기
함수 진입점을 '커널'의 형태로 얻었고, 함수에 전달할 매개변수도 '메모리'의 형태로 구성하였다. 이제 커널과 매개변수를 연계해보겠다. OpenCL 소스에서 (out, ptr1, ptr2)의 순서로 선언하였으므로 이 순서대로 메모리 객체를 커널에 연계한다. 이 때 쓰이는 함수는 clSetKernelArg이다.