我一直在尝试使用下面的源代码'动态调用函数'。在使用仅接受前两个参数的testing_function成功测试此代码之后,我在第三个中添加并在调用函数时决定“不提供参数”。我注意到,当我这样做时,第三个参数的值不是(必然)0,而是一个“随机”值,我不知道它的来源。
问题如下:
源代码的前言如下:
我正在使用Linux运行,使用GCC 4.6.3编译/调用链接器,并且在使用此代码时不会收到编译/链接警告/错误。此代码执行'完美'。我打电话给gcc如下:
gcc -x c -ansi -o (output file) (input file, .c suffix)
源代码如下:
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
/* Function for testing. */
int testing_function(char* something, char* somethingelse, int somethingadditional)
{
int alt_errno = errno;
if ((something != NULL)&&(somethingelse != NULL))
{
errno = 0;
if (fprintf(stdout, "testing_function(\"%s\", \"%s\", %d);\n", something, somethingelse, somethingadditional) <= 0)
{
if (errno != 0)
{
int alt_alt_errno = errno;
perror("fprintf(stdout, \"testing_function(\\\"%%s\\\", \\\"%%s\\\", %%d);\\n\", something, somethingelse, somethingadditional)");
errno = alt_errno;
return alt_alt_errno;
}
else
{
errno = ENOSYS;
perror("fprintf(stdout, \"testing_function(\\\"%%s\\\", \\\"%%s\\\", %%d);\\n\", something, somethingelse, somethingadditional)");
errno = alt_errno;
return ENOSYS;
}
}
else
{
errno = alt_errno;
return 0;
}
}
else
{
errno = ENOSYS;
perror("testing_function(char* something, char* somethingelse, int somethingadditional)");
errno = alt_errno;
return ENOSYS;
}
}
/* Main function. */
int main(int argc, char** argv)
{
int (*function)(char*, char*);
*(void**) (&function) = testing_function;
exit(function("Hello", "world!"));
}
答案 0 :(得分:7)
这些值来自哪里?
通常它们将是以前操作中的内存或寄存器垃圾。
另外,参数如何传递给函数?
这取决于平台ABI;通常在指定的一组寄存器中或在“堆栈指针”的固定偏移处。
不传递参数是不好的做法吗?
是。它触发“未定义的行为”;编译器有权在你执行该程序时崩溃你的程序,或者更糟。
可以准备添加函数的参数而无需使用函数重新编译代码吗? (示例:动态加载的库的函数获得了一个接受的参数,但是不会重新编译使用该函数的代码。)
没有。每当您更改属于库ABI的C函数的参数列表时,必须也更改其名称。 (你可以在源代码API中隐藏这些技巧,但它们都是改变函数名称的基本策略。)
在C ++中,当然更改的参数列表是一个新的重载,但这是由编译器为您更改名称而实现的。
答案 1 :(得分:2)
根据编译器使用的C ABI传递函数参数。这可能意味着它们在堆栈或寄存器中或两者的组合中传递。我相信32位英特尔系统通常会在堆栈中传递,而64位英特尔主要在寄存器中传递堆栈溢出。
未批处理参数的随机值来自哪里?它们来自应该保持该值的寄存器或堆栈位置。被调用的函数不知道参数没有被传递,所以它无论如何都会拉它。
如果所有参数都应该在堆栈上,这可能会导致错误的问题,因为该函数将拉出比存在更多的堆栈项。在最坏的情况下,它会消灭函数返回地址。
使用寄存器时,除了随机值之外没什么问题。
根据以上信息,您应该能够收集到它不受支持,您不应该这样做,一般情况下它不会起作用。
将工作的是可变参数列表。例如,printf
就可以了。 open()
POSIX函数也是如此。公开声明如下:
extern int open (__const char *__file, int __oflag, ...);
看到三点?那声明了一个变量参数列表。它可以包含0到任意数量的参数。可以使用特殊功能访问它们。了解期望有多少个参数的唯一方法是先前的参数之一。如果是open()
,则为oflag
值。对于printf()
格式字符串。
答案 2 :(得分:1)
调用参数太少的函数是非常危险的。在大多数ABI下,参数的堆栈槽不是调用保留的,这意味着编译器可以自由地为覆盖堆栈的这一部分的函数代码生成。如果调用者不知道被调用者期望的实际参数数量,因此没有为他们留下足够的空间,被调用者将很乐意破坏调用者的本地存储,甚至可能包括返回地址。
在一些带有pass-by-register的体系结构/ ABI上,这个不适用,直到你超过寄存器中传递的参数数量,但是在其他的pass-by-register系统(MIPS出现在脑海中),堆栈上的参数槽保留(并且被调用者可以自由地破坏它们),即使对于在寄存器中传递的参数也是如此。
简而言之,不要使用错误的数字或类型的参数调用函数。由于很好的理由,它是未定义的。
答案 3 :(得分:0)
在所有计算环境中,函数参数被收集并安排在某个地方的顺序存储器中 - 通常在CPU堆栈上,但对于某些体系结构,它可以是一系列CPU寄存器 - 或寄存器和存储器的组合。
只有少数CPU为被调用函数提供了一种机制来确定和验证传递给它的参数数量。 VAX CPU就是一个很好的例子。
大多数架构都依赖程序员做正确的事情:如果声明一个函数接受三个参数,那么无论在哪里调用该函数,最好有(至少)三个参数。如果没有,C标准表示你会得到“未定义的行为”。在您的具体情况下,无论最后写入应该放置第三个参数的位置,都是您得到的。对于x86上的gcc / Linux,它将是CPU堆栈内存。
答案 4 :(得分:0)
这些价值来源于哪些?
编译器在建立之前设置一个调用。输入函数时,它知道如何定位其参数以及存储其返回值的位置。具体来说,编译器有一个规范,允许它说“Ok。给定函数签名,我可以期望这个参数在这个寄存器中”或者如果参数在堆栈上传递“通过将当前堆栈位置偏移N个字节”。这基于体系结构的ABI(应用程序二进制接口)指定的调用约定。因此,参数可以存储在寄存器中和/或堆栈上,并且还保留返回值的位置。该函数还知道堆栈上的当前位置。
因此该函数只是从它期望它们存在的位置读取参数。通常,您未传递的参数是从寄存器或堆栈中读取的垃圾值,这些值在调用之前未写入。请注意,您的函数不仅可以读取这些值,还可以编写它们。
另外,如何将参数传递给函数?
编译器只是将它们写入ABI指定的寄存器或堆栈区域。
不传递参数是不好的做法吗?
是。例外情况是一个va列表(本身就是危险的野兽):int foo(int a, ...);
,其中函数使用诸如标记和格式说明符等机制来指定它的期望。
是否可以准备添加函数的参数而无需使用函数重新编译代码? (示例:动态加载的库的函数获得了一个接受的参数,但是不会重新编译使用该函数的代码。)
C函数可以动态定位并调用(对于C ++,btw会失败)。因此,通常最好考虑动态加载时冻结的已发布API的签名,或链接到与您的翻译可见的标题不同步的静态图像。
现在,你可以假装其中一些并让它工作,但这通常是一个坏主意,因为一个小的滑动,你可以引入未定义的行为。