【问题标题】:String-handling practices in C [closed]C中的字符串处理实践[关闭]
【发布时间】:2011-01-02 20:21:13
【问题描述】:

我正在使用纯 C (c99) 语言开始一个新项目,该项目主要使用文本。由于外部项目限制,此代码必须非常简单和紧凑,由一个源代码文件组成,没有外部依赖项或库,除了 libc 和类似的普遍存在的系统库。

了解这些之后,有哪些最佳实践、陷阱、技巧或其他技术可以帮助项目的字符串处理更加健壮和安全?

【问题讨论】:

  • 首先使用 C 算作陷阱吗? /* ouch, stop! just kidding! */
  • @anatolyg:为什么有人要使用宽字符?呃。
  • 这个问题太棒了……很多错误答案都是基于对显然“安全”解决方案的误解。哇,C 让安全的字符串处理真的很难
  • @Konrad:唯一让事情变得困难的是大量的错误信息和不良例子。
  • @R..:我认为 Konrad 的意思是“安全”字符串处理的做法在 C 中并不明显。当然,如果你知道怎么做,一切都很容易。但是 C 是一种“足够的绳索”语言,几乎没有方向和很多不好的先例。这使得做“正确”的事情变得困难。

标签: c security string robustness


【解决方案1】:

如果没有关于您的代码在做什么的任何其他信息,我建议您设计所有界面,如下所示:

size_t foobar(char *dest, size_t buf_size, /* operands here */)

语义如snprintf:

  • dest 指向大小至少为 buf_size 的缓冲区。
  • 如果buf_size 为零,则dest 可接受空/无效指针,并且不会写入任何内容。
  • 如果 buf_size 非零,dest总是以空值终止。
  • 每个函数foobar返回完整的非截断输出的长度;如果buf_size 小于或等于返回值,则输出已被截断。

这样,当调用者可以很容易地知道所需的目标缓冲区大小时,可以提前获得足够大的缓冲区。如果调用者不能轻易知道,它可以使用buf_size 的零参数或使用“可能足够大”的缓冲区调用该函数一次,并且仅在空间不足时重试。

您还可以制作类似于 GNU asprintf 函数的此类调用的包装版本,但如果您希望代码尽可能灵活,我会避免在实际字符串函数中进行任何分配。在调用者级别处理失败的可能性总是更容易,并且许多调用者可以通过使用本地缓冲区或在程序中更早获得的缓冲区来确保永远不会失败,以便更大的操作成功或失败是原子的(极大地简化了错误处理)。

