C库的函数是否应该总是期望字符串的长度?


15

我目前正在使用C语言编写的库。该库的许多功能都希望在其参数中包含char*或字符串const char*。我从那些函数开始就一直期望字符串的长度为a,size_t这样就不需要空终止。但是,在编写测试时,这导致频繁使用strlen(),例如:

const char* string = "Ugh, strlen is tedious";
libFunction(string, strlen(string));

信任用户传递正确终止的字符串将导致安全性降低,但更为简洁和(在我看来)可读的代码:

libFunction("I hope there's a null-terminator there!");

那么,这里的明智做法是什么?使API使用起来更加复杂,但会迫使用户考虑他们的输入,或者记录对以空值结尾的字符串的需求并信任调用者?

Answers:


4

最绝对地,绝对地携带长度。标准的C库以这种方式臭名昭著,这在处理缓冲区溢出方面没有任何痛苦。这种方法是太多仇恨和痛苦的焦点,以至于现代编译器在使用这种标准库函数时实际上会发出警告,抱怨和抱怨。

真是太糟糕了,如果您在面试中遇到这个问题-并且您的技术面试官看起来他有几年的经验-纯粹的狂热分子可能会胜任这份工作-如果您可以引用,您实际上可以取得很大的进步的先例 开枪实施API寻找C字符串终止符的。

撇开它的情感,在字符串的末尾,在读取和操纵它时,NULL可能会出错很多,而且它确实直接违反了现代设计概念,例如纵深防御(不一定适用于安全性,而是适用于API设计)。带有很多长度的C API的示例-例如。Windows API。

实际上,这个问题在90年代的某个时候得到了解决,如今逐渐形成的共识是,您甚至都不应碰触任何东西

以后的编辑:这是一场现场辩论,因此我要补充一点,相信您上方和下方的每个人都很好,并可以使用str *函数库,直到您看到经典的东西,例如output = malloc(strlen(input)); strcpy(output, input);while(*src) { *dest=transform(*src); dest++; src++; }。我几乎可以在后台听到莫扎特的《泪之歌》。


1
我不理解您的Windows API示例,该示例要求调用方提供字符串的长度。例如,典型的Win32 API函数(例如)CreateFileLPTCSTR lpFileName参数作为输入。调用者不应期望该字符串的长度。实际上,以NUL终止的字符串的使用是如此根深蒂固,以至于文档甚至都没有提到文件名必须以NUL终止(但必须是)。
格雷格(Greg Hewgill)2012年

1
实际上,在Win32中,该LPSTR类型表示字符串可能是NUL终止的,如果不是,则将在关联的规范中指出。因此,除非特别指出,否则Win32中的此类字符串应为NUL终止。
格雷格·休吉尔

好点,我是不精确的。考虑到CreateFile及其束自Windows NT 3.1(90年代初)以来就存在。当前的API(即,自XP SP2中引入Strsafe.h以来-带有Microsoft的公开道歉)已明确弃用了所有可能的以NULL终止的内容。微软第一次真正为使用NULL终止的字符串感到非常遗憾,当时他们不得不在OLE 2.0规范中引入BSTR,以便以某种方式将VB,COM和旧的WINAPI集成在一起。
vski 2012年

1
即使在StringCbCat例如目的地中,也只有最大缓冲区才有意义。所述仍然是一个普通NUL终止的C字符串。也许您可以通过澄清输入参数和输出参数之间的差异来改善答案。输出参数应始终具有最大缓冲区长度。输入参数通常是NUL终止的(有例外,但根据我的经验很少)。
格雷格(Greg Hewgill)2012年

1
是。在平台级别的JVM / Dalvik和.NET CLR以及许多其他语言中,字符串都是不可变的。我会走得更远,推测由于a)遗留问题(通过仅使部分字符串不变)并不能真正获得太多收益),b可能还不能使本地世界做到这一点(C ++ 11标准)。 ),您确实需要一个GC和一个字符串表才能完成这项工作,C ++ 11中的作用域分配器无法完全削减它。
vski 2012年

