【问题标题】:Calling a function pointer whose assigned function has less arguments then the pointer type调用函数指针,其分配的函数的参数少于指针类型
【发布时间】:2015-07-17 15:38:30
【问题描述】:

考虑以下代码:

#include <iostream>

typedef int (*test_func_t) (int, int, int);

int print_integer (int a)
{
    std::cout << "num: " << a << "\n";
    return a;
}

int main (int argc, char * argv[])
{
    test_func_t func = (test_func_t) &print_integer;
    std::cout << "calling with 3 parameters func(5,7,9)\n";
    func(5,7,9);
    return 0;
}

如您所见,类型 (test_func_t) 定义为具有 3 个 int 参数的函数。函数指针(func)被分配一个指向“print_integer”的指针,它只接收1个参数,然后用3个参数(5、7、9)调用函数指针。

此代码有效并产生“num: 5”输出。

gdb disas 输出(Intel 语法)

disas main
...
   0x080486cb <+9>:     mov    DWORD PTR [esp+0x1c],0x804867d
...
   0x080486e0 <+37>:    mov    DWORD PTR [esp+0x8],0x9
   0x080486e8 <+45>:    mov    DWORD PTR [esp+0x4],0x7
   0x080486f0 <+53>:    mov    DWORD PTR [esp],0x5
   0x080486f7 <+60>:    mov    eax,DWORD PTR [esp+0x1c]
   0x080486fb <+64>:    call   eax

disas print_integer
   ...
   0x08048683 <+6>:     mov    DWORD PTR [esp+0x4],0x8048830
   0x0804868b <+14>:    mov    DWORD PTR [esp],0x8049ae0
   0x08048692 <+21>:    call   0x8048530 <std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)@plt>
   0x08048697 <+26>:    mov    edx,DWORD PTR [ebp+0x8]
   0x0804869a <+29>:    mov    DWORD PTR [esp+0x4],edx
   0x0804869e <+33>:    mov    DWORD PTR [esp],eax
   0x080486a1 <+36>:    call   0x80484d0 <std::ostream::operator<<(int)@plt>

如您所见,其余参数([ebp+0x12] 和 [ebp+0x16])根本没有使用。

我的问题:

  1. 这似乎适用于具有 __cdecl 调用约定的 Linux x86。在其他架构和调用约定上也安全吗?
  2. 是否有任何 C/C++ 标准允许/定义从需要较少参数的函数分配函数指针的结果?

这样的使用示例:node.js 的NODE_MODULE 注册了一个函数,其type 有3 个参数[exports, module, priv]。它是called with those 3,但正式示例显示了使用12 参数注册函数。

【问题讨论】:

  • No 和 No. 这是未定义的行为。而且哪里都不安全。
  • 它没有明确定义,一般来说,这是一个坏主意,也不安全。尽管如此,它曾经是相当普遍的做法,并且在它工作的调用约定下,它工作得很好。
  • @EugeneSh。 “不安全”——我完全接受。有没有一个例子导致这种类型的调用“破坏”事物? (除了明显错误的使用和滥用)
  • @The-Q 这样的例子可能是gcc 的下一个版本。 当前不一定是不安全的。
  • 我确实在 node.js 中看到了这种用法(如原始问题中所述),我无意使用它来实现任何东西。在了解它可能不安全之后,我想知道目前是否可以找到可能破坏某些东西的实际案例。谢谢@EugeneSh。 !

标签: c++ c function-pointers calling-convention


【解决方案1】:

引用 C++11 标准expr.reinterpret.cast 6

函数指针可以显式转换为不同类型的函数指针。调用的效果 通过指向与定义中使用的类型不同的函数类型(8.3.5)的指针的函数 函数的未定义

所以,我会说它一般来说是安全的。这是未定义的行为。也就是说,我不知道 C++ 的其他实现在这种情况下的表现如何。

【讨论】:

    【解决方案2】:

    因此适用于所有使用 __cdecl 调用约定的平台。此调用约定将参数从右到左推入堆栈,然后调用该函数。因此,需要的参数少于推送的参数将根本无法访问更多向右的参数,这些参数被较早地推送并且在堆栈上更高。

    // func(5,7,9);
    push 9
    push 7
    push 5
    call func
    ---> func:
         push bp
         mov bp, sp
         mov ax, [bp+8]  ; get 5
    
    // stack:
    9
    7
    5                     [bp+8]
    <return address>      [bp+4]
    <value of old bp>        ^
        new bp: -------------+
    

    【讨论】:

    • 可能适用于 __cdecl 调用......实现可以添加运行时检测以检查此类未定义的行为并在运行时中止,或者优化器可以决定什么您尝试做的是未定义并完全删除呼叫。这就是未定义行为的乐趣。
    • @Jonathan-wakely,不允许任何 C 实现这样做。 C 语言允许它,它是程序员的工具。还需要向后兼容不使用原型的 C 版本。 Paolo 的引述进一步说 effect 是未定义的,这意味着标准没有规定任何行为并将其留给程序员,这是正确的。标准的实现,也不是聪明的实现应该与我的编程结合在一起,尤其是当我非常清楚自己在做什么的时候。
    • 我很确定带有#include &lt;iostream&gt; 的程序不是C。也许通过说 C 和 C++ 的规则不同来限定你的答案。
    • 未定义的行为绝对不意味着它由程序员决定。这表明当你打破语言规则时,你可以决定新规则,显然这不是真的。如果您取消引用未初始化的指针,您将无法决定会发生什么!
    猜你喜欢
    • 1970-01-01
    • 2011-04-20
    • 1970-01-01
    • 1970-01-01
    • 2020-04-12
    • 1970-01-01
    • 2019-06-12
    • 2015-01-05
    • 2014-01-17
    相关资源
    最近更新 更多