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 를 사용한 것이다.

참고

2017년 1월 14일 토요일

dwDesiredAccess 와 dwShareMode 정확히 사용하기

이 글에서는 CreateFile() API의 두번째, 세번째 파라미터인 dwDesiredAccessdwShareMode 에 대해서 정확히 파악해보고자 합니다.
먼저 퀴즈로 시작해 보겠습니다.
A 라는 프로세스는 abc.log 파일에 데이터를 쓰고, B 라는 프로세스는 해당 파일을 읽어서 화면에 출력하는 코드를 작성한다고 가정합니다. A 프로세스는 abc.log 파일에 데이터를 쓰기 위해 FILE_GENERIC_WRITE 접근 권한을 요청하고, B 프로세스가 해당 로그파일을 읽을 수 있도록 FILE_SHARE_READ 공유권한을 요청합니다. ( A 프로세스가 먼저 실행된다고 가정합니다 )
// A 프로세스의 코드
HANDLE fileHandle = CreateFile('abc.log',
                               FILE_WRITE_DATA,     // 로그를 쓰려면 쓰기 권한으로 파일을 열어야죠.
                               FILE_SHARE_READ,     // B 프로세스가 해당 파일을 읽어야 하니까 `공유 읽기` 를.
                               ...
                               ...);


// B 프로세스의 코드
HANDLE fileHandle = CreateFile('abc.log',
                               FILE_READ_DATA,      // 로그 파일을 읽어야 하니까
                               FILE_SHARE_READ,     // A 프로세스의 코드랑 그냥 같은걸로???
                               ...
                               ...);
..
..
..
..
..
..
..
이 코드는 생각대로, 잘 동작하지 않습니다. 바로 무엇이 문제인지 대답하실 수 있다면 더이상 이 글을 읽지 않으셔도 됩니다.
..
..
..
..
..
..
..
CreateFile() 함수는 Windows API 중 가장 많이 사용되는 API 중 하나이고, 가장 어렵고, 가장 많은것을 알아야 하는 API 중 하나입니다.
HANDLE WINAPI CreateFile(
  _In_     LPCTSTR               lpFileName,
  _In_     DWORD                 dwDesiredAccess,
  _In_     DWORD                 dwShareMode,
  _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  _In_     DWORD                 dwCreationDisposition,
  _In_     DWORD                 dwFlagsAndAttributes,
  _In_opt_ HANDLE                hTemplateFile
);
두번째 dwDesiredAccess 는 해당 파일에 어떤 권한으로 접근을 하려고 하는지를 명시합니다 (읽기를 할것인지, 쓰기를 할것인지, 삭제를 할것인지). 세번째 dwShareMode 는 해당 파일에 접근하고있는 동안 다른 프로세스가 해당 파일에 접근하고자 하는 경우 어떤 권한을 허용할지를 명시합니다 (읽기를 허용할지, 쓰기를 허용할지, 삭제를 용할지).
Windows Kernel 은 CreateFile 요청이 발생하면 다양하고, 복잡한 함수호출을 통해서 특정 파일에 대한 오브젝트(FILE_OBJECT)를 생성하거나 이미 생성된 FILE_OBJECT 에 접근합니다. FILE_OBJECT 는 Windows 커널이 파일을 표현하기 위해서 커널내부적으로 사용하는 자료구조이며, 특정프로세스에 종속되지 않는 커널전체에서 공유되는 자원입니다. 매번 파일을 열고자 할때마다 새롭게 FILE_OBJECT 를 생성하면 효율적이지 못할테니까요. 응용프로그램이 사용하는 HANDLE 은 Windows 커널이 관리하는 오브젝트 배열의 인덱스 값 같은 것입니다. 이에 대한 자세한 내용은 Windows Internals 또는 제 예전 블로그 포스트를 참고하시면 도움이 될것 같습니다.
커널은 특정 오브젝트로의 접근 요청을 받으면 해당 오브젝트에 접근이 가능한 권한이 있는 지 확인하는 과정을 거치는데, 파일의 경우 추가적으로 요청권한 및 공유권한을 확인하는 과정을 거칩니다. 이 공유위반이 발생하면 ERROR_SHARING_VIOLATION에러코드를 리턴하게 됩니다.
Windows Kernel 이 FILE_OBJECT 에 요청된 접근권한(dwDesiredAccess)과 공유권한(dwShareMode)가 유효한지 판단하는 과정을 자세히 확인해보겠습니다.
FILE_OBJECT 는 매우 복잡하고, 알아야 할것도 많고, 모르는것도 굉장히 많지만, 우리에게 필요한 필드는 아래와 같습니다.
kd> dt nt!_file_object
   ...
   +0x04a ReadAccess       : UChar
   +0x04b WriteAccess      : UChar
   +0x04c DeleteAccess     : UChar
   +0x04d SharedRead       : UChar
   +0x04e SharedWrite      : UChar
   +0x04f SharedDelete     : UChar
   ...