16

在C语言中,习惯用法是字符串是NUL终止的,因此遵守常规做法是有意义的-实际上,库用户使用非NUL终止的字符串的可能性相对较小(因为这些字符串需要额外的打印工作)使用printf并在其他上下文中使用)。使用任何其他类型的字符串都是不自然的,并且可能相对罕见。

同样,在这种情况下,您的测试对我来说有点奇怪,因为要正常工作(使用strlen),您首先要假设一个NUL终止的字符串。如果打算让库使用非NUL终止的字符串,则应测试它们的大小写。


-1,很抱歉,这是不明智的选择。
vski 2012年

在过去,并非总是如此。我使用二进制协议进行了大量工作,这些协议将字符串数据放入不以NULL终止的固定长度字段中。在这种情况下,处理耗时的函数非常费力。不过,我十年来都没做过C。

4
@vski,如何在调用目标函数之前强制用户调用“ strlen”以防止缓冲区溢出问题?至少如果您在目标函数中自行检查长度,则可以确定使用的是哪种长度(包括是否为null)。
Charles E. Grant

@Charles E. Grant:请参阅上面有关Strsafe.h中有关StringCbCat和StringCbCatN的评论。如果您只是一个char *而没有长度,那么实际上您除了使用str *函数外别无选择,但是重点是带上了length-around,因此它成为str *和strn *之间的一个选择。后者的功能是首选。
vski 2012年

2
@vski不需要传递字符串的长度。还有就是需要绕过一个缓冲区的长度。并非所有的缓冲区都是字符串,也不是所有的字符串都是缓冲区。
jamesdlin

10

