C中的字符串处理实践

时间:2011-01-02 20:21:14

标签: c security string robustness

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

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

7 个答案:

答案 0 :(得分:31)

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

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

答案 1 :(得分:10)

来自长期嵌入式开发人员的一些想法,其中大部分都是关于您对简单性的要求并且不是C特定的:

  • 决定您需要哪些字符串处理功能,并尽可能减少该设置,以尽量减少故障点。

  • 按照R.的建议定义一个在所有字符串处理程序中保持一致的清晰界面。严格,小但详细的规则允许您使用模式匹配作为调试工具:您可以怀疑任何看起来与其他代码不同的代码。

  • 正如Bart van Ingen Schenau所说,跟踪缓冲区长度与字符串长度无关。如果你总是使用文本,那么使用标准的空字符表示字符串结尾是安全的,但是你应该确保text + null适合缓冲区。 / p>

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

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

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

  • 尽快检查错误。未检测到的缓冲区溢出可能导致“跳弹”错误,这些错误很难找到。

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

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

  • 为内存管理例程创建包装器,在分配期间在缓冲区的任一端分配“fence bytes”,并在取消分配时检查它们。您也可以在字符串处理程序中验证它们,可能是在设置了STR_DEBUG宏时。 警告:您需要彻底测试您的诊断程序,以免造成其他故障点。

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

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

  • 首先编写测试。这将有助于您理解每个函数的契约 - 正是它对调用者的期望,以及调用者对它的期望。您会发现自己正在考虑使用它的方式,它可能会破坏的方式,以及它必须处理的边缘情况。

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

答案 2 :(得分:7)

查看strlcpystrlcat,有关详细信息,请参阅original paper

答案 3 :(得分:2)

两分钱:

  1. 始终使用字符串函数的“n”版本:strncpy,strncmp,(或wcsncpy,wcsncmp等)
  2. 始终使用+1惯用语分配:例如: char * str [MAX_STR_SIZE + 1],然后传递MAX_STR_SIZE作为字符串函数的“n”版本的大小,并以str [MAX_STR_SIZE] ='\ 0'结束;确保所有字符串都已正确完成。
  3. 最后一步很重要,因为如果达到最大尺寸,字符串函数的“n”版本在复制后不会附加'\ 0'。

答案 4 :(得分:0)

  • 使用堆栈上的数组 只要有可能并正确初始化它们。您无需跟踪分配,大小和初始化。

    char myCopy[] = { "the interesting string" };
    
  • 对于中等大小的字符串,C99具有VLA。 因为你,它们的可用性稍差 无法初始化它们。但你仍然有 上面的前两个 优点

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

答案 5 :(得分:0)

一些重要的问题是:

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

答案 6 :(得分:0)

说到时间与空间,不要忘记从here

中选择标准位

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

相关问题