2018년 9월 27일 목요일

코드 리뷰







BoB 에서 프로젝트 멘토링을 하면서 코드 리뷰를 하다가, 기록을 남겨두면 좋을것 같아서 적어봅니다.
아래 함수는 특정 레지스트리 키 경로를 입력받고, 키 내부의 값을 읽어서 출력하는 함수로 보이네요.

int Reg_Read(const char* subkey, TCHAR* value) { LONG ret; DWORD data_size = 1024; DWORD data_type; TCHAR* data_buffer = (TCHAR*)malloc(data_size); HKEY hKey; RtlZeroMemory(data_buffer, sizeof(data_buffer)); ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, subkey, 0, KEY_ALL_ACCESS, &hKey); if (ret != ERROR_SUCCESS) { printf("RegOpenKey Failed! \n "); return 0; } //memset(data_buffer, 0, sizeof(data_buffer)); //data_size = sizeof(data_buffer); RegQueryValueEx(hKey, "UninstallString", 0, &data_type, (LPBYTE)data_buffer, (DWORD*)&data_size); RegCloseKey(hKey); value = data_buffer; printf("Value : %s\n", value); free(data_buffer); data_buffer = NULL; return 1; } int main() { TCHAR* value = nullptr; const char* subkey = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{19DD1D8D-927F-45DF-ADF4-75D38267848D}"; Reg_Read(subkey, value); printf("Value : %s\n", value); getchar(); }
몇 가지 고쳤으면 하는 점들이 보이네요.
  • 리턴 값의 의미가 모호하다.
  • 입력값 검증이 없다.
  • 변수의 사용
    • 사용하는 시점에 선언하자.
    • 변수는 선언시 초기화하자.
  • 리소스 관리
    • 할당한 메모리는 반드시 소멸하자.
    • 시스템 리소스 (레지스트리 키 핸들 등)는 반드시 반환해야 한다.
  • 유니코드 함수와 안시코드 함수, 변수를 구분하자.
    • char* 변수를 입력으로 받는 함수는 RegOpenKeyExA()
    • wchar* 변수를 입력으로 받는 함수는 RegOpenKeyExW()
    • 귀찮으면 TCHAR* 타입의 변수를 사용하고, RegOpenKeyEx()
    • 당연히 printf 도 _tprintf() 를 사용해야 합니다.
    • char*, wchar* 를 정확히 인지하지 않은상태에서 혼용하면 안됨
  • 에러로그를 남길때는 최소한 어떤 이유로 에러가 났는지 추적에 필요한 단서를 남기자.
    • 리턴값 또는 GetLastError() 코드 정도는 기록하자.
    • 에러로그는 에러 추적을 위해 남기는 것이다.
  • 리턴값이 있는 외부 함수를 호출했으면 반드시 리턴 값을 확인하자.
  • 함수 설계 상의 문제
    • value 값이 output 파라미터로 사용되고 있으나
    • value 값의 타입을 어디에서도 정확히 명시 하지 않고 있다(string 인지, dword 인지).
    • 전체적인 느낌으로는 string 타입이라고 가정하고 작성한 것 같네요.
  • 아주 심각한 문제인데, 소멸된 메모리에 접근하는 문제가 있습니다.
    • out-parameter 로 사용된 value 가 가리키는 포인터는 이미 소멸되었습니다.
