Visual C++ - 윈도우 환경에서 _execv 동작
Windows의 CreateProcess 기능을 리눅스에서는 fork + exec로 표현할 수 있습니다.
그리고 exec... 계열의 함수는 자신의 프로세스 공간에 대상 프로그램을 로딩하는 건데요, 재미있는 건 Visual C++의 CRT 함수에도 동일한 이름의 함수들이 제공됩니다.
하지만, 당연히 리눅스처럼 현재 프로세스 공간에 이미지를 로드하지는 못하고, 대신 자식 프로세스를 생성하는 식으로 우회합니다. 재미있는 건, 이 함수의 사용법인데요, 예를 들어 _execv 함수로,
_execv, _wexecv
; https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/execv-wexecv
다음과 같이 코딩을 할 수 있을 텐데요,
#include <iostream>
#include <process.h>
#include <Windows.h>
#pragma comment(lib, "Shlwapi.lib")
int main(int argc, char** argv)
{
const char* const argMark[] = {
"1",
0 };
if (argc == 1) // 인자가 1개인 경우에만, 즉 최초 실행했을 때만 자신을 다시 실행
{
char path[4096] = { 0 };
GetModuleFileNameA(nullptr, path, 4096);
auto result = _execv(path, argMark);
if (result == -1)
{
std::cout << "result: " << errno << "\n";
}
}
std::cout << "Hello World!\n";
}
하지만, 위의 프로그램을 실행하면 errno 값이 12(ENOMEM)로 나오면서 실패합니다. 이유는, 2번째 인자에 넘겨주는 값(위의 경우 argMark)의 형식이, 자신의 경로를 [0]번째 인자에 포함시키는 형식으로 전달해야 하기 때문입니다.
그러니까, 위에서처럼 정말 "1" 인자만 전달한 경우에는 argc 값이 1이 되므로 CreateProcess를 무한정 반복해 말 그대로 ENOMEM 오류가 발생하는 것입니다. 결국, 위의 코드는 아래와 같이 수정해야 합니다.
char path[4096] = { 0 };
GetModuleFileNameA(nullptr, path, 4096);
const char* const argMark[] = { path, "1", 0 };
_execv(path, argMark);
참고로, 윈도우 환경에서의 exec... 함수들은 마치 자신의 프로세스 공간에 이미지를 로드하는 것처럼 흉내 내기 위해 저 함수를 성공적으로 실행한 경우에는 호출 측 프로세스가 바로 종료합니다.
기왕 해본 김에, _execv 함수의 구현을 따라가 볼까요?
// 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));
}
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
{
// ...[생략]...
}
return -1;
}
보는 바와 같이 common_spawnv는 약간의 validation 과정을 거친 후 execute_command로 전달하는데,
// C:\Program Files (x86)\Windows Kits\10\Source\10.0.10240.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;
DWORD creation_flags = 0;
if (mode == _P_DETACH)
creation_flags |= DETACHED_PROCESS;
if (should_create_unicode_environment(Character()))
creation_flags |= CREATE_UNICODE_ENVIRONMENT;
_doserrno = 0;
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,
creation_flags,
environment_block.get(),
nullptr,
&startup_info,
&process_info);
__crt_unique_handle process_handle(process_info.hProcess);
__crt_unique_handle thread_handle(process_info.hThread);
if (!create_process_status)
{
__acrt_errno_map_os_error(GetLastError());
return -1;
}
if (mode == _P_OVERLAY)
{
// Destroy ourselves:
_exit(0);
}
else if (mode == _P_WAIT)
{
WaitForSingleObject(process_info.hProcess, static_cast<DWORD>(-1));
// ...[생략]...
}
else if (mode == _P_DETACH)
{
/* like totally detached asynchronous spawn, dude,
close process handle, return 0 for success */
return 0;
}
else
{
// Asynchronous spawn: return process handle:
return reinterpret_cast<intptr_t>(process_handle.detach());
}
}
이때 execute_command의 첫 번째 mode 인자에 따라,
1) _P_OVERLAY인 경우, 호출 측, 즉 부모를 exit(0) 함수로 종료하거나 2) _P_WAIT인 경우 자식 프로세스의 종료까지 대기하거나, 3) _P_DETACH인 경우 곧바로 반환을 합니다.
exec... 함수는 저 모드가 무조건 _P_OVERLAY로 고정된 반면 spawn... 계열 함수들은 mode를 선택할 수 있습니다. 일례로,
_spawnv, _wspawnv
; https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/spawnv-wspawnv
intptr_t _spawnv(
int mode,
const char *cmdname,
const char *const *argv
);
intptr_t _wspawnv(
int mode,
const wchar_t *cmdname,
const wchar_t *const *argv
);
저 함수들의 mode로 가능한 값이 _P_OVERLAY, _P_WAIT, _P_DETACH인 것입니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]