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)
13516정성태1/7/20249694닷넷: 2196. IIS - AppPool의 "Disable Overlapped Recycle" 옵션의 부작용
13515정성태1/6/20249641닷넷: 2195. async 메서드 내에서 C# 7의 discard 구문 활용 사례 [1]
13514정성태1/5/20249534개발 환경 구성: 702. IIS - AppPool의 "Disable Overlapped Recycle" 옵션
13513정성태1/5/20249631닷넷: 2194. C# - WebActivatorEx / System.Web의 PreApplicationStartMethod 특성
13512정성태1/4/20249906개발 환경 구성: 701. IIS - w3wp.exe 프로세스의 ASP.NET 런타임을 항상 Warmup 모드로 유지하는 preload Enabled 설정
13511정성태1/4/20249626닷넷: 2193. C# - ASP.NET Web Application + OpenAPI(Swashbuckle) 스펙 제공
13510정성태1/3/20249378닷넷: 2192. C# - 특정 실행 파일이 있는지 확인하는 방법 (Linux)
13509정성태1/3/202410558오류 유형: 887. .NET Core 2 이하의 프로젝트에서 System.Runtime.CompilerServices.Unsafe doesn't support netcoreapp2.0.
13508정성태1/3/202410044오류 유형: 886. ORA-28000: the account is locked
13507정성태1/2/202411132닷넷: 2191. C# - IPGlobalProperties를 이용해 netstat처럼 사용 중인 Socket 목록 구하는 방법파일 다운로드1
13506정성태12/29/202310277닷넷: 2190. C# - 닷넷 코어/5+에서 달라지는 System.Text.Encoding 지원
13505정성태12/27/202312106닷넷: 2189. C# - WebSocket 클라이언트를 닷넷으로 구현하는 예제 (System.Net.WebSockets)파일 다운로드1
13504정성태12/27/202311222닷넷: 2188. C# - ASP.NET Core SignalR로 구현하는 채팅 서비스 예제파일 다운로드1
13503정성태12/27/202310405Linux: 67. WSL 환경 + mlocate(locate) 도구의 /mnt 디렉터리 검색 문제
13502정성태12/26/202310941닷넷: 2187. C# - 다른 프로세스의 환경변수 읽는 예제파일 다운로드1
13501정성태12/25/202310280개발 환경 구성: 700. WSL + uwsgi - IPv6로 바인딩하는 방법
13500정성태12/24/202310788디버깅 기술: 194. Windbg - x64 가상 주소를 물리 주소로 변환
13498정성태12/23/202312459닷넷: 2186. 한국투자증권 KIS Developers OpenAPI의 C# 래퍼 버전 - eFriendOpenAPI NuGet 패키지
13497정성태12/22/202310639오류 유형: 885. Visual Studiio - error : Could not connect to the remote system. Please verify your connection settings, and that your machine is on the network and reachable.
13496정성태12/21/202310558Linux: 66. 리눅스 - 실행 중인 프로세스 내부의 환경변수 설정을 구하는 방법 (gdb)
13495정성태12/20/202310806Linux: 65. clang++로 공유 라이브러리의 -static 옵션 빌드가 가능할까요?
13494정성태12/20/202310898Linux: 64. Linux 응용 프로그램의 (C++) so 의존성 줄이기(ReleaseMinDependency) - 두 번째 이야기
13493정성태12/19/202310997닷넷: 2185. C# - object를 QueryString으로 직렬화하는 방법
13492정성태12/19/202310386개발 환경 구성: 699. WSL에 nopCommerce 예제 구성
13491정성태12/19/20239394Linux: 63. 리눅스 - 다중 그룹 또는 사용자를 리소스에 권한 부여
13490정성태12/19/202310109개발 환경 구성: 698. Golang - GLIBC 의존을 없애는 정적 빌드 방법
... 16  [17]  18  19  20  21  22  23  24  25  26  27  28  29  30  ...