정도가 될 것 같습니다.
int Reg_Read(const char* subkey, TCHAR* value) { LONG ret; DWORD data_size = 1024; DWORD data_type; TCHAR* data_buffer = (TCHAR*)malloc(data_size); HKEY hKey; RtlZeroMemory(data_buffer, sizeof(data_buffer)); ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, subkey, 0, KEY_ALL_ACCESS, &hKey); if (ret != ERROR_SUCCESS) { printf("RegOpenKey Failed! \n "); // // 이렇게 리턴해버리면 data_buffer 에 할당한 메모리는 해제하지 못하고 // 프로그램이 종료될 때 까지 자원만 차지하고 있게됩니다. // 이건 아주 심각한 문제입니다. // return 0; } //memset(data_buffer, 0, sizeof(data_buffer)); //data_size = sizeof(data_buffer); RegQueryValueEx(hKey, "UninstallString", 0, &data_type, (LPBYTE)data_buffer, (DWORD*)&data_size); RegCloseKey(hKey); // // value 에 포인터를 할당해 두고, free 해버리면 value 는 // 더이상 유효한 메모리를 가리키고 있지 않게됩니다. // value = data_buffer; printf("Value : %s\n", value); free(data_buffer); data_buffer = NULL; return 1; } int main() { TCHAR* value = nullptr; const char* subkey = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{19DD1D8D-927F-45DF-ADF4-75D38267848D}"; // // Reg_Read() 가 실패했다면 value 는 여전히 nullptr 입니다. // Reg_Read(subkey, value); // // value 는 // - Reg_Read() 가 성공했다면 이미 유효한 포인터가 아니므로 펑펑펑~~ // - Reg_Read() 가 실패했다면 nullptr 이니까 당연히 펑펑펑~~ // 어쨌거나 펑펑펑~ ㅅㅂ // printf("Value : %s\n", value); getchar(); // // 리턴값도 없어요. 아마 컴파일러에서 에러를 뿜었을텐데...(컴파일도 안해본건가요...ㅠㅠ)? // }
고쳐볼까요?
#include "stdafx.h" #define WIN32_LEAN_AND_MEAN #include #include #include #include /// @brief subkey 내부 value 값을 읽어 리턴한다. /// @param subkey 레지스트리 키 /// @param value 읽을 대상 키 이름 /// @return 성공시 true 를 리턴하고, value 포인터에 읽은 문자열 포인터를 할당 /// 호출자는 반드시 value 포인터를 소멸시켜주어야 함 bool Reg_Read( _In_z_ const TCHAR* subkey, _Outptr_ TCHAR* value ) { _ASSERTE(nullptr != subkey); _ASSERTE(nullptr == value); if (nullptr == subkey || nullptr != value) return false; HKEY hKey = NULL; DWORD ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, subkey, 0, KEY_ALL_ACCESS, &hKey); if (ret != ERROR_SUCCESS) { _tprintf(_T("RegOpenKey Failed!, ret=0x%08x\n"), ret); return false; } // // 여기서는 절대로 hKey 가 NULL 이 아니어야 합니다. // _ASSERTE(NULL != hKey); // // 이제 앞으로 어떤 일이 발생하든지간에 hKey 는 반드시 // RegCloseHandle() 로 닫아주어야 합니다. // DWORD data_size = 1024; TCHAR* data_buffer = (TCHAR*)malloc(data_size); if (nullptr == data_buffer) { _tprintf(_T("Not enough memory \n")); RegCloseKey(hKey); return false; } _ASSERTE(nullptr != data_buffer); RtlZeroMemory(data_buffer, sizeof(data_buffer)); DWORD data_type = REG_NONE; ret = RegQueryValueEx(hKey, _T("UninstallString"), 0, &data_type, (LPBYTE)data_buffer, (DWORD*)&data_size); if (ret != ERROR_SUCCESS) { _tprintf(_T("RegQueryValueEx Failed!, ret=0x%08x\n"), ret); // // 잊지 말고 핸들과 메모리를 소멸시켜 주어야 합니다. // free(data_buffer); RegCloseKey(hKey); return false; } // // value 가 string 타입이 아니라면 문제가 있을 수 있으니 // 확인해주어야 합니다. // if (data_type != REG_SZ || data_type != REG_MULTI_SZ || data_type != REG_EXPAND_SZ) { _tprintf(_T("Type of value is not string. actual type=%u"), data_type); // // 역시 잊지 말고 핸들과 메모리를 소멸시켜 주어야 합니다. // free(data_buffer); RegCloseKey(hKey); return false; } // // 역시 잊지 말고 핸들을 닫아주어야지요. // RegCloseKey(hKey); // // data_buffer 는 free 하면 안됍니다. // value = data_buffer; //free(data_buffer); _tprintf(_T("Value : %s\n"), value); data_buffer = NULL; return true; } int main() { TCHAR* value = nullptr; const TCHAR* subkey = _T("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{19DD1D8D-927F-45DF-ADF4-75D38267848D}"); if (true != Reg_Read(subkey, value)) { _tprintf(_T("Reg_Read() failed. \n")); return -1; } // // 여기까지 왔다면 value 는 절대 nullptr 이 아니어야 합니다 . // 그리고, value 는 잊지 말고 소멸시켜주어야 합니다. // _ASSERTE(nullptr != value); _tprintf(_T("Value : %s\n"), value); free(value); getchar(); return 0; }
수정된 내용을 보면
  • sal notation 적용
    • 코드를 읽는데 도움이 되는것 뿐만 아니라 컴파일러가 오류도 잡아줍니다.
    • 강추! 강추! Visual Studio 를 사용한다면 무조건 쓰세요.
  • _ASSERT 코드 (아무리 강조해도 지나치지 않습니다.)
  • 리턴값 체크
  • 변수 선언은 최대한 늦게, 생성즉시 초기화하기
  • char, wchar_t 를 TCHAR 로 통일
  • 리소스 릭 제거 (레지스트리 핸들, 메모리)
  • 레지스트리 value 타입 확인
  • value 포인터 잘 다루기 (ㅠ,.ㅠ)
  • 등등...
정도 입니다.
하지만 아직 많은 문제가 있습니다.
  • 실수하기 너무 쉬운 코드
    • memory 소멸을 여러군데에서 하고있고, 성공하는 경우 호출자가 value 메모리를 해제해야 합니다.
    • 코드가 길어지거나, 로직이 더 복잡해 지거나, 다른 사람이 코드를 수정한다면 실수하기 딱 좋은 코드입니다.
  • RegQueryValueEx() 함수의 사용
    • 현재는 1024 바이트로 고정된 버퍼를 사용합니다.
    • 더 긴 문자열을 읽어야 한다면?
그래서 한번 더 고쳤습니다.
#include "stdafx.h" #define WIN32_LEAN_AND_MEAN #include #include #include #include class SafeRegHandle { public: SafeRegHandle(_In_ HKEY key) : _key(key) { _ASSERTE(NULL != key); } ~SafeRegHandle() { _ASSERTE(NULL != _key); RegCloseKey(_key); } private: HKEY _key; }; /// @brief subkey 내부 value 값을 읽어 리턴한다. /// @param subkey 레지스트리 키 /// @param value 읽을 대상 키 이름 /// @return 성공시 true 를 리턴하고, value 포인터에 읽은 문자열 포인터를 할당 /// 호출자는 반드시 value 포인터를 소멸시켜주어야 함 bool Reg_Read( _In_z_ const TCHAR* subkey, _Outptr_ TCHAR* value ) { _ASSERTE(nullptr != subkey); _ASSERTE(nullptr == value); if (nullptr == subkey || nullptr != value) return false; HKEY hKey = NULL; DWORD ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, subkey, 0, KEY_ALL_ACCESS, &hKey); if (ret != ERROR_SUCCESS) { _tprintf(_T("RegOpenKey Failed!, ret=0x%08x\n"), ret); return false; } _ASSERTE(NULL != hKey); // // SafeRegHandle 객체는 함수가 리턴되는 시점에 알아서 소멸자가 호출되고 // hKey 는 SafeRegHandle 클래스의 소멸자에서 닫아줍니다. // 따라서 이제 hKey는 잊고 살면 됩니다. // SafeRegHandle safe_key(hKey); // // RegQueryValueEx() 함수는 입력받은 버퍼가 부족한 경우 ERROR_MORE_DATA 를 리턴합니다. // 만일 입력받은 버퍼가 NULL 을 입력한 경우 필요한 사이즈를 data_size 변수에 바이트 단위로 // 설정하고 ERROR_SUCCESS 를 리턴합니다. // // 참고로 가변길이의 데이터를 다루는 대부분의 Windows API 가 이런 패턴을 보입니다. // // 따라서 // 1) RegQueryValueEx( NULL, size_need ) 호출로 필요한 메모리 크기를 구하고 // 2) 메모리를 할당하고, // 3) RegQueryValueEx() 를 호출 // 데이터를 읽어옵니다. // DWORD data_type = REG_NONE; DWORD data_size = 0; char* data_buffer = nullptr; // 필요한 사이즈 구하기 ret = RegQueryValueEx(hKey, _T("UninstallString"), 0, &data_type, (LPBYTE)data_buffer, (DWORD*)&data_size); if (ret != ERROR_SUCCESS) { _tprintf(_T("RegQueryValueEx Failed!, ret=0x%08x\n"), ret); return false; } // null terminate 를 위한 여분의 메모리를 추가 할당 data_buffer = (char*)malloc(data_size + sizeof(TCHAR)); if (nullptr == data_buffer) { _tprintf(_T("Not enough memory \n")); return false; } _ASSERTE(nullptr != data_buffer); // 데이터를 읽습니다. ret = RegQueryValueEx(hKey, _T("UninstallString"), 0, &data_type, (LPBYTE)data_buffer, (DWORD*)&data_size); if (ret != ERROR_SUCCESS) { _tprintf(_T("RegQueryValueEx Failed!, ret=0x%08x\n"), ret); // // 잊지 말고, 메모리를 소멸시켜 주어야 합니다. // free(data_buffer); return false; } // // value 가 string 타입이 아니라면 문제가 있을 수 있으니 // 확인해주어야 합니다. // if (data_type != REG_SZ || data_type != REG_MULTI_SZ || data_type != REG_EXPAND_SZ) { _tprintf(_T("Type of value is not string. actual type=%u"), data_type); // // 잊지 말고, 메모리를 소멸시켜 주어야 합니다. // free(data_buffer); return false; } // // data_buffer 는 free 하면 안됍니다. // value = (TCHAR*)data_buffer; _tprintf(_T("Value : %s\n"), value); data_buffer = NULL; return true; } int main() { TCHAR* value = nullptr; const TCHAR* subkey = _T("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{19DD1D8D-927F-45DF-ADF4-75D38267848D}"); if (true != Reg_Read(subkey, value)) { _tprintf(_T("Reg_Read() failed. \n")); return -1; } // // 여기까지 왔다면 value 는 절대 nullptr 이 아니어야 합니다 . // 그리고, value 는 잊지 말고 소멸시켜주어야 합니다. // _ASSERTE(nullptr != value); _tprintf(_T("Value : %s\n"), value); free(value); getchar(); return 0; }
다시 한번 수정 된 내용을 보면
  • SafeRegHandle 클래스 추가
  • RegQueryValueEx() 함수 사용 패턴 변화
    • Windows API 사용시 아주 기본적인 사용 패턴이니까 익숙해지도록...
더 고치고 싶은것은 있지만, 이쯤에서 마무리 해야겠습니다.

2018년 3월 30일 금요일

PreviousMode (한글 번역)

MSDN 원문주소: https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/previousmode

PreviousMode

유저모드 애플리케이션이 Nt 또는 Zw 버전의 네이티브 시스템 서비스 루틴을 호출하면 시스템 콜 매커니즘이 호출 스레드를 커널모드로 트랩합니다. 매개 변수 값이 유저모드에서 시작되었음을 알리기 위해 호출자 thread object PreviousMode 필드를 UserMode 로 설정합니다. 네이티브 시스템 서비스 루틴은 매개변수가 유저모드로부터 왔는지 확인하기 위해 호출 스레드의 PreviousMode 필드를 확인합니다.
만일 커널모드 드라이버가 네이티브 시스템 서비스 루틴을 호출하고 매개 변수를 커널모드 루틴에 전달하는 경우 현재 스레드 오브젝트의 PreviousMode 필드는 반드시 KernelMode 이어야 합니다.
커널모드 드라이버는 임의의 스레드 컨텍스트에서 실행 될 수 있고, PreviousMode 필드가 UserMode로 설정될 수 있습니다. 이 경우 커널모드 드라이버는 Zw 버전의 네이티브 서비스 루틴을 호출하여 매개 변수 값이 신뢰할 수 있는, 커널모드 호출임을 알릴 수 있습니다. Zw 호출은 현재 스레드 객체의 PreviousMode 값을 겹쳐 쓰는 간단한 래퍼함수로 이동합니다. 래퍼함수는 PreviousMode  KernelMode로 설정하고 Nt 버전의 루틴을 호출합니다. Nt 버전 루틴이 리턴하면 래퍼함수는 스레드 오브젝트의 원래 PreviousMode 값으로 복원하고 반환합니다.
커널모드 드라이버는 Nt 버전의 네이티브 시스템 서비스 루틴을 직접 호출할 수 있습니다. 커널모드 드라이버가 유저모드나 커널모드에서 시작될 수 있는 I/O 요청을 처리할때, 드라이버는 Nt 버전의 루틴을 호출하여 루틴이 실행되는 동안 현재 스레드의 PreviousMode 값이 변경되지 않도록 할 수 있습니다. NtXxx 루틴은 호출 스레드의 PreviousMode 값을 통해 매개변수가 유저모드에서 왔는지 커널모드 콤포넌트에서 왔는지 판단하고, 적절하게 처리합니다.
커널모드 드라이버가 NtXxx 루틴을 호출하고, 현재 스레드 오브젝트의 PreviousMode 값이 매개변수의 출처가 유저모드인지, 커널모드인지를 정확하게 가리키지 못한다면 오류가 발생 할 수 있습니다.
예를 들어, 커널모드 드라이버가 임의의 스레드 컨텍스트에서 실행중이고, PreviousMode 값이 UserMode 인 경우를 가정합니다. 만일 드라이버가 커널모드 파일 핸들을 NtClose루틴에 전달한 경우 이 루틴은 PreviousMode 값을 확인하고, 핸들이 유저모드 핸들이어야 한다고 결정합니다. NtClose가 유저모드 핸들테이블에서 핸들을 찾을 수 없으므로 STATUS_INVALID_HANDLE 에러코드를 리턴합니다. 반면에 드라이버는 절대 핸들을 닫을 수 없기 때문에 핸들 누수 상황을 만들게 됩니다.
다른 예로, 만일 NtXxx 루틴의 매개변수에 입력 또는 출력 버퍼가 포함되어있고, PreviousMode = UserMode 인 경우 루틴은 버퍼를 검증하기 위해 ProbeForRead 또는 ProbeForWrite 루틴을 호출합니다. 만일 버퍼가 유저모드 메모리가 아니라 시스템 메모리에 할당되어있는 경우 ProbeForXxx 루틴은 예외를 발생시키고, NtXxx 루틴은 STATUS_ACCESS_VIOLATION 에러코드를 리턴하게 됩니다.
필요한 경우 드라이버는 ExGetPreviousMode 루틴을 호출해서 현재 스레드 오브젝트의 PreviousMode 값을 가져올 수 있습니다. 또는 요청된 I/O 작업 정보를 담고있는 IRP 구조체의 RequestorMode 필드를 읽을 수 있습니다. RequestorMode 필드는 작업을 요청한 스레드의 PreviousMode 값의 사본을 포함합니다.


Using Nt and Zw Versions of the Native System Services Routines (한글 번역)

MSDN 원문주소 : https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/using-nt-and-zw-versions-of-the-native-system-services-routines

Using Nt and Zw Versions of the Native System Services Routines

Windows 의 네이티브 서비스 API 는 커널모드에서 동작하는 루틴들의 집합으로 이루어져있습니다. 이 루틴들의 이름은 Nt 또는 Zw 로 시작합니다. 커널모드 드라이버들은 이 루틴들을 직접 호출할 수 있으며, 유저모드 어플리케이션들은 시스템 콜을 통해서 이 루틴들을 호출할 수 있습니다.
몇몇 예외사항이 있긴 하지만 각각의 네이티브 시스템 서비스 루틴들은 비슷한 이름의 접두어만 다른 두가지 버전이 있습니다. 예를 들어 NtCreateFile  ZwCreateFile 호출은 비슷한 작업을 수행하지만 사실은 동일한 커널모드 시스템 루틴에 의해서 처리됩니다. 유저모드에서의 Nt  Zw 버전 루틴은 동일하지만 커널모드에서의 Nt  Zw 버전 루틴들은 호출자가 전달한 파라미터 값을 다루는 방식이 다릅니다.
커널모드 드라이버는 전달되는 매개변수가 신뢰 할 수 있는 커널모드에서 전달된 것임을 알리기 위해 Zw 버전의 네이티브 서비스 루틴을 호출합니다. 이 경우 루틴은 매개변수를 검증하지 않고 안전하게 사용할 수 있다고 가정합니다. 그러나 매개변수가 유저모드 또는 커널모드 중 하나일 경우라면 드라이버는 Nt 버전의 루틴을 호출하는데 이는 루틴을 호출한 스레드의 기록을 기반으로 매개변수가 유저모드에서 전달되었는지 커널모드에서 전달되었는지를 구별합니다. 커널모드 매개변수와 유저모드 매개변수를 구분하는 방법에 대한 자세한 내용은 PreviousMode를 참조하십시오.
유저모드 애플리케이션이 Nt 또는 Zw 버전의 네이티브 시스템 서비스 루틴을 호출하면 루틴은 항상 매개변수는 신뢰할 수 없는 유저모드에서 전달된 것으로 간주하고 매개변수를 사용하기 전에 철저히 검증합니다. 특히 루틴은 호출자가 제공한 버퍼를 검사하여 버퍼가 유효한 유저모드 메모리에 존재하고 있으며 올바르게 정렬되어있는지 확인합니다.
네이티브 시스템 서비스 루틴은 매개변수에 대한 추가적인 가정을 합니다. 만일 커널모드 드라이버가 할당한 버퍼의 포인터를 매개변수로 받은 경우 버퍼는 유저모드 메모리가 아니라 시스템 메모리에 할당되어있다고 가정합니다. 만일 유저모드 애플리케이션에서 열어둔 핸들을 받으면 루틴은 커널모드 핸들테이블이 아니라 유저모드 핸들테이블에서 핸들을 찾습니다.
몇 가지 예에서 매개변수 값의 의미는 사용자 모드 및 커널모드에서의 호출간에 더 크게 다릅니다. 예를 들어 ZwNotifyChangeKey 루틴 (또는 해당 NtNotifyChangeKey 버전)에는 입력 매개변수 쌍이 있습니다. ApcRoutine  ApcContext 는 유저모드에서의 호출인지 커널모드에서의 호출인지에 따라 의미가 다릅니다. 유저모드 호출인 경우 ApcRoutine은 APC 루틴을 가리키고, ApcContext는 APC 루틴을 호출할 때 운영체제가 제공하는 컨텍스트 값을 가리킵니다. 커널모드 호출인 경우 ApcRoutine  WORK_QUEUE_ITEM 구조체를 가리키고 ApcContext  WORK_QUEUE_ITEM 구조체에 의해서 기술되는 워크 큐 아이템의 타입을 가리킵니다.

2017년 5월 23일 화요일

vcpkg 사용하기

vcpkg

vcpkg는 curl, boost, openssl 같은 자주 사용되는 3rd party 라이브러리들을 Visual Studio 에서 쉽게 사용할 수 있게 해주는 패키지 매니저입니다. openssl 같은 라이브러를 직접 빌드하려면 정말 귀찮고, 짜증 나는데 vcpkg 를 사용하면 이런 귀찮은 일들을 알아서 자동으로 다 해줍니다.

Quick start

vcpkg 를 사용하려면 github 을 clone 하고, 로컬에서 빌드만 하면 됩니다. Visual Studio 가 참조하는 헤더파일, 라이브러리 경로 설정들도 자동으로 다 해줍니다.
  1. c:\work.vcpkg 에 clone 한다. (경로는 편한대로 알아서 하면 됩니다)
  2. c:\work.vcpkg\bootstrap-vcpkg.bat 을 실행하면 vcpkg.exe 를 생성합니다.
  3. vcpkg integrate install 명령으로 Visual Studio 설정을 해줍니다. (최초 실행시 관리자 권한 필요)
  4. vcpkg.exe 을 통해서 패키지 인스톨/업데이트/삭제 등등을 할 수 있습니다 (쉽습니다).
     C:\work.vcpkg>vcpkg.exe /?
     Commands:
       vcpkg search [pat]              Search for packages available to be built
       vcpkg install ...          Install a package
       vcpkg remove ...           Uninstall a package
       vcpkg remove --outdated         Uninstall all out-of-date packages
       vcpkg list                      List installed packages
       vcpkg update                    Display list of packages for updating
       vcpkg hash  [alg]         Hash a file by specific algorithm, default SHA512
    
       vcpkg integrate install         Make installed packages available user-wide. Requires admin privileges on first use
       vcpkg integrate remove          Remove user-wide integration
       vcpkg integrate project         Generate a referencing nuget package for individual VS project use
    
       vcpkg export ... [opt]...  Exports a package
       vcpkg edit                 Open up a port for editing (uses %EDITOR%, default 'code')
       vcpkg import               Import a pre-built library
       vcpkg create  
                 [archivename]        Create a new package
       vcpkg owns                 Search for files in installed packages
       vcpkg cache                     List cached compiled packages
       vcpkg version                   Display version information
       vcpkg contact                   Display contact information to send feedback
    
     Options:
       --triplet                    Specify the target architecture triplet.
                                       (default: %VCPKG_DEFAULT_TRIPLET%, see 'vcpkg help triplet')
    
       --vcpkg-root              Specify the vcpkg root directory
                                       (default: %VCPKG_ROOT%)
    
     For more help (including examples) see the accompanying README.md.
    
     C:\work.vcpkg>
    

라이브러리 설치

vcpkg.exe install package 명령으로 패키지(라이브러리)를 설치하면 기본적으로 x86 dynamic 으로 설치합니다. 하지만 개발환경에 따라서 x86 static/dynamic, x64 static/dynamic 버전 등등이 필요합니다. 사용가능 한 버전은 vcpkg install package:triplet 명령으로 확인가능합니다.
C:\work.vcpkg>vcpkg help triplet
Available architecture triplets:
  arm-uwp
  x64-uwp
  x64-windows-static
  x64-windows
  x86-uwp
  x86-windows-static
  x86-windows
>vcpkg install package:triplet 명령으로 설치 각 버전의 라이브러리를 설치할 수 있습니다. 아래 예제는 boost, curl, sqlite 등의 라이브러리를 설치합니다.
>vcpkg install boost:x86-windows boost:x86-windows-static boost:x64-windows boost:x64-windows-static
>vcpkg install curl:x86-windows curl:x86-windows-static curl:x64-windows curl:x64-windows-static
>vcpkg install sqlite3:x86-windows sqlite3:x86-windows-static sqlite3:x64-windows sqlite3:x64-windows-static
>vcpkg install jsoncpp:x86-windows jsoncpp:x86-windows-static jsoncpp:x64-windows jsoncpp:x64-windows-static
설치된 패키지는 Visual Studio 에서 별다른 설정을 하지 않아도 바로 사용가능합니다.

라이브러리 강제 지정하기 (x86, x64 또는 /MD, /MT)

vcpkg 는 기본으로 /MD (Multi Threaded DLL) 로 라이브러리를 빌드하고, 참조하게 합니다. 하지만 프로젝트 환경에 따라서 static 하게 링크해야 하는 경우도 있습니다. 패키지 컴파일시에 triplet 을 지정해서 /MD, /MT 등으로 설치는 가능한데, 어떻게 지정하는지에 대해서는 공식적인 내용을 찾지 못했습니다. 구글의 도움을 받아 찾은 해결책은 몇 가지 있었습니다만 깔끔하게 해결할 수 있는 방법은 아직 모르겠습니다. 아시는 분께서는 좀 알려주세요.
  • default 가 md (using dll) 로 링크되는데, 관련 설명은 있는데, 어떻게 하라는것인지 잘 모르겠고, 읽기도 귀찮음.
  • c:\work.vcpkg\scripts\buildsystems\msbuild\vcpkg.targets 파일을 수정하는 방법
    • 프로젝트 빌드 과정에서 참조되는 파일로 VcpkgEnabledVcpkgTriplet 변수를 적당히 수정해주면 됨
    • 전역 파일을 수정하는것이 맘에 들지 않음.
저는 아래 처럼 프로젝트의 설정파일을 직접 편집합니다. 편한 텍스트 에디터를 이용해서 .vcxproj 파일을 열고, Globals 아래에 내용을 추가합니다. vcpkg 로 패키지를 설치하면 vcpkg-root\installed\x64-windowsvcpkg-root\installed\x64-windows-static 등의 경로에 패키지가 설치되는데 이 경로를 강제로 지정하는 것입니다. Visual Studio 에서 사용하는 VcpkgRoot 환경변수를 바꿔치기 해주는 것이지요. :-)
vcxproj

