首页 > 编程知识 正文

使用正确方式和正确使用方式,小孩使用开塞露的正确方式

时间:2023-05-03 16:43:12 阅读:259311 作者:654

​这次遇到的奔溃问题,和从一次奔溃谈谈strncpy_s中有异曲同工之处:程序在执行到某个点后,瞬间干干净净的退出,也没有dmp文件生成。根据环境确定了怀疑点后,在TerminateProcess函数上加断点,然后运行程序,程序在TerminateProcess上中断下来,堆栈如下(简化模型):

 

问题出在_snprintf_s上,奔溃原因是_invalid_parameter_noinfo函数中调用了TerminateProcess函数,看意思是参数有问题。

 

_snprintf_s的有两种重载方式(stdio.h中),第一种如下:

__DEFINE_CPP_OVERLOAD_SECURE_FUNC_0_2_ARGLIST( _Success_(return >= 0) int, _snprintf_s, _vsnprintf_s, _Post_z_ char, _Buffer, _In_ size_t, _BufferCount, _In_z_ _Printf_format_string_ char const*, _Format )

 

第二种是:

_Success_(return >= 0)_Check_return_opt__CRT_STDIO_INLINE int __CRTDECL _snprintf_s( _Out_writes_z_(_BufferCount) char* const _Buffer, _In_ size_t const _BufferCount, _In_ size_t const _MaxCount, _In_z_ _Printf_format_string_ char const* const _Format, ...)​

 

第一种是只有一个长度参数_BufferCount,第二种有两个长度参数:_BufferCount和_MaxCount。

 

乍一看之下,第一种和第二种之间没啥区别,只在于差一个长度参数。

 

等等,第一种好像有点怪。这里先卖个关子(如果眼力尖,可能已经看出来了),后续揭晓。

 

MSDN对_snprintf_s参数的解释如下:

1. _BufferCount:_Buffer的大小

2. _MaxCount:可存储的最大字符数

 

MSDN对返回值的说明如下:

1. _snprintf_s returns the number of characters stored in buffer, not counting the terminating null character.

 

2. If the storage required to store the data and a terminating null exceeds sizeOfBuffer, the invalid parameter handler is invoked, as described in Parameter Validation. If execution continues after the invalid parameter handler, these functions set buffer to an empty string, set errno to ERANGE, and return -1.

 

3. If buffer or format is a NULL pointer, or if count is less than or equal to zero, the invalid parameter handler is invoked. If execution is allowed to continue, these functions set errno to EINVAL and return -1.

 

备注:sizeOfBuffer就是_BufferCount。

 

大致意思是:

 

1. _snprintf_s返回缓冲区中存储的字符数,不包括终止 null 字符

 

2. 如果存储数据和终止 null 值所需的存储空间超过sizeOfBuffer,则会调用无效参数处理程序。如果在无效的参数处理程序之后继续执行,则这些函数将缓冲区设置为空字符串,将errno设置为ERANGE,并返回-1。

 

3. 如果buffer或format为空指针,或者count小于或等于零,则调用无效的参数处理程序。如果允许继续执行, 则这些函数会将errno设置为EINVAL , 并返回-1。

 

其中无效的参数处理程序就是终止程序,EINVAL值是22,ERANGE值是34。errno的取值中,22表示Invalid argument,34表示Numerical result out of range.

 

简化后的示例源码如下:

int main(){ char szDest[8] = { 0 }; _snprintf_s(szDest, sizeof(szDest), "1234567%d", 8);     return   0;}

 

根据msdn对_snprintf_s的说明,目标缓冲区大小为8字节,格式化后的结果长度是8,因此触发无效参数处理程序,抛出异常,程序终止。

 

具体奔溃点如下:

 

奔溃点是在common_vsnprintf_s函数中,源码如下:

