Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일

(시리즈 글이 5개 있습니다.)
C/C++: 170. Windows - STARTUPINFO의 cbReserved2, lpReserved2 멤버 사용자 정의
; https://www.sysnet.pe.kr/2/0/13723

C/C++: 171. C/C++ - 윈도우 운영체제에서의 file descriptor와 HANDLE
; https://www.sysnet.pe.kr/2/0/13726

C/C++: 172. Windows - C 런타임에서 STARTUPINFO의 cbReserved2, lpReserved2 멤버를 사용하는 이유
; https://www.sysnet.pe.kr/2/0/13728

C/C++: 174. C/C++ - 윈도우 운영체제에서의 file descriptor, FILE*
; https://www.sysnet.pe.kr/2/0/13738

C/C++: 175. C++ - WinMain/wWinMain 호출 전의 CRT 초기화 단계
; https://www.sysnet.pe.kr/2/0/13750




Windows - C 런타임에서 STARTUPINFO의 cbReserved2, lpReserved2 멤버를 사용하는 이유

자, 이제야 준비가 된 것 같군요. ^^

Windows - STARTUPINFO의 cbReserved2, lpReserved2 멤버 사용자 정의
; https://www.sysnet.pe.kr/2/0/13723

C/C++ - 윈도우 운영체제에서의 file descriptor와 HANDLE
; https://www.sysnet.pe.kr/2/0/13726

그러니까, C 런타임 라이브러리(CRT)에서 cbReserved2, lpReserved2를 왜 사용하는지에 대한 이야기입니다. 사실, 윈도우는 이미 HANDLE 상속을 지원하므로 그에 속하는 파일도 자연스럽게 상속 및 사용이 가능합니다.

그런데, 왜 cbReserved2, lpReserved2를 사용해서 부가적인 정보를 또 전달하는 것일까요?

그 이유는? HANDLE은 CRT에서 사용하는 file descriptor가 아니라는 간단한 이유 때문입니다. 다시 말해, HANDLE뿐만 아니라 C/C++ 프로그램에서는 file descriptor도 함께 자식에게 내려줘야 하는데 윈도우 운영체제는 fork와 같은 실행 방식을 지원하지 않으므로 다른 방법을 (아마도 고육지책으로) 고안해 냈던 것이 바로 cbReserved2, lpReserved2 2개의 필드였을 것입니다.

일단, 이런 방식으로 file descriptor를 전달하는 것은 CreateProcess Win32 API에서는 처리해 주지 않습니다. 즉, cbReserved2, lpReserved2를 통해 file descriptor를 전달하는 것은 CRT에서만 제한적으로 사용하는데, 대표적인 함수가 바로 exec..., spawn... 계열입니다.

Visual C++ - 윈도우 환경에서 _execv 동작
; https://www.sysnet.pe.kr/2/0/13716

*NIX 운영체제에서 execv를 호출하면 file descriptor를 자식 프로세스에게 전달하는 것을, 윈도우도 최대한 그것을 맞춰주려 노력을 한 것입니다.

이 과정을 소스코드로 직접 추적해 볼까요? 지난 글의 예제를 활용해 _spawnv를 호출하면,

_spawnv(_P_WAIT, filePath, arglist);

// 또는, _execv(filePath, arglist);

내부적으로 common_spawnv을 거쳐 실질적인 처리를 담당하는 execute_command로 넘어갑니다.

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\exec\spawnv.cpp

extern "C" intptr_t __cdecl _execv(
    char const*        const file_name,
    char const* const* const arguments
    )
{
    return common_spawnv(_P_OVERLAY, file_name, arguments, static_cast<char const* const*>(nullptr));
}

extern "C" intptr_t __cdecl _spawnv(
    int                const mode,
    char const*        const file_name,
    char const* const* const arguments
    )
{
    return common_spawnv(mode, file_name, arguments, static_cast<char const* const*>(nullptr));
}

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\exec\spawnv.cpp

