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;
}
}
결국, 컴퓨터 세계에서 마법이란? 누군가의 노력으로 빚어낸 코드에 불과합니다. ^^
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]