sprintf_s
和 swprintf_s
在 Release 和 Debug 不同模式表现不同
int sprintf_s( char /buffer, size_t sizeOfBuffer, const char /format [, argument] ... ); int swprintf_s( wchar_t /buffer, size_t sizeOfBuffer, const wchar_t /format [, argument]... );
msdn 对第二个参数的解释仅仅是"Maximum number of characters to store." 即,可存储的最大字符个数。按照常规(至少我)的理解是这样去理解这个参数的: 当输入的字符个数大于 sizeOfBuffer 时,会进行截断等限制, 用以保证不会让 buffer 溢出;当输入的个数小于或者等于 sizeOfBuffer 时,正常赋值即可,sprintf 和 sprintf_s 的区别仅在于加了一个 sizeOfBuffer 的判断。
msdn 的解释是:
The other main difference between sprintf_s and sprintf is that sprintf_s takes a length parameter specifying the size of the output buffer in characters. If the buffer is too small for the text being printed then the buffer is set to an empty string and the invalid parameter handler is invoked.
可是实际上,在 Debug 和 Release 下 sprintf_s
表现形式不同:
Debug 模式:
#include <iostream> #include <cstdio> int main() { const int buf_size = 32; char buf1[buf_size]; memset(buf1, 0, buf_size); char buf2[buf_size]; memset(buf2, 1, buf_size); // breakpoint1 sprintf_s(buf2, buf_size*2, "%s", "hello, world!"); // breakpoint2 }
在 sprintf_s 上下插入一个端点,如上代码注释,breakpoint1 时内存块:
0x002EFC0C 01 01 01 01 01 01 01 01 ........ <-- buf2 [8 * 4 = 32] 0x002EFC14 01 01 01 01 01 01 01 01 ........ 0x002EFC1C 01 01 01 01 01 01 01 01 ........ 0x002EFC24 01 01 01 01 01 01 01 01 ........ 0x002EFC2C cc cc cc cc cc cc cc cc ???????? 0x002EFC34 00 00 00 00 00 00 00 00 ........ <-- buf1 [8 * 4 = 32] 0x002EFC3C 00 00 00 00 00 00 00 00 ........ 0x002EFC44 00 00 00 00 00 00 00 00 ........ 0x002EFC4C 00 00 00 00 00 00 00 00 ........
breakpoint2 时内存块:
<code>0x002EFC0C 68 65 6c 6c 6f 2c 20 77 hello, w <-- buf2 [8 * 8 = 64] 0x002EFC14 6f 72 6c 64 21 00 fe fe orld!.?? 0x002EFC1C fe fe fe fe fe fe fe fe ???????? 0x002EFC24 fe fe fe fe fe fe fe fe ???????? 0x002EFC2C fe fe fe fe fe fe fe fe ???????? 0x002EFC34 fe fe fe fe fe fe fe fe ???????? <-- buf1 [被覆盖了 8 * 3 = 24] 0x002EFC3C fe fe fe fe fe fe fe fe ???????? 0x002EFC44 fe fe fe fe fe fe fe fe ???????? 0x002EFC4C 00 00 00 00 00 00 00 00 ........ </code>
可以看出由于 sizeOfBuffer 传入值大于 buf_size,导致了越界,覆盖了高地址的内存块(修改了 buf1 的值)。
Release 验证代码:
#include <iostream> #include <cstdio> int main() { const int buf_size = 32; char buf1[buf_size]; memset(buf1, 0, buf_size); std::cout << buf1 << std::endl; char buf2[buf_size]; memset(buf2, 1, buf_size); int ret_size = sprintf_s(buf2, buf_size*4, "%s", "hello, world!"); std::cout << ret_size << std::endl; }
运行到最后的内存块:
0x0030F768 68 65 6c 6c 6f 2c 20 77 hello, w <-- buf1 [8 * 4 = 32] 0x0030F770 6f 72 6c 64 21 00 01 01 orld!... 0x0030F778 01 01 01 01 01 01 01 01 ........ 0x0030F780 01 01 01 01 01 01 01 01 ........ 0x0030F788 00 00 00 00 00 00 00 00 ........ <-- buf2 [8 * 4 = 32] 0x0030F790 00 00 00 00 00 00 00 00 ........ 0x0030F798 00 00 00 00 00 00 00 00 ........ 0x0030F7A0 00 00 00 00 00 00 00 00 ........
尽管我刻意的修改了几次 sprintf_s 的 sizeOfBuffer 传入值,但是它始终没有越界。
总结:
- 在 Debug 模式下 sprintf_s 是这样干的,它做的第一件事情是给 buffer 的空间长度扩展为 sizeOfBuffer, 不论实际上是否需要 sizeOfBuffer 长度的空间,也不考虑 buffer 自身的内存大小,然后进行后面的赋值操作。 这样带来的一个问题就是假如 sizeOfBuffer 的实际值大于 buffer 自身的内存大小,就会覆盖掉不属于自己内存块,进而引发 bug。
- 在 Release 模式下,按照我们所想的那样执行(从表象看:范围检查,不会进行内存扩充操作)。
我进行了源码分析,结果追踪到 _vsnprintf_s_l
这个方法以后,就找不到源码了,所以最终也没有看到微软对于 sprintf_s
的实现方式
(网上也没有搜到具体的源码实现)。我感觉造成这种不同有两种可能,一种是针对 DEBUG 和 RELEASE 分别对应两个实现版本;第二种是在 Release
模式下,编译器对代码安全检查和优化,而 Debug 模式下故意将问题暴漏出来。
int sprintf_s( char *buffer, size_t sizeOfBuffer, const char *format [, argument] ... ); int swprintf_s( wchar_t *buffer, size_t sizeOfBuffer, const wchar_t *format [, argument]... );
Parameters
- buffer -> Storage location for output
- sizeOfBuffer -> Maximum number of characters to store.
- format -> Format-control string
- argument -> Optional arguments
Return Value
The number of characters written, or –1 if an error occurred. If buffer or format is a null pointer, sprintf_s and swprintf_s return -1 and set errno to EINVAL. sprintf_s returns the number of bytes stored in buffer, not counting the terminating null character. swprintf_s returns the number of wide characters stored in buffer, not counting the terminating null wide character.
Remarks
The sprintf_s function formats and stores a series of characters and values in buffer. Each argument (if any) is converted and output according to the corresponding format specification in format. The format consists of ordinary characters and has the same form and function as the format argument for printf. A null character is appended after the last character written. If copying occurs between strings that overlap, the behavior is undefined. One main difference between sprintf_s and sprintf is that sprintf_s checks the format string for valid formatting characters, whereas sprintf only checks if the format string or buffer are NULL pointers. If either check fails, the invalid parameter handler is invoked, as described in Parameter Validation. If execution is allowed to continue, the function returns -1 and sets errno to EINVAL. The other main difference between sprintf_s and sprintf is that sprintf_s takes a length parameter specifying the size of the output buffer in characters. If the buffer is too small for the text being printed then the buffer is set to an empty string and the invalid parameter handler is invoked. Unlike snprintf, sprintf_s guarantees that the buffer will be null-terminated (unless the buffer size is zero). swprintf_s is a wide-character version of sprintf_s; the pointer arguments to swprintf_s are wide-character strings. Detection of encoding errors in swprintf_s may differ from that in sprintf_s. The versions of these functions with the _l suffix are identical except that they use the locale parameter passed in instead of the current thread locale.
PS: 如果有不同的理解,或者找到了源码,欢迎追加。