在通过类型转换的函数指针调用此函数后,为什么该函数调用具有合理的行为?

Div*_*ano 35 c++ gcc function-pointers

我有以下代码。有一个函数需要两个int32。然后,我将指针指向它,并将其强制转换为需要三个int8的函数并进行调用。我预计会出现运行时错误,但程序运行正常。为什么这样可能?

main.cpp:

#include <iostream>

using namespace std;

void f(int32_t a, int32_t b) {
    cout << a << " " << b << endl;
}

int main() {
    cout << typeid(&f).name() << endl;
    auto g = reinterpret_cast<void(*)(int8_t, int8_t, int8_t)>(&f);
    cout << typeid(g).name() << endl;
    g(10, 20, 30);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

输出:

PFviiE
PFvaaaE
10 20
Run Code Online (Sandbox Code Playgroud)

如我所见,第一个函数的签名需要两个整数,第二个函数需要三个字符。Char小于int,我想知道为什么a和b仍然等于10和20。

tem*_*def 36

正如其他人指出的那样,这是未定义的行为,因此所有赌注都没有涉及原则上可能发生的情况。但是,假设您使用的是x86机器,那么对于为什么会看到这种情况,有一个合理的解释。

在x86上,g ++编译器并不总是通过将参数压入堆栈来传递参数。相反,它将前几个参数存储到寄存器中。如果我们反汇编该f函数,请注意,前几条指令将参数移出寄存器,并显式移入堆栈:

    push    rbp
    mov     rbp, rsp
    sub     rsp, 16
    mov     DWORD PTR [rbp-4], edi  # <--- Here
    mov     DWORD PTR [rbp-8], esi  # <--- Here
    # (many lines skipped)
Run Code Online (Sandbox Code Playgroud)

同样,请注意如何在中生成呼叫main。参数被放入这些寄存器中:

    mov     rax, QWORD PTR [rbp-8]
    mov     edx, 30      # <--- Here
    mov     esi, 20      # <--- Here
    mov     edi, 10      # <--- Here
    call    rax
Run Code Online (Sandbox Code Playgroud)

由于整个寄存器都用于保存参数,因此此处参数的大小无关紧要。

此外,由于这些参数是通过寄存器传递的,因此不必担心以错误的方式调整堆栈的大小。一些调用约定(cdecl)离开调用者进行清理,而另一些调用约定()stdcall要求被调用者进行清理。但是,这两者都不重要,因为堆栈没有被触及。

  • @Fureeish我经常发现,当您遇到未定义的行为时,有时仍然可以查看实际发生的情况。它通常会教给您很多有关编译器在后台执行的操作! (26认同)
  • @Fureeish泄漏抽象之类的法则,与讨论讨论编译器在做什么的地方一样好。 (3认同)
  • 看起来很多不必要的工作来解释UB的潜在结果。但是,由于第一个句子回答了问题,因此+1。 (2认同)

sel*_*bie 9

正如其他人指出的那样,这可能是不确定的行为,但是老派的C程序员知道这种事情是可行的。

另外,由于我能感觉到语言律师起草我要说的诉讼文件和法院请愿书的能力,因此我将使用undefined behavior discussion。这是通过undefined behavior同时敲打我的鞋子说三遍来实现的。这使得语言律师不见了,所以我可以解释为什么奇怪的事情在没有被起诉的情况下会发生。

回到我的答案:

我下面讨论的所有内容都是编译器特定的行为。我所有的模拟都是使用Visual Studio编译为32位x86代码的。我怀疑它在类似的32位体系结构上也可以与gcc和g ++一起工作。

这就是为什么您的代码恰好可以正常工作和一些警告的原因。

  1. 当函数调用参数推入堆栈时,它们以相反的顺序推入。当f正常调用时,编译器会生成代码到推b所述前参数压入堆栈a的参数。这有助于简化可变参数参数,例如printf。因此,当您的函数f访问a和时b,它只是访问堆栈顶部的参数。通过调用时g,有一个额外的参数被压入堆栈(30),但首先被压入。接下来是20,然后是栈顶的10。f只查看堆栈中的前两个参数。

  2. 至少在经典的ANSI C中,IIRC(字符和短裤)总是在被放入堆栈之前被提升为int类型。这就是为什么当您使用调用它时g,立即数10和20作为全尺寸int而不是8位int放置在堆栈中的原因。但是,当您重新定义f采用64位长而不是32位int时,程序的输出就会更改。

    void  f(int64_t a, int64_t b) {
        cout << a << " " << b << endl;
    }
Run Code Online (Sandbox Code Playgroud)

导致此结果由您的主设备输出(使用我的编译器)

85899345930 48435561672736798
Run Code Online (Sandbox Code Playgroud)

如果您转换为十六进制:

140000000a effaf00000001e
Run Code Online (Sandbox Code Playgroud)

1420并且0A10。我怀疑这1e是您30被推到栈顶的原因。因此,当通过调用参数时g,这些参数被压入堆栈,但以某种编译器特定的方式合并在一起。(再次出现未定义的行为,但是您可以看到参数已被推送)。

  1. 当您调用一个函数时,通常的行为是,从被调用函数返回时,调用代码将修复堆栈指针。同样,这是出于可变函数和与K&R C兼容的其他遗留原因。printf不知道您实际传递给它多少个参数,并且它依赖于调用方在返回时修复堆栈。因此,当您通过调用时g,编译器生成的代码会将3个整数压入堆栈,调用该函数,然后进行编码以弹出相同的值。此刻,您更改编译器选项以使被调用方清理堆栈(__stdcall在Visual Studio中为ala ):
    void  __stdcall f(int32_t a, int32_t b) {
        cout << a << " " << b << endl;
    }
Run Code Online (Sandbox Code Playgroud)

现在,您显然处于未定义的行为领域。通过调用g将三个int参数推入堆栈,但是编译器仅生成用于f在返回时从堆栈中弹出两个int参数的代码。返回时,堆栈指针已损坏。

  • 整个事情都是未定义的行为领域,通过与实际函数类型不兼容的函数指针来调用函数是UB (2认同)