【讨论】:

    【解决方案2】:

    来自长期嵌入式开发人员的一些想法,其中大部分都详细说明了您对简单性的要求,而不是 C 特定的:

    • 确定您需要哪些字符串处理函数,并将该集合保持尽可能小,以最大限度地减少故障点。

    • 按照 R. 的建议定义一个清晰的接口,该接口在所有字符串处理程序中都是一致的。一组严格的、小而详细的规则允许您将模式匹配用作调试工具:您可能会怀疑任何看起来与其他代码不同的代码。

    • 正如 Bart van Ingen Schenau 所说,跟踪缓冲区长度与字符串长度无关。如果您将始终使用文本,则使用标准空字符表示字符串结尾是安全的,但您需要确保 text+null 适合缓冲区。

    • 确保所有字符串处理程序的行为一致,尤其是在缺少标准函数的情况下:截断、空输入、空终止、填充、

    • 如果您绝对需要违反任何规则,请为此创建一个单独的函数并适当命名。换句话说,给每个函数一个明确的行为。因此,您可以将str_copy_and_pad() 用于始终用空值填充其目标的函数。

    • 尽可能使用安全的内置函数(例如 memmove() Jonathan Leffler)来完成繁重的工作。但是测试他们以确保他们正在做你认为他们正在做的事情!

    • 尽快检查错误。未检测到的缓冲区溢出可能会导致众所周知的难以定位的“弹跳”错误。

    • 每个函数编写测试,以确保它满足合同。一定要覆盖边缘情况(关闭 1,空/空字符串,源/目标重叠,等。)这听起来很明显,但请确保您了解如何创建和检测缓冲区underrun/overrun,然后编写显式生成并检查这些问题的测试。 (我的 QA 人员可能已经厌倦了听到我的指示“不要只是测试以确保它有效;测试以确保它不会损坏。”)

    以下是一些对我有用的技巧:

    • 为内存管理例程创建包装器,在分配期间在缓冲区的任一端分配“围栏字节”,并在释放时检查它们。您还可以在字符串处理程序中验证它们,也许在设置 STR_DEBUG 宏时。 警告:您需要彻底测试您的诊断,以免造成额外的故障点。

    • 创建一个封装缓冲区及其长度的数据结构。 (如果你使用它们,它也可以包含栅栏字节。)警告:你现在有一个非标准的数据结构,你的整个代码库必须管理,这可能意味着大量的重写(和因此会增加额外的故障点)。

    • 让您的字符串处理程序验证其输入。如果函数禁止空指针,请明确检查它们。如果它需要一个有效的字符串(如strlen() should)并且您知道缓冲区长度,请检查缓冲区是否包含空字符。换句话说,验证您可能对代码或数据所做的任何假设。

    • 首先编写测试。这将帮助您理解每个函数的契约——确切地说它对调用者的期望是什么,以及调用者应该从它那里得到什么。您会发现自己在思考如何使用它、它可能会破坏的方式以及它必须处理的极端情况。

    非常感谢您提出这个问题!我希望更多的开发人员能够考虑这些问题——尤其是在他们开始编码之前。祝你好运,并祝愿产品强大、成功!

    【讨论】:

    • 前半段和最后一段+1。他们已经足够好,以至于我在下半场暂缓评判。 :-) 空指针测试是我考虑的有害因素之一,虽然花哨的结构可以帮助您调试,但它们也使您的代码在使用和与其他代码集成时变得更加痛苦。相反,我会严格测试你的函数以满足他们的合同,然后你不需要任何进一步的运行时检查混乱。
    • @R.:我同意彻底的测试是关键,而且“花式结构”通常弊大于利。但是,正确使用简单的也可以增加健壮性和可测试性,特别是如果它们是设计的一个组成部分。你能解释一下你不喜欢空指针测试吗?我喜欢测试我能做的所有事情,但我经常使用宏(例如#ifdef TEST)来围绕可能昂贵或多余的测试。感谢您的想法!
    • 我在关于空指针测试优缺点的长时间讨论中的部分是:stackoverflow.com/questions/4390007/…
    • 感谢(非空)参考!那里有很多好主意。我认为 tylerl 的情况很不寻常,因为他从头开始,因此控制了整个设计。因此,在这里,它不再是“与他人相处融洽”的问题,而是关于定义一套连贯的严格规则并在任何地方强制执行的问题。至少在代码成熟之前,我宁愿在过度验证方面犯错,也不愿错过通过开发系统传播的细微错误。
    【解决方案3】:

    查看strlcpystrlcat,详情请参阅original paper

    【讨论】:

    • 连接几乎总是一个坏习惯(在安全性、性能和复杂性方面)。
    【解决方案4】:

    两美分:

    1. 始终使用“n”版本的字符串函数:strncpy、strncmp(或 wcsncpy、wcsncmp 等)
    2. 始终使用 +1 成语进行分配:例如char* str[MAX_STR_SIZE+1],然后传递 MAX_STR_SIZE 作为“n”版本字符串函数的大小,并以 str[MAX_STR_SIZE] = '\0' 结束;以确保所有字符串都正确完成。

    最后一步很重要,因为如果达到最大大小,“n”版本的字符串函数将不会在复制后附加 '\0'。

    【讨论】:

    • -1 s/always/never/ for strncpy。这个函数并不像人们认为的那样做,而且几乎从来没有用过。
    • @R..:你认为它到底在做什么?能举个具体的例子吗?
    • strncpy 不会空终止,并且会空填充目标缓冲区的整个剩余部分,这非常浪费时间。您可以通过添加自己的空填充来解决前者,但不能解决后者。它从未打算用作“安全字符串处理”功能,而是用于处理 Unix 目录表和数据库文件中的固定大小字段。 snprintf(dest, n, "%s", src) 是标准 C 中唯一正确的“安全 strcpy”,但它可能会慢很多。
    • 顺便说一句,截断本身可能是一个主要错误,在某些情况下可能会导致特权提升或 DoS,因此抛出“安全”字符串函数来截断其输出不是一种方法使其“安全”或“安全”。相反,您应该确保目标缓冲区的大小正确并简单地使用strcpy(或者更好的是,如果您已经知道源字符串长度,则使用memcpy)。
    • 请注意,strncat() 在其界面中比strncpy() 更令人困惑——这个长度参数究竟是什么?根据您提供的strncpy() 等,这不是您所期望的 - 所以它甚至比strncpy() 更容易出错。对于复制字符串,我越来越认为有一个强有力的论点是你只需要memmove(),因为你总是提前知道所有的大小并确保提前有足够的空间。优先使用memmove(),而不是strcpy()strcat()strncpy()strncat()memcpy()
    【解决方案5】:
    • 使用堆栈上的数组 只要有可能并正确初始化它们。您不必跟踪分配、大小和初始化。

      char myCopy[] = { "the interesting string" };
      
    • 对于中等大小的字符串,C99 具有 VLA。 它们不太有用,因为你 无法初始化它们。但你还有 以上的前两个 优势。

      char myBuffer[n];
      myBuffer[0] = '\0';
      

    【讨论】:

    • VLA 是一个等待发生的堆栈溢出。 (那种糟糕的 SO,而不是为你做功课的好那种。:-)
    • @R,是的,如果有人滥用该功能。但是有适当的用途,需要进行必要的检查。毕竟,您也不会从未经检查的用户输入中执行malloc
    • @Matthew:我发现 VLA 的唯一用途需要一些严肃的发明,而且它仍然只是一类理论上的问题:递归问题,所有递归级别的总内存使用量为已知为O(n),但任何一个级别最多可以使用C*n 空间(显然只有有限数量的级别实际上可以使用这么多)。 VLA 是唯一可以在没有malloc 的情况下实现的工具,假设O(n^2) 不适合堆栈。但对于我见过的每个真实案例,一个中等大小的常数维度与 VLA 一样好。
    • @R.. VLA 的某些情况还不错,特别是指向 VLA 的指针甚至可以与 malloc 一起使用,如果您愿意的话。这样,您就可以拥有某种非常基本的动态类型。但可以肯定的是,这里是题外话。对于在堆栈上使用它们,我会坚持我在答案中提到的“中等大小”。
    【解决方案6】:

    一些重要的问题是:

    • 在 C 中,字符串长度和缓冲区大小之间完全没有关系。一个字符串always 一直运行到(包括)第一个'\0'-字符。作为程序员,您有责任确保可以在该字符串的保留缓冲区中找到该字符。
    • 始终明确跟踪缓冲区大小。编译器会跟踪数组的大小,但这些信息会在您不知不觉中丢失。

    【讨论】:

      【解决方案7】:

      说到时间与空间,别忘了从here中挑选标准位旋转

      在我早期的固件项目中,我使用查找表来计算 O(1) 操作效率中设置的位。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-10-11
        • 2020-12-10
        • 1970-01-01
        • 2013-07-18
        相关资源
        最近更新 更多