template <typename Character>_Success_(return >= 0)static int __cdecl common_vsnprintf_s( unsigned __int64 const options, _Out_writes_z_(buffer_count) Character* const buffer, size_t const buffer_count, size_t const max_count, Character const* const format, _locale_t const locale,      va_list             const   arglist ) throw(){      _VALIDATE_RETURN(format != nullptr, EINVAL, -1); if (max_count == 0 && buffer == nullptr && buffer_count == 0) return 0; // No work to do​ _VALIDATE_RETURN(buffer != nullptr && buffer_count > 0, EINVAL, -1);​ int result = -1; if (buffer_count > max_count) { errno_t const saved_errno = errno; result = common_vsprintf<format_validation_base>(options, buffer, max_count + 1, format, locale, arglist);          if (result == -2) { // The string has been truncated; return -1: _SECURECRT__FILL_STRING(buffer, buffer_count, max_count + 1); if (errno == ERANGE)                errno = saved_errno; return -1; } } else {#pragma warning(suppress:__WARNING_UNUSED_SCALAR_ASSIGNMENT) // 28931 errno_t const saved_errno = errno; result = common_vsprintf<format_validation_base>(options, buffer, buffer_count, format, locale, arglist);          buffer[buffer_count - 1] = 0; // We allow truncation if count == _TRUNCATE if (result == -2 && max_count == _TRUNCATE) { if (errno == ERANGE)                errno = saved_errno; return -1; }      } if (result < 0) { buffer[0] = 0; _SECURECRT__FILL_STRING(buffer, buffer_count, 1); if (result == -2) { _VALIDATE_RETURN(("Buffer too small", 0), ERANGE, -1);          } return -1;      } _SECURECRT__FILL_STRING(buffer, buffer_count, result + 1);      return result < 0 ? -1 : result;}

 

奔溃点在_VALIDATE_RETURN 这个宏上,宏已经说明了具体原因:缓冲区太小。

 

在调_VALIDATE_RETURN 之前,先调用了_SECURECRT__FILL_STRING 。

 

_SECURECRT__FILL_STRING宏定义如下:

 

其他相关宏定义如下:

#define _SECURECRT_FILL_BUFFER_PATTERN 0xFE#define _TRUNCATE ((size_t)-1)#define ERANGE          34

 

根据上述代码,_SECURECRT__FILL_STRING 的作用是将_String的第一个字节填充为’’,后续字节填充为0xFE。奔溃时szDest的内存布局如下:

 

_snprintf_s函数的最终实现方是common_vsnprintf_s,并且common_vsnprintf_s的入参长度buffer_count和max_count均为8,这是如何做到的?

 

调用堆栈如下:

 

仔细看堆栈,我们看到_snprintf_s实际对应的版本是:

_snprintf_s<8>(char[8]   & _Buffer, unsigned int _BufferCount, const char * _Format, ...) 

 

有没有看到重点?

 

我们调用的是_snprintf_s的第一种重载方式,也即只有一个长度参数,最终到common_vsnprintf_s中是两个长度参数,且调用的版本取得了szDest的缓冲区长度8.

 

How?

 

再次贴一下_snprintf_s一个长度参数的重载形式:

__DEFINE_CPP_OVERLOAD_SECURE_FUNC_0_2_ARGLIST( _Success_(return >= 0) int, _snprintf_s, _vsnprintf_s, _Post_z_ char, _Buffer, _In_ size_t, _BufferCount, _In_z_ _Printf_format_string_ char const*, _Format )

 

仔细看,这里不是声明,貌似的入参缓冲区是char, _Buffer,,注意char和_Buffer之间的逗号,并且,不是char指针。从这个声明看不出啥,我们通过预处理文件,看看到底是什么情况:

template <size_t _Size>inline int __cdecl _snprintf_s( char (&_Buffer)[_Size], size_t _BufferCount, char const* _Format, ...) throw(){ va_list _ArgList; ((void)(__vcrt_va_start_verify_argument_type<decltype(_Format)>(), ((void)(_ArgList = (va_list)(&const_cast<char&>(reinterpret_cast<const volatile char&>(_Format))) + ((sizeof(_Format) + sizeof(int) - 1) & ~(sizeof(int) - 1)))))); return _vsnprintf_s(_Buffer, _Size, _BufferCount, _Format, _ArgList); } __pragma(warning(pop));}

 

从预处理文件中,可以看到_snprintf_s第一种重载方式的真身,基于模板的声明。

template <size_t _Size>inline int __cdecl _snprintf_s(char (&_Buffer)[_Size], size_t _BufferCount, char const* _Format, ...) throw()

 

到这里,一切都清晰明了,整个调用链就清楚了:为什么只传入了一个长度参数,最终是两个长度参数起作用。并且,我们也知道了,对于第一种重载方式,传入的_BufferCount,最终被用为了_MaxCount。因为,真正使用的_BufferCount是通过模板推导出来的。

 

从common_vsnprintf_s的实现可以看出,如果接收缓冲区长度不足以接收格式化后的数据以及’’结束符,那么会抛出“Buffer too small”的异常(Debug下弹框,Release下程序直接退出)。

 

那么,要画重点了。

 

对于_snprintf_s第一种重载方式,稳妥用法是:_snprintf_s( szBuf, sizeof(szBuf) - 1, format),其中,szBuf是字符串数组。

 

对于_snprintf_s第二种重载,接收缓冲区是char*,有两个长度参数,一个是缓冲区长度,一个是最多拷贝多少字符。这个重载也会调用到common_vsnprintf_s函数,因此,稳妥用法是:

_snprintf_s(szBuf, szBuf大小, szBuf大小 – 1, format)

 

同时,_snprintf_s第二种重载也适用于第一种重载的字符串数组缓冲区(反之不成立),因此,_snprintf_s的正确打开方式是:

_snprintf_s(szBuf, szBuf大小, szBuf大小 – 1, format)

 

 

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。