字符串格式溢出
最紧迫的问题是您正在使用 sprintf 和 sscanf。避免使用 sprintf 和 sscanf - 它们很容易意外地创建您在此处看到的错误类型,即缓冲区溢出(在您的客户端和服务器上)。
考虑当您的“计数器”值为 1729 时,您的客户端会发生什么。您的代码将运行
sprintf(ack, "%d", 1729);
1729 的 C 样式字符串表示为5 个字节长- char 值'1'
、'7'
、'2'
、'9'
和各一个字节'\0'
。但是您的 ack 缓冲区只有 4 个字节长!现在您已将最后一个零字节写入您从未分配的一些内存块中。在 C/C++ 中,这是未定义的行为,这意味着您的程序可能会崩溃,也可能不会,如果它没有崩溃,它可能会在稍后出现微妙的错误,或者它可能工作得很好,或者它可能工作得最多的时间,除了它在星期二休息。
这不是一个好地方。
你可能想知道,“如果这太糟糕了,为什么不sprintf
直接返回一个错误或者我用一个太小的缓冲区调用它?” 答案1是sprintf
无法进行检查,因为它无法告诉您ack
实际有多大。当您的代码在这里调用sprintf
时,您知道 ack 的长度为 4 个字节(因为您刚刚创建了它),但是 sprintf 看到的所有内容都是指向某个内存的指针——您没有告诉它长度,所以它只需要一味地希望你给它的那块内存足够大。
盲目地希望是编写软件的一种非常糟糕的方式。
您可以在这里考虑一些替代方案。
- 如果您实际上只是想通过网络发送一个 int,则根本不需要对 int 进行字符串化 - 只需通过将其
reinterpret_cast<char*>(&counter)
作为缓冲区传递给 sendto 2并以 sizeof(counter) 作为相应的缓冲区来以本机格式发送它长度。在另一端的 recvfrom 中使用类似的结构。请注意,如果您的发送者和接收者具有不同的 int 底层表示(例如,如果它们使用不同的字节序),这将中断,但是由于您在这里谈论的是 Winsock,我假设您假设两端都是最近的不会有问题的 Windows 版本。
- 如果您确实需要首先对内容进行字符串化,请使用可识别大小的字符串转换函数,例如boost::format(隐式识别大小,因为它处理 std::string 而不是原始 char* 缓冲区)或_snprintf_s / _snscanf_s(它显式地接受缓冲区长度参数,但是是特定于 Microsoft 的)。
Recvfrom 访问冲突
sscanf/sprintf 中的溢出不一定能解释这一点,但是:
我只想补充一点,错误发生在 sscanf 行中。如果我评论该行,则会在 recvfrom 行中发生错误。
对此的一种可能解释可能是没有为远程地址提供足够的空间,但只要您remote_size
正确反映了您的remote
,我希望这会导致recvfrom
返回错误3,而不是崩溃。另一种可能性是传递错误的内存/句柄(例如,如果您将new
操作员设置为不抛出失败,或者如果您的套接字初始化失败并且您没有退出)。如果没有看到初始化所有变量的代码,以及理想情况下您在那种情况下得到的实际错误,就不可能准确地说出。
1尽管 sprintf 无法捕捉到这一点,静态分析工具(如 Visual Studio 2012/2013 中包含的那些)非常有能力捕捉到这个特定的错误。如果您通过默认的 Visual Studio 2012 代码分析器运行发布的代码,它会报错:
错误 C4996:“sprintf”:此函数或变量可能不安全
2static_cast<char*>(static_cast<void*>(&counter))
有些人喜欢reinterpret_cast<char*>(&counter)
。两者都有效,它本质上是一种编码约定选择。
3例如,如果您初始化remote
为 aSOCKADDR_IN
而不是 a SOCKADDR_STORAGE
,如果您碰巧从 IPv6 地址接收,您可能会遇到这样的错误。这个答案贯穿了一些相关的血腥细节。