在 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_ptr
和 fnD_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).
是否可以在 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_ptr
和 fnD_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).