代码之家  ›  专栏  ›  技术社区  ›  sleske

如果我转换一个函数指针,改变参数的数目,会发生什么?

  •  10
  • sleske  · 技术社区  · 15 年前

    我刚刚开始在C语言中对函数指针进行全面介绍。为了理解函数指针的转换是如何工作的,我编写了以下程序。它基本上创建一个函数指针,指向一个接受一个参数的函数,将其转换为具有三个参数的函数指针,并调用该函数,提供三个参数。我很好奇会发生什么:

    #include <stdio.h>
    
    int square(int val){
      return val*val;
    }
    
    void printit(void* ptr){
      int (*fptr)(int,int,int) = (int (*)(int,int,int)) (ptr);
      printf("Call function with parameters 2,4,8.\n");
      printf("Result: %d\n", fptr(2,4,8));
    }
    
    
    int main(void)
    {
        printit(square);
        return 0;
    }
    

    它编译和运行时没有错误或警告(Linux/x86上的gcc-wall)。我的系统上的输出是:

    Call function with parameters 2,4,8.
    Result: 4
    

    显然,多余的论点只是被悄悄地抛弃了。

    现在我想知道这里到底发生了什么。

    1. 关于合法性:如果我理解 Casting a function pointer to another type 正确地说,这只是未定义的行为。所以事实上,这运行并产生一个合理的结果只是纯粹的运气,对吗?(或编译器编写者的良好性)
    2. 为什么海湾合作委员会不警告我,即使有墙?这是编译器无法检测到的吗?为什么?

    我来自Java,在那里打字检查要严格得多,所以这种行为让我有点困惑。也许我正在经历文化冲击。

    8 回复  |  直到 12 年前
        1
  •  15
  •   Franci Penov    15 年前

    额外的参数不会被丢弃。它们被正确地放置在堆栈上,就好像调用了一个需要三个参数的函数一样。但是,由于您的函数只关心一个参数,所以它只查看堆栈的顶部,不接触其他参数。

    事实上,这一呼吁起作用是纯粹的运气,基于以下两个事实:

    • 对于函数和转换指针,第一个参数的类型相同。如果您将函数更改为获取指向字符串的指针并尝试打印该字符串,您将得到一个很好的崩溃,因为代码将尝试取消引用指向地址内存2的指针。
    • 默认情况下,调用方使用的调用约定是清除堆栈。如果您更改了调用约定,以便被调用方清理堆栈,您将以调用方在堆栈上推送三个参数而结束,然后被调用方清理(或更确切地说尝试)一个参数。这可能会导致堆栈损坏。

    编译器不可能出于一个简单的原因就这样的潜在问题向您发出警告——在一般情况下,它在编译时不知道指针的值,因此它无法评估它指向的内容。假设函数指针指向在运行时创建的类虚拟表中的一个方法?所以,你告诉编译器它是一个指向一个有三个参数的函数的指针,编译器会相信你的。

        2
  •  12
  •   William Bell    15 年前

    如果你把一辆车当作锤子来铸成锤子,编译器会告诉你这辆车是一把锤子,但这并不能把它变成锤子。编译器可能会成功地使用汽车来驱动钉子,但这取决于实现的好运。这仍然是一件不明智的事情。

        3
  •  3
  •   anon    15 年前
    1. 是的,这是未定义的行为——任何事情都有可能发生,包括它看起来“有效”。

    2. 强制转换可防止编译器发出警告。此外,编译器不需要诊断可能导致未定义行为的原因。这样做的原因是,这样做不可能,或者这样做太困难和/或造成太多的开销。

        4
  •  3
  •   sleske    12 年前

    强制转换的最大错误是将数据指针强制转换为函数指针。它比签名更改更糟糕,因为无法保证函数指针和数据指针的大小相等。和很多相反 理论的 不确定的行为,这一个可以在野外遇到,甚至在先进的机器上(不仅在嵌入式系统上)。

    在嵌入式平台上,您可能很容易遇到不同大小的指针。甚至有些处理器的数据指针和函数指针都能处理不同的事情(一个是RAM,另一个是ROM),这就是所谓的哈佛架构。在实模式下的x86上,可以混合16位和32位。watcom-c有一个特殊的DOS扩展模式,数据指针宽48位。尤其是对于C,人们应该知道并不是所有的东西都是POSIX,因为C可能是异域硬件上唯一可用的语言。

    有些编译器允许混合内存模型,其中代码保证在32位大小内,数据可以通过64位指针或相反的指针寻址。

    编辑: 结论:不要将数据指针强制转换为函数指针。

        5
  •  2
  •   Mark Wilkins    15 年前

    行为由调用约定定义。如果使用调用方推送和弹出堆栈的调用约定,那么在这种情况下,它会很好地工作,因为它只意味着调用期间堆栈上有额外的几个字节。我现在没有GCC,但是有了Microsoft编译器,下面的代码:

    int ( __cdecl * fptr)(int,int,int) = (int (__cdecl * ) (int,int,int)) (ptr);
    

    为调用生成以下程序集:

    push        8
    push        4
    push        2
    call        dword ptr [ebp-4]
    add         esp,0Ch
    

    注意调用后添加到堆栈的12个字节(0ch)。在此之后,堆栈就可以了(假设在本例中被调用方是uu cdecl,因此它不会尝试同时清理堆栈)。但使用以下代码:

    int ( __stdcall * fptr)(int,int,int) = (int (__stdcall * ) (int,int,int)) (ptr);
    

    这个 add esp,0Ch 不是在程序集中生成的。在这种情况下,如果被调用方是uu cdecl,则堆栈将损坏。

        6
  •  1
  •   Mark Elliot    15 年前
    1. 我不敢肯定,但如果运气好的话,你肯定不想利用这种行为。 如果它是特定于编译器的。

    2. 这不值得警告,因为演员表是明确的。通过强制转换,您将通知编译器您知道得更好。尤其是,你在 void* ,因此,您要说的是“获取这个指针表示的地址,并使其与另一个指针相同”——强制转换只是通知编译器,您确定目标地址上的内容实际上是相同的。尽管如此,我们知道这是错误的。

        7
  •  1
  •   David Gladfelter    15 年前

    我应该在某个时候刷新对C调用约定的二进制布局的记忆,但我确信这就是发生的事情:

    • 1:这不是纯粹的运气。C调用约定是定义良好的,堆栈上的额外数据不是调用站点的一个因素,尽管被调用方可能会覆盖它,因为被调用方不知道它。
    • 2:使用括号的“硬”强制转换告诉编译器你知道你在做什么。因为所有需要的数据都在一个编译单元中,所以编译器可以足够聪明地发现这显然是非法的,但是C的设计者并没有集中精力捕捉到可以验证的不正确的角大小写。简单地说,编译器相信你知道你在做什么(也许在许多C/C++程序员的情况下是不明智的!)
        8
  •  0
  •   t0mm13b    15 年前

    要回答您的问题:

    1. 纯粹的运气-您可以很容易地践踏堆栈并覆盖指向下一个执行代码的返回指针。由于您用3个参数指定了函数指针,并调用了函数指针,剩下的两个参数被“丢弃”,因此,行为是未定义的。想象一下,如果第二个或第三个参数包含一个二进制指令,并弹出调用过程堆栈…

    2. 当您使用 void * 指针和投射。在编译器看来,这是相当合法的代码,即使您已经明确指定了 -Wall 开关。 编译器假定您知道自己在做什么! 这就是秘密。

    希望这有帮助, 最好的问候, 汤姆。