协慌网

登录 贡献 社区

C 中的函数指针如何工作?

我最近在 C 中使用了函数指针。

继续回答你自己的问题的传统,我决定对那些需要快速深入研究这个主题的人进行一些基本的总结。

答案

C 中的函数指针

让我们从一个基本功能开始,我们将指向

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方法将无法访问,同时保持对getlength访问,并强制 “构造函数” 接受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 ,指向getlength方法的函数指针实际上是通过遍历base变量(即内部存储的String对象)来引用String.getString.length方法。

使用函数指针可以实现从超类继承方法。

我们可以进一步继续C 中的多态性

例如,如果我们想要改变length方法的行为,由于某种原因在ImmutableString类中一直返回0 ,那么所有必须做的就是:

  1. 添加一个将用作覆盖length方法的函数。
  2. 转到 “构造函数” 并将函数指针设置为覆盖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 机器代码的字节。 0xC3x86 ret指令

你通常不会手工编写这些,你用汇编语言编写,然后使用像nasm这样的汇编程序将它组装成一个平面二进制文件,然后将其转换为 C 字符串文字。

  1. 返回 EAX 寄存器上的当前值

    int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
  2. 写一个交换函数

    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);
  3. 将 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
  4. 您甚至可以编写一个计数为 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 在所有正常的调用约定中都被调用保留,因此将其用作临时寄存器而不保存 / 恢复它可以轻松地使调用者崩溃。