¿Cómo funcionan los punteros de función en C?
Últimamente tuve algo de experiencia con punteros de función en C.
Entonces, siguiendo con la tradición de responder tus propias preguntas, decidí hacer un pequeño resumen de los conceptos básicos, para aquellos que necesitan una inmersión rápida en el tema.
Punteros de función en C
Comencemos con una función básica a la que señalaremos :
int addInt(int n, int m) {
return n+m;
}
Primero, definamos un puntero a una función que recibe 2 int
s y devuelve un int
:
int (*functionPtr)(int,int);
Ahora podemos señalar con seguridad nuestra función:
functionPtr = &addInt;
Ahora que tenemos un puntero a la función, usémoslo:
int sum = (*functionPtr)(2, 3); // sum == 5
Pasar el puntero a otra función es básicamente lo mismo:
int add2to3(int (*functionPtr)(int, int)) {
return (*functionPtr)(2, 3);
}
También podemos usar punteros de función en los valores de retorno (intenta seguir el ritmo, se complica):
// this is a function called functionFactory which receives parameter n
// and returns a pointer to another function which receives two ints
// and it returns another int
int (*functionFactory(int n))(int, int) {
printf("Got parameter %d", n);
int (*functionPtr)(int,int) = &addInt;
return functionPtr;
}
Pero es mucho mejor usar un typedef
:
typedef int (*myFuncDef)(int, int);
// note that the typedef name is indeed myFuncDef
myFuncDef functionFactory(int n) {
printf("Got parameter %d", n);
myFuncDef functionPtr = &addInt;
return functionPtr;
}
Los punteros de función en C se pueden utilizar para realizar programación orientada a objetos en C.
Por ejemplo, las siguientes líneas están escritas en C:
String s1 = newString();
s1->set(s1, "hello");
Sí, ->
y la falta de un new
operador es un claro indicio, pero parece implicar que estamos configurando el texto de alguna String
clase para que sea "hello"
.
Al utilizar punteros de función, es posible emular métodos en C.
¿Cómo se logra esto?
La String
clase en realidad tiene struct
un montón de punteros de función que actúan como una forma de simular métodos. La siguiente es una declaración parcial de la String
clase:
typedef struct String_Struct* String;
struct String_Struct
{
char* (*get)(const void* self);
void (*set)(const void* self, char* value);
int (*length)(const void* self);
};
char* getString(const void* self);
void setString(const void* self, char* value);
int lengthString(const void* self);
String newString();
Como puede verse, los métodos de la String
clase son en realidad punteros de función a la función declarada. Al preparar la instancia de String
, newString
se llama a la función para configurar los punteros de función a sus respectivas funciones:
String newString()
{
String self = (String)malloc(sizeof(struct String_Struct));
self->get = &getString;
self->set = &setString;
self->length = &lengthString;
self->set(self, "");
return self;
}
Por ejemplo, la getString
función que se llama invocando el get
método se define de la siguiente manera:
char* getString(const void* self_obj)
{
return ((String)self_obj)->internal->value;
}
Una cosa que se puede notar es que no existe el concepto de una instancia de un objeto y de tener métodos que en realidad sean parte de un objeto, por lo que se debe pasar un "objeto propio" en cada invocación. (Y internal
es solo un elemento oculto struct
que se omitió en la lista de códigos anterior; es una forma de ocultar información, pero eso no es relevante para los punteros de función).
Entonces, en lugar de poder hacer s1->set("hello");
, uno debe pasar el objeto para realizar la acción s1->set(s1, "hello")
.
Una vez eliminada esa pequeña explicación de tener que pasar una referencia a usted mismo, pasaremos a la siguiente parte, que es la herencia en C.
Digamos que queremos crear una subclase de String
, digamos un ImmutableString
. Para hacer que la cadena sea inmutable, el set
método no será accesible, mientras se mantiene el acceso a get
y length
, y se fuerza al "constructor" a aceptar un char*
:
typedef struct ImmutableString_Struct* ImmutableString;
struct ImmutableString_Struct
{
String base;
char* (*get)(const void* self);
int (*length)(const void* self);
};
ImmutableString newImmutableString(const char* value);
Básicamente, para todas las subclases, los métodos disponibles son nuevamente punteros de función. Esta vez, la declaración del set
método no está presente, por lo tanto, no se puede llamar en un archivo ImmutableString
.
En cuanto a la implementación de ImmutableString
, el único código relevante es la función "constructora", la newImmutableString
:
ImmutableString newImmutableString(const char* value)
{
ImmutableString self = (ImmutableString)malloc(sizeof(struct ImmutableString_Struct));
self->base = newString();
self->get = self->base->get;
self->length = self->base->length;
self->base->set(self->base, (char*)value);
return self;
}
Al crear una instancia de ImmutableString
, los punteros de función a los métodos get
y length
en realidad se refieren al método String.get
y String.length
, pasando por la base
variable que es un String
objeto almacenado internamente.
El uso de un puntero de función puede lograr la herencia de un método de una superclase.
Podemos continuar con el polimorfismo en C.
Si por ejemplo quisiéramos cambiar el comportamiento del length
método para que regrese 0
todo el tiempo en la ImmutableString
clase por algún motivo, todo lo que tendríamos que hacer es:
- Agregue una función que servirá como
length
método primordial. - Vaya al "constructor" y establezca el puntero de función en el
length
método principal.
length
Se puede agregar un método primordial ImmutableString
agregando lengthOverrideMethod
:
int lengthOverrideMethod(const void* self)
{
return 0;
}
Luego, el puntero de función para el length
método en el constructor se conecta a lengthOverrideMethod
:
ImmutableString newImmutableString(const char* value)
{
ImmutableString self = (ImmutableString)malloc(sizeof(struct ImmutableString_Struct));
self->base = newString();
self->get = self->base->get;
self->length = &lengthOverrideMethod;
self->base->set(self->base, (char*)value);
return self;
}
Ahora, en lugar de tener un comportamiento idéntico para el length
método en ImmutableString
la clase como para la String
clase, ahora el length
método se referirá al comportamiento definido en la lengthOverrideMethod
función.
Debo agregar un descargo de responsabilidad de que todavía estoy aprendiendo a escribir con un estilo de programación orientada a objetos en C, por lo que probablemente hay puntos que no expliqué bien, o que simplemente pueden estar fuera de lugar en términos de la mejor manera de implementar la programación orientada a objetos. en C. Pero mi propósito era intentar ilustrar uno de los muchos usos de los punteros de función.
Para obtener más información sobre cómo realizar programación orientada a objetos en C, consulte las siguientes preguntas:
- ¿Orientación a objetos en C?
- ¿Puedes escribir código orientado a objetos en C?
La guía para ser despedido: Cómo abusar de los punteros de función en GCC en máquinas x86 compilando su código a mano:
Estos literales de cadena son bytes de código de máquina x86 de 32 bits. 0xC3
es una instrucción x86ret
.
Normalmente no los escribirías a mano, los escribirías en lenguaje ensamblador y luego usarías un ensamblador para nasm
ensamblarlos en un binario plano que volcarías hexadecimalmente en un literal de cadena C.
Devuelve el valor actual en el registro EAX.
int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
Escribe una función de intercambio
int a = 10, b = 20; ((void(*)(int*,int*))"\x8b\x44\x24\x04\x8b\x5c\x24\x08\x8b\x00\x8b\x1b\x31\xc3\x31\xd8\x31\xc3\x8b\x4c\x24\x04\x89\x01\x8b\x4c\x24\x08\x89\x19\xc3 <- This swaps the values of a and b")(&a,&b);
Escriba un contador de bucle hasta 1000, llamando a alguna función cada vez
((int(*)())"\x66\x31\xc0\x8b\x5c\x24\x04\x66\x40\x50\xff\xd3\x58\x66\x3d\xe8\x03\x75\xf4\xc3")(&function); // calls function with 1->1000
Incluso puedes escribir una función recursiva que cuente hasta 100.
const char* lol = "\x8b\x5c\x24\x4\x3d\xe8\x3\x0\x0\x7e\x2\x31\xc0\x83\xf8\x64\x7d\x6\x40\x53\xff\xd3\x5b\xc3\xc3 <- Recursively calls the function at address lol."; i = ((int(*)())(lol))(lol);
Tenga en cuenta que los compiladores colocan cadenas literales en la .rodata
sección (o .rdata
en Windows), que está vinculada como parte del segmento de texto (junto con el código de las funciones).
El segmento de texto tiene permiso Read+Exec, por lo que la conversión de literales de cadena a punteros de función funciona sin necesidad mprotect()
de VirtualProtect()
llamadas al sistema como las que necesitaría para la memoria asignada dinámicamente. (O gcc -z execstack
vincula el programa con pila + segmento de datos + ejecutable del montón, como un truco rápido).
Para desensamblarlos, puede compilar esto para poner una etiqueta en los bytes y usar un desensamblador.
// at global scope
const char swap[] = "\x8b\x44\x24\x04\x8b\x5c\x24\x08\x8b\x00\x8b\x1b\x31\xc3\x31\xd8\x31\xc3\x8b\x4c\x24\x04\x89\x01\x8b\x4c\x24\x08\x89\x19\xc3 <- This swaps the values of a and b";
Al compilar gcc -c -m32 foo.c
y desensamblar con objdump -D -rwC -Mintel
, podemos obtener el ensamblado y descubrir que este código viola la ABI al dañar EBX (un registro de llamadas preservadas) y generalmente es ineficiente.
00000000 <swap>:
0: 8b 44 24 04 mov eax,DWORD PTR [esp+0x4] # load int *a arg from the stack
4: 8b 5c 24 08 mov ebx,DWORD PTR [esp+0x8] # ebx = b
8: 8b 00 mov eax,DWORD PTR [eax] # dereference: eax = *a
a: 8b 1b mov ebx,DWORD PTR [ebx]
c: 31 c3 xor ebx,eax # pointless xor-swap
e: 31 d8 xor eax,ebx # instead of just storing with opposite registers
10: 31 c3 xor ebx,eax
12: 8b 4c 24 04 mov ecx,DWORD PTR [esp+0x4] # reload a from the stack
16: 89 01 mov DWORD PTR [ecx],eax # store to *a
18: 8b 4c 24 08 mov ecx,DWORD PTR [esp+0x8]
1c: 89 19 mov DWORD PTR [ecx],ebx
1e: c3 ret
not shown: the later bytes are ASCII text documentation
they're not executed by the CPU because the ret instruction sends execution back to the caller
Este código de máquina (probablemente) funcionará en código de 32 bits en Windows, Linux, OS X, etc.: las convenciones de llamadas predeterminadas en todos esos sistemas operativos pasan argumentos en la pila en lugar de hacerlo de manera más eficiente en los registros. Pero EBX conserva las llamadas en todas las convenciones de llamadas normales, por lo que usarlo como un registro temporal sin guardarlo o restaurarlo puede hacer que la persona que llama falle fácilmente.
Uno de mis usos favoritos para los punteros de función es como iteradores fáciles y baratos:
#include <stdio.h>
#define MAX_COLORS 256
typedef struct {
char* name;
int red;
int green;
int blue;
} Color;
Color Colors[MAX_COLORS];
void eachColor (void (*fp)(Color *c)) {
int i;
for (i=0; i<MAX_COLORS; i++)
(*fp)(&Colors[i]);
}
void printColor(Color* c) {
if (c->name)
printf("%s = %i,%i,%i\n", c->name, c->red, c->green, c->blue);
}
int main() {
Colors[0].name="red";
Colors[0].red=255;
Colors[1].name="blue";
Colors[1].blue=255;
Colors[2].name="black";
eachColor(printColor);
}