我最近在C语言中有一些函数指针的经验。

因此,按照回答自己问题的传统,我决定对最基本的问题做一个简短的总结,以供那些需要快速切入主题的人使用。


当前回答

被解雇的指南:如何在x86机器上通过手工编译代码滥用GCC中的函数指针:

这些字符串文字是32位x86机器代码的字节。0xC3是x86 ret指令。

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

返回EAX寄存器上的当前值int eax=((int(*)())(“\xc3<-返回eax寄存器的值”)();编写交换函数整数a=10,b=20;((void(*)(int*,int*))“\x8b\x44\x04\x8b\x5c\x24\x08\x80\x80\x8b\ux1b\x31\x31\xc3\xcb\x4c\x24\x04\x09\x01\xcb\x4c\x04\x08\x49\xc3<-这将交换a和b的值”)(-a,-b);将for循环计数器写入1000,每次调用一些函数((int(*)())“\x66\x31\xc0\x8b\x5c\x24\x64\x66\x40\x50\xxf\xd3\x58\x66\x3d\xC8\x03\x75\xf4\xc3”)(函数);//调用函数1->1000您甚至可以编写一个计数为100的递归函数const char*lol=“\x8b\x5c\x24\x4\x3\x8\x3\x0\x0\x7e\x2\x31\x80\x83\x48\x64\x7d\x6\x40\x53\xdf\xd3\x5b\xc3\xc3<-递归调用地址lol处的函数。”;i=((int(*)())(lol))(lol);

请注意,编译器将字符串文本放在.rodata部分(或Windows上的.rata),该部分作为文本段的一部分链接(以及函数代码)。

文本段具有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位代码中工作:所有这些OS上的默认调用约定都在堆栈中传递参数,而不是在寄存器中更有效地传递参数。但EBX在所有正常的调用约定中都保留了调用,因此将其用作临时寄存器而不保存/恢复它很容易使调用者崩溃。

其他回答

被解雇的指南:如何在x86机器上通过手工编译代码滥用GCC中的函数指针:

这些字符串文字是32位x86机器代码的字节。0xC3是x86 ret指令。

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

返回EAX寄存器上的当前值int eax=((int(*)())(“\xc3<-返回eax寄存器的值”)();编写交换函数整数a=10,b=20;((void(*)(int*,int*))“\x8b\x44\x04\x8b\x5c\x24\x08\x80\x80\x8b\ux1b\x31\x31\xc3\xcb\x4c\x24\x04\x09\x01\xcb\x4c\x04\x08\x49\xc3<-这将交换a和b的值”)(-a,-b);将for循环计数器写入1000,每次调用一些函数((int(*)())“\x66\x31\xc0\x8b\x5c\x24\x64\x66\x40\x50\xxf\xd3\x58\x66\x3d\xC8\x03\x75\xf4\xc3”)(函数);//调用函数1->1000您甚至可以编写一个计数为100的递归函数const char*lol=“\x8b\x5c\x24\x4\x3\x8\x3\x0\x0\x7e\x2\x31\x80\x83\x48\x64\x7d\x6\x40\x53\xdf\xd3\x5b\xc3\xc3<-递归调用地址lol处的函数。”;i=((int(*)())(lol))(lol);

请注意,编译器将字符串文本放在.rodata部分(或Windows上的.rata),该部分作为文本段的一部分链接(以及函数代码)。

文本段具有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位代码中工作:所有这些OS上的默认调用约定都在堆栈中传递参数,而不是在寄存器中更有效地传递参数。但EBX在所有正常的调用约定中都保留了调用,因此将其用作临时寄存器而不保存/恢复它很容易使调用者崩溃。

指向函数的指针很有用,因为正如《C编程语言》一书所说,C中的函数不是变量。这意味着,

// Say you have add function
int add(int x, int y){
    return x + y;
}

// Say you have another add function
int another_add(int x, int y){
    return y + x;
}


int main(){
    // Although the types of another_add and add are same
    // You can't do
    another_add = add
    
    // You have a compute function that takes a function of int's signature
    int (*compute)(int, int);
   
    // You won't even be able to pass functions to other functions
    // (Although when you do, C is just passing the pointer to that function)
    // So, compute(add) is really compute(&add)
    // But you can create a pointer to functions that are variables
    // you can assign to and/or pass to other functions

    int (*operation)(int, int);
    // Now you can do
    operation = &add;
    // You could also do, the following to do the same thing
    // When a function is passed in right hand side of assignment,
    // C knows that you mean pointer, and you don't need explicit &
    operation = add;
}

同样,数组也不是C中的变量。您可以编写一个与上面类似的示例并进行测试。

C中的函数指针可以用于在C中执行面向对象的编程。

例如,以下行用C编写:

String s1 = newString();
s1->set(s1, "hello");

是的,->和缺少新运算符是一个致命的漏洞,但这似乎意味着我们正在将某些String类的文本设置为“hello”。

通过使用函数指针,可以模拟C语言中的方法。

这是如何实现的?

String类实际上是一个带有一组函数指针的结构,用作模拟方法的方法。以下是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;
}

需要注意的一点是,没有对象实例的概念,也没有方法实际上是对象的一部分,因此每次调用都必须传递一个“self-object”。(内部结构只是一个隐藏结构,它在前面的代码列表中被省略了——这是一种执行信息隐藏的方式,但这与函数指针无关。)

因此,与其能够执行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方法的函数指针实际上引用String.get和String.length方法,方法是通过内部存储的String对象的基变量。

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

我们可以进一步研究C。

例如,如果出于某种原因,我们希望将长度方法的行为更改为在ImmutableString类中始终返回0,那么所要做的就是:

添加将用作重写长度方法的函数。转到“构造函数”并将函数指针设置为重写长度方法。

在ImmutableString中添加重写长度方法可以通过添加lengthOverrideMethod来执行:

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;
}

