어제 직원이 strcpy_s를 사용하면 예외가 잡히지 않는다며 코드를 보내왔다. 아래 코드다. 어떻게 컴파일하든 catch가 출력되는 것을 볼 수 없다. strcpy_s대신 strcpy를 하면 잘 된다는 이야기.
#include <Windows.h>
int main()
{
__try
{
strcpy_s(0, 10, "aaaa");
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
printf("catch\n");
}
}
그래서 알아보기로 했다. 왜 예외가 잡히지 않는지를… strcpy_s는 다음과 같이 구현되어 있다.
extern "C" errno_t __cdecl strcpy_s(
char* const destination,
size_t const size_in_elements,
char const* const source
)
{
return common_tcscpy_s(destination, size_in_elements, source);
}
common_tcscpy_s로 들어가보면 다음과 같다.
// _strcpy_s() and _wcscpy_s()
template <typename Character>
_Success_(return == 0)
static errno_t __cdecl common_tcscpy_s(
_Out_writes_z_(size_in_elements) Character* const destination,
_In_ size_t const size_in_elements,
_In_z_ Character const* const source
) throw()
{
_VALIDATE_STRING(destination, size_in_elements);
_VALIDATE_POINTER_RESET_STRING(source, destination, size_in_elements);
Character* destination_it = destination;
Character const* source_it = source;
size_t available = size_in_elements;
while ((*destination_it++ = *source_it++) != 0 && --available > 0)
{
}
if (available == 0)
{
_RESET_STRING(destination, size_in_elements);
_RETURN_BUFFER_TOO_SMALL(destination, size_in_elements);
}
_FILL_STRING(destination, size_in_elements, size_in_elements - available + 1);
_RETURN_NO_ERROR;
}
_VALIDATE_STRING 매크로에서 destination이 NULL인 경우 체크가 들어간다는 것을 알 수 있다.
/* validations */
#define _VALIDATE_STRING_ERROR(_String, _Size, _Ret) \
_VALIDATE_RETURN((_String) != NULL && (_Size) > 0, EINVAL, (_Ret))
#define _VALIDATE_STRING(_String, _Size) \
_VALIDATE_STRING_ERROR((_String), (_Size), EINVAL)
_VALIDATE_RETURN 매크로는 _INVALID_PARAMETER 호출로 이어진다.
#define _VALIDATE_RETURN(expr, errorcode, retexpr) \
{ \
int _Expr_val = !!(expr); \
_ASSERT_EXPR((_Expr_val), _CRT_WIDE(#expr)); \
if (!(_Expr_val)) \
{ \
*_errno() = (errorcode); \
_INVALID_PARAMETER(_CRT_WIDE(#expr)); \
return (retexpr); \
} \
}
_INVALID_PARAMETER는 릴리즈 버전에서 _invalid_parameter_noinfo 호출로 이어진다.
#ifdef _DEBUG
#define _INVALID_PARAMETER(expr) _invalid_parameter(expr, __FUNCTIONW__, __FILEW__, __LINE__, 0)
#else
#define _INVALID_PARAMETER(expr) _invalid_parameter_noinfo()
#endif
_invalid_parameter_noinfo는 최종적으로 _invalid_parameter_internal 호출로 이어진다.
extern "C" void __cdecl _invalid_parameter_noinfo()
{
_invalid_parameter(nullptr, nullptr, nullptr, 0, 0);
}
extern "C" void __cdecl _invalid_parameter(
wchar_t const* const expression,
wchar_t const* const function_name,
wchar_t const* const file_name,
unsigned int const line_number,
uintptr_t const reserved
)
{
__crt_cached_ptd_host ptd;
return _invalid_parameter_internal(expression, function_name, file_name, line_number, reserved, ptd);
}
_invalid_parameter_internal을 살펴보면 다음과 같다. _thread_local_iph 핸들러가 있는 경우 해당 핸들러를, 없는 경우 글로벌 핸들러를, 그마저도 없는 경우에는 _invoke_watson으로 이어진다. 일반적으로 핸들러 설정이 되어 있지 않기 때문에 _invoke_watson 호출로 진행된다.
extern "C" void __cdecl _invalid_parameter_internal(
wchar_t const* const expression,
wchar_t const* const function_name,
wchar_t const* const file_name,
unsigned int const line_number,
uintptr_t const reserved,
__crt_cached_ptd_host& ptd
)
{
__acrt_ptd * const raw_ptd = ptd.get_raw_ptd_noexit();
if (raw_ptd && raw_ptd->_thread_local_iph)
{
raw_ptd->_thread_local_iph(expression, function_name, file_name, line_number, reserved);
return;
}
_invalid_parameter_handler const global_handler = __crt_fast_decode_pointer(__acrt_invalid_parameter_handler.value(ptd));
if (global_handler)
{
global_handler(expression, function_name, file_name, line_number, reserved);
return;
}
_invoke_watson(expression, function_name, file_name, line_number, reserved); <== null 인경우 이쪽으로 도달
}
_invoke_watson은 다음과 같이 구현되어 있다. 일반적으로 PF_FASTFAIL_AVAILABLE 플래그가 설정되어 있어서 __fastfail 호출로 이어진다.
extern "C" __declspec(noreturn) void __cdecl _invoke_watson(
wchar_t const* const expression,
wchar_t const* const function_name,
wchar_t const* const file_name,
unsigned int const line_number,
uintptr_t const reserved
)
{
UNREFERENCED_PARAMETER(expression );
UNREFERENCED_PARAMETER(function_name);
UNREFERENCED_PARAMETER(file_name );
UNREFERENCED_PARAMETER(line_number );
UNREFERENCED_PARAMETER(reserved );
if (IsProcessorFeaturePresent(PF_FASTFAIL_AVAILABLE))
{
__fastfail(FAST_FAIL_INVALID_ARG);
}
// Otherwise, raise a fast-fail exception and termintae the process:
__acrt_call_reportfault(
_CRT_DEBUGGER_INVALIDPARAMETER,
STATUS_INVALID_CRUNTIME_PARAMETER,
EXCEPTION_NONCONTINUABLE);
TerminateProcess(GetCurrentProcess(), STATUS_INVALID_CRUNTIME_PARAMETER);
}
__fastfail은 아래 문서를 참고하면 int 0x29로 구현된 컴파일러 함수다.
https://docs.microsoft.com/en-us/cpp/intrinsics/fastfail?view=msvc-170
결국 strcpy_s는 최종적으로 int 0x29로 이어진다. fastfail이란 말에서 알 수 있듯이 해당 인터럽트는 예외에 잡히지 않는다. 결국 strcpy_s 내부적으로 널체크를 해서 특수한 처리를 했기 때문에 예외에 잡히지 않는다. strcpy_s((char*)0x123, 10, "aaaa"); 등과 같이 호출하면 정상적으로 예외 처리가 이루어진다.
int main()
{
__try
{
asm int 0x29
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
printf("catch\n");
}
}
strcpy_s 입장에서는 NULL이면 프로그램을 종료시켜버리는 게 안전한 구현일 수 있다. 하지만 경우에 따라서는 오바하는 함수가 될수도 있겠다. 내가 알아서 할건데 왜 고작 문자열 복사하는 기능만 하면 될 함수가 릴리즈 버전에서 널체크까지 해가며 멋대로 강종시키지? 프로그램을? 예외처리도 안되게?