0

我有一个服务器和一个客户端。我正在使用winsock2。客户端发送 4 个字节:

char *ack = new char[4];
sprintf( ack, "%d", counter );
sendto( clientSocket, ack, 4, 0, ( struct sockaddr* )&remote, sizeof( remote ) );

服务器接收到这 4 个字节:

char* acks = new char[4];
if( ( bytes = recvfrom( serverSocket, acks, 4, 0, ( struct sockaddr* )&remote, &remote_size ) ) == SOCKET_ERROR ) {
    cout << "socket error = " << WSAGetLastError() << endl;
    break;
}
if( bytes > 0 ) {
    sscanf( acks, "%d", &i );
}

我收到了这个错误,我不知道如何解决它:

>Critical error detected c0000374
>
>server.exe has triggered a breakpoint.

我知道指针和内存分配有问题。但我的 C++ 技能是基本的。

4

1 回答 1

1

字符串格式溢出

最紧迫的问题是您正在使用 sprintf 和 sscanf。避免使用 sprintf 和 sscanf - 它们很容易意外地创建您在此处看到的错误类型,即缓冲区溢出(在您的客户端和服务器上)。

考虑当您的“计数器”值为 1729 时,您的客户端会发生什么。您的代码将运行

sprintf(ack, "%d", 1729);

1729 的 C 样式字符串表示为5 个字节长- char 值'1''7''2''9'和各一个字节'\0'。但是您的 ack 缓冲区只有 4 个字节长!现在您已将最后一个零字节写入您从未分配的一些内存块中。在 C/C++ 中,这是未定义的行为,这意味着您的程序可能会崩溃,也可能不会,如果它没有崩溃,它可能会在稍后出现微妙的错误,或者它可能工作得很好,或者它可能工作得最多的时间,除了它在星期二休息。

这不是一个好地方。

你可能想知道,“如果这太糟糕了,为什么不sprintf直接返回一个错误或者我用一个太小的缓冲区调用它?” 答案1sprintf无法进行检查,因为它无法告诉您ack实际有多大。当您的代码在这里调用sprintf时,知道 ack 的长度为 4 个字节(因为您刚刚创建了它),但是 sprintf 看到的所有内容都是指向某个内存的指针——您没有告诉它长度,所以它只需要一味地希望你给它的那块内存足够大。

盲目地希望是编写软件的一种非常糟糕的方式。

您可以在这里考虑一些替代方案。

  1. 如果您实际上只是想通过网络发送一个 int,则根本不需要对 int 进行字符串化 - 只需通过将其reinterpret_cast<char*>(&counter)作为缓冲区传递给 sendto 2并以 sizeof(counter) 作为相应的缓冲区来以本机格式发送它长度。在另一端的 recvfrom 中使用类似的结构。请注意,如果您的发送者和接收者具有不同的 int 底层表示(例如,如果它们使用不同的字节序),这将中断,但是由于您在这里谈论的是 Winsock,我假设您假设两端都是最近的不会有问题的 Windows 版本。
  2. 如果您确实需要首先对内容进行字符串化,请使用可识别大小的字符串转换函数,例如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 地址接收,您可能会遇到这样的错误。这个答案贯穿了一些相关的血腥细节。

于 2013-11-04T08:50:54.133 回答