这种滥用函数声明是否会调用未定义的行为?

时间:2013-05-11 02:04:15

标签: c language-lawyer undefined-behavior function-declaration

考虑以下计划:

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

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

3 个答案:

答案 0 :(得分: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)。

所以我认为可以合理地说明程序有不确定的行为。

答案 1 :(得分:0)

我会说在任何一种情况下这都可能是不明确的。

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

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

答案 2 :(得分:0)

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

  

如果使用与。不兼容的类型定义函数   表达式的表达式类型(表达式)   被称为函数,行为未定义。

所有这些总是讨论两个不同的实体,一个是被评估的函数表达式,另一个是被调用的函数。提示表达式的标识符(您的错误声明的exit)永远不会进入游戏。所以在你的情况下,函数被正确调用,没有UB。

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

只是一个挑剔,我认为你应该对编译器有所帮助并在这种情况下给出一个原型。参数转换形式0是微不足道的,在这种情况下是正确的,但实际上非常容易出错。

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

会更好。

更新:鉴于迈克尔的回答,我同意如果您使用非库函数完成所有这些操作,上述情况就是如此。但7.1.3 p1明确禁止使用与标题中声明的原型不同的标识符exit,然后使用p。 2州

  

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