我正在使用纯 C (c99) 语言开始一个新项目,该项目主要使用文本。由于外部项目限制,此代码必须非常简单和紧凑,由一个源代码文件组成,没有外部依赖项或库,除了 libc 和类似的普遍存在的系统库。
有了这种理解,有哪些最佳实践、陷阱、技巧或其他技术可以帮助使项目的字符串处理更加健壮和安全?
我正在使用纯 C (c99) 语言开始一个新项目,该项目主要使用文本。由于外部项目限制,此代码必须非常简单和紧凑,由一个源代码文件组成,没有外部依赖项或库,除了 libc 和类似的普遍存在的系统库。
有了这种理解,有哪些最佳实践、陷阱、技巧或其他技术可以帮助使项目的字符串处理更加健壮和安全?
如果没有关于您的代码正在做什么的任何其他信息,我建议您设计所有接口,如下所示:
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
或使用“可能足够大”的缓冲区调用函数一次,并且仅在空间不足时重试。
您还可以制作类似于 GNUasprintf
函数的此类调用的包装版本,但如果您希望代码尽可能灵活,我将避免在实际字符串函数中进行任何分配。在调用者级别处理失败的可能性总是更容易,并且许多调用者可以通过使用本地缓冲区或在程序中更早获得的缓冲区来确保永远不会失败,以便更大的操作成功或失败是原子的(极大地简化了错误处理)。
来自长期嵌入式开发人员的一些想法,其中大部分都详细说明了您对简单性的要求,而不是 C 特定的:
确定您需要哪些字符串处理函数,并将该集合保持尽可能小,以最大限度地减少故障点。
按照 R. 的建议定义一个清晰的接口,该接口在所有字符串处理程序中都是一致的。一组严格的、小而详细的规则允许您将模式匹配用作调试工具:您可以怀疑任何看起来与其他代码不同的代码。
正如 Bart van Ingen Schenau 所说,跟踪缓冲区长度与字符串长度无关。如果您将始终使用文本,则使用标准空字符表示字符串结尾是安全的,但您需要确保 text+null 适合缓冲区。
确保所有字符串处理程序的行为一致,特别是在缺少标准函数的情况下:截断、空输入、空终止、填充等。
如果您绝对需要违反任何规则,请为此目的创建一个单独的函数并适当地命名它。换句话说,给每个函数一个明确的行为。所以你可以使用str_copy_and_pad()
一个总是用空值填充它的目标的函数。
在可能的情况下,使用安全的内置函数(例如 memmove()
Jonathan Leffler)来完成繁重的工作。但是测试他们以确保他们正在做你认为他们正在做的事情!
尽快检查错误。未检测到的缓冲区溢出会导致众所周知的难以定位的“弹跳”错误。
为每个函数编写测试以确保它满足合同。确保覆盖边缘情况(关闭 1、空/空字符串、源/目标重叠等)这听起来很明显,但请确保您了解如何创建和检测缓冲区欠载/溢出,然后编写测试明确生成并检查这些问题。(我的 QA 人员可能已经厌倦了听到我的指示“不要只是测试以确保它有效;测试以确保它不会损坏。”)
以下是一些对我有用的技术:
为内存管理例程创建包装器,在分配期间在缓冲区的任一端分配“栅栏字节”,并在释放时检查它们。您还可以在字符串处理程序中验证它们,也许在设置 STR_DEBUG 宏时。 警告:您需要彻底测试您的诊断,以免它们产生额外的故障点。
创建一个封装缓冲区及其长度的数据结构。(如果你使用它们,它也可以包含栅栏字节。) 警告:你现在有一个非标准的数据结构,你的整个代码库必须管理,这可能意味着大量的重写(因此会有额外的故障点)。
让您的字符串处理程序验证他们的输入。如果函数禁止空指针,请明确检查它们。如果它需要一个有效的字符串(如strlen()
should)并且您知道缓冲区长度,请检查缓冲区是否包含空字符。换句话说,验证您可能对代码或数据所做的任何假设。
先写你的测试。这将帮助您理解每个函数的契约——确切地说它对调用者的期望是什么,以及调用者应该从它那里得到什么。你会发现自己在思考如何使用它,它可能会破坏的方式,以及它必须处理的边缘情况。
非常感谢您提出这个问题!我希望更多的开发人员能够考虑这些问题——尤其是在他们开始编码之前。祝你好运,并祝愿产品强大、成功!
看看strlcpy
和strlcat
,查看original paper
详情。
两分钱:
最后一步很重要,因为如果达到最大大小,则字符串函数的“n”版本在复制后不会附加 '\0'。
尽可能使用堆栈上的数组并正确初始化它们。您不必跟踪分配、大小和初始化。
char myCopy[] = { "the interesting string" };
对于中型琴弦,C99 具有 VLA。由于您无法初始化它们,因此它们的可用性有所降低。但是您仍然具有上述前两个优势。
char myBuffer[n];
myBuffer[0] = '\0';
一些重要的问题是:
'\0'
。作为程序员,您有责任确保可以在该字符串的保留缓冲区中找到该字符。当谈到时间与空间时,别忘了从这里挑选标准位
在我早期的固件项目中,我使用查找表来计算 O(1) 操作效率中设置的位。