【问题标题】:Direct access to the function stack直接访问函数栈
【发布时间】:2014-04-03 07:38:46
【问题描述】:

我之前曾向a question 询问过带有未指定数量参数的 C 函数,例如void foo() { /* code here */ } 并且可以使用 未指定数量的未指定类型的参数调用

当我询问像 void foo() { /* code here */ } 这样的函数是否有可能获取调用它的参数时,例如foo(42, "random")有人说:

您唯一能做的就是使用调用约定和所运行架构的知识,并直接从堆栈中获取参数。 source

我的问题是:

如果我有这个功能

void foo()
{
    // get the parameters here
};

我称之为:foo("dummy1", "dummy2") 是否可以直接从堆栈中获取foo 函数中的 2 个参数?

如果是,如何?是否可以访问完整堆栈?例如,如果我递归调用一个函数,是否有可能以某种方式访问​​每个函数状态?

如果不是,那么带有未指定数量参数的函数有什么意义?这是 C 编程语言中的错误吗?在哪种情况下,任何人都希望 foo("dummy1", "dummy2") 为标头为 void foo() 的函数编译和运行良好?

【问题讨论】:

  • 如果你知道它是如何被调用的(参数的类型是什么),你可以以与机器相关的方式来做到这一点(但是为什么要有这样的功能呢?)如果你不知道不知道,你不知道。
  • 还要注意(无论如何在现代 C 中)void foo() {} 不接受任何参数。另一方面,void foo();(仅声明,未与定义结合)并没有说明foo 需要什么。因此,在您的两个示例中,您都不能将任何内容传递给 foo。

标签: c function callstack


【解决方案1】:

很多'如果':

  1. 您只使用一个版本的编译器。
  2. 一组编译器选项。
  3. 设法说服您的编译器永远不要在寄存器中传递参数。
  4. 说服您的编译器不要将两个调用 f(5, "foo") 和 f(&i, 3.14) 对同一函数的不同参数视为错误。 (这曾经是早期 DeSmet C 编译器的一个特性)。

然后函数的激活记录是可预测的(即您查看生成的程序集并假设它始终相同):返回地址将在某处和保存的 bp(基指针,如果您的架构有一个),并且参数的顺序将是相同的。那么你怎么知道传递了哪些实际参数呢?您必须对它们进行编码(它们的大小、偏移量),大概在第一个参数中,就像 printf 所做的那样。

递归(即在递归调用中没有区别)每个实例都有其激活记录(我是否说过你必须说服你的编译器从不优化尾调用?),但在 C 中,与 Pascal 不同,你不需要由于没有嵌套函数声明,因此具有指向调用者激活记录(即局部变量)的反向链接。访问完整堆栈,即当前实例之前的所有激活记录非常乏味,容易出错,并且对想要操纵返回地址的恶意代码编写者很感兴趣。

因此,基本上没有任何问题和假设。

【讨论】:

    【解决方案2】:

    是的,您可以直接通过堆栈访问传递的参数。但是不,您不能使用旧式函数定义来创建具有可变数量和参数类型的函数。以下代码显示了如何通过堆栈指针访问参数。它完全依赖于平台,所以我不知道它是否可以在你的机器上工作,但你可以理解

    long foo();
    
    int main(void)
    {
        printf( "%lu",foo(7));
    }
    
    long foo(x)
     long x;
    {
        register void* sp asm("rsp");
        printf("rsp = %p rsp_ value = %lx\n",sp+8, *((long*)(sp + 8)));
        return *((long*)(sp + 8)) + 12;
    }
    
    1. 获取堆栈头指针(我机器上的 rsp 寄存器)
    2. 将传递参数的偏移量添加到 rsp => 你会得到指向栈中 long x 的指针
    3. 取消引用指针,加 12(做任何你需要的事情)并返回值。

    偏移量是个问题,因为它取决于编译器、操作系统以及谁知道还有什么。 对于这个例子,我在调试器中简单地检查了它,但如果它对你真的很重要,我认为你可以为你的机器解决方案提供一些“通用”。

    【讨论】:

    • @Jason Swartz:这是原始答案,我使用了不同的选项,如果您没有在函数定义中定义参数,则不会传递给函数。当您尝试传递更多参数时,函数定义中会出现冗余参数省略。
    【解决方案3】:

    如果您声明void foo(),那么您将收到foo("dummy1", "dummy2") 的编译错误。

    您可以声明一个接受未指定数量参数的函数,如下所示(例如):

    int func(char x,...);
    

    如您所见,至少必须指定一个参数。这样一来,在函数内部,您将能够访问 last 指定参数之后的所有参数。

    假设你有以下调用:

    short y = 1000;
    int sum = func(1,y,5000,"abc");
    

    以下是实现func 并访问每个未指定参数的方法:

    int func(char x,...)
    {
        short y = (short)((int*)&x+1)[0]; // y = 1000
        int   z = (int  )((int*)&x+2)[0]; // z = 5000
        char* s = (char*)((int*)&x+3)[0]; // s[0...2] = "abc"
        return x+y+z+s[0];                // 1+1000+5000+'a' = 6098
    }
    

    如您所见,这里的问题是每个参数的类型和参数的总数是未知的。因此,任何带有“不适当”参数列表的 func 调用都可能(并且可能会)导致运行时异常。

    因此,通常,第一个参数是一个字符串 (const char*),它指示以下每个参数的类型,以及参数的总数。此外,还有用于提取未指定参数的标准宏 - va_startva_end

    例如,您可以通过以下方式实现与printf 行为类似的函数:

    void log_printf(const char* data,...)
    {
        static char str[256] = {0};
        va_list args;
        va_start(args,data);
        vsnprintf(str,sizeof(str),data,args);
        va_end(args);
        fprintf(global_fp,str);
        printf(str);
    }
    

    P.S.:上面的例子不是线程安全的,这里只是作为例子...

    【讨论】:

    • 您的示例给出了原型,OP 要求给出函数声明的情况。阅读他链接的问题和答案stackoverflow.com/a/22074190/2549281
    • 使用gcc 4.8.1 调用foo("dummy1", "dummy2") 时不会出错。我确实知道可变参数函数,但这不是我的问题。但是,我不知道您用于int func(char x,...) 的方法,我只知道这个示例:gnu.org/software/libc/manual/html_node/…
    • @Dabo 你为什么删除你的答案?它正在工作,我现在正在更仔细地阅读它?...
    • @Jason Swartz:不确定您的评论是否有帮助。如果没有,请告诉我,我会删除它。
    • @barakmanos 我希望你保留它。正如 Dabo 所说,现代 C 不允许此代码,但某些编译器确实可以成功编译它,例如GCC.
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-22
    • 2012-10-08
    • 2015-09-20
    • 2013-09-19
    • 1970-01-01
    • 2012-12-03
    相关资源
    最近更新 更多