根据C标准,将函数转换为各种类型的函数指针是否合法?

3

我分析了一些用C语言编写的源代码,找到了以下代码片段:

#include <stdio.h>

struct base_args_t {
int a0;
};

struct int_args_t {
struct base_args_t base;
int a1;
};

struct uint_args_t {
struct base_args_t base;
unsigned int a1;
};

void print_int(struct int_args_t *a)
{
    // print int
    printf("%i\n", a->a1);
    return;
}

void print_uint(struct uint_args_t *a)
{
    // print unsigned int
    printf("%u\n", a->a1);
    return;
}

int main()
{
    struct uint_args_t uint_args = {.a1 = 7};
    typedef void (*f_print_type)(struct int_args_t *);
    void (*print)(struct int_args_t *a) = (f_print_type)print_uint;

    print((void *)&uint_args);

    return 0;
}

我想知道是否可以将函数转换为各种类型的函数指针,就像在以下示例中所做的那样:
void (*print)(struct int_args_t *a) = (f_print_type)print_uint;

另外,我已测试启用CFI污点分析器的示例,它显示:

运行时错误:在间接函数调用期间,类型为“void(struct int_args_t *)”的控制流完整性检查失败

但很难说是否正确无误。


只要不违反C语言的规则,用C实现任何东西都不是非法的。C是否具有特定的功能或能力作为自然属性是另一个问题。 - ryyker
1
@ryyker 我认为他的意思是“合法”,即符合语言规则。 - Jonathon Reinhart
@Jonathon Reinhart 没错。感谢您指出这一点! - lol lol
1
这个问题 https://dev59.com/WXRB5IYBdhLWcg3wpotm 不是重复的吗? - KamilCuk
@KamilCuk 谢谢!看起来很相似。 - lol lol
3个回答

4
使用函数指针调用必须与函数类型相同。
main()中,语句print(...);使用void (*print)(struct int_args_t *a)调用print_uint,但print_uint的类型为void print_uint(struct uint_args_t *a)。该调用是未定义行为。

依据C标准,将一个函数强制转换为各种函数类型的指针是否合法?

如果可以将函数转换为各种函数类型的指针[...]。

当另一个类型是函数指针时,转换或强制转换始终是安全的。任何函数指针都可以始终转换为任何其他函数指针类型。你必须使用与函数指针类型相同的函数指针类型来调用函数(更确切地说,必须使用兼容的函数指针类型调用函数)。

是的,这是未定义行为。 - KamilCuk
1
@lollol:对于两个结构体struct int_args_tstruct uint_args_t来说,要想兼容,它们必须使用相同的标签(或者没有标签),在相同的顺序下具有相应的成员、相同的名称、兼容的类型和对齐说明符,对于不完整的声明等情况,C 2018 6.2.7 1中有一些特殊规定。兼容并不意味着“我认为它们在内存中的布局相同”,而是指它们遵循C标准中关于有效相同类型的特定规则,其中影响语义和其他问题。 - Eric Postpischil
@lollol:C 2018 6.5.2.2 6:“……如果函数定义的类型包括原型,并且参数在提升后的类型与参数的类型不兼容,则行为未定义……”print_uint是使用原型定义的,其参数类型为struct uint_args_t *,而您使用struct int_args_t *调用它,这是不兼容的,因此行为未定义。除了6.2.5 28说结构体指针具有相同的表示方式,脚注49告诉我们这意味着它们应该是可互换的。 - Eric Postpischil
@Eric Postpischil 看起来非常复杂。 - lol lol
1
@RobertSsupportsMonicaCellio:是的,这意味着在翻译单元内它甚至更加严格:不适用于在不同单元中声明的兼容性规则,因此结构体的唯一规则是“它们的类型必须相同”。 - Eric Postpischil
显示剩余9条评论

2

根据C标准,将指向函数的指针转换为各种类型的函数指针是否合法?

是的,您可以将函数指针分配给任何其他类型的函数指针:

"一个类型的函数指针可以转换为另一种类型的函数指针,并再次转换回来;结果应与原始指针相等。 如果使用转换后的指针调用与指向的类型不兼容的函数,则行为未定义。"

来源:C11,6.3.2.3/8

因此,赋值语句:

void (*print)(struct int_args_t *a) = (f_print_type)print_uint;

是正确且合法的。


引起未定义行为的是使用指针 print()来调用 print_uint

print((void *)&uint_args);

