Is this a universal function pointer and is it dangerous?

Studying and corrupting function pointers, I noticed a way to initialize void function pointers and cast them. However, although I do not receive any warnings or errors, either with the GCC or VS compiler, I wanted to know if it was dangerous or bad practice to do this, since I do not see this way of initializing function pointers often to the Internet. Moreover, do we call this a universal function pointer?

#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; } 
+7
c pointers
source share
4 answers

Is this a pointer to a common function

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

and is it dangerous?

Yes it is. This evil.


There are a few things you need to know. Firstly, if you are not using a POSIX compliant system,

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

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

Secondly, void (*fp)() and void (*fp)(void) different. The first declaration allows fp to accept any number of parameters of any type, and the number of arguments and their types will be displayed when the compiler sees the first function call (pointer).

Another important aspect is that function pointers are guaranteed to be converted into each other (AFAIK is manifested in them, having the same presentation and alignment requirements). This means that any function pointer can be assigned (address) to any function (after the appropriate cast), if you do not call the function using a pointer to an incompatible type. A behavior is correctly defined if and only if you call the pointer back to the original type before calling it.

So, if you need a β€œgeneric” function pointer, you can just write something like

 typedef void (*fn_ptr)(void); 

and then you can assign a function to any pointer to an object of type fn_ptr . You should pay attention to this, again, converting to the desired type when calling the function, as in:

 int add(int a, int b); fn_ptr fp = (fn_ptr)add; // legal fp(); // WRONG! int x = ((int (*)(int, int))fp)(1, 2); // good 
+8
source share

There are two serious problems here:

  • Casting a function from a function pointer to an object pointer (for example, void * ) causes undefined behavior: in principle, this can lead to a failure of your system (although in practice there are many systems where it will work normally). Instead of void * it is better to use a function pointer type for this purpose.
  • You trick the compiler by unknowingly passing int function that expects uint8_t . This is also undefined behavior, and it is very dangerous. Since the compiler does not know that he does this, he cannot even perform the most basic necessary steps to avoid smashing the stack - you really gamble here. Likewise, it is a bit more subtle, but you also fool the compiler by passing two int -s to a function that uint16_t two uint16_t -s.

And two more problems:

  • Designations for types of function pointers by themselves β€” for example, in cast form; is embarrassing. I think it's better to use typedef: typedef void (*any_func_ptr)(); any_func_ptr foo = (any_func_ptr)(bar) typedef void (*any_func_ptr)(); any_func_ptr foo = (any_func_ptr)(bar) .
  • Undefined behavior for calling a function pointer with a different signature than the actual function. You can avoid this with careful coding - more careful than your current code; but it is difficult and risky.
+5
source share

You can damage the call stack with this, depending on the calling agreement, in particular, who performs the cleanup: http://en.wikipedia.org/wiki/X86_calling_conventions With clearing the called party, the compiler is not able to find out how many variables you passed on the stack at the time of cleaning, so sending the wrong number of parameters or parameters of the wrong size will damage the call stack.

On x64, everyone uses caller flushing, so you are sure of that. However, the parameter values ​​will be generally useless. In your example, on x64, they will be what was in the corresponding registers at that time.

+2
source share

C11 Β§6.3.2.3 (8) states:

A pointer to a function of one type can be converted to a pointer to a function of another type and vice versa; The result is compared with the original pointer. If the converted pointer is used to call a function whose type is incompatible with the reference type, the behavior is undefined.

And in Β§6.7.6.3 (15) it speaks of compatible types of functions:

[...] If one type has a list of parameter types, and another type is specified by the function declarator, which is not part of the function definition and contains an empty list of identifiers, the parameter list should not have an ellipsis terminator and the type of each parameter must be compatible with the type, which results from default promotions. [...]

So, if you had add and chr to accept int arguments (int has at least 16 bits width), that would be fine (if you didn't point to a void * pointer), but as it is, it's UB.

+2
source share

All Articles