curl 라이브러리 문제 해결하기

curl 라이브러리는 아주 많은 application network protocol 구현을 제공하는 라이브러리입니다. 디폴트 동작방식인 /MD 라면 추가적인 설정이 필요없지만 static link 를 사용하는 경우 추가적인 설정이 필요합니다. 우선 static 링크를 사용하기 위해서 c/c++ -> Runtime Library 를 /MT 로 변경합니다.
set_mt
vcxproj 파일도 static 버전을 참조할 수 있도록 수정합니다.
set_vcxproj

CURL_STATICLIB

프로젝트를 빌드하면 아래와 유사한 에러가 발생합니다.
1>libcurl.lib(schannel.obj) : error LNK2019: unresolved external symbol __imp__CertFreeCertificateContext@4 referenced in function _schannel_connect_step3
1>libeay32.lib(e_capi.obj) : error LNK2001: unresolved external symbol __imp__CertFreeCertificateContext@4
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_init referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_unbind_s referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_set_option referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_simple_bind_s referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_search_s referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_msgfree referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_err2string referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_first_entry referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_next_entry referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_first_attribute referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_next_attribute referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_get_values_len referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_value_free_len referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_get_dn referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_memfree referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ber_free referenced in function __ldap_free_urldesc
1>libeay32.lib(e_capi.obj) : error LNK2019: unresolved external symbol __imp__CertOpenStore@20 referenced in function _capi_open_store
1>libeay32.lib(e_capi.obj) : error LNK2019: unresolved external symbol __imp__CertCloseStore@8 referenced in function _capi_find_key
1>libeay32.lib(e_capi.obj) : error LNK2019: unresolved external symbol __imp__CertEnumCertificatesInStore@8 referenced in function _capi_find_cert
1>libeay32.lib(e_capi.obj) : error LNK2019: unresolved external symbol __imp__CertFindCertificateInStore@24 referenced in function _capi_find_cert
1>libeay32.lib(e_capi.obj) : error LNK2019: unresolved external symbol __imp__CertDuplicateCertificateContext@4 referenced in function _capi_load_ssl_client_cert
1>libeay32.lib(e_capi.obj) : error LNK2019: unresolved external symbol __imp__CertGetCertificateContextProperty@16 referenced in function _capi_cert_get_fname
1>C:\work.zzig\out\x86_debug\update_host.exe : fatal error LNK1120: 23 unresolved externals
이 문제는 CURL_STATICLIB 전처리기를 정의하면 됩니다. 아래 처럼 curl.h 를 include 하기 전에 정의해도 되고, project 설정파일에 정의해주어도 됩니다.
#define CURL_STATICLIB
#include "curl/curl.h"
사실 이 문제는 vcpkg 의 문제가 아니라 curl 라이브러리 구현상의 특징이며 curl/curl.h 파일에 관련된 코드가 있습니다.
/*
* libcurl external API function linkage decorations.
*/

