如何确定堆栈上的返回地址?

Posted

技术标签:

【中文标题】如何确定堆栈上的返回地址?【英文标题】:How can I determine the return address on stack? 【发布时间】:2010-12-14 03:52:44 【问题描述】:

我知道,如果我在某个函数 foo() 中,该函数是从 bar() 函数的某处调用的,那么这个返回地址会被压入堆栈。

    #include <stdio.h>

    void foo()
    
            unsigned int x;
            printf("inside foo %x\n", &x);
    
    int main()
    
            foo();
            printf("in main\n");
            return 0;
    

在上面的代码中,当 foo 函数处于活动状态时,我将获得第一个压入堆栈的局部变量的地址。如何访问在堆栈上此变量之前某处推送的返回地址(主要称为 foo)?该位置是否固定并且可以相对于第一个局部变量进行访问?我该如何修改它?

编辑:我的环境是带有 gcc 编译器的 x86 处理器上的 Ubuntu 9.04。

【问题讨论】:

"如何修改?" - 考虑使用 setjmp/longjmp。 我猜 void * __builtin_return_address (unsigned int level) 不会解决我的问题。它会返回给我一个返回地址,而不是返回地址的位置。如果我应该重新表述上述问题陈述,请告诉我。 没有简单、可靠的方法来做你想做的事,即使你已经限制在 x86 上使用 gcc。我只能建议您提出另一个问题来说明您的实际问题。如果不出意外,修改链接指针并不能保证成功返回到您在其中写入的地址。调用站点 A 在调用 foo 之前保存了一些寄存器。调用站点 B 可能保存了不同的寄存器,并且无论恢复它们所做的一切都会失败,因为堆栈处于 A 离开它的状态,而不是 B 期望的状态。 【参考方案1】:

试试这个

//test1.cc
//compile with
//g++ -g test1.cc -o test1 

#include <stdio.h>

void
print_function(void *p) 
    char cmd[128];
    FILE *fp;

    snprintf(cmd, sizeof(cmd), "addr2line -e %s -f %p", "test1", p);
    fp = popen(cmd, "r");
    if (fp) 
    char buf[128];
    while (fgets(buf, sizeof(buf), fp)) 
        printf("%s", buf); 
    
    


void
f2(void) 
    print_function(__builtin_return_address(0));


void
f1(void) 
    f2();


int
main(int argc, char *argv[]) 
    f1();
    return(0);

输出应该是这样的

_Z2f1v
/home/<user>/<dir>/test1.cc:30

【讨论】:

【参考方案2】:

还请注意,通常 C 语言不能保证您的返回地址在堆栈上,或者实际上根本就在 RAM 中的任何地方。

有些处理器架构将返回地址存储在寄存器中,只有在调用开始嵌套时才使用 RAM。还有其他架构,其中有一个单独的返回地址堆栈,CPU 不可读。这两个仍然可以为它们实现 C 编译器。

这就是为什么你需要更清楚你的环境。

【讨论】:

@unwind 我已经编辑了问题以指定我的环境,我以后会记住这一点。【参考方案3】:

你可以像这样探索堆栈

// assuming a 32 bit machine here
void digInStack(void) 

    int i;
    long sneak[1];

    // feel free to adjust the search limits
    for( i = -32; i <= 32; ++i) 
        printf("offset %3d: data 0x%08X\n", i, sneak[i]);
    
 

你可以侥幸逃脱,因为 C 以不太特别在索引数组的方式而闻名。在这里,您在堆栈上声明了一个虚拟数组,然后相对于它查看 +/-。

正如 Rob Walker 所指出的,您肯定需要了解您的编译器调用约定,才能理解您正在查看的数据。您可能会打印出一些函数的地址,并查找在相似范围内的值,并直观地知道返回地址在哪里,相对于虚拟数组。