因为:
“如果使用转换后的指针调用与指向类型不兼容的函数,则行为未定义。”
类型为“带有struct uint_args_t参数并返回void”的print_uint函数
与类型为“带有struct int_args_t参数并返回void”的函数不兼容,而print被声明为指向该函数。
参数类型和被调用的指针类型不同。
结构本身既不相同也不兼容。
关于兼容性:
对于两个函数类型来说,它们必须指定兼容的返回类型。此外,如果两个参数类型列表都存在,则它们应在参数数量和省略号终止符的使用上达成一致;相应的参数应具有兼容的类型。如果一个类型具有参数类型列表,而另一个类型由不是函数定义的函数声明符指定,并且包含空标识符列表,则参数列表不得有省略号终止符,并且每个参数的类型都应与从应用默认参数提升到相应标识符的类型所得到的类型兼容。如果一个类型具有参数类型列表,而另一个类型由包含(可能为空)标识符列表的函数定义指定,则两者应在参数数量上达成一致,并且每个原型参数的类型都应与将默认参数提升应用于相应标识符的类型所得到的类型兼容。(在确定类型兼容性和组合类型时,每个声明为函数或数组类型的参数都被视为具有调整后的类型,每个声明为限定类型的参数都被视为具有其声明类型的未限定版本。)
如果两个函数类型都是“旧样式”,则不会比较参数类型。
出处:C18,§6.7.6.3/15
两种类型如果它们的类型相同,则具有兼容的类型。确定两种类型是否兼容的其他规则在类型说明符的6.7.2、类型限定符的6.7.3和声明符的6.7.6中描述。
两种类型不必相同就可以兼容。
出处:C18,§6.2.7/1

EXAMPLE 2 After the declarations

typedef structs1 { int x; } t1, *tp1;
typedef structs2 { int x; } t2, *tp2;

type t1and the type pointed to by tp1 are compatible. Type t1 is also compatible with type structs1, but not compatible with the types structs2, t2, the type pointed to by tp2, or int.

C18, §6.7.8/5

不同标签的两个结构体即使成员和对齐方式相同也不兼容,这里的情况也不例外,因为两个结构体类型中成员a的类型不同。


1
“compatible” 是如何定义的?因为这两个结构体是 ABI 相同的,所以不应该(也不会)引起任何问题。 - Blindy
@Blindy 它们不是相同的。a1 有不同的类型。 - RobertS supports Monica Cellio

0
在这种特定情况下,它是完全安全的,因为:
  1. int_args_tuint_args_t在内存布局方面是相同的。而且具体来说,intuint是相同的(没有有符号/无符号寄存器或内存位置)。

  2. 即使第1点不成立,两个函数定义具有相同的签名-它们接收指针并返回void。

  3. 函数体在汇编级别上也是相同的,因为您使用从接收到的指针相同偏移量处的相同字段,并且您使用的字段具有相同的内存布局(如第1点所讨论的)。

如果你将所有内容简化为编译器生成的基本汇编代码,则你的代码是完全安全的。 消毒剂告诉你的是,实际的C类型定义不太可互换,但最终这并不重要。


关于#3,传递给printf的字符串参数在两个函数之间显然是不同的,但我的意思是汇编指令本身是相同的,而不是它们的参数。 - Blindy
1
这个问题被标记为“语言律师”,这意味着答案应该基于语言的正式规范。对汇编语言的推理在语言律师问题中本质上是错误的。(我看到标签已经被编辑了,但是当这个答案发布时,它似乎是一个“语言律师”问题。) - Eric Postpischil
语言的正式规范允许他所做的事情,这一点可以从它编译和运行的事实中得到证明,而我的解释也说明了它为什么能够运行。我认为我的回答没有问题,当然如果你愿意,可以给我点个踩。事实上,我的回答比Kamil的回答更正确,因为他声称必须使用与指针类型相同的函数调用(这是完全错误的,整个COM都在谴责你)。 - Blindy
2
使用语言的形式规范意味着从C标准所说的内容进行推理。从“它既可以编译又可以运行”进行推理是错误的。从Microsoft的组件对象模型进行推理也是错误的。一个程序在你尝试的编译器上编译和运行以及Microsoft的COM的陈述都不能证明C标准说这个程序必须以同样的方式运行。关于“语言律师标签”的问题是:“语言标准说什么”,而不是编译器做了什么或者Microsoft说了什么。 - Eric Postpischil
1
再次强调,问题不在于是否存在这样的编译器。语言律师标签要求使用语言的正式规范。回答问题的唯一正确方式是基于C标准所说的内容。你认为编译器会或不会做什么都是无关紧要的。那不是问题所在。问题是“C标准对此有何规定?”。就是这样。 - Eric Postpischil
显示剩余6条评论

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