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

비밀번호

댓글 작성자
 




... 46  47  48  49  50  51  52  53  54  [55]  56  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
12561정성태3/12/202116669VS.NET IDE: 159. Visual Studio에서 개행(\n, \r) 등의 제어 문자를 치환하는 방법 - 정규 표현식 사용
12560정성태3/11/202117732개발 환경 구성: 549. ssh-keygen으로 생성한 PKCS#1 개인키/공개키 파일을 각각 PKCS8/PEM 형식으로 변환하는 방법
12559정성태3/11/202117971.NET Framework: 1028. 닷넷 5 환경의 Web API에 OpenAPI 적용을 위한 NSwag 또는 Swashbuckle 패키지 사용 [2]파일 다운로드1
12558정성태3/10/202117047Windows: 192. Power Automate Desktop (Preview) 소개 - Bitvise SSH Client 제어 [1]
12557정성태3/10/202115264Windows: 191. 탐색기의 보안 탭에 있는 "Object name" 경로에 LEFT-TO-RIGHT EMBEDDING 제어 문자가 포함되는 문제
12556정성태3/9/202113547오류 유형: 703. PowerShell ISE의 Debug / Toggle Breakpoint 메뉴가 비활성 상태인 경우
12555정성태3/8/202116841Windows: 190. C# - 레지스트리에 등록된 DigitalProductId로부터 라이선스 키(Product Key)를 알아내는 방법파일 다운로드2
12554정성태3/8/202116404.NET Framework: 1027. 닷넷 응용 프로그램을 위한 PDB 옵션 - full, pdbonly, portable, embedded
12553정성태3/5/202116402개발 환경 구성: 548. 기존 .NET Framework 프로젝트를 .NET Core/5+ 용으로 변환해 주는 upgrade-assistant, try-convert 도구 소개 [4]
12552정성태3/5/202115857개발 환경 구성: 547. github workflow/actions에서 Visual Studio Marketplace 패키지 등록하는 방법
12551정성태3/5/202114215오류 유형: 702. 비주얼 스튜디오 - The 'CascadePackage' package did not load correctly. (2)
12550정성태3/5/202113962오류 유형: 701. Live Share 1.0.3713.0 버전을 1.0.3884.0으로 업데이트 이후 ContactServiceModelPackage 오류 발생하는 문제
12549정성태3/4/202115243오류 유형: 700. VsixPublisher를 이용한 등록 시 다양한 오류 유형 해결책
12548정성태3/4/202116373개발 환경 구성: 546. github workflow/actions에서 nuget 패키지 등록하는 방법
12547정성태3/3/202117035오류 유형: 699. 비주얼 스튜디오 - The 'CascadePackage' package did not load correctly.
12546정성태3/3/202116867개발 환경 구성: 545. github workflow/actions에서 빌드시 snk 파일 다루는 방법 - Encrypted secrets
12545정성태3/2/202119729.NET Framework: 1026. 닷넷 5에 추가된 POH (Pinned Object Heap) [10]
12544정성태2/26/202119943.NET Framework: 1025. C# - Control의 Invalidate, Update, Refresh 차이점 [2]
12543정성태2/26/202117915VS.NET IDE: 158. C# - 디자인 타임(design-time)과 런타임(runtime)의 코드 실행 구분
12542정성태2/20/202119583개발 환경 구성: 544. github repo의 Release 활성화 및 Actions를 이용한 자동화 방법 [1]
12541정성태2/18/202117152개발 환경 구성: 543. 애저듣보잡 - Github Workflow/Actions 소개
12540정성태2/17/202118266.NET Framework: 1024. C# - Win32 API에 대한 P/Invoke를 대신하는 Microsoft.Windows.CsWin32 패키지
12539정성태2/16/202118188Windows: 189. WM_TIMER의 동작 방식 개요파일 다운로드1
12538정성태2/15/202118657.NET Framework: 1023. C# - GC 힙이 아닌 Native 힙에 인스턴스 생성 - 0SuperComicLib.LowLevel 라이브러리 소개 [2]
12537정성태2/11/202119353.NET Framework: 1022. UI 요소의 접근은 반드시 그 UI를 만든 스레드에서! - 두 번째 이야기 [2]
12536정성태2/9/202118199개발 환경 구성: 542. BDP(Bandwidth-delay product)와 TCP Receive Window
... 46  47  48  49  50  51  52  53  54  [55]  56  57  58  59  60  ...