注意事项 - 阅读所有内容,但不要使用该数组修改任何内容,除非 (a) 您完全确定要修改堆栈的哪一部分,或者 (b) 只是想看到一个有趣/不可预测的崩溃模式。

【讨论】:

【参考方案4】:

为此有一个内置的 gcc:void * __builtin_return_address (unsigned int level)

见http://gcc.gnu.org/onlinedocs/gcc/Return-Address.html

在某些架构上,您可以在相对于第一个参数的堆栈中找到它。例如,在 ia32 上,参数被推送(以相反的顺序),然​​后调用将推送返回地址。请记住,堆栈几乎总是(在 ia32 上)向下增长。虽然从技术上讲,您的语言和硬件平台需要 ABI调用约定(有时称为 链接约定),但实际上您通常可以猜测是否你知道过程调用机器操作是如何工作的。

函数的第一个参数和返回地址在堆栈上的位置之间的关系比本地和返回地址之间的关系更可能是一个可靠的固定值。但是,您当然可以打印出本地地址和第一个参数的地址,并且您经常会在两者之间找到 PC。

$ expand < ra.c
#include <stdio.h>

int main(int ac, char **av) 
  printf("%p\n", __builtin_return_address(0));
  return 0;

$ cc -Wall ra.c; ./a.out
0xb7e09775
$ 

【讨论】:

@DigitalRoss 似乎不起作用,你能粘贴一个简单的例子吗? @DigitalRoss 所以你的意思是返回地址在函数和局部变量的第一个参数之间? 一般来说,是的。参数被推送,然后是 PC,然后堆栈进一步递减,用于本地和编译器需要的任何临时对象。由于您可以获取参数地址和本地地址,因此您可以追踪您的 PC。请记住,负下标在 C 中工作得很好。f(int p) printf("%p", (&amp;p)[-1]); 哦,好吧..我想这就是 __builtin_return_address 必须能够确定返回地址的方式。我也猜想,由于 C 调用约定从右到左推送参数,这个负订阅将与第一个参数一起使用。如果我在这里错了,请纠正我。 正如 DigitalRoss 所说,这一切都非常依赖于架构。在 ARM 上(我提到这个问题是因为问题被标记为“linux gcc”,而不是“linux gcc x86”),链接指针在 r14 中,而不是(必然)在堆栈上。您返回MOV r15,r14BX R14。所以在 gcc 上你应该总是使用内置函数而不是试图猜测调用约定。【参考方案5】:

要知道返回地址在哪里,您需要知道calling convention 是什么。这通常由编译器设置并取决于平台,但您可以以特定于平台的方式强制它,例如在 Windows 上使用__declspec(stdcall)。优化编译器还可以为没有外部作用域的函数发明自己的调用约定。

除非使用编译器内置函数来获取返回地址,否则您将不得不求助于内联汇编器来获取值。其他似乎在调试中起作用的技术对于编译器优化来说非常容易搞砸。

【讨论】:

【参考方案6】:

当您声明局部变量时,它们也在堆栈中 - 例如 x。

如果您随后声明 int * xptr 并将其初始化为 &amp;x,它将指向 x。

没有什么(太多)可以阻止您减少该指针以稍早查看,或将其增加以稍后查看。附近有你的退货地址。

【讨论】:

谢谢卡尔,如果我能从 x (正/负)得到准确的偏移量,那就太好了。 这取决于你的编译器。您应该在调试器中运行并查看您的程序,它将显示所有必要的细节。如果您没有调试器,请在 x 周围打印几个单词并进行一些实验!这将是一次学习体验。

以上是关于如何确定堆栈上的返回地址?的主要内容,如果未能解决你的问题,请参考以下文章

在缓冲区溢出中,新的返回地址如何完美对齐?

堆栈和堆栈基地址

在C语言中,如何给函数分配内存?

调试器如何跟踪堆栈?

如何在Linux下禁用ARP协议

使用 WAMP 堆栈修复 localhost/phpmyadmin/ 地址