별다른 설명이 없더라도 dwDesiredAccess 와 dwShareMode 인걸 알 수 있을 것입니다. FILE_OBJECT 의 값들과 우리가 입력한 파라미터가 매칭되는 방식은 아래 코드와 같습니다.
FILE_OBJECT.ReadAccess = (BOOLEAN) ((DesiredAccess & (FILE_EXECUTE| FILE_READ_DATA)) != 0);
FILE_OBJECT.WriteAccess = (BOOLEAN) ((DesiredAccess & (FILE_WRITE_DATA | FILE_APPEND_DATA)) != 0);
FILE_OBJECT.DeleteAccess = (BOOLEAN) ((DesiredAccess & DELETE) != 0);
...
FILE_OBJECT.SharedRead = (BOOLEAN) ((DesiredShareAccess & FILE_SHARE_READ) != 0);
FILE_OBJECT.SharedWrite = (BOOLEAN) ((DesiredShareAccess & FILE_SHARE_WRITE) != 0);
FILE_OBJECT.SharedDelete = (BOOLEAN) ((DesiredShareAccess & FILE_SHARE_DELETE) != 0);
Windows kernel 은 유효성 여부를 검사와 해당 FILE_OBJECT 에 대한 요청들을 추적하기 위해서 SHARE_ACCESS라는 자료구조를 사용하는데, windbg 를 통해서 확인할 수 있습니다. FILE_OBJECT 접근에 성공하면 각 필드를 업데이트 합니다.
kd> dt nt!share_access
   +0x000 OpenCount        : Uint4B
   +0x004 Readers          : Uint4B
   +0x008 Writers          : Uint4B
   +0x00c Deleters         : Uint4B
   +0x010 SharedRead       : Uint4B
   +0x014 SharedWrite      : Uint4B
   +0x018 SharedDelete     : Uint4B
