为什么此函数通过类型函数指针调用后,呼叫明智地行为
Why does this function call behave sensibly after calling it through a typecasted function pointer?
我有以下代码。有一个函数需要两个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;
}
输出:
PFviiE
PFvaaaE
10 20
我可以看到第一个功能的签名需要两个int,第二个功能需要三个字符。char比int小,我想知道为什么A和B仍然等于10和20。
正如其他人指出的那样,这是未定义的行为,因此,所有原则上可能发生的事情都没有下注。但是,假设您在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)
同样,请注意main
中如何生成调用。这些论点放入这些寄存器中:
mov rax, QWORD PTR [rbp-8]
mov edx, 30 # <--- Here
mov esi, 20 # <--- Here
mov edi, 10 # <--- Here
call rax
由于整个寄存器都用于保存参数,因此参数的大小在这里无关。
此外,由于这些论点是通过寄存器通过的,因此不担心以不正确的方式调整堆栈的大小。一些呼叫约定(cdecl
(使呼叫者进行清理,而另一些人(stdcall
(要求Callee进行清理。但是,这里都不重要,因为堆栈没有触摸。
正如其他人指出的那样,它可能是不确定的行为,但是老式的C程序员知道这种类型的工作。
另外,因为我可以感觉到语言律师起草他们的诉讼文件和法院请愿书,所以我要施放undefined behavior discussion
的咒语。这是通过说undefined behavior
的三遍,同时敲打我的鞋子。这使语言律师消失了,所以我可以解释为什么奇怪的事情只是在不被起诉的情况下工作。
回到我的答案:
我在下面讨论的所有内容都是编译器特定的行为。我的所有模拟都是在Visual Studio中编译为32位X86代码。我怀疑它在类似的32位体系结构上与GCC和G 的工作。
这就是为什么您的代码恰好工作和一些警告的原因。
-
当功能调用参数被推到堆栈上时,它们会以相反的顺序推动。当正常调用
f
时,编译器会在a
参数之前生成代码将b
参数推向堆栈。这有助于促进诸如printf之类的变异参数函数。因此,当您的功能f
访问a
和b
时,它只是访问堆栈顶部的参数。当通过g
调用时,堆栈有一个额外的论点(30(,但首先被推了。接下来将20个推开,其次是堆栈顶部的10。f
仅查看堆栈上的前两个参数。 -
iirc,至少在经典的ANSI C,Chars和Shorts中,在被放置在堆栈上之前,请始终晋升为INT。这就是为什么当您使用
g
调用它时,将文字10和20作为全尺寸的INT而不是8位INT放置在堆栈上。但是,当您重新定义f
进行64位的朗(而不是32位INT(时,程序的输出会更改。
void f(int64_t a, int64_t b) {
cout << a << " " << b << endl;
}
导致您的主(与我的编译器(
获得输出85899345930 48435561672736798
如果您转换为十六进制:
140000000a effaf00000001e
14
是20
,0A
是10
。我怀疑1e
是您的30
被推到堆栈上。因此,当通过g
调用时,这些论点被推到了堆栈,但以某种编译器的方式被弹出。(未定义的行为,但是您可以看到参数被推(。
- 当您调用函数时,通常的行为是调用代码将在从称为函数返回时修复堆栈指针。同样,这是为了variadic函数和其他遗留原因与k&amp; r C.
printf
不知道您实际传递了多少参数,并且它依赖呼叫者在返回时修复堆栈。因此,当您通过g
调用时,编译器会生成代码将3个整数推向堆栈,调用功能,然后代码将相同的值弹出。当时,您将编译器选项更改为清理堆栈(Visual Studio上的ALA__stdcall
(:
void __stdcall f(int32_t a, int32_t b) {
cout << a << " " << b << endl;
}
现在,您显然处于不确定的行为领域。通过g
调用将三个INT参数推入堆栈上,但是编译器仅生成f
的代码,以在返回时将两个INT参数从堆栈中弹出。返回后,堆栈指针会损坏。
正如其他指出的那样,这完全是未定义的行为,您得到的将取决于编译器。它仅在您有特定的呼叫约定时才起作用,不使用堆栈而是寄存器传递参数。
我用Godbolt查看了产生的组件,您可以在此处全部检查
相关功能调用在这里:
mov edi, 10
mov esi, 20
mov edx, 30
call f(int, int) #clang totally knows you're calling f by the way
它不会在堆栈上推动参数,而只是将它们放入寄存器中。最有趣的是,mov
指令不仅不会更改寄存器的下部8位,但是所有这些都不是32位移动。这也意味着无论以前的登记册中的内容如何,当您像F一样阅读32位时,您将始终获得正确的价值。
如果您想知道为什么32位移动,事实证明,在几乎每种情况下,在X86或AMD64体系结构上,编译器将始终使用32位字面移动或64位字面移动(仅当值时太大了32位(。移动8位值不会使寄存器的上部(8-31(归零,并且如果该值最终被提升,则可能会产生问题。使用32位文字指令要比首先要归零的额外说明更简单。
您必须记住的一件事是,它确实在尝试调用f
,就好像它具有8位参数一样,因此,如果您放置了一个大价值,它将截断字面意思。例如,1000
将成为-24
,因为1000
的较低位是E8
,使用签名整数时,即-24
。您还将获得警告
<source>:13:7: warning: implicit conversion from 'int' to 'signed char' changes value from 1000 to -24 [-Wconstant-conversion]
第一个C编译器以及在C标准发布之前的大多数编译器,将通过以左右订单推动参数,使用平台的"使用"来处理函数调用"调用subroutine"指令调用该功能,然后在返回功能后,弹出所有参数。函数将按顺序分配给其参数的地址刚刚从刚从"呼叫"指令推动的任何信息开始。
。即使在诸如经典的Macintosh之类的平台上,弹出参数的责任通常都在于称为函数(并且在不推动正确数量的参数通常会损坏堆栈的情况下(,C编译器通常使用表现出的呼叫约定像第一个C编译器。打电话时需要使用" pascal"预选赛,或者用其他语言编写的代码(例如pascal(来调用的函数。
在标准之前存在的大多数语言实现中,可以写一个函数:
int foo(x,y) int x,y
{
printf("Heyn");
if (x)
{ y+=x; printf("y=%dn", y); }
}
并将其称为例如foo(0)
或foo(0,0)
,前者稍快。试图称其为例如foo(1);
可能会损坏堆栈,但是如果该功能从未使用过对象y
,则无需通过它。但是方便地扩展语言。
- 无匹配函数呼叫getline()
- 通过呼叫constexpr函数来定义静态constexpr成员
- 来自QvariantList的std ::函数的通用呼叫
- 将整数(文字)与函数相关联,让呼叫者查询拖鞋的数量
- 错误:呼叫构造器的匹配函数无匹配功能
- 当在函数调用中递增值时,程序正常工作,但是如果我们在单独的行中增加值而不是呼叫函数,则会出现错误.为什么
- 在呼叫运算符函数const中调用运算符时错误
- 在函数呼叫时,请从异质初始化列表中构建元组
- 使用makeword函数创建错误e0109-表观呼叫的括号前表达式必须具有(指针到 - )函数类型
- C 函数呼叫没有足够的模板参数
- 为什么此函数通过类型函数指针调用后,呼叫明智地行为
- 如何根据呼叫线以不同的方式求解函数
- C 线程 - 无匹配函数供呼叫
- 无匹配函数呼叫
- C 使用lambda进行隐式构造函数呼叫期望函数指针
- 无匹配函数呼叫
- Android本机C 函数呼叫导致应用程序崩溃
- 无匹配函数呼叫(类,C )
- C 错误:在自定义类的构造函数呼叫期间使用已删除的函数
- 有多少操作数可以超载函数呼叫操作员采用