template <typename Character>
static intptr_t __cdecl common_spawnv(
    int                     const mode,
    Character const*        const file_name,
    Character const* const* const arguments,
    Character const* const* const environment
    ) throw()
{
    // ...[생략]...

    if (traits::tcsrchr(end_of_directory, '.'))
    {
        // If an extension was provided, just invoke the path:
        if (traits::taccess_s(mutated_file_name, 0) == 0)
        {
            return execute_command(mode, mutated_file_name, arguments, environment);
        }
    }
    else
    {
        // ...[생략]...
        static extension_type const extensions[4] =
        {
            { '.', 'c', 'o', 'm', '\0' },
            { '.', 'e', 'x', 'e', '\0' },
            { '.', 'b', 'a', 't', '\0' },
            { '.', 'c', 'm', 'd', '\0' }
        };

        // ...[생략]...
        for (auto it = first_extension; it != last_extension; ++it)
        {
            _ERRCHECK(traits::tcscpy_s(extension_buffer, 5, *it));

            if (traits::taccess_s(buffer.get(), 0) == 0)
            {
                errno = saved_errno;
                return execute_command(mode, buffer.get(), arguments, environment);
            }
        }
    }

    return -1;
}

execute_command가 호출하는 함수 중 accumulate_inheritable_handles가 바로 lpReserved2에 전달하는 데이터를 구성하는데요,

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\exec\spawnv.cpp

// Spawns a child process.  The mode must be one of the _P-modes from <process.h>.
// The return value depends on the mode:
// * _P_OVERLAY:  On success, calls _exit() and does not return. Returns -1 on failure.
// * _P_WAIT:     Returns (termination_code << 8 + result_code)
// * _P_DETACH:   Returns 0 on success; -1 on failure
// * Others:      Returns a handle to the process. The caller must close the handle.
template <typename Character>
static intptr_t __cdecl execute_command(
    int                     const mode,
    Character const*        const file_name,
    Character const* const* const arguments,
    Character const* const* const environment
    ) throw()
{
    // ...[생략]...

    __crt_unique_heap_ptr<BYTE> handle_data;
    size_t                      handle_data_size;
    if (!accumulate_inheritable_handles(handle_data.get_address_of(), &handle_data_size, mode != _P_DETACH))
        return -1;

    // ...[생략]...

    STARTUPINFOW startup_info = { };
    startup_info.cb          = sizeof(startup_info);
    startup_info.cbReserved2 = static_cast<WORD>(handle_data_size);
    startup_info.lpReserved2 = handle_data.get();

    PROCESS_INFORMATION process_info;
    BOOL const create_process_status = traits::create_process(
        const_cast<Character*>(file_name),
        command_line.get(),
        nullptr,
        nullptr,
        TRUE, // HANDLE 상속은 고정!
        creation_flags,
        environment_block.get(),
        nullptr,
        &startup_info,
        &process_info);

    // ...[생략]...
}

소스코드에 나오듯이 accumulate_inheritable_handles 함수는,

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\exec\spawnv.cpp

// Accumulates the inheritable file handles into *data, in the structure expected
// by the spawnee (see the lowio initialization code for the logic that decodes
// this data structure).  On success, *data and *size have the handle data array
// and the size of the handle data, and true is returned.  The caller must free
// *data.  On failure, false is returned and errno is set.
static bool accumulate_inheritable_handles(
    BYTE**  const data,
    size_t* const size,
    bool    const include_std_handles
    ) throw()
{
    return __acrt_lock_and_call(__acrt_lowio_index_lock, [&]() -> bool
    {
        *data = nullptr;
        *size = 0;

        // Count the number of handles to be inherited:
        size_t handle_count = 0;
        for (handle_count = _nhandle; handle_count != 0 && _osfile(handle_count - 1) != 0; --handle_count)
        {
        }

        size_t const max_handle_count = (USHRT_MAX - sizeof(int)) / (sizeof(char) + sizeof(intptr_t));
        _VALIDATE_RETURN_NOEXC(handle_count < max_handle_count, ENOMEM, false);

        size_t const handle_data_header_size  = sizeof(int);
        size_t const handle_data_element_size = sizeof(char) + sizeof(intptr_t);

        unsigned short const handle_data_size = static_cast<unsigned short>(
            handle_data_header_size +
            handle_count * handle_data_element_size);

        // ...[생략]...

        // Copy the handle data:
        auto flags_it  = first_flags;
        auto handle_it = first_handle;
        for (size_t i = 0; i != handle_count; ++i, ++flags_it, ++handle_it)
        {
            __crt_lowio_handle_data* const pio = _pioinfo(i);
            if ((pio->osfile & FNOINHERIT) == 0)
            {
                *flags_it  = pio->osfile;
                *handle_it = pio->osfhnd;
            }
            else
            {
                *flags_it  = 0;
                *handle_it = reinterpret_cast<intptr_t>(INVALID_HANDLE_VALUE);
            }
        }

        // ...[생략]...

        *data = handle_data.detach();
        *size = handle_data_size;
        return true;
    });
}