您的“安全”论点并不成立。如果您不信任用户在您记录的内容(以及纯C的“规范”)之后递给您一个以空值结尾的字符串,那么您就不能真正相信他们给您的长度(他们会可能通过使用获得strlen如果他们没有方便的话,像您正在那样,并且如果“字符串”不是一开始就不是字符串,则失败。

但是有充分的理由要求长度:如果您希望函数在子字符串上工作,那么传递长度可能比让用户来回复制魔术来获取空字节要容易得多(有效)。在正确的位置(并且一路走来可能会有一个错误)。
在某些情况下,能够处理其中空字节不是终止符的编码,或者能够(故意)处理嵌入了空值的字符串会很有用(取决于函数的用途)。
能够处理非空终止数据(定长数组)也很方便。
简而言之:取决于您在库中正在执行的操作以及您希望用户处理的数据类型。

这可能还涉及性能。如果您的函数需要提前知道字符串的长度,并且您希望用户至少通常已经知道该信息,则让他们传递信息(而不是您计算信息)可能会缩短几个周期。

但是,如果您的库希望使用普通的纯ASCII文本字符串,并且您没有令人讨厌的性能约束,并且对用户与库的交互方式有很好的了解,那么添加length参数听起来并不是一个好主意。如果字符串未正确终止,则length参数可能与伪造一样。我认为您不会从中受益匪浅。


强烈不同意这种方法。永远不要相信您的调用者(尤其是在库API的支持下),请尽最大努力去质疑他们给您的内容,并使其优雅地失败。保持精简的长度,使用以NULL终止的字符串并不是“对呼叫者宽松而对呼叫者严格”的意思。
vski 2012年

2
大部分都同意您的立场,但是您似乎对该长度参数非常信任-没有理由比空终止符更可靠。我的立场是,这取决于库的功能。
Mat Mat

字符串中的NULL终止符可能会出错,而不是按值传递长度会导致更多错误。在C语言中,唯一会信任该长度的原因是因为它不合理且不切实际-携带缓冲区长度不是一个好的答案,这只是考虑替代方法的最佳选择。这就是为什么字符串(通常是缓冲区)被整齐地打包和封装在RAD语言中的原因之一。
vski 2012年

2

否。字符串始终按定义以null终止,字符串长度是多余的。

非空终止的字符数据永远不应称为“字符串”。通常对其进行处理(并抛出长度)应该封装在一个库中,而不是API的一部分。仅为了避免单个strlen()调用而将长度作为参数很可能是过早优化。

不信任API函数的调用者 不安全;如果不满足记录的前提条件,则未定义的行为完全可以。

当然,设计良好的API不应包含陷阱,并且应易于正确使用。这只是意味着它应该尽可能简单明了,避免重复并遵循该语言的约定。


不仅完全可以,而且实际上是不可避免的,除非人们使用一种内存安全的单线程语言。可能已放弃了一些其他必要的限制...
重复数据删除器

1

您应该始终保持身高。例如,您的用户可能希望在其中包含NULL。其次,不要忘记这strlen是O(N),需要触摸整个字符串再见缓存。第三,它使子集的传递更容易-例如,它们的长度可能小于实际长度。


4
库函数是否处理字符串中的嵌入式NULL需要非常有据可查。大多数C库函数以NULL或长度停止,以先到者为准。(并且,如果写得很熟练,那些没有花时间的人也不会strlen在循环测试中使用。)
弄乱了机器人

1

您应该区分传递字符串和传递缓冲区

在C语言中,字符串传统上是NUL终止的。期望这是完全合理的。因此,通常不需要绕过字符串的长度。strlen如果需要,可以使用进行计算。

通过缓冲区时,尤其是要写入,绝对应该传递缓冲区的大小。对于目标缓冲区,这使被调用方可以确保它不会溢出缓冲区。对于输入缓冲区,它允许被调用方避免读到末尾,特别是在输入缓冲区包含源自不受信任源的任意数据的情况下。

可能会有一些混乱,因为字符串和缓冲区都可以,char*并且因为许多字符串函数通过写入目标缓冲区来生成新的字符串。然后有人得出结论,字符串函数应采用字符串长度。但是,这是不准确的结论。在缓冲区中包含大小的做法(无论该缓冲区是否用于字符串,整数数组,结构等),是一种更有用,更通用的方法。

(在来自不受信任的源读取一个字符串(如网络套接字)的情况下,它提供长度很重要,因为输入可能没有NULL结尾的。 但是,你应该考虑输入是一个字符串。您应该将其视为可能包含字符串的任意数据缓冲区(但直到实际验证它才知道),因此这仍然遵循以下原则:缓冲区应具有关联的大小,并且字符串不需要它们。


这正是问题和其他答案遗漏的。
Blrfl

0

如果函数主要用于字符串文字,则可以通过定义一些宏来最大程度地减少处理显式长度的麻烦。例如,给定一个API函数:

void use_string(char *string, int length);

可以定义一个宏:

#define use_strlit(x) use_string(x, sizeof ("" x "")-1)

然后按如下所示调用它:

void test(void)
{
  use_strlit("Hello");
}

尽管可能会提出“创意”的东西来传递将编译但实际上无法工作的宏,但是""在“ sizeof”的求值中在字符串的两侧使用时应捕获偶然使用字符的尝试除可分解的字符串文字以外的其他指针[在没有这些字符串的情况下"",尝试传递字符指针将错误地将长度指定为指针的大小减一。

C99中的另一种方法是定义“指针和长度”结构类型,并定义将字符串文字转换为该结构类型的复合文字的宏。例如:

struct lstring { char const *ptr; int length; };
#define as_lstring(x) \
  (( struct lstring const) {x, sizeof("" x "")-1})

请注意,如果使用这种方法,则应按值传递这样的结构,而不是传递其地址。否则类似:

struct lstring *p;
if (foo)
{
  p = &as_lstring("Hello");
}
else
{
  p = &as_lstring("Goodbye!");
}
use_lstring(p);

可能会失败,因为复合文字的生存期将在其封闭语句的结尾处结束。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.