#ifdef CURL_STATICLIB
#  define CURL_EXTERN
#elif defined(WIN32) || defined(_WIN32) || defined(__SYMBIAN32__)
#  if defined(BUILDING_LIBCURL)
#    define CURL_EXTERN  __declspec(dllexport)
#  else
#    define CURL_EXTERN  __declspec(dllimport)
#  endif
#elif defined(BUILDING_LIBCURL) && defined(CURL_HIDDEN_SYMBOLS)
#  define CURL_EXTERN CURL_EXTERN_SYMBOL
#else
#  define CURL_EXTERN
#endif

crypt32.lib 에러

하지만 여전히 또 다른 링크에러들이 발생합니다.
>libcurl.lib(schannel.obj) : error LNK2019: unresolved external symbol __imp__CertFreeCertificateContext@4 referenced in function _schannel_connect_step3
1>libeay32.lib(e_capi.obj) : error LNK2001: unresolved external symbol __imp__CertFreeCertificateContext@4
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_init referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_unbind_s referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_set_option referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_simple_bind_s referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_search_s referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_msgfree referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_err2string referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_first_entry referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_next_entry referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_first_attribute referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_next_attribute referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_get_values_len referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_value_free_len referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_get_dn referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_memfree referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ber_free referenced in function __ldap_free_urldesc
1>libeay32.lib(e_capi.obj) : error LNK2019: unresolved external symbol __imp__CertOpenStore@20 referenced in function _capi_open_store
1>libeay32.lib(e_capi.obj) : error LNK2019: unresolved external symbol __imp__CertCloseStore@8 referenced in function _capi_find_key
1>libeay32.lib(e_capi.obj) : error LNK2019: unresolved external symbol __imp__CertEnumCertificatesInStore@8 referenced in function _capi_find_cert
1>libeay32.lib(e_capi.obj) : error LNK2019: unresolved external symbol __imp__CertFindCertificateInStore@24 referenced in function _capi_find_cert
1>libeay32.lib(e_capi.obj) : error LNK2019: unresolved external symbol __imp__CertDuplicateCertificateContext@4 referenced in function _capi_load_ssl_client_cert
1>libeay32.lib(e_capi.obj) : error LNK2019: unresolved external symbol __imp__CertGetCertificateContextProperty@16 referenced in function _capi_cert_get_fname
1>C:\work.zzig\out\x86_debug\update_host.exe : fatal error LNK1120: 23 unresolved externals
CertFindCertificateInStore 등의 함수는 crypt32.lib 에서 export 하는 함수이므로 아래처럼 해당 라이브러리를 link 해주거나 visual studio 의 link 옵션에서 지정해주면 됩니다.
#define CURL_STATICLIB
#include "curl/curl.h"
#pragma comment(lib, "crypt32.lib")