现在,长度方法将引用lengthOverrideMethod函数中定义的行为,而不是ImmutableString类中的长度方法与String类具有相同的行为。

我必须补充一句免责声明,我仍在学习如何在C语言中使用面向对象的编程风格进行编写,因此可能有一些点我没有很好地解释,或者在如何在C中最好地实现OOP方面可能是不正确的。但我的目的是试图说明函数指针的多种用途之一。

有关如何在C语言中执行面向对象编程的更多信息,请参阅以下问题:

C语言中的对象定向?你能用C语言编写面向对象的代码吗?

函数指针的另一个好用法:在版本之间轻松切换

当您需要在不同的时间或不同的开发阶段使用不同的功能时,它们非常方便。例如,我正在一台有控制台的主机上开发一个应用程序,但该软件的最终版本将放在Avnet ZedBoard(它有显示器和控制台的端口,但最终版本不需要这些端口)上。所以在开发过程中,我将使用printf查看状态和错误消息,但当我完成后,我不希望打印任何内容。以下是我所做的:

版本.h

// First, undefine all macros associated with version.h
#undef DEBUG_VERSION
#undef RELEASE_VERSION
#undef INVALID_VERSION


// Define which version we want to use
#define DEBUG_VERSION       // The current version
// #define RELEASE_VERSION  // To be uncommented when finished debugging

#ifndef __VERSION_H_      /* prevent circular inclusions */
    #define __VERSION_H_  /* by using protection macros */
    void board_init();
    void noprintf(const char *c, ...); // mimic the printf prototype
#endif

// Mimics the printf function prototype. This is what I'll actually 
// use to print stuff to the screen
void (* zprintf)(const char*, ...); 

// If debug version, use printf
#ifdef DEBUG_VERSION
    #include <stdio.h>
#endif

// If both debug and release version, error
#ifdef DEBUG_VERSION
#ifdef RELEASE_VERSION
    #define INVALID_VERSION
#endif
#endif

// If neither debug or release version, error
#ifndef DEBUG_VERSION
#ifndef RELEASE_VERSION
    #define INVALID_VERSION
#endif
#endif

#ifdef INVALID_VERSION
    // Won't allow compilation without a valid version define
    #error "Invalid version definition"
#endif

在版本c中,我将定义版本h中的2个函数原型

版本.c

#include "version.h"

/*****************************************************************************/
/**
* @name board_init
*
* Sets up the application based on the version type defined in version.h.
* Includes allowing or prohibiting printing to STDOUT.
*
* MUST BE CALLED FIRST THING IN MAIN
*
* @return    None
*
*****************************************************************************/
void board_init()
{
    // Assign the print function to the correct function pointer
    #ifdef DEBUG_VERSION
        zprintf = &printf;
    #else
        // Defined below this function
        zprintf = &noprintf;
    #endif
}

/*****************************************************************************/
/**
* @name noprintf
*
* simply returns with no actions performed
*
* @return   None
*
*****************************************************************************/
void noprintf(const char* c, ...)
{
    return;
}

注意函数指针在版本.h中如何原型化为void(*zprintf)(constchar*,…);当它在应用程序中被引用时,它将在其指向的任何位置开始执行,这一点尚未定义。在版本.c中,注意在board_init()函数中,zprintf被分配了一个唯一的函数(其函数签名匹配),这取决于版本中定义的版本

运行代码将如下所示:

主程序c

#include "version.h"
#include <stdlib.h>
int main()
{
    // Must run board_init(), which assigns the function
    // pointer to an actual function
    board_init();

    void *ptr = malloc(100); // Allocate 100 bytes of memory
    // malloc returns NULL if unable to allocate the memory.

    if (ptr == NULL)
    {
        zprintf("Unable to allocate memory\n");
        return 1;
    }

    // Other things to do...
    return 0;
}

如果处于调试模式,上述代码将使用printf,如果处于发布模式,则不执行任何操作。这比浏览整个项目并注释或删除代码要容易得多。我所需要做的就是在版本.h中更改版本,其余的都由代码完成!

函数指针通常由typedef定义,并用作参数和返回值。

上面的答案已经解释了很多,我只举一个完整的例子:

#include <stdio.h>

#define NUM_A 1
#define NUM_B 2

// define a function pointer type
typedef int (*two_num_operation)(int, int);

// an actual standalone function
static int sum(int a, int b) {
    return a + b;
}

// use function pointer as param,
static int sum_via_pointer(int a, int b, two_num_operation funp) {
    return (*funp)(a, b);
}

// use function pointer as return value,
static two_num_operation get_sum_fun() {
    return &sum;
}

// test - use function pointer as variable,
void test_pointer_as_variable() {
    // create a pointer to function,
    two_num_operation sum_p = &sum;
    // call function via pointer
    printf("pointer as variable:\t %d + %d = %d\n", NUM_A, NUM_B, (*sum_p)(NUM_A, NUM_B));
}

// test - use function pointer as param,
void test_pointer_as_param() {
    printf("pointer as param:\t %d + %d = %d\n", NUM_A, NUM_B, sum_via_pointer(NUM_A, NUM_B, &sum));
}

// test - use function pointer as return value,
void test_pointer_as_return_value() {
    printf("pointer as return value:\t %d + %d = %d\n", NUM_A, NUM_B, (*get_sum_fun())(NUM_A, NUM_B));
}

int main() {
    test_pointer_as_variable();
    test_pointer_as_param();
    test_pointer_as_return_value();

    return 0;
}