在 C 中解码函数指针

Decode function pointer in C

是否可以在 C 中存储函数指针内容。我知道您可以将各种指针存储在变量中。但是,如果我可以 "unwrap" 一个整数指针(指向一个整数)或字符串指针(指向一个无符号字符),我是否能够 解码 函数指针。

更清楚一点,我的意思是将机器代码指令存储在一个变量中。

您遗漏了一个重要事实:函数不是 C 中的 (first-class) 对象。

C中有两种基本类型的指针:数据指针和函数指针。两者都可以使用 *.

取消引用

相似之处到此结束。数据对象有一个 存储值 ,因此取消引用数据指针会访问此值:

int a = 5;
int *b = &a;
int c = *b; // 5

一个函数就是这个,一个函数。您可以 调用 一个函数,因此您可以调用取消引用函数指针的结果。它没有储值:

int x(void) { return 1; }
int (*y)(void) = &x; // valid also without the address-of operator

// ...
int main(void)
{
    int a = (*y)();  // valid also without explicit dereference like int a = y();
}

为了便于处理,C 允许在将函数分配给函数指针时省略 & 运算符,并且在通过函数指针调用函数时也省略显式取消引用。

简而言之:使用指针不会改变 数据对象 函数 .

的语义

另请注意,在此上下文中,函数指针和数据指针不兼容。您不能将函数指针分配给 void *。甚至可以有一个平台,其中函数指针的大小与数据指针的大小不同。


实际上,在函数指针 具有 与数据指针相同格式的平台上,您可以“说服”您的编译器通过将指针指向 const char * 来访问位于那里的实际二进制代码。但请注意这是未定义的行为。

C中的指针是内存中某个对象的地址。一个int *是一个int的地址,一个函数指针是函数代码在内存中的地址。

虽然您可以从内存中的函数地址读取一些字节,但它们只是字节而已。您需要知道如何解释这些字节才能 "store the machine code instructions in a variable"。而这里真正的问题是知道在哪里停止,一个函数的代码在哪里结束,另一个函数的代码在哪里开始。

这些东西不是由语言定义的,它们取决于许多因素:处理器架构、OS、编译器、用于编译代码的编译器标志(用于优化 f.e .).

这里真正的问题是:假设您可以“将机器代码指令存储在一个变量中”您希望如何使用它?它只是一个字节序列,对大多数人来说毫无意义,不能用于执行函数。如果您不是在编写编译器、链接器、仿真器、操作系统或类似的东西,那么您可以使用函数的机器代码指令做任何有用的事情。 (如果你写的是上面的一个,那么你就知道答案了,你不会在 SO 或其他地方问这样的问题。)

这里的代码应该是将代码注入程序的骨架。但是,如果您在 Linux 或 Windows 等 SO 中执行它,您将在 fn_ptr 点的第一条指令执行之前得到异常。

#include <stdio.h>
#include <malloc.h>

typedef int FN(void);

int main(void)
{
        FN * fn_ptr;
        char * x;

        fn_ptr = malloc(10240);
        x = (char *)fn_ptr;

        // ... Insert code into x that points the same memory of fn_ptr;
        x[0]='\xeb'; x[1]='\xfe'; // jmp $ that is like while(1)
        fn_ptr();

        return 0;
}

如果您使用 gdb 执行此代码,您将获得以下结果:

(gdb) l
2   #include <malloc.h>
3   
4   typedef int FN(void);
5   
6   int main(void)
7   {
8       FN * fn_ptr;
9       char * x;
10  
11      fn_ptr = malloc(10240);
12      x = (char *)fn_ptr;
13  
14      // ... Insert code into x that points the same memory of fn_ptr;
15      x[0]='\xeb'; x[1]='\xfe'; // jmp $ that is like while(1)
16      fn_ptr();
17  
18      return 0;
19  }
(gdb) b 11
Breakpoint 1 at 0x400535: file p.c, line 11.
(gdb) r
Starting program: /home/sergio/a.out 

Breakpoint 1, main () at p.c:11
11      fn_ptr = malloc(10240);
(gdb) p fn_ptr
 = (FN *) 0x7fffffffde30
(gdb) n
12      x = (char *)fn_ptr;
(gdb) n
15      x[0]='\xeb'; x[1]='\xfe'; // jmp $ that is like while(1)
(gdb) p x[0]
 = 0 '[=11=]0'
(gdb) n
16      fn_ptr();
(gdb) p x[0]
 = -21 '3'
(gdb) p x[1]
 = -2 '6'
(gdb) s

Program received signal SIGSEGV, Segmentation fault.
0x0000000000602010 in ?? ()
(gdb) where
#0  0x0000000000602010 in ?? ()
#1  0x0000000000400563 in main () at p.c:16
(gdb) 

你如何看待 GDB 在 fn_ptr 指向的地址发出 SIGSEGV, Segmentation fault 信号,尽管我们进入内存的指令是有效指令。

请注意,LM 代码:EB FE 仅对英特尔(或兼容)处理器有效。此 LM 代码对应于汇编代码:jmp $.

假设我们正在谈论冯·诺依曼架构。

基本上我们有一个包含指令和数据的内存。然而,现代 OSes 能够控制内存访问权限 (read/write/execute)。

从标准上讲,将函数指针转换为数据指针是未定义的行为。尽管如果我们说 Linux、gcc 和现代 x86-64 CPU,您可能会进行这样的转换,但您将得到一个指向只读可执行内存段的指针。

例如看看这个简单的程序:

#include <stdio.h>

int func() {
  return 1;
}