__pioinfo 테이블을 열거하면서 그중에서 FNOINHERIT이 설정되어 있지 않은 핸들에 대해 속성과 HANDLE 값을 기록해 둡니다. 이렇게 해서 최종 직렬화된 데이터는 다음과 같은 식으로 구성된다고 설명했는데요,

DWORD  count; 
BYTE   flags[count]; 
HANDLE handles[count]; 

// in memory these are layed out sequentially: 
[ count ][ flags... ][ handles... ] 

얼핏 file descriptor 정보는 없어 의아할 수도 있겠지만 사실 저 배열의 인덱스 자체가 file descriptor를 의미하므로 정상적인 구성이 맞습니다.




저 코드로 인해 영향을 받는 코드를 작성해 볼까요? ^^ 이를 위해 간단하게 file을 하나 생성한 후 자식 프로세스를 실행하는 코드를 작성합니다.

#include <stdio.h>
#include <iostream>
#include <fstream>
#include <process.h>
#include <string.h>

#include <io.h>
#include <fcntl.h>

#include <Windows.h>

int main(int argc, char **args)
{
    FILE* fs;
    errno_t result = fopen_s(&fs, "test.txt", "a+");
    if (result != 0)
    {
        printf("Error opening file\n");
        return 1;
    }

    char filePath[MAX_PATH];
    ::GetModuleFileNameA(nullptr, filePath, MAX_PATH);

    char drive[_MAX_DRIVE];
    char dir[_MAX_DIR];
    char fname[_MAX_FNAME];
    char ext[_MAX_EXT];

    _splitpath_s(filePath, drive, dir, fname, ext);
    _makepath_s(filePath, drive, dir, "ConsoleApplication2", ext);

    char arg1[MAX_PATH];
    sprintf_s(arg1, "\"%s\"", filePath);

    int file_descriptor = _fileno(fs);

    printf("[parent] file_descriptor: %d\n", file_descriptor);
    // 출력 결과: [parent] file_descriptor: 3

    char arg2[10];
    _itoa_s(file_descriptor, arg2, 10, 10);

    const char* const arglist[] = { arg1, arg2, 0 };

    _spawnv(_P_WAIT, filePath, arglist);
    // filePath == c:\temp\ConsoleApplication2.exe
    // arglist == ["c:\temp\ConsoleApplication2.txt", "3", "\0"]

    fclose(fs);

    return 0;
}

위의 코드에서 fopen_s(...)은 상속 가능한 HANDLE을 생성합니다. 그리고 특별히 다른 조작을 하지 않았다면 표준 I/O/Error에 각각 0, 1, 2가 할당될 것이므로 이때 할당받은 file descriptor는 3입니다.

이어서 spawnv 함수에 file descriptor를 함께 전달하고 있는데요, 이후 자식 프로세스에서는 저 숫자를 받아,

#include <iostream>
#include <Windows.h>

#include <io.h>

int main(int argc, char** argv)
{
    std::cout << "Hello World!\n";

    if (argc >= 2)
    {
        char *fd_text = argv[1]; // argv[1] == "3"
        int fd = atoi(fd_text);

        printf("[child] file_descriptor: %d\n", fd);

        _write(fd, "Hello from child\n", 18);
        _close(fd);
    }

    return 0;
}

별다른 open 과정 없이 그대로 사용할 수 있습니다. 저게 가능한 이유는, CRT 내부에서 (Windows 환경에서는 __pioinfo 테이블의 인덱스에 불과한) file descriptor를 lpReserved2에 전달해 주기 때문입니다.

Visual C++ 나름대로 *NIX 환경과 유사한 방식으로 file descriptor를 다루도록 엄청 물밑 작업을 해 놓은 것입니다. ^^




심심한데, lpReserved2로부터 file descriptor를 어떻게 추출하지는지도 마저 알아볼까요? ^^ 지난 글에 테스트한 대로, CRT를 사용하지 않았다고 해도 Windows Loader로 인해 로드되는 ucrtbase.dll에 의해 이 과정은 (윈도우 환경에서) 무조건 이뤄집니다.

