【问题标题】:Is it safe to pass "too many" arguments to a external function?将“太多”参数传递给外部函数是否安全?
【发布时间】:2010-09-09 14:29:39
【问题描述】:

这种情况只有在没有名字修饰的情况下才会发生(我相信),所以下面的代码是 C。 假设在 A.c 中有一个函数 A 定义为

void A(int x, int y){
    //Do stuff
}

现在还有一个单独的文件 B.c:

extern "C"{
    void A(int x, int y, int z);
}

void B(){
    A(1, 2, 3);
}

A 最初被声明为只有 2 个参数,但是当在 B.c 中声明时它有一个额外的参数,并且在 B() 中使用第三个参数调用它。 我知道有可能发生这种情况,例如在与 fortran 子例程链接时,或者在动态链接时。

我想将额外的参数传递给函数是不安全的,谁能解释当调用函数并将参数传递给它时内存中发生了什么?因此,传递这个既不使用也不想要的“额外”参数是多么安全。

额外的参数是否有可能覆盖函数中使用的内存空间?还是对 A 的函数调用为参数分配内存空间,然后告诉 A 参数内存块的开头在哪里,A 读取前两个参数并忽略最后一个参数,使其完全安全?

任何有关该功能的信息都会非常有启发性,谢谢。

【问题讨论】:

  • 请注意,这里的几乎每个答案都假设 x86 ——在几个平台上,这永远不会有效。它完全取决于平台和调用约定。

标签: c linker fortran dynamic-linking


【解决方案1】:

联动是实现定义的,所以没有办法肯定。

也就是说,C 的其他特性(尤其是 vardic 参数)强制执行通常允许它的实现。

例如,我不知道如果您编写任何实现会失败:

 printf("%d", 1, 2);

然而,它只会打印“1”。

这里的许多人提出了cdeclpascal__stdcall 调用约定。但是,这些都不是标准的一部分,并且是某些实现的所有功能。这让我们回到我的第一句话。

【讨论】:

  • 可变参数函数的存在实际上并不强制编译器对非可变参数函数使用特定的调用约定。事实上,这正是为什么必须在范围内使用正确的原型调用可变参数函数 - 以允许编译器使用特殊的调用约定。
【解决方案2】:

这取决于使用的调用约定。使用cdecl,调用者以从右到左的顺序将参数压入堆栈,然后被调用者通过偏移堆栈指针来访问它们。在这种情况下,调用太多参数不会破坏任何东西。

但是,如果您有一个从左到右的调用约定,那么事情就会中断。

【讨论】:

  • 其实这里的关键不只是cdecl从左到右推送,还有cdecl意味着负责从堆栈中清除参数的函数与推送它们的函数相同。另一方面,stdcall 以相同的顺序推送参数,但依赖于调用的函数来清理堆栈,这会破坏堆栈。
  • @torak:明白。但是如果有一个从左到右的调用约定(不过我想不出一个例子),被调用者会看到无意义的参数。所以事情会以不同的方式破裂。
  • 有关 x86 调用约定的列表,请参阅 en.wikipedia.org/wiki/X86_calling_conventionspascal 约定是从左向右推动的约定。
【解决方案3】:

使用cdeclcalling convention,调用者负责清理堆栈,所以这是安全的。相比之下,pascal 调用约定使被调用者负责清理,因此这很危险。

【讨论】:

  • +1 -- 请注意__stdcall 还需要被调用者清理堆栈。另请注意,调用约定完全是特定于平台的,标准对它们完全没有说明。
  • -1 根据实现细节给出答案,忽略语言标准所说的内容。
【解决方案4】:

在 C 中,这是违反约束的,因此会导致未定义的行为。

“如果表示被调用函数的表达式具有包含原型的类型,则参数的数量应与参数的数量一致。” (C99,§6.5.2.2)

也就是说,实际上它主要取决于底层调用约定。

【讨论】:

    【解决方案5】:

    至少在 C 和 C++ 中它不会造成任何伤害。参数从右向左推送,被调用者负责堆栈清理。

    但是,除非您使用可变参数或强制转换函数类型,否则编译器不会让您这样做。例如:

    #include <stdio.h>
    
    static void foo (int a, int b, int c, int d, int e, int f, int g)
    {
        printf ("A:%d B:%d C:%d D:%d E:%d F:%d G:%d \n",
                a, b, c, d, e, f, g);
    }
    
    int main ()
    {
        typedef void (*bad_foo) (int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int);
        foo (1, 2, 3, 4, 5, 6, 7);
        bad_foo f = (bad_foo) (&foo);
        f (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17);
    }
    

    如果您查看汇编代码,所有参数都被推入寄存器,但额外的一次被忽略。

    【讨论】:

      【解决方案6】:

      这样的代码违反了One Definition Rule(好吧,无论如何,C 等价于它......)它是否工作完全是特定于平台的。

      特别是在 x86 上,如果函数声明为 __cdecl,那么它将起作用,因为调用者清理堆栈,但如果它是 __stdcall(大多数 Win32 函数都是)​​,则被调用者清理堆栈,并且在那种情况下会错误地清理它(因为它有太多参数)。因此,这将取决于所使用的外部函数的调用约定。

      我不明白你为什么要这样做。

      【讨论】:

      • 我想你的意思是“如果它是__stdcall(就像大多数win32函数一样),被调用者会清理堆栈”。
      • __stdcall 是指“被调用者”还是“被调用函数”?它现在又说“来电者”了。
      【解决方案7】:

      如果我猜对了,这可能会导致您的程序从内存中执行随机代码。当一个函数被调用时,一些值,包括返回地址(函数完成后程序将跳转回的地址)被压入堆栈。之后,函数参数 (x, y, z) 被压入堆栈,程序跳转到函数的入口点。然后该函数将从堆栈中弹出参数(x,y),做一些事情,然后从堆栈中弹出返回地址(在本例中为z,这是错误的)并跳回它。

      这里是堆栈详细信息的一个很好的描述:http://www.tenouk.com/Bufferoverflowc/Bufferoverflow2a.html

      【讨论】:

      • 默认 (__cdecl) 调用约定不是这种情况。
      猜你喜欢
      • 2017-05-19
      • 1970-01-01
      • 2021-09-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-01-28
      • 2021-09-14
      • 1970-01-01
      相关资源
      最近更新 更多