代码之家  ›  专栏  ›  技术社区  ›  Jacob Krieg

直接访问函数堆栈

  •  1
  • Jacob Krieg  · 技术社区  · 11 年前

    我以前问过 a question 关于采用未指定数量参数的C函数,例如。 void foo() { /* code here */ } 并且可以用 未指定类型的参数数量未指定 .

    当我问到这样的函数是否可行时 void foo(){/*代码此处*/} 获取调用它的参数。 foo(42, "random") 有人说:

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

    我的问题是:

    如果我有这个功能

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

    我称之为: foo("dummy1", "dummy2") 是否可以在 foo 函数?

    如果是,怎么做?是否可以访问整个堆栈?例如,如果我递归地调用一个函数,是否可以以某种方式访问每个函数状态?

    如果没有,那么参数数量未指定的函数有什么意义?这是C编程语言中的错误吗?在哪种情况下,有人会想要 foo(“dummy1”,“dummy2”) 为标头为 void foo() ?

    3 回复  |  直到 11 年前
        1
  •  1
  •   user1666959    11 年前

    很多“如果”:

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

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

    递归(即在递归调用中没有区别)每个实例都有它的激活记录(我说过你必须说服你的编译器永远不会优化尾部调用吗?),但在C中,与Pascal不同,你没有返回到调用者激活记录(即局部变量)的链接,因为没有嵌套的函数声明。访问完整的堆栈(即当前实例之前的所有激活记录)非常乏味,容易出错,并且对那些希望操纵返回地址的恶意代码编写者来说,这是最感兴趣的。

    所以,这是一个很大的麻烦和假设,基本上没有什么。

        2
  •  1
  •   Dabo    11 年前

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

    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=>你得到堆栈上长x的指针
    3. 取消引用指针,添加12(根据需要执行任何操作)并返回值。

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

        3
  •  0
  •   barak manos    11 年前

    如果您声明 void foo() ,则您将得到的编译错误 foo("dummy1", "dummy2") .

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

    int func(char x,...);
    

    正如你所看到的, 至少 必须指定一个参数。这样,在函数内部,您就可以访问 最后的 指定的参数。

    假设您有以下呼叫:

    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
    }
    

    正如您所看到的,这里的问题是每个参数的类型和参数总数都是未知的。所以任何打电话给 函数 如果参数列表“不适当”,可能(而且可能会)导致运行时异常。

    因此,通常,第一个参数是字符串( const char* )它指示以下每个参数的类型以及参数的总数。此外,还有用于提取未指定参数的标准宏- va_start va_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);
    }
    

    注:上面的例子是 线程安全,仅在此处提供 作为一个例子 ...