그래서 Visual C++의 CRT DLL인 ucrtbase.dll (디버그 버전 ucrtbased.dll)의 DllMain 함수에서 그 과정이 시작됩니다.

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\dll\appcrt_dllmain.cpp

BOOL WINAPI __acrt_DllMain(HINSTANCE const hInstance, DWORD const fdwReason, LPVOID const lpReserved)
{
    if (fdwReason == DLL_PROCESS_ATTACH)
    {
        // The /GS security cookie must be initialized before any exception
        // handling targeting the current image is registered.  No function
        // using exception handling can be called in the current image until
        // after __security_init_cookie has been called.
        __security_init_cookie();
    }

    // The remainder of the DllMain implementation is in a separate, noinline
    // function to ensure that no code that might touch the security cookie
    // runs before the __security_init_cookie function is called.  (If code
    // that uses EH or array-type local variables were to be inlined into
    // this function, that would cause the compiler to introduce use of the
    // cookie into this function, before the call to __security_init_cookie.
    // The separate, noinline function ensures that this does not occur.)
    return DllMainDispatch(hInstance, fdwReason, lpReserved);
}

static __declspec(noinline) BOOL DllMainDispatch(HINSTANCE, DWORD const fdwReason, LPVOID const lpReserved)
{
    BOOL result = FALSE;
    switch (fdwReason)
    {
    case DLL_PROCESS_ATTACH:
        result = DllMainProcessAttach();
        break;

    case DLL_PROCESS_DETACH:
        result = DllMainProcessDetach(lpReserved != nullptr);
        break;

    case DLL_THREAD_ATTACH:
        result = __acrt_thread_attach() ? TRUE : FALSE;
        break;

    case DLL_THREAD_DETACH:
        result = __acrt_thread_detach() ? TRUE : FALSE;
        break;
    }

    __acrt_end_boot();
    return result;
}

static BOOL DllMainProcessAttach()
{
    if (!__vcrt_initialize())
        return FALSE;

    if (!__acrt_initialize())
    {
        __vcrt_uninitialize(false);
        return FALSE;
    }

    // Increment flag indicating the process attach completed successfully:
    ++__acrt_process_attached;
    return TRUE;
}

이후 __acrt_initialize를 따라가다 보면,

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\internal\initialization.cpp
__crt_bool __cdecl __acrt_initialize()
{
    #if defined CRTDLL
    __isa_available_init();
    #endif

    return __acrt_execute_initializers(
        __acrt_initializers,
        __acrt_initializers + _countof(__acrt_initializers)
        );
}

STL에서 iterator를 사용하는 것처럼 각각에 대해 시작 ~ 끝을 열거하며 init 함수를 실행하는 과정을 거치는데요,

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\internal\shared_initialization.cpp

extern "C" bool __cdecl __acrt_execute_initializers(
    __acrt_initializer const* const first,
    __acrt_initializer const* const last
    )
{
    if (first == last)
        return true;

    // Execute the initializers in [first, last), in order:
    __acrt_initializer const* it = first;
    for (; it != last; ++it)
    {
        if (it->_initialize == nullptr)
            continue;

        if (!(it->_initialize)())
            break;
    }

    // If we reached the end, all initializers completed successfully:
    if (it == last)
        return true;

    // Otherwise, the initializer pointed to by it failed.  We need to roll back
    // the initialization by executing the uninitializers corresponding to each
    // of the initializers that completed successfully:
    for (; it != first; --it)
    {
        // During initialization roll back, we do not execute uninitializers
        // that have no corresponding initializer:
        if ((it - 1)->_initialize == nullptr || (it - 1)->_uninitialize == nullptr)
            continue;

        (it - 1)->_uninitialize(false);
    }

    return false;
}

first ~ last의 함수 포인터는 (물론 버전마다 다르겠지만) 대충 다음과 같은 함수 목록으로 구성됩니다.

initialize_global_variables // C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\internal\initialization.cpp
initialize_pointers // C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\internal\initialization.cpp
__acrt_initialize_winapi_thunks // C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\internal\winapi_thunks.cpp
initialize_global_state_isolation // C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\internal\initialization.cpp
__acrt_initialize_locks // C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\internal\locks.cpp
__acrt_initialize_heap // C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\heap\heap_handle.cpp
__acrt_initialize_ptd // C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\internal\per_thread_data.cpp
__acrt_initialize_lowio // C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\lowio\ioinit.cpp
__acrt_initialize_command_line // C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\startup\argv_data.cpp
__acrt_initialize_multibyte // C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\mbstring\mbctype.cpp
initialize_environment // C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\internal\initialization.cpp
initialize_c // C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\internal\initialization.cpp

