这是一个通用的函数指针,它是危险的吗?

时间:2020-11-29 09:51:15

Learning and messing up with function pointers, I noticed a way to initialize void function pointers and cast them. Yet, although I don‘t receive any warning or error, either with GCC or VS’s compiler, I wanted to know whether it was dangerous or a bad practice to do this as I don't see this way of initializing function pointers often on the Internet. Moreover, do we call this generic function pointer?

学习并弄乱了函数指针,我注意到了一种初始化void函数指针并转换它们的方法。然而,虽然我没有收到任何警告或错误,无论是使用GCC还是VS的编译器,我都想知道这样做是危险的还是坏的做法,因为我没有看到这种方法经常初始化函数指针互联网。而且,我们称之为通用函数指针吗?

#include <stdio.h>
#include <stdint.h>
#include <conio.h>

#define PAUSE (_getch())

uint16_t add(const uint16_t x, const uint16_t y) {
    return x + y;
}

char chr(uint8_t test) {
    return (char)test;
}

int main(void) {

    void(*test)() = (void*)add;

    const uint16_t x = 1, y = 1;
    uint16_t value = ((uint16_t(*)())test)(x, y);

    test = (void*)chr;

    printf("%d\n", add(x, y));                    // 2
    printf("%d\n", value);                        // 2
    printf("%c\n", ((char(*)())test)(100));       // d

    PAUSE;
    return 0;
}

4 个解决方案

#1


8  

Is this a generic function pointer

这是一个通用的函数指针

No, if I'm not terribly mistaken, there's no such thing as a "generic function pointer" in C.

不,如果我不是非常错误,那么C中没有“泛型函数指针”这样的东西。

and is it dangerous?

这是危险的吗?

Yes, it is. It is evil.

是的。这是邪恶的。


There are a couple of things you need to know. First, unless you are running a system that conforms to POSIX,

您需要了解一些事项。首先,除非您运行的系统符合POSIX,

void(*test)() = (void*)add;

is wrong. void * is a pointer-to-object type, and as such, it is not compatible with function pointers. (At least not in standard C -- as I mentioned, POSIX requires it to be compatible with function pointers too.)

是错的。 void *是一个指向对象的指针类型,因此它与函数指针不兼容。 (至少在标准C中没有 - 正如我所提到的,POSIX也要求它与函数指针兼容。)

The second thing is that void (*fp)() and void (*fp)(void) are different. The former declaration permits fp to take any number of parameters of any type, and the number of arguments and their types will be inferred when the compiler sees the first call to the function (pointer).

第二件事是void(* fp)()和void(* fp)(void)是不同的。前一个声明允许fp获取任意类型的任意数量的参数,并且当编译器看到对函数(指针)的第一次调用时,将推断出参数的数量及其类型。

Another important aspect is that function pointers are guaranteed to be convertible across each other (AFAIK this manifests in them having the same representation and alignment requirements). This means that any function pointer can be assigned to (the address of) any function (after an appropriate cast), so long as you do not call a function through a pointer to an incompatible type. The behavior is well-defined if and only if you cast the pointer back to the original type before calling it.

另一个重要的方面是保证函数指针可以相互转换(AFAIK这表明它们具有相同的表示和对齐要求)。这意味着任何函数指针都可以分配给任何函数的(地址)(在适当的强制转换之后),只要你不通过指向不兼容类型的指针调用函数。当且仅当您在调用指针之前将指针强制转换回原始类型时,行为才是明确定义的。

So, if you want a "generic" function pointer, you can just write something like

所以,如果你想要一个“通用”函数指针,你可以写一些像

typedef void (*fn_ptr)(void);

and then you could assign any pointer to function to an object of type fn_ptr. What you have to pay attention to is, again, the conversion to the right type when invoking the function, as in:

然后你可以将任何指向函数的指针赋给fn_ptr类型的对象。您需要注意的是,在调用函数时,再次转换为正确的类型,如:

int add(int a, int b);

