重新定义标准库中的函数

8

背景:最近在交流中,有人问“gcc/clang是否在编译时执行strlen("static string")?”经过一些测试,答案似乎是肯定的,无论优化级别如何。我有点惊讶地发现,甚至在-O0级别下也会执行此操作,因此我进行了一些测试,最终得出以下代码:

#include <stdio.h>

unsigned long strlen(const char* s) {
  return 10;
}

unsigned long f() {
  return strlen("abcd");
}

unsigned long g(const char* s) {
  return strlen(s);
}

int main() {
  printf("%ld %ld\n",f(),g("abcd"));
  return 0;
}

令我惊讶的是,它打印出4 10而不是10 10。我尝试使用gccclang编译,并使用各种标志(-pedantic-O0-O3-std=c89-std=c11,...),测试结果一致。
由于我没有包含string.h,我期望使用我的strlen定义。但是汇编代码确实显示,strlen("abcd")基本上被替换为return 4(这就是我运行程序时观察到的)。
此外,编译器在-Wall -Wextra下没有警告(更准确地说,与此问题无关的警告:它们仍然警告在我的strlen定义中参数s未使用)。
有两个(相关的)问题(我认为它们足够相关,可以在同一个问题中提问):
- 在未包含声明它的头文件的情况下,是否允许重新定义C中的标准函数?
- 这个程序的行为是否符合预期?如果是,会发生什么?

2
如果您使用 gcc -Wall -Wstrict-prototypes -Werror 编译上述代码,它会报错,如“函数声明不是原型”。因此,我认为这明确了您提到的问题:“在未包含声明标准函数的头文件的情况下,是否允许重新定义C中的标准函数?” - Achal
@achal 噢,我不知道 -Wstrict-prototypes,谢谢,我学到了些东西。但是,如果我在 fmain 的参数中使用 void 来消除这些警告,程序的行为没有改变(它仍然打印出 4 10),所以我不确定我是否理解你评论的最后一部分。 - Dada
1
我猜 strlen("abcd") 显然是编译器已经替换为值 4 的常量,因为它识别了 strlen()。然而,g("abcd") 不显然是常量,所以编译器会发出调用你的 strlen() 函数的代码(但尝试将 g() 和 strlen() 声明为 static inline)。 - Yann Droneaud
5
重新使用标准库函数名称进行恶意用途可能会导致未定义的行为。 - Lundin
1个回答

10

根据C语言标准草案N1570的7.1.3 1和2,以下所有带有外部链接的标识符...始终保留用作具有外部链接的标识符。

如果程序在保留上下文中声明或定义标识符(除非允许7.1.4中的操作),或将保留标识符定义为宏名称,则其行为是未定义的。

“下列子句”指定了标准C库,包括strlen。您的程序定义了strlen,因此其行为是未定义的。

您观察到的情况是:

  • 编译器知道strlen应该如何运行,无论您的定义如何,因此,在优化f中的strlen("abcd")时,它在编译时计算strlen,结果为四个字符。
  • g("abcd")中,由于定义了g,编译器无法识别这相当于strlen("abcd"),因此不会在编译时对其进行优化。相反,它将其编译成对g的调用,并将g编译成调用strlen,还编译了您对strlen的定义,结果导致g("abcd")调用g,它又调用您的strlen,返回十个字符。

C标准允许编译器完全丢弃您对strlen的定义,这样g将返回四个。然而,一个好的编译器应该警告说您的程序定义了一个保留标识符。


有没有办法用“4”替换strlen(“abcd”);,从而消除源代码中函数调用所需的序列点? - Andrew Henle
2
@AndrewHenle:编译器在优化时负责确保语义保持正确。在这种情况下,由6.5.2.2 10指定的序列点仅保证在(a)函数设计者和参数的评估以及(b)实际调用之间。这里没有任何副作用,因此序列点是无关紧要的。参数和函数调用是否相对于其他事物是由该序列点控制的。 - Eric Postpischil

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接