C 코드로 변경해보면 아래와 같고, 각각의 의미는 코드의 주석으로 표현해 두었습니다.
typedef struct _SHARE_ACCESS
{
    uint32_t OpenCount;     // 성공한 파일 열기 횟수

    uint32_t Readers;       // 성공한 읽기 권한 횟수
    uint32_t Writers;       // 성공한 쓰기 권한 횟수
    uint32_t Deleters;      // 성공한 삭제 권한 횟수

    uint32_t SharedRead;    // 성공한 공유 읽기 권한 횟수
    uint32_t SharedWrite;   // 성공한 공유 쓰기 권한 횟수
    uint32_t SharedDelete;  // 성공한 공유 삭제 권한 횟수
} SHARE_ACCESS;
SHARE_ACCESS 와 dwDesiredAccessdwShareMode 간의 관계는 아래와 같이 정리 할 수 있습니다.
[규칙 #1]
`SHARE_ACCESS.Shared[Read|Write|Delete]` 가 OpenCount 보다 작고,
`[Read|Write|Delete]` 권한을 요청하는 경우 SHARE VIOLATION.

기존에 생성된 공유권한이 없다면 공유권한과 매칭되는 접근권한을 요청 할 수 없습니다.
예를 들어 이전 호출 중 `FILE_SHARE_READ` 공유권한 요청이 없었다면, `FILE_READ_DATA` 접근 권한을 요청할 수 없습니다.

[규칙 #2]
`SHARE_ACCESS.[Readers|Writers|Deleters]` 가 0 이 아닌데,
요청 된 공유권한이 `FILE_SHARE_[READ|WRITE|DELETE]` 가 0 인 경우 SHARE VIOLATION.

기존에 요청된 접근권한과 매칭되는 공유권한이 현재 요청에 없다면 요청에 실패합니다.
예를 들어 이전 호출 중 `FILE_READ_DATA` 접근권한 요청이 있었다면, 현재 요청에는 반드시 `FILE_SHARE_READ` 공유권한이 포함되어 있어야 합니다.
앞에서 보여드렸던 아래의 코드는 ERROR_SHARING_VIOLATION 를 리턴하게 됩니다.
// A 프로세스의 코드
HANDLE fileHandle = CreateFile('abc.log',
                               FILE_WRITE_DATA,     // 로그를 쓰려면 쓰기 권한으로 파일을 열어야죠.
                               FILE_SHARE_READ,     // B 프로세스가 해당 파일을 읽어야 하니까 `공유 읽기` 를.
                               ...
                               ...);

// B 프로세스의 코드
HANDLE fileHandle = CreateFile('abc.log',
                               FILE_READ_DATA,      // 로그 파일을 읽어야 하니까
                               FILE_SHARE_READ,     // A 프로세스의 코드랑 그냥 같은걸로???
                               ...
                               ...);
왜 그럴까요?
A 프로세스의 코드가 실행되면 해당 FILE_OBJECT 의 상태는 아래와 같습니다.
SHARE_ACCESS.OpenCount = 1;

SHARE_ACCESS.Readers = 0;
SHARE_ACCESS.Writers = 1;       // FILE_GENERIC_WRITE 를 지정했으니까.
SHARE_ACCESS.Deleters = 0;

SHARE_ACCESS.SharedRead = 1;    // FILE_SHARE_READ 를 정했으니까.
SHARE_ACCESS.SharedWrite = 0;
SHARE_ACCESS.SharedDelete = 0;
B 프로세스의 코드가 실행되는 시점에 공유위반 검사를 해보면
  • 규칙 #1, SHARE_ACCESS.SharedRead < SHARE_ACCESS.OpenCount 조건이 거짓이므로 공유위반 아님
  • 규칙 #2, SHARE_ACCESS.Writers != 0 이고, 요청된 공유모드가 FILE_SHARE_WRITE 가 0 입니다. (FILE_SHARE_READ 만 지정했으니까요)
즉 [규칙 #2] 에 의해서 공유위반입니다.
따라서 위의 코드는 아래처럼 작성해야 합니다.
// A 프로세스의 코드
HANDLE fileHandle = CreateFile('abc.log',
                               FILE_WRITE_DATA,     // 로그를 쓰려면 쓰기 권한으로 파일을 열어야죠.
                               FILE_SHARE_READ,     // B 프로세스가 해당 파일을 읽어야 하니까 `공유 읽기` 를.
                               ...
                               ...);

// B 프로세스의 코드
HANDLE fileHandle = CreateFile('abc.log',
                               FILE_READ_DATA,      // 로그 파일을 읽어야 하니까
                               FILE_SHARE_READ | FILE_SHARE_WRITE,
                               ...
                               ...);
사실 상식적으로 생각해보면 굉장히 당연한 규칙입니다만, 코드를 말로 풀어서 설명하다보니 쓸데없이 길어진 것 같습니다. 의외로 예제로 보여드린 코드와 유사한 실수를 저지르는 경우가 많고, 정확히 규칙을 모르는 분들이 많은것 같습니다 (저도 정확히 모르고 대충 썼습니다).
매번 정확히 한번 따져봐야지 생각만 하고 미루다가 드디어 정확히 알게된것 같아 기쁩니다 ^__^

2016년 11월 28일 월요일

FLT_STREAM_CONTEXT vs FLT_STREAMHANDLE_CONTEXT

몇년만에 드라이버코드를 작성할 일이 있어서 공부중입니다.
파일시스템이나 미니필터는 바닥부터 작성해본적이 없어서 공부하기가 쉽지 않네요.
FLT_STREAM_CONTEXT 가 FLT_STREAMHANDLE_CONTEXT 뭐가 다른건지 헤깔려서 찾아보다가 정리했습니다.


FLT_STREAM_CONTEXT vs FLT_STREAMHANDLE_CONTEXT

STREAM_CONTEXT


  • file stream 마다 붙일 수 있는 context.
  • FILE_OBJECT.FsContext 를 추적하는데 사용.

STREAMHANDLE_CONTEXT


  • 개개의 file open 시 생성되는 file object (I/O 서브시스템이 생성하는)마다 붙일 수 있는 context.
  • FILE_OBJECT 를 추적하는데 사용.



File Streams, Stream Contexts, and Per-Stream Contexts (MSDN 원문)


File Stream


파일 데이터를 저장하는데 사용되는 바이트 시퀀스이다.
보통 파일은 하나의 file stream 을 가지는데, 요걸 파일의 default data stream 이라고 한다.
그러나 multiple data stream 을 지원하는 파일시스템에서는 각각의 파일은 여러개의 file stream 을 가질 수 있다.
그 중 하나는 default data stream 이고, 얘는 unnamed 이다.
다른 놈들은 alternate data stream 이다. 파일을 열면, 실제로는 해당 파일의 스트림을 여는 것이다.


file system 이 최초로 파일을 열때, file control block(FCB) 이나 stream control block (SCB) 같은
file-system-specific stream context 구조체를 생성하고, 이 구조체의 주소를 file object 의 FsContext 멤버에 저장한다.


로컬 파일시스템에서, 이미 열려있는 file streeam 이 다시 열리면 (shared read access 같은 경우로 인해),
I/O 서브시스템은 새로운 file object 를 생성하지만, file system 은 새로운 stream context 를 생성하지는 않는다.
따라서 로컬 파일시스템에서는 stream context pointer 는 file stream 을 식별하는 유니크한 값으로 사용될 수 있다.


per-stream context 를 지원하는 네트워크 파일시스템에서는 이미 열려있는 file stream 이 동일한 network share name 이나 IP 주소 등을
이용해서 다시 열린다면 로컬 파일시스템과 동일하게 동작한다.
I/O 서브시스템은 새로운 file object 를 생성하지만 file system 은 새로운 stream context 를 생성하지 않고,
두 file object 에 동일한 FsContext 포인터를 할당한다.


그러나 file stream 이 서로 다른 경로로 열리는 경우 (다른 share name 또는 IP 가 다른 경우), file system 은
새로운 file stream context 를 생성한다.
그러므로 per-stream context 를 지원하는 네트워크 파일시스템에서는 FsContext 는 file stream 을 구분하는
유일한 값으로 사용될 수 없다.


per-stream contextFSRTL_PER_STREAM_CONTEXT 구조체를 멤버로 포함하는
filter-defined 구조체이다. 필터드라이버는 이 구조체를 file system 이 열어놓은 각각의 file stream 에 대한 정보를 추적하는데 사용한다.


File Sytem Support for Per-Stream Contexts


Microsoft Windows XP 이후, per-stream context 를 지원하는 파일시스템은 FSRTL_ADVANCED_FCB_HEADER구조체를 포함하는
stream context 구조체를 사용해야 한다.


특정 file stream 에 연관된 per-stream context 의 global list 는 file system 이 관리한다.
file system 이 file stream 을 위한 새 stream context (FSRTL_ADVANCED_FCB_HEADER object)를 생성할 때, 이 list 를 초기화하기 위해
FsRtlSetupAdvancedHeader 를 호출한다.
file system filter 드라이버가 FsRtlInsertPerStreamContext 함수를 호출하면, filter 가 생성한 per-steram context 는
그 global list 에 추가된다.


file system 이 file stream 에 대한 stream context 를 삭제하때, filter 가 가진 file stream 에 연관된 모든 per-stream context 를
제거하기 위해 FsRtlTeardownPerStreamContexts 를 호출한다.
이 루틴은 global list 에 있는 각각의 per-stream context 에 대해서 FreeCallback루틴을 호출한다.
FreeCallback 루틴은 file stream 과 연관된 file object 가 이미 소멸되었음을 인지하고 있어야 한다.


file system 이 file object 로 대표되는 file stream 에 대해서 per-stream context 를 지원하는지 확인하려면, 해당 file object 를 통해서
FsRtlSupportsPerStreamContexts를 호출할 수 있다. file system 은 어떤 파일 타입에 대해서는 per-stream context 를 지원할 수 도 있고,
그렇지 않을 수도 있다. 예를들어 NTFS 와 FAT 는 paging file 에 대해서 per-stream context 를 지원하지 않는다.
따라서 FsRtlSupportsPerStreamContexts 함수는 하나의 file stream 에 대해서는 TRUE 를 리턴하지만, 모든 file stream 에 대해서
TRUE 를 리턴하지는 않을것이다.