wldap32.lib 에러

하지만 여전히 또 다른 링크에러들이 발생합니다.
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_init referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_unbind_s referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_set_option referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_simple_bind_s referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_search_s referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_msgfree referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_err2string referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_first_entry referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_next_entry referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_first_attribute referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_next_attribute referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_get_values_len referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_value_free_len referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_get_dn referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ldap_memfree referenced in function __ldap_free_urldesc
1>libcurl.lib(ldap.obj) : error LNK2019: unresolved external symbol __imp__ber_free referenced in function __ldap_free_urldesc
이 에러는 아래처럼 wldap32.lib 을 link 해주면 해결됩니다.
#define CURL_STATICLIB
#include "curl/curl.h"
#pragma comment(lib, "crypt32.lib")
#pragma comment(lib, "wldap32.lib")

LNK4099 링크 경고

1>libcurl.lib(file.obj) : warning LNK4099: PDB 'libcurl.pdb' was not found with 'libcurl.lib(file.obj)' or at 'C:\work.zzig\out\x86_release\libcurl.pdb'; linking object as if no debug info
1>libcurl.lib(timeval.obj) : warning LNK4099: PDB 'libcurl.pdb' was not found with 'libcurl.lib(timeval.obj)' or at 'C:\work.zzig\out\x86_release\libcurl.pdb'; linking object as if no debug info
모든 설정을 다 하고 빌드하면 LNK4099 경고가 발생하는데, 이건 어떻게 해야할지 모르겠네요. 아시는 분은 알려주십셔 (ㅠ,.ㅠ)

