我才刚刚开始将我的头缠绕在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. 关于合法性:如果我理解答案 将功能指针铸造为另一种类型 正确地,这只是不确定的行为。因此,运行并产生合理的结果的事实只是纯粹的运气,对吗? (或编译器作家的不错)
  2. 为什么GCC也不会警告我,即使有墙?这是编译器无法检测到的东西吗?为什么?

我来自Java,Typechecking更加严格,所以这种行为使我有些困惑。也许我正在经历文化冲击:-)。

有帮助吗?

解决方案

额外的参数未丢弃。它们被正确放置在堆栈上,就好像调用是对一个期望三个参数的函数进行的。但是,由于您的功能仅关心一个参数,因此仅查看堆栈顶部,并且不会触摸其他参数。

基于这两个事实,这个呼叫起作用的事实是纯粹的运气:

  • 第一个参数的类型对于函数和铸件指针是相同的。如果将函数更改为将指针用于字符串并尝试打印该字符串,则您将获得一个不错的崩溃,因为该代码将尝试取消指针以解决内存2。
  • 默认情况下使用的调用约定是为了清理堆栈。如果您更改调用约定,以便Callee清理堆栈,最终将呼叫者在堆栈上推出三个参数,然后将Callee清理(或更确切地说是尝试)一个参数。这可能会导致堆叠腐败。

由于一个简单的原因,编译器无法警告您这样的潜在问题 - 在一般情况下,它不知道编译时指针的价值,因此它无法评估它的指向。想象一下,功能指针指向在运行时创建的类虚拟表中的方法?因此,它告诉编译器,它是指向具有三个参数的函数的指针,编译器会相信您。

其他提示

如果您乘坐汽车并将其施放为锤子,则编译器将带您说汽车是锤子,但这不会将汽车变成锤子。编译器可能会成功使用汽车驾驶指甲,但这是实施依赖的好运。这仍然是不明智的。

  1. 是的,这是不确定的行为 - 任何事情都可能发生,包括“工作”。

  2. 演员们阻止编译器发出警告。同样,编译器无需诊断可能的原因不确定行为。这样做的原因是,要么不可能这样做,要么这样做会太困难和/或导致太多的开销。

演员阵容的最糟糕的进攻是将数据指针投入到功能指针。它比签名更改还要糟糕,因为不能保证功能指针和数据指针的大小相等。与很多 理论 未定义的行为,即使在高级机器(不仅在嵌入式系统上)也可以在野外遇到这种行为。

您可能会在嵌入式平台上轻松遇到不同尺寸的指针。甚至在处理器中,数据指针和功能指针确实解决了不同的问题(一个RAM,一个ROM,另一个是ROM),即所谓的哈佛架构。在X86上,在实际模式下,您可以混合16位和32位。 WATCOM-C具有DOS扩展器的特殊模式,其中数据指针宽48位。尤其是在C中,一个人应该知道并非所有内容都是Posix,因为C可能是外来硬件上唯一可用的语言。

一些编译器允许混合存储器模型,其中保证代码在32位尺寸之内,并且数据可通过64位指针或相反。

编辑: 结论,切勿将数据指针投入到功能指针上。

该行为由呼叫惯例定义。如果您使用呼叫者按下并弹出堆栈的呼叫约定,则在这种情况下它可以正常工作,因为这只是在通话过程中堆栈上有几个字节。目前,我没有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)。之后,堆栈很好(在这种情况下,假设Callee为__cdecl,因此也不会尝试清理堆栈)。但是使用以下代码:

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

add esp,0Ch 未在组件中生成。如果Callee在这种情况下为__cdecl,则堆栈将被损坏。

  1. 公认我不确定,但是如果幸运的话,您绝对不想利用这种行为 或者 如果是针对编译器的。

  2. 它不值得警告,因为演员是明确的。通过铸造,您将通知编译器您了解得更好。特别是,您正在铸造 void*, ,因此,您是说“取下该指针代表的地址,并将其与其他指针相同” - 演员们简单地告知编译器,您可以确定目标地址实际上是什么相同。尽管在这里,我们知道这是不正确的。

我应该在某个时候刷新C呼叫约定的二进制布局,但我很确定这就是发生的事情:

  • 1:这不是纯粹的运气。 C调用约定的定义明确,堆栈上的额外数据并不是呼叫站点的一个因素,尽管Callee可能会被Callee覆盖,因为Callee不知道。
  • 2:使用括号的“硬”演员告诉编译器您知道自己在做什么。由于所有所需的数据都在一个编译单元中,因此编译器可能足够聪明,以确定这显然是非法的,但是C的设计师并没有专注于捕获Corner Case可验证的不正确性。简而言之,编译器相信您知道自己在做什么(也许在许多C/C ++程序员的情况下都不愿意!)

回答您的问题:

  1. 纯净的运气 - 您可以轻松践踏堆栈并覆盖返回指针到下一个执行代码。由于您用3个参数指定了函数指针,并调用了函数指针,因此其余两个参数被“丢弃”,因此,行为不确定。想象一下,如果该第二个或第3个参数包含二进制指令,并弹出了呼叫过程堆栈。

  2. 当您使用的时候没有警告 void * 指针和铸造。在编译器眼中,这是一个合法的代码,即使您已经明确指定了 -Wall 转变。 编译器假设您知道自己在做什么! 那是秘密。

希望这对汤姆有帮助,汤姆。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top