让我们从一个基本功能开始,我们将指向 :
int addInt(int n, int m) {
return n+m;
}
首先,让我们定义一个指向函数的指针,该函数接收 2 个int
并返回一个int
:
int (*functionPtr)(int,int);
现在我们可以安全地指出我们的功能:
functionPtr = &addInt;
现在我们有了一个指向函数的指针,让我们使用它:
int sum = (*functionPtr)(2, 3); // sum == 5
将指针传递给另一个函数基本相同:
int add2to3(int (*functionPtr)(int, int)) {
return (*functionPtr)(2, 3);
}
我们也可以在返回值中使用函数指针(尝试跟上,它变得混乱):
// 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;
}
但是使用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;
}
C 中的函数指针可用于在 C 中执行面向对象的编程。
例如,以下行用 C 编写:
String s1 = newString();
s1->set(s1, "hello");
是的, ->
和缺少一个new
运算符是一个死的赠品,但它肯定暗示我们将一些String
类的文本设置为"hello"
。
通过使用函数指针, 可以在 C 中模拟方法 。
这是如何完成的?
String
类实际上是一个带有一堆函数指针的struct
,它充当模拟方法的方法。以下是String
类的部分声明:
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();
可以看出, String
类的方法实际上是声明函数的函数指针。在准备String
的实例时,调用newString
函数以设置指向其各自函数的函数指针:
String newString()
{
String self = (String)malloc(sizeof(struct String_Struct));
self->get = &getString;
self->set = &setString;
self->length = &lengthString;
self->set(self, "");
return self;
}
例如,通过调用get
方法调用的getString
函数定义如下:
char* getString(const void* self_obj)
{
return ((String)self_obj)->internal->value;
}
可以注意到的一件事是,没有对象实例的概念,并且具有实际上是对象的一部分的方法,因此必须在每次调用时传入 “自身对象”。 (而internal
只是一个隐藏的struct
,它在前面的代码清单中被省略了 - 它是一种执行信息隐藏的方法,但这与函数指针无关。)
所以,而不是能够做s1->set("hello");
,必须传入对象以对s1->set(s1, "hello")
执行操作。
由于这个小的解释必须传递给你自己的引用,我们将转到下一部分,即C 中的继承 。
假设我们想要创建一个String
的子类,比如一个ImmutableString
。为了使字符串不可变, set
方法将无法访问,同时保持对get
和length
访问,并强制 “构造函数” 接受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);
基本上,对于所有子类,可用的方法再次是函数指针。这次, set
方法的声明不存在,因此,它不能在ImmutableString
调用。
至于ImmutableString
的实现,唯一相关的代码是 “构造函数” 函数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;
}
在实例化ImmutableString
,指向get
和length
方法的函数指针实际上是通过遍历base
变量(即内部存储的String
对象)来引用String.get
和String.length
方法。
使用函数指针可以实现从超类继承方法。
我们可以进一步继续C 中的多态性 。
例如,如果我们想要改变length
方法的行为,由于某种原因在ImmutableString
类中一直返回0
,那么所有必须做的就是:
length
方法的函数。 length
方法。 可以通过添加lengthOverrideMethod
来执行在ImmutableString
添加重写length
方法:
int lengthOverrideMethod(const void* self)
{
return 0;
}
然后,构造函数中length
方法的函数指针连接到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;
}
现在,不是将ImmutableString
类中的length
方法作为String
类具有相同的行为,现在length
方法将引用lengthOverrideMethod
函数中定义的行为。
我必须添加一个免责声明,我仍然在学习如何使用 C 语言中的面向对象编程风格进行编写,因此可能有一点我没有解释得很好,或者可能只是在如何最好地实现 OOP 在 C. 但我的目的是试图说明函数指针的许多用法之一。
有关如何在 C 中执行面向对象编程的更多信息,请参阅以下问题:
被触发的指南:如何通过手动编译代码来滥用 x86 机器上 GCC 中的函数指针:
这些字符串文字是 32 位 x86 机器代码的字节。 0xC3
是x86 ret
指令 。
你通常不会手工编写这些,你用汇编语言编写,然后使用像nasm
这样的汇编程序将它组装成一个平面二进制文件,然后将其转换为 C 字符串文字。
返回 EAX 寄存器上的当前值
int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
写一个交换函数
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);
将 for 循环计数器写入 1000,每次调用一些函数
((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
您甚至可以编写一个计数为 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);
请注意,编译器将字符串文字放在.rodata
部分(或 Windows 上的.rdata
)中,该部分链接为文本段的一部分(以及函数代码)。
文本段具有 Read + Exec 权限,因此将字符串文字转换为函数指针无需像动态分配的内存那样需要mprotect()
或VirtualProtect()
系统调用。 (或者gcc -z execstack
将程序与堆栈 + 数据段 + 堆可执行文件链接起来,作为快速入侵。)
要反汇编这些,您可以编译它以在字节上放置标签,并使用反汇编程序。
// 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";
使用gcc -c -m32 foo.c
编译并使用objdump -D -rwC -Mintel
反汇编,我们可以获得程序集,并通过破坏 EBX(一个调用保留寄存器)发现此代码违反了 ABI,并且通常效率低下。
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
这个机器代码(可能)在 Windows,Linux,OS X 等上以 32 位代码工作:所有这些操作系统上的默认调用约定都在堆栈中传递 args 而不是在寄存器中更有效。但 EBX 在所有正常的调用约定中都被调用保留,因此将其用作临时寄存器而不保存 / 恢复它可以轻松地使调用者崩溃。