C 直接访问函数堆栈

C 直接访问函数堆栈,c,function,callstack,C,Function,Callstack,我之前问过一些C函数,这些函数接受的参数数量不确定,例如,void foo(){/*code here*/},并且可以使用数量不确定的参数调用 当我问像void foo(){/*code here*/}这样的函数是否可以获取调用它的参数时,例如foo(42,“random”)有人说: 您唯一能做的就是使用调用约定和正在运行的体系结构知识,并直接从堆栈中获取参数 我的问题是: 如果我有这个功能 void foo() { // get the parameters here }; 我称之为

我之前问过一些C函数,这些函数接受的参数数量不确定,例如,
void foo(){/*code here*/}
,并且可以使用数量不确定的参数调用

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

您唯一能做的就是使用调用约定和正在运行的体系结构知识,并直接从堆栈中获取参数

我的问题是:

如果我有这个功能

void foo()
{
    // get the parameters here
};
我称之为:
foo(“dummy1”、“dummy2”)
是否可以直接从堆栈中获取
foo
函数中的两个参数

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


如果没有,那么参数数量未指定的函数有什么意义?这是C编程语言中的错误吗?在哪种情况下,有人希望
foo(“dummy1”、“dummy2”)
为头为
void foo()
的函数编译并正常运行?

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

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;
}
  • 获取堆栈头指针(我的计算机上的rsp寄存器)
  • 将传递参数的偏移量添加到rsp=>您将得到指向堆栈上长x的指针
  • 取消对指针的引用,添加12(执行任何需要的操作)并返回值
  • 偏移量是个问题,因为它取决于编译器、操作系统,谁知道还有什么。
    对于这个示例,我在debugger中简单地检查了它,但如果它对您来说真的很重要,我认为您可以为您的机器解决方案提供一些“通用”的工具

    如果声明
    void foo()
    ,则会得到
    foo(“dummy1”、“dummy2”)
    的编译错误

    您可以声明一个函数,该函数采用以下未指定数量的参数(例如):

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

    假设您有以下调用:

    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_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);
    }
    
    注意:上面的例子不是线程安全的,这里只给出一个例子,

    大量的“如果”:

  • 您坚持使用编译器的一个版本
  • 一组编译器选项
  • 设法说服编译器永远不要在寄存器中传递参数
  • 说服编译器不要将对同一函数使用不同参数的两个调用f(5,“foo”)和f(&i,3.14)视为错误。(例如,这曾经是早期DeSmet C编译器的一个功能)
  • 然后,函数的激活记录是可预测的(即查看生成的程序集并假设它始终是相同的):返回地址将在某个地方,保存的bp(基本指针,如果您的体系结构有),参数的顺序将是相同的。那么,您如何知道传递了哪些实际参数呢?您必须对它们进行编码(它们的大小、偏移量),大概在第一个参数中,printf就是这样做的

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


    所以这是一个很大的麻烦和假设,基本上什么都不知道。

    如果你知道它是如何被调用的(参数的类型是什么),你可以用一种依赖于机器的方式来做这件事(但是为什么要有这样一个函数呢?),如果你不知道,你就不知道。还要注意(在现代C语言中)
    void foo(){
    不接受任何参数<代码>void foo()(仅声明,不与定义连接)没有说明
    foo
    需要什么。因此,在两个示例中,都不能将任何内容传递给foo。您的示例给出了原型,OP要求给出函数声明的情况。阅读他与
    gcc4.8.1
    链接的问题和答案,我在调用
    foo(“dummy1”、“dummy2”)时没有收到错误。我知道可变函数,但这不是我的问题。然而,我不知道您用于
    int func(char x,…)
    的方法,我只知道这个例子:@Dabo为什么要删除您的
    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);
    }