fn_ptr fp = (fn_ptr)add; // legal
fp(); // WRONG!
int x = ((int (*)(int, int))fp)(1, 2); // good

#2


5  

There are two serious problems here:

这里有两个严重的问题:

  1. A cast from a function pointer to an object pointer (such as void *) triggers undefined behavior: in principle, it could crash your system (though in practice there are many systems where it will work fine). Instead of void *, it's better to use a function-pointer type for this purpose.
  2. 从函数指针到对象指针(例如void *)的转换会触发未定义的行为:原则上,它可能会使系统崩溃(尽管在实践中有许多系统可以正常工作)。而不是void *,最好为此目的使用函数指针类型。

  3. You're tricking the compiler into unknowingly passing an int to a function expecting a uint8_t. That's also undefined behavior, and it's very dangerous. Since the compiler doesn't know that it's doing this, it can't even take the most basic necessary steps to avoid smashing the stack — you're really gambling here. Similarly, this is a bit more subtle, but you're also tricking the compiler into passing two int-s into a function expecting two uint16_t-s.
  4. 你在不知不觉中将编译器传递给一个期望uint8_t的函数。这也是未定义的行为,而且非常危险。由于编译器不知道它正在这样做,它甚至不能采取最基本的必要步骤来避免粉碎堆栈 - 你真的在这里赌博。类似地,这有点微妙,但你也欺骗编译器将两个int-s传递给一个期望两个uint16_t-s的函数。

And two lesser problems:

还有两个较小的问题:

  1. The notation for function pointer types on their own — e.g., in a cast — is confusing. I think it's better to use a typedef: typedef void (*any_func_ptr)(); any_func_ptr foo = (any_func_ptr)(bar).
  2. 函数指针类型的表示法(例如,在演员表中)令人困惑。我认为最好使用typedef:typedef void(* any_func_ptr)(); any_func_ptr foo =(any_func_ptr)(bar)。

  3. It's undefined behavior to call a function pointer with a different signature than the actual function has. You can avoid that with careful coding — more careful than your current code — but it's tricky and risky.
  4. 调用具有与实际函数不同的签名的函数指针是未定义的行为。您可以通过仔细编码来避免这种情况 - 比您当前的代码更加谨慎 - 但这很棘手且风险很大。

#3


2  

You may corrupt the call stack with this, depending on the calling convention, specifically who's doing the cleanup: http://en.wikipedia.org/wiki/X86_calling_conventions With the callee cleanup, the compiler has no way of knowing how many variables you have passed on the stack at the point of cleanup, so passing the wrong number of parameters or parameters of the wrong size will end corrupting the call stack.

您可能会使用此方法来破坏调用堆栈,具体取决于调用约定,特别是谁正在进行清理:http://en.wikipedia.org/wiki/X86_calling_conventions对于被调用者清理,编译器无法知道您有多少变量已经在清理点上传递了堆栈,因此传递错误数量的参数或错误大小的参数将最终破坏调用堆栈。

On x64, everyone uses the caller cleanup, so you're safe in this regard. The parameter values, however, will in general be a mess. In your example, on x64, they will be whatever was in the corresponding registers at the time.

在x64上,每个人都使用调用者清理,因此在这方面你是安全的。然而,参数值通常是一团糟。在您的示例中,在x64上,它们将是当时相应寄存器中的任何内容。

#4


2  

C11 §6.3.2.3 (8) says:

C11§6.3.2.3(8)说:

A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function whose type is not compatible with the referenced type, the behavior is undefined.

指向一种类型的函数的指针可以被转换为指向另一种类型的函数的指针并且再次返回;结果应该等于原始指针。如果转换的指针用于调用类型与引用类型不兼容的函数,则行为未定义。

And §6.7.6.3 (15) says about compatible types of functions:

§6.7.6.3(15)说关于兼容的功能类型:

[…] If one type has a parameter type list and the other type is specified by a function declarator that is not part of a function definition and that contains an empty identifier list, the parameter list shall not have an ellipsis terminator and the type of each parameter shall be compatible with the type that results from the application of the default argument promotions. […]

