6

考虑以下程序:

int main()
{
    int exit();
    ((void(*)())exit)(0);
}

如您所见,exit用错误的返回类型声明,但从不使用错误的函数类型调用。这个程序的行为是否明确?

4

3 回答 3

4

MSVC 对这个程序没有问题,但 gcc 有问题(至少 gcc 4.6.1)。它发出以下警告:

test.c: In function 'main':
test.c:3:9: warning: conflicting types for built-in function 'exit' [enabled by default]
test.c:4:22: warning: function called through a non-compatible type [enabled by default]
test.c:4:22: note: if this code is reached, the program will abort

而且,正如所承诺的,它在运行时确实会崩溃。崩溃并不是由于调用约定不正确或其他原因造成的 - gcc 实际上生成了一条带有操作码 0x0b0f 的未定义指令以显式强制崩溃(gdb 将其反汇编为ud2- 我没有查看 CPU 手册可能对操作码说什么):

main:
.LFB0:
    .cfi_startproc
    push    ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    mov ebp, esp
    .cfi_def_cfa_register 5
        .value  0x0b0f
    .cfi_endproc

我不愿意说 gcc 这样做是错误的,因为我确信编写该编译器的人比我更了解 C。但这是我如何阅读标准的内容;我相信有人会指出我所缺少的:

C99 说明了函数指针的转换(6.3.2.3/8“指针”):

指向一种类型的函数的指针可以转换为指向另一种类型的函数的指针,然后再返回;结果应与原始指针比较。如果转换后的指针用于调用类型与指向的类型不兼容的函数,则行为未定义。

在表达式中,标识符的exit计算结果为函数指针。

子表达式((void(*)())exit)exit求值为 的函数指针转换为 类型的函数指针void (*)()。然后通过该指针进行函数调用,并传递int参数 0。

标准库包含一个名为的函数,该函数exit具有以下原型:

void exit(int status);

该标准还说(7.1.4/2“使用库函数”):

如果可以在不引用头文件中定义的任何类型的情况下声明库函数,则也允许声明函数并在不包括其关联头文件的情况下使用它。

您的程序不包含包含该原型的标头,但通过转换后的指针进行的函数调用使用强制转换中提供的“声明”。强制转换中的声明不是原型声明,所以我们需要判断exit标准库定义的函数类型和你程序中转换后的函数指针的函数类型是否兼容。标准说(6.7.5.3/15“函数声明器(包括原型)”)

对于要兼容的两种函数类型,两者都应指定兼容的返回类型。...如果一种类型具有参数类型列表,而另一种类型由不属于函数定义的函数声明符指定并且包含空标识符列表,则参数列表不应有省略号终止符,并且类型每个参数都应与应用默认参数提升所产生的类型兼容

在我看来,转换后的函数指针具有兼容的函数类型 - 返回类型相同 ( void) 并且单个参数的类型int在默认参数提升之后。所以在我看来,这里没有未定义的行为。


更新:经过更多思考,将 7.1.4/2 解释为必须正确声明“自我声明”的库函数名称可能是合理的(尽管不一定使用原型,但具有正确的返回类型)。特别是因为该标准还规定“以下任何子条款中具有外部链接的所有标识符......始终保留用作具有外部链接的标识符”(7.1.3)。

所以我认为可以合理地论证该程序具有未定义的行为。

于 2013-05-11T07:34:44.933 回答
0

我想说这在任何一种情况下都可能定义不明确。

我的论点是生成的两个调用的代码((void(*)())exit)(0);exit();可能是不同的。因此,如果int exit()仅声明 when (您感兴趣的那个),主要问题可能是 和 的二进制布局int exit(void)不一定void exit(int)相同。

如果int exit()还定义了 if ,则很可能由于以下原因而崩溃。那里有许多调用约定,并且问题可能会出现,例如,当返回值的空间被保留在堆栈上时。因此,当((void(*)())exit)(0);使用时,编译器显然不会保留堆栈上的空间(特别是用于返回值),而函数本身(int exit())不知道这一点,因此仍会尝试将int返回值推入运行时中的预期内存单元(应该保留但没有保留的内存单元),这肯定会以崩溃告终。

于 2013-05-11T02:23:28.963 回答
0

我认为这已经定义了行为。标准的相关部分是关于参数(p6,有点冗长)和类型:

如果函数定义的类型与表示被调用函数的表达式所指向的(表达式的)类型不兼容,则行为未定义。

所有这些总是谈论两个不同的实体,一个是被评估的函数表达式,第二个是被调用的函数。引发表达式的标识符(您的错误声明exit)永远不会进入游戏。因此,在您的情况下,该函数被正确调用并且没有 UB。

一般来说,如果那是UB,它会破坏很多代码,即用于将函数指针存储在数组中的代码,例如,然后根据有关上下文的一些额外知识通过强制转换调用函数。

只是一个挑剔,我认为你应该帮编译器一个忙,在这种情况下给出一个原型。参数转换形式0很简单,在这种情况下是正确的,但确实很容易出错。

((void(*)(int))exit)(0);

会更好。

更新:鉴于迈克尔的回答,我同意如果您使用非图书馆功能完成所有这些,上述内容将是正确的。但是 7.1.3 p1 明确禁止确实使用与exit标头中声明的原型不同的标识符,然后是 p。2个州

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

于 2013-05-11T06:51:19.627 回答