直接访问功能堆栈

时间:2014-03-02 11:01:47

标签: c function callstack

我之前曾询问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()的函数?

3 个答案:

答案 0 :(得分:1)

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

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(执行您需要的任何操作)并返回值。
  4. 偏移是问题,因为它依赖于编译器,操作系统,谁知道还有什么。 对于这个例子,我在调试器中检查了它,但如果它对你真的很重要,我想你可以为你的机器解决方案带来一些“通用”。

答案 1 :(得分:1)

很多'if's:

  1. 您坚持使用一个版本的编译器。
  2. 一组编译器选项。
  3. 以某种方式设法说服你的编译器永远不要在寄存器中传递参数。
  4. 说服你的编译器不要将两个调用f(5,“foo”)和f(& i,3.14)用与错误相同的函数的不同参数处理。 (这曾经是早期DeSmet C编译器的一个特征)。
  5. 然后一个函数的激活记录是可预测的(即你看看生成的程序集并假设它总是相同的):返回地址将在某处和保存的bp(基指针,如果你的体系结构有一个) ),并且参数的顺序将是相同的。那你怎么知道实际的参数传递了什么?你必须对它们进行编码(它们的大小,偏移量),大概是在第一个参数中,就像printf那样。

    递归(即在递归调用中没有区别)每个实例都有其激活记录(我说你必须说服你的编译器永远不会优化尾调用吗?),但是在C中,与Pascal不同,你没有因为没有嵌套的函数声明,所以有一个向后调用调用者激活记录的链接(即局部变量)。获取完整堆栈的访问权限,即当前实例之前的所有激活记录都非常繁琐,容易出错,而且对于想要操纵返回地址的恶意代码编写者而言,这些记录最为感兴趣。

    因此,基本上没有任何麻烦和假设。

答案 2 :(得分:0)

如果您声明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。:上面的示例是线程安全,并且仅在此处给出作为示例 ......