[...]如果一个类型具有参数类型列表而另一个类型由函数声明符指定,该函数声明符不是函数定义的一部分且包含空标识符列表,则参数列表不应具有省略号终止符和类型每个参数应与应用默认参数促销产生的类型兼容。 [...]

So, if you had add and chr to take int arguments (an int has at least a width of 16 bit) that would be OK (if you didn't cast the function pointer to void *), but, as it is, it is UB.

所以,如果你有add和chr来获取int参数(一个int的宽度至少为16位),那就没问题(如果你没有将函数指针强制转换为void *),但实际上,它是是UB。

#1


8  

Is this a generic function pointer

这是一个通用的函数指针

No, if I'm not terribly mistaken, there's no such thing as a "generic function pointer" in C.

不,如果我不是非常错误,那么C中没有“泛型函数指针”这样的东西。

and is it dangerous?

这是危险的吗?

Yes, it is. It is evil.

是的。这是邪恶的。


There are a couple of things you need to know. First, unless you are running a system that conforms to POSIX,

您需要了解一些事项。首先,除非您运行的系统符合POSIX,

void(*test)() = (void*)add;

is wrong. void * is a pointer-to-object type, and as such, it is not compatible with function pointers. (At least not in standard C -- as I mentioned, POSIX requires it to be compatible with function pointers too.)

是错的。 void *是一个指向对象的指针类型,因此它与函数指针不兼容。 (至少在标准C中没有 - 正如我所提到的,POSIX也要求它与函数指针兼容。)

The second thing is that void (*fp)() and void (*fp)(void) are different. The former declaration permits fp to take any number of parameters of any type, and the number of arguments and their types will be inferred when the compiler sees the first call to the function (pointer).

第二件事是void(* fp)()和void(* fp)(void)是不同的。前一个声明允许fp获取任意类型的任意数量的参数,并且当编译器看到对函数(指针)的第一次调用时,将推断出参数的数量及其类型。

Another important aspect is that function pointers are guaranteed to be convertible across each other (AFAIK this manifests in them having the same representation and alignment requirements). This means that any function pointer can be assigned to (the address of) any function (after an appropriate cast), so long as you do not call a function through a pointer to an incompatible type. The behavior is well-defined if and only if you cast the pointer back to the original type before calling it.

另一个重要的方面是保证函数指针可以相互转换(AFAIK这表明它们具有相同的表示和对齐要求)。这意味着任何函数指针都可以分配给任何函数的(地址)(在适当的强制转换之后),只要你不通过指向不兼容类型的指针调用函数。当且仅当您在调用指针之前将指针强制转换回原始类型时,行为才是明确定义的。

So, if you want a "generic" function pointer, you can just write something like

所以,如果你想要一个“通用”函数指针,你可以写一些像

typedef void (*fn_ptr)(void);

and then you could assign any pointer to function to an object of type fn_ptr. What you have to pay attention to is, again, the conversion to the right type when invoking the function, as in:

然后你可以将任何指向函数的指针赋给fn_ptr类型的对象。您需要注意的是,在调用函数时,再次转换为正确的类型,如:

int add(int a, int b);

fn_ptr fp = (fn_ptr)add; // legal
fp(); // WRONG!
int x = ((int (*)(int, int))fp)(1, 2); // good

#2


5  

There are two serious problems here:

这里有两个严重的问题:

  1. A cast from a function pointer to an object pointer (such as void *) triggers undefined behavior: in principle, it could crash your system (though in practice there are many systems where it will work fine). Instead of void *, it's better to use a function-pointer type for this purpose.
  2. 从函数指针到对象指针(例如void *)的转换会触发未定义的行为:原则上,它可能会使系统崩溃(尽管在实践中有许多系统可以正常工作)。而不是void *,最好为此目的使用函数指针类型。

  3. You're tricking the compiler into unknowingly passing an int to a function expecting a uint8_t. That's also undefined behavior, and it's very dangerous. Since the compiler doesn't know that it's doing this, it can't even take the most basic necessary steps to avoid smashing the stack — you're really gambling here. Similarly, this is a bit more subtle, but you're also tricking the compiler into passing two int-s into a function expecting two uint16_t-s.
  4. 你在不知不觉中将编译器传递给一个期望uint8_t的函数。这也是未定义的行为,而且非常危险。由于编译器不知道它正在这样做,它甚至不能采取最基本的必要步骤来避免粉碎堆栈 - 你真的在这里赌博。类似地,这有点微妙,但你也欺骗编译器将两个int-s传递给一个期望两个uint16_t-s的函数。

And two lesser problems:

还有两个较小的问题:

  1. The notation for function pointer types on their own — e.g., in a cast — is confusing. I think it's better to use a typedef: typedef void (*any_func_ptr)(); any_func_ptr foo = (any_func_ptr)(bar).
  2. 函数指针类型的表示法(例如,在演员表中)令人困惑。我认为最好使用typedef:typedef void(* any_func_ptr)(); any_func_ptr foo =(any_func_ptr)(bar)。

  3. It's undefined behavior to call a function pointer with a different signature than the actual function has. You can avoid that with careful coding — more careful than your current code — but it's tricky and risky.
  4. 调用具有与实际函数不同的签名的函数指针是未定义的行为。您可以通过仔细编码来避免这种情况 - 比您当前的代码更加谨慎 - 但这很棘手且风险很大。

#3


2  

You may corrupt the call stack with this, depending on the calling convention, specifically who's doing the cleanup: http://en.wikipedia.org/wiki/X86_calling_conventions With the callee cleanup, the compiler has no way of knowing how many variables you have passed on the stack at the point of cleanup, so passing the wrong number of parameters or parameters of the wrong size will end corrupting the call stack.

您可能会使用此方法来破坏调用堆栈,具体取决于调用约定,特别是谁正在进行清理:http://en.wikipedia.org/wiki/X86_calling_conventions对于被调用者清理,编译器无法知道您有多少变量已经在清理点上传递了堆栈,因此传递错误数量的参数或错误大小的参数将最终破坏调用堆栈。

On x64, everyone uses the caller cleanup, so you're safe in this regard. The parameter values, however, will in general be a mess. In your example, on x64, they will be whatever was in the corresponding registers at the time.

在x64上,每个人都使用调用者清理,因此在这方面你是安全的。然而,参数值通常是一团糟。在您的示例中,在x64上,它们将是当时相应寄存器中的任何内容。

#4


2  

C11 §6.3.2.3 (8) says:

C11§6.3.2.3(8)说:

A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function whose type is not compatible with the referenced type, the behavior is undefined.

指向一种类型的函数的指针可以被转换为指向另一种类型的函数的指针并且再次返回;结果应该等于原始指针。如果转换的指针用于调用类型与引用类型不兼容的函数,则行为未定义。

And §6.7.6.3 (15) says about compatible types of functions:

§6.7.6.3(15)说关于兼容的功能类型:

[…] If one type has a parameter type list and the other type is specified by a function declarator that is not part of a function definition and that contains an empty identifier list, the parameter list shall not have an ellipsis terminator and the type of each parameter shall be compatible with the type that results from the application of the default argument promotions. […]

[...]如果一个类型具有参数类型列表而另一个类型由函数声明符指定,该函数声明符不是函数定义的一部分且包含空标识符列表,则参数列表不应具有省略号终止符和类型每个参数应与应用默认参数促销产生的类型兼容。 [...]

So, if you had add and chr to take int arguments (an int has at least a width of 16 bit) that would be OK (if you didn't cast the function pointer to void *), but, as it is, it is UB.

所以,如果你有add和chr来获取int参数(一个int的宽度至少为16位),那就没问题(如果你没有将函数指针强制转换为void *),但实际上,它是是UB。