이것들 중에서 file descriptor 테이블을 복원하는 코드는 __acrt_initialize_lowio가 담당합니다.

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\lowio\ioinit.cpp

extern "C" bool __cdecl __acrt_initialize_lowio()
{
    __acrt_lock(__acrt_lowio_index_lock);
    bool result = false;
    __try
    {
        // First, allocate and initialize the initial array of lowio files:
        if (__acrt_lowio_ensure_fh_exists(0) != 0)
            __leave;

        // Next, process and initialize all inherited file handles:
        initialize_inherited_file_handles_nolock();

        // Finally, initialize the stdio handles, if they were not inherited:
        initialize_stdio_handles_nolock();
        result = true;
    }
    __finally
    {
        __acrt_unlock(__acrt_lowio_index_lock);
    }

    return result;
}

__acrt_lowio_ensure_fh_exists 함수에서 __pioinfo 영역을 확보하고,

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\lowio\osfinfo.cpp

extern "C" errno_t __cdecl __acrt_lowio_ensure_fh_exists(int const fh)
{
    _VALIDATE_RETURN_ERRCODE(static_cast<unsigned>(fh) < _NHANDLE_, EBADF);

    errno_t status = 0;

    __acrt_lock(__acrt_lowio_index_lock);
    __try
    {
        for (size_t i = 0; fh >= _nhandle; ++i)
        {
            if (__pioinfo[i])
            {
                continue;
            }

            __pioinfo[i] = __acrt_lowio_create_handle_array(); 
            if (!__pioinfo[i])
            {
                status = ENOMEM;
                __leave;
            }

            _nhandle += IOINFO_ARRAY_ELTS;
        }
    }
    __finally
    {
        __acrt_unlock(__acrt_lowio_index_lock);
    }

    return status;
}

initialize_inherited_file_handles_nolock 코드를 통해 __pioinfo에 lpReserved2로부터 전달받은 file descriptor를 복원합니다.

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.22621.0\ucrt\lowio\ioinit.cpp

static void __cdecl initialize_inherited_file_handles_nolock() throw()
{
    STARTUPINFOW startup_info;
    GetStartupInfoW(&startup_info);

    // First check and see if we inherited any file handles.  If we didn't, then
    // we don't have anything to initialize:
    if (startup_info.cbReserved2 == 0 || startup_info.lpReserved2 == nullptr)
        return;

    // Get the number of inherited handles:
    int const handle_count = *reinterpret_cast<UNALIGNED int*>(startup_info.lpReserved2);

    // Compute the start of the passed file info and OS HANDLEs:
    unsigned char* const first_file =
        reinterpret_cast<unsigned char*>(startup_info.lpReserved2) + sizeof(int);

    UNALIGNED intptr_t* const first_handle =
        reinterpret_cast<UNALIGNED intptr_t*>(first_file + handle_count);

    // Do not attempt to inherit more than the maximum number of supported handles:
    int handles_to_inherit = handle_count < _NHANDLE_ 
        ? handle_count
        : _NHANDLE_;

    // Attempt to allocate the required number of handles.  If we fail for any
    // reason, we'll inherit as many handles as we can:
    __acrt_lowio_ensure_fh_exists(handles_to_inherit);
    if (handles_to_inherit > _nhandle)
        handles_to_inherit = _nhandle;

    // Validate and copy the provided file information:
    unsigned char*      it_file   = first_file;
    UNALIGNED intptr_t* it_handle = first_handle;

    for (int fh = 0; fh != handles_to_inherit; ++fh, ++it_file, ++it_handle)
    {
        HANDLE const real_handle = reinterpret_cast<HANDLE>(*it_handle);

        // If the provided information does not appear to describe an open,
        // valid file or device, skip it:
        if (real_handle == INVALID_HANDLE_VALUE)
            continue;

        if (*it_handle == _NO_CONSOLE_FILENO)
            continue;

        if ((*it_file & FOPEN) == 0)
            continue;

        // GetFileType cannot be called for pipe handles since it may "hang" if
        // there is a blocked read pending on the pipe in the parent.
        if ((*it_file & FPIPE) == 0 && GetFileType(real_handle) == FILE_TYPE_UNKNOWN)
            continue;

        // Okay, the file looks valid:
        __crt_lowio_handle_data* const pio = _pioinfo(fh);
        pio->osfhnd = *it_handle;
        pio->osfile = *it_file;
    }
}