int main() {
  unsigned char * code = (void*)func;
  printf("%02x\n%02x%02x%02x\n%02x%02x%02x%02x%02x\n%02x\n%02x\n", 
      *code, 
      *(code+1), *(code+2), *(code+3), 
      *(code+4), *(code+5), *(code+6), *(code+7), *(code+8),
      *(code+9),
      *(code+10));
}

编译:

gcc -O0 -o tst tst.c

它在我的机器上的输出是:

55         // push rbp
4889e5     // mov rsp, rbp
b801000000 // mov eax, 0x1
5d         // pop rbp
c3         // ret

如您所见,这确实是我们的职能。

由于 OS 为您提供了标记内存可执行的能力,您实际上可以在运行时编写您的函数,您所需要的只是生成当前平台操作码并标记内存可执行。这正是 JIT 编译器的工作方式。有关此类编译器的优秀示例,请查看 LuaJIT。

这是一个使用函数指针的示例,其中 LM 代码被复制到内存区域并执行。

下面的程序没有做任何特别的事情!它运行数组 prg[][] 中的代码,将其复制到内存映射区域。它使用两个函数指针 fnI_ptrfnD_ptr 都指向同一个内存区域。程序将内存中的LM代码交替复制两个代码之一,然后执行"loaded"代码。

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <malloc.h>
#include <sys/mman.h>
#include <stdint.h>
#include <inttypes.h>

typedef int FNi(int,int);
typedef double FNd(double,double);

const char prg[][250] = {
    // int multiply(int x,int y)
    {
    0x55,                       // push     %rbp
    0x48,0x89,0xe5,             // mov      %rsp,%rbp
    0x89,0x7d,0xfc,             // mov      %edi,-0x4(%rbp)
    0x89,0x75,0xf8,             // mov      %esi,-0x8(%rbp)
    0x8B,0x45,0xfc,             // mov      -0x4(%rbp),%eax
    0x0f,0xaf,0x45,0xf8,        // imul     -0x8(%rbp),%eax
    0x5d,                       // pop      %rbp
    0xc3                        // retq
    },

    // double multiply(double x,double y)
    {
    0x55,                       // push     %rbp
    0x48,0x89,0xe5,             // mov    %rsp,%rbp
    0xf2,0x0f,0x11,0x45,0xf8,   // movsd  %xmm0,-0x8(%rbp)
    0xf2,0x0f,0x11,0x4d,0xf0,   // movsd  %xmm1,-0x10(%rbp)
    0xf2,0x0f,0x10,0x45,0xf8,   // movsd  -0x8(%rbp),%xmm0
    0xf2,0x0f,0x59,0x45,0xf0,   // mulsd  -0x10(%rbp),%xmm0
    0xf2,0x0f,0x11,0x45,0xe8,   // movsd  %xmm0,-0x18(%rbp)
    0x48,0x8b,0x45,0xe8,        // mov    -0x18(%rbp),%rax
    0x48,0x89,0x45,0xe8,        // mov    %rax,-0x18(%rbp)
    0xf2,0x0f,0x10,0x45,0xe8,   // movsd  -0x18(%rbp),%xmm0
    0x5d,                       // pop    %rbp
    0xc3                        // retq
    }
};

int main(void)
{
#define FMT "0x%016"PRIX64

    int ret=0;

    FNi * fnI_ptr=NULL;
    FNd * fnD_ptr=NULL;

    void * x=NULL;

    //uint64_t p = PAGE(K), l =  p*4; //Max memory to use!
    uint64_t p = 0, l =  0, line=0; //Max memory to use!

    do {
        p = getpagesize();line = __LINE__;
        if (!p) {
            ret=line;
            break;
        }

        l=p*2;
        printf("Mem page size  = "FMT"\n",p);
        printf("Mem alloc size = "FMT"\n\n",l);

        x = mmap(NULL, l, PROT_EXEC | PROT_READ | PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0);line = __LINE__;
        if (x==MAP_FAILED) {
            x=NULL;
            ret=line;
            break;
        }

        //Prepares function-pointers. They point the same memory! :)
        fnI_ptr=(FNi *)x;
        fnD_ptr=(FNd *)x;

        printf("from x="FMT" to "FMT"\n\n",(int64_t)x,(int64_t)x + l);

        // Calling the functions coded into the array prg

        puts("Copying prg[0]");

        // It injects the function prg[0]
        memcpy(x,prg[0],sizeof(prg[0]));

        // It executes the injected code
        printf("executing int-mul = %d\n",fnI_ptr(10,20));

        puts("--------------------------");
        puts("Copying prg[1]");

        // It injects the function prg[1]
        memcpy(x,prg[1],sizeof(prg[1]));

        //Prepares function pointers.

        // It executes the injected code
        printf("executing dbl-mul = %f\n\n",fnD_ptr(12.3,3.21));


    } while(0); // Fake loop to be breaked when an error occurs!

    if (x!=NULL)
        munmap(x,l);

    if (ret) {
        printf("[line"
               "=%d] Error %d - %s\n",ret,errno,strerror(errno));
    }
    return errno;
}

prg[][]中有两个LM函数:

  • 第一个乘以两个整数值和returns一个整数值作为结果

  • 第二个将两个双精度值相乘,returns一个双精度值作为结果。

我不讨论可移植性。 prg[][] 中的代码是通过使用 gcc ( gcc (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4 ) 编译获得的对象的 objdump -S prgname > prgname.s 获得的,没有优化以下代码:

int multiply(int a, int b)
{
    return a*b;
}

double dMultiply(double a, double b)
{
    return a*b;
}

以上代码已在配备 Intel I3 CPU(64 位)和 SO Linux(3.13.0-116-generic #163-Ubuntu 的 PC 上编译SMP 3 月 31 日星期五 14:13:22 UTC 2017 x86_64).