了解extern和void函数指针

时间:2014-05-14 16:02:00

标签: c function-pointers

我的主要看起来像这样:

int main(int argv,char **argc) 
{
     extern system,puts; 
     void (*fn)(char*)=(void(*)(char*))&system;
     char buf[256];

     fn=(void(*)(char*))&puts;
     strcpy(buf,argc[1]);
     fn(argc[2]);
     exit(1);
}

我对extern关键字的含义及其在此代码中的工作原理有一个大概的了解。我对fn函数的声明感到困惑。是在这里声明的函数指针,还是函数?另外,为什么&system&puts位于fn定义的末尾。

4 个答案:

答案 0 :(得分:3)

这段代码是一个非常人为的例子,说明如何使用缓冲区溢出来调用代码最初不能调用的函数。它不应该被视为编写良好的代码的例子,事实恰恰相反。

以下行声明了一个指向函数的指针,并将其初始化为指向system函数。

 void (*fn)(char*)=(void(*)(char*))&system;

请注意,编写后,代码实际上从不调用system函数,因为以下行将指针更改为指向fputs函数。

 fn=(void(*)(char*))&puts;

程序中的漏洞就在这一行

strcpy(buf,argc[1]);

如果strlen的{​​{1}}大于缓冲区大小,则缓冲区溢出可能会将值argc[1]更改为指向某个任意函数,然后将由此行调用

fn

附注:正如有人在评论中指出的那样,应切换fn(argc[2]); argc的名称。

答案 1 :(得分:2)

首先让我说这是一种可怕的编码方式。

那就是说,让我们看看它是如何运作的:

extern system, puts;

这表示systemputs在其他地方被定义,链接器将负责为您提供地址。编译器不知道它们具有什么类型,因此,gcc至少假定它是int并发出警告。我找不到标准中的条款,说明这是明确定义的,实现定义的还是未定义的行为,但肯定是坏的。这特别糟糕,因为这些符号不是int。我确实找到了这个(C11,附件J.2 - 未定义的行为):

  

同一对象或函数的两个声明指定不兼容的类型   (6.2.7)。

int也可能太小而无法容纳指针。这是多么糟糕的事情是无止境的。

所以,让我们在内存中看到它的外观:

system (as 4-byte int)           system (as function)
  BYTE1                             INSTRUCTION1
  BYTE2                             INSTRUCTION2
  BYTE3                             INSTRUCTION3
  BYTE4                             INSTRUCTION4
                                    INSTRUCTION5
                                    INSTRUCTION6
                                    INSTRUCTION7
                                    INSTRUCTION8
                                    ...

所以链接器的作用是使system的地址在每个编译对象上相同(有效地将它们链接在一起)。另一方面,system的内容取决于链接器不知道或不关心的类型。

所以在你的文件中,如果紧接在:

extern system, puts;
你写道:

printf("%d\n", system);

您将获得system函数的前几个指令,就像它是int并打印一样。这很糟糕,不要这样做。

另一方面,正如我所说,程序中定义为system的{​​{1}}的地址与真实的extern功能相同。所以如果你拿地址:

system

并将其转换为正确的类型:

&system

你得到一个函数指针,指向真正的(void(*)(char*))&system 函数。请注意,system现在只是system,我们将其称为函数,这本身就是未定义的行为:

  

C11附件J-2(未定义的行为):

     

对象的存储值不是由允许类型的左值访问   (6.5)。

     

C11 6.5(强调我的):

     

6。用于访问其存储值的对象的有效类型是对象的声明类型(如果有)。 ...

     

7。对象的存储值只能由具有以下类型之一的左值表达式访问:88)

     
      
  • 与对象的有效类型兼容的类型
  •   
  • 与对象的有效类型兼容的类型的限定版本,
  •   
  • 对应于有效类型的有符号或无符号类型的类型   对象,
  •   
  • 与对象的有效类型的限定版本对应的有符号或无符号类型的类型,
  •   
  • 聚合或联合类型,其成员中包含上述类型之一(包括递归地,子聚合或包含联合的成员),或
  •   
  • 字符类型。
  •   

通过所有这些,您现在有一个包含int实际地址的函数指针。然后,您可以根据自己的心意调用它。使用system也是如此。


那很糟糕。这就是实际应该如何做到的:

puts

除了使用实现提供的头文件的好处外,让我们看看这个头文件是如何实际工作的。

在头文件中,您有许多具有许多声明的行。 #include <stdlib.h> #include <stdio.h> int main(int argv,char **argc) { char buf[256]; /* system was unused */ strcpy(buf,argc[1]); puts(argc[2]); return 0; /* 0 means no error */ } 的内容可能如下所示:

puts

首先,请注意代码中的函数指针实际上是如何使函数签名错误的?那很不好。这很糟糕,因为调用int puts(const char *s); 函数,调用者不需要为它提供返回值空间,而void期望有一个返回值空间,因此puts将写入返回值它期望找到它,但实际上空间属于其他东西。这称为堆栈损坏。

无论如何,当编译器看到这样的一行时:

puts
  1. 它会知道int puts(const char *s); 是外部定义:

      

    C11 6.2.2(强调我的):

         

    5。 如果函数的标识符声明没有存储类说明符,则确定其链接与使用存储类说明符extern 声明的完全相同。如果对象的标识符声明具有文件范围而没有存储类说明符,则其链接是外部的。

  2. 它会知道它是一个功能,它的签名是什么。

  3. 所以只需调用puts,编译器就可以正确地完成所有操作。

答案 2 :(得分:0)

extern system, puts;
// equivalent to
extern int system, puts;

以上语句声明类型为system的两个变量putsint,这是未指定时的默认类型(gcc会发出警告)对此)。声明意味着它引入了变量名称和类型,但没有为它分配空间。

  

是在这里声明了一个函数指针,还是一个函数?

void (*fn)(char*); 

fn定义为指向函数的指针,该函数接受char *并且不返回任何值。表达式

(void(*)(char*))&system

获取变量system的地址并将其强制转换为类型为void(*)(char *)的指针,即指向函数的指针,该函数接受char *并且不返回任何值。因此声明

void (*fn)(char*)=(void(*)(char*))&system;

system的地址转换为指针fn的正确类型后指定地址。

  

为什么&amp; system和&amp;放在fn

的定义的末尾

那是因为fn是一个函数指针。在编译的链接阶段,标识符systemputs与库函数链接。这意味着在分配给函数指针fn之前,必须将变量的地址强制转换为适当的类型。

然而,这不是正确的方法。必须使用包含在其中的头文件来包含函数原型。

答案 3 :(得分:0)

fn的声明不是声明一个函数,它声明一个函数指针,并初始化它。这一行:

void (*fn)(char*)=(void(*)(char*))&system;

相当于:

void (*fn)(char*);
fn = (void(*)(char*)) &system;

其中声明fn是一个指向函数的指针,该函数接收一个指向char的指针(并且不返回任何内容)。然后,为此指针分配system()的地址,该地址已转换为适当的类型以匹配fn的类型。