【问题标题】:Decode function pointer in C在 C 中解码函数指针
【发布时间】:2017-06-26 09:19:04
【问题描述】:

是否可以在 C 中存储函数指针内容。我知道您可以将各种指针存储在变量中。但是,如果我可以“解包”一个整数指针(指向一个整数)或字符串指针(指向一个无符号字符),我就不能 decode 一个函数指针.

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

【问题讨论】:

  • 我的意思是存储指针的内容,而不是指针。
  • 是的,您可以将 LM 代码存储在一个变量中!但是如果操作系统在 procted 模式下运行(Intel - Windows/Linux),您可能会遇到一些异常问题!
  • @SpilledMango 你能解释一下你想在那里画什么区别吗?
  • 是的,将函数的机器码指令存储在变量中。
  • 特定条件下可以memcpy一个函数:stackoverflow.com/questions/1716663/c-memcpy-a-function

标签: c function-pointers


【解决方案1】:

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

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 允许在将函数分配给函数指针时省略 & 运算符,并在通过函数指针调用函数时省略显式取消引用。

简而言之:使用指针不会改变 数据对象 vs 函数的语义。


还要注意,在这种情况下,函数和数据指针不兼容。您不能将函数指针分配给void *。甚至可以有一个平台,其中函数指针的大小与数据指针的大小不同。


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

【讨论】:

  • 好的,但是内核在将程序加载到RAM并跳转到它时,它不会也将函数加载到RAM中吗?
  • @SpilledMango 这超出了 C 的规范。操作系统会将二进制文件加载到内存映射中并跳转到执行的入口点。这个二进制文件可能是任何可执行代码,从用汇编手写到用任何语言编译……(非常简短且不完整的描述)
【解决方案2】:

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

虽然您可以从内存中的函数地址读取一些字节,但它们只是字节,没有别的。您需要知道如何解释这些字节以便“将机器代码指令存储在变量中”。而这里真正的问题是要知道在哪里停止,一个函数的代码在哪里结束,另一个函数的代码从哪里开始。

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

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

【讨论】:

    【解决方案3】:

    假设我们谈论的是冯诺依曼架构。

    基本上,我们有一个包含指令和数据的内存。然而,现代操作系统能够控制内存访问权限(读/写/执行)。

    通常,将函数指针转换为数据指针是未定义的行为。虽然如果我们说的是 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
    

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

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

    【讨论】:

      【解决方案4】:

      这里的代码应该是一个将代码注入程序的骨架。但是,如果您在诸如 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
      $1 = (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]
      $3 = 0 '\000'
      (gdb) n
      16      fn_ptr();
      (gdb) p x[0]
      $5 = -21 '\353'
      (gdb) p x[1]
      $6 = -2 '\376'
      (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 仅对 Intel(或兼容)处理器有效。这个LM Code对应汇编代码:jmp $.

      【讨论】:

      • 请注意,mprotect() 应该能够为 Linux 解决这个问题。
      • .. 并在此处查看 Windows stackoverflow.com/questions/16677410/…
      • @FelixPalmen,应该是,但我尝试使用mprotect(),它返回errno = EINVAL,尽管我已经四舍五入了变量fp_ptr使用的内存地址,两者都是4KB页面大小或 4MB 页面大小。
      【解决方案5】:

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

      下面的程序没有什么特别的!它运行数组prg[][] 中的代码,并将其复制到内存映射区域中。它使用两个函数指针fnI_ptrfnD_ptr 都指向同一个内存区域。程序将内存中的LM代码交替复制两个代码之一,然后执行“加载”的代码。

      #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 函数:

      • 第一个将两个整数值相乘并返回一个整数值作为结果

      • 第二个将两个双精度值相乘并返回一个双精度值作为结果。

      我不讨论可移植性。 prg[][] 中的代码是通过使用 gcc ( gcc (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4 ) 编译获得的对象的objdump -S prgname &gt; 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 SMP Fri Mar 31 14:13:22 UTC 2017 x86_64)的 PC 上编译。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2010-12-08
        • 2011-01-08
        • 1970-01-01
        • 2014-11-26
        • 2013-08-19
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多