6

我一直在使用下面的源代码尝试“动态调用函数”。在使用仅接受前两个参数的 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!"));
}
4

5 回答 5

7

这些价值观从何而来?

通常,它们将是来自先前操作的内存或寄存器垃圾。

另外,如何将参数传递给函数?

它取决于平台 ABI;通常要么在一组指定的寄存器中,要么在“堆栈指针”的固定偏移处。

不传递论点是不好的做法吗?

是的。它触发“未定义的行为”;编译器有权在您执行此操作的那一刻或更糟的情况下使您的程序崩溃。

是否可以在不使用函数重新编译代码的情况下准备添加函数的参数?(例如:动态加载的库的函数获得一个可接受的参数,但使用该函数的代码不会被重新编译)。

不可以。每当您更改作为库 ABI 一部分的 C 函数的参数列表时,您也必须更改其名称。(您可以使用一些技巧将其隐藏在源代码级 API 中,但它们都是对更改函数名称的基本策略的伪装。)

在 C++ 中,更改的参数列表当然是一个新的重载,但这是由编译器为您更改名称来实现的。

于 2013-05-14T21:30:07.090 回答
2

函数参数的传递取决于编译器使用的 C ABI。这可能意味着它们在堆栈或寄存器中或两者的组合中传递。我相信 32 位 Intel 系统通常在堆栈中传递,而 64 位 Intel 系统主要在寄存器中传递,溢出在堆栈中进行。

未传递参数的随机值从何而来?它们来自应该保存该值的寄存器或堆栈位置。被调用的函数不知道参数没有被传递,所以无论如何它都会拉取它。

如果所有参数都应该在堆栈上,这可能会导致严重的问题,因为该函数将拉出比现有更多的堆栈项。在最坏的情况下,它将清除函数返回地址。

使用寄存器时,除了随机值之外,这不是什么大问题。

从上述信息中,您应该能够收集到它不受支持,您不应该这样做,而且通常它不会工作。

可行的是可变参数列表。例如,printf做。open()POSIX 函数也是如此。开放声明如下所示:

extern int open (__const char *__file, int __oflag, ...);

看到三点了吗?这声明了一个变量参数列表。它可以包含 0 到任意数量的参数。使用特殊功能访问它们。知道预期有多少参数的唯一方法是前面的参数之一。在 的情况下open()oflag值。对于printf()格式字符串。

于 2013-05-14T21:42:36.190 回答
1

调用参数太少的函数是非常危险的。在大多数 ABI 中,参数的堆栈槽不是调用保留的,这意味着编译器可以为覆盖堆栈的这一部分的函数代码自由生成。如果调用者不知道被调用者期望的参数的实际数量,因此没有为它们留下足够的空间,则被调用者将愉快地破坏调用者的本地存储,甚至可能包括返回地址。

在某些具有按寄存器传递的架构/ABI 上,这在超过寄存器中传递的参数数量之前不适用,但在其他按寄存器传递系统(想到 MIPS)上,堆栈上的参数槽是保留的(并且被调用者可以自由地破坏它们)即使对于在寄存器中传递的参数也是如此。

简而言之,不要使用错误数量或类型的参数调用函数。出于很好的原因,它是未定义的。

于 2013-05-15T02:50:53.763 回答
0

在所有计算环境中,函数参数都被收集并排列在某个位置的顺序内存中——通常在 CPU 堆栈上,但对于某些架构,它可能位于一系列 CPU 寄存器中——或寄存器和内存的组合。

只有少数 CPU 为被调用函数提供了一种机制来确定和验证传递给它的参数数量。VAX CPU 就是一个主要的例子。

大多数架构依赖于程序员做正确的事情:如果一个函数被声明为接受三个参数,那么无论在哪里调用该函数,最好有(至少)三个参数。如果没有,C 标准说你会得到“未定义的行为”。在您的特定情况下,最后一次写入第三个参数应该放置的位置就是您得到的。对于 x86 上的 gcc/Linux,它将是 CPU 堆栈内存。

于 2013-05-14T21:38:48.993 回答
0

这些价值观从何而来?

编译器会在调用之前设置它。当一个函数被输入时,它知道如何定位它的参数以及在哪里存储它的返回值。具体来说,编译器有一个规范,允许它说“好的。给定函数签名,我可以在这个寄存器中期待这个参数”,或者如果参数在堆栈上传递“通过将当前堆栈位置偏移 N 个字节”。这是基于架构的 ABI(应用程序二进制接口)指定的调用约定。因此参数可以存储在寄存器和/或堆栈中,并且还保留返回值的位置。该函数还知道堆栈上的当前位置。

所以该函数只是从它期望它们存在的地方读取参数。通常,您未传递的参数是从寄存器或堆栈中读取的垃圾值,这些值在调用之前未写入。请注意,您的函数不仅可以读取这些值,还可以写入它们。

另外,如何将参数传递给函数?

编译器只是将它们写入 ABI 指定的寄存器或堆栈区域。

不传递论点是不好的做法吗?

是的。一个例外是 va 列表(本身就是危险的野兽):,int foo(int a, ...);其中函数使用诸如哨兵和格式说明符之类的机制指定其期望。

是否可以在不使用函数重新编译代码的情况下准备添加函数的参数?(例如:动态加载的库的函数获得一个可接受的参数,但使用该函数的代码不会被重新编译)。

C 函数可以动态定位和调用(对于 C++,它会失败,顺便说一句)。因此,通常最好考虑在动态加载或链接到与您的翻译可见的标题不同步的静态图像时冻结已交付 API 的签名。

现在,您可以伪造其中的一些并使其工作,但这通常是一个坏主意,因为一个小失误,您可能会引入未定义的行为。

于 2013-05-14T21:40:12.920 回答