2017년 2월 15일 수요일

ASSERT, ASSERT, ASSERT

아래의 코드를 봅시다. busy_write 라는 전역변수를 통해서 아주 간단한 배타적 접근을 구현하고 있습니다.
int busy_write = 0;
int value_write = 0;

int funcA()
{
    //
    //  #1 lock 을 걸어주고
    //
    if (1 == InterlockedCompareExchange(&busy_write, 1, 0))
    {
        goto Cleanup;
    }

    //
    //  #2 뭔가 열심히 복잡한 작업을 하고, value_write 값을 갱신합니다.
    //
    ...
    value_write++;
    ...

    //
    //  #3 이 ASSERT 가 필요한가?
    //
    ASSERT(1 == StreamContext->busy_write);

Cleanup:
    //
    //  #4 busy_write 를 다시 0 으로.
    //
    InterlockedAnd(&busy_write, 0);
    return 0;
}
코드 #3 에 있는 ASSERT 는 별로 필요없어 보입니다. 위에서 busy_write 를 1로 변경한 상태이기 때문에 언제나 true 입니다. 따라서 당연히 ASSERT 를 넣는것이 이해가 되지 않을 수도 있습니다. 하지만 시간이 흘러, 어찌 어찌하다 보니 이 함수가 #2 에서 재귀호출을 하도록 수정되어버렸다고 가정해 봅시다. 어떻게 될까요?
#2 에서 재귀호출이 발생하면 #4 로 jump 하게 될것이고, busy_write 를 0 으로 변경한 후 리턴될겁니다. 그럼 원래 호출했던 코드에서는 #3 ASSERT 에 걸리게 되고, 개발자는 바로 이 코드에 문제가 있음을 알아챌 수 있을겁니다.
만일 #3 에 ASSERT 가 없었다면 어떻게 되었을까요? 아마 코드의 문제를 인지하고, 디버깅 하는데 ASSERT 가 있었던 상황보다 분명히 더 많은 시간이 소요될 겁니다.
ASSERT 를 사용하는 규칙은, 당연히 확실한 상태라고 믿어 의심치 않아서, ASSERT 가 전혀 필요없어 보이는, 그런코드에 ASSERT 를 사용하는것입니다.
가끔 강의를 하거나 코드 리뷰를 할때 ASSERT 사용을 가볍게 여기는 개발자분들을 많이 봐서, 오늘 겪은 좋은 예가 있길래 글로 남겼습니다.
#초보_개발자_참고용 #손가락이_아플때까지