결국, 컴퓨터 세계에서 마법이란? 누군가의 노력으로 빚어낸 코드에 불과합니다. ^^





[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 9/22/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 16  17  18  19  20  21  22  23  24  [25]  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13312정성태4/8/202311630Windows: 244. Win32 - 시간 만료를 갖는 MessageBox 대화창 구현 (개선된 버전)파일 다운로드1
13311정성태4/7/202312850C/C++: 163. Visual Studio 2022 - DirectShow 예제 컴파일(WAV Dest)
13310정성태4/6/202311968C/C++: 162. Visual Studio - /NODEFAULTLIB 옵션 설정 후 수동으로 추가해야 할 library
13309정성태4/5/202312436.NET Framework: 2107. .NET 6+ FileStream의 구조 변화
13308정성태4/4/202312476스크립트: 47. 파이썬의 time.time() 실숫값을 GoLang / C#에서 사용하는 방법 [1]
13307정성태4/4/202311602.NET Framework: 2106. C# - .NET Core/5+ 환경의 Windows Forms 응용 프로그램에서 HINSTANCE 구하는 방법
13306정성태4/3/202311599Windows: 243. Win32 - 윈도우(cbWndExtra) 및 윈도우 클래스(cbClsExtra) 저장소 사용 방법
13305정성태4/1/202312881Windows: 242. Win32 - 시간 만료를 갖는 MessageBox 대화창 구현 (쉬운 버전) [1]파일 다운로드1
13304정성태3/31/202313218VS.NET IDE: 181. Visual Studio - C/C++ 프로젝트에 application manifest 적용하는 방법
13303정성태3/30/202311458Windows: 241. 환경 변수 %PATH%에 DLL을 찾는 규칙
13302정성태3/30/202312189Windows: 240. RDP 환경에서 바뀌는 %TEMP% 디렉터리 경로
13301정성태3/29/202312757Windows: 239. C/C++ - Windows 10 Version 1607부터 지원하는 /DEPENDENTLOADFLAG 옵션 [1]파일 다운로드1
13300정성태3/28/202311908Windows: 238. Win32 - Modal UI 창에 올바른 Owner(HWND)를 설정해야 하는 이유
13299정성태3/27/202311715Windows: 237. Win32 - 모든 메시지 루프를 탈출하는 WM_QUIT 메시지
13298정성태3/27/202311682Windows: 236. Win32 - MessageBeep 소리가 안 들린다면?
13297정성태3/26/202313146Windows: 235. Win32 - Code Modal과 UI Modal
13296정성태3/25/202312232Windows: 234. IsDialogMessage와 협업하는 WM_GETDLGCODE Win32 메시지 [1]파일 다운로드1
13295정성태3/24/202312391Windows: 233. Win32 - modeless 대화창을 modal처럼 동작하게 만드는 방법파일 다운로드1
13294정성태3/22/202312355.NET Framework: 2105. LargeAddressAware 옵션이 적용된 닷넷 32비트 프로세스의 가용 메모리 - 두 번째
13293정성태3/22/202311758오류 유형: 853. dumpbin - warning LNK4048: Invalid format file; ignored
13292정성태3/21/202312506Windows: 232. C/C++ - 일반 창에도 사용 가능한 IsDialogMessage파일 다운로드1
13291정성태3/20/202312745.NET Framework: 2104. C# Windows Forms - WndProc 재정의와 IMessageFilter 사용 시의 차이점
13290정성태3/19/202312461.NET Framework: 2103. C# - 윈도우에서 기본 제공하는 FindText 대화창 사용법파일 다운로드1
13289정성태3/18/202311413Windows: 231. Win32 - 대화창 템플릿의 2진 리소스를 읽어들여 자식 윈도우를 생성하는 방법파일 다운로드1
13288정성태3/17/202311619Windows: 230. Win32 - 대화창의 DLU 단위를 pixel로 변경하는 방법파일 다운로드1
13287정성태3/16/202311600Windows: 229. Win32 - 대화창 템플릿의 2진 리소스를 읽어들여 윈도우를 직접 띄우는 방법파일 다운로드1
... 16  17  18  19  20  21  22  23  24  [25]  26  27  28  29  30  ...