2017년 2월 13일 월요일

FLTFL_OPERATION_REGISTRATION_SKIP_PAGING_IO

delete 미니필터 샘플 을 보면 아래와 같이 IRP_MJ_SET_INFORMATION 콜백 핸들러를 등록할때 FLTFL_OPERATION_REGISTRATION_SKIP_PAGING_IO 를 사용하는 것을 확인할 수 있다.

    CONST FLT_OPERATION_REGISTRATION Callbacks[] = {
        ...
        { IRP_MJ_SET_INFORMATION,
        FLTFL_OPERATION_REGISTRATION_SKIP_PAGING_IO,
        DfPreSetInfoCallback,
        DfPostSetInfoCallback },
        ...
        { IRP_MJ_OPERATION_END }
    };
이유가 궁금해서 검색을 해보니 관련된 질문과 답이 있었다.
내용을 정리해두자면, On demand paging 을 구현하기 위해 VMM(Virtual Memory Manager)이 생성한 IRP 기반의 I/O 요청이 Paging I/O 이다. IRP_MJ_CREATE, IRP_MJ_CLEAN_UP 에는 Paging I/O 가 없고, Paging I/O 를 지원하는 I/O 는 아래와 같다.
  • IRP_MJ_READ
  • IRP_MJ_WRITE
  • IRP_MJ_SET_INFORMATION
  • IRP_MJ_QUERY_INFORMATION
IRP_MJ_READ 나 IRP_MJ_WRITE 의 는 Paging I/O 를 지원하는 것이 당연하고, IRP_MJ_SET_INFORMATION 이나 IRP_MJ_QUERY_INFORMATION 은 메모리 매니저가 섹션 객체를 생성하거나 접근할 때 필요할것이다.
해당 샘플(delete)의 경우 파일 삭제를 식별하기 위해 IRP_MJ_SET_INFORMATION 콜백을 등록하고, FileDispositionInformation, FileDispositionInformationEx 를 모니터링 한다. 이 I/O 는 Paging I/O 와는 상관없기때문에 FLTFL_OPERATION_REGISTRATION_SKIP_PAGING_IO 를 사용한 것이다.

참고