【问题标题】:previous stack variables以前的堆栈变量
【发布时间】:2012-05-02 01:46:11
【问题描述】:

我有这个问题,我在 C 中递归调用一个函数,而 C 是词法范围的,所以我只能访问当前的堆栈帧。我想从前一个堆栈帧中提取参数和局部变量,该堆栈帧是在当前堆栈帧上时在前一个函数调用下创建的

我知道上一次递归调用的值仍在堆栈中,但我无法访问这些值,因为它们被“埋”在活动堆栈框架下?

我想从上一个堆栈中提取参数和局部变量,并将它们复制到copy_of_buried_arg和copy_of_buried_loc;

要求使用内联汇编使用GAS来提取变量,这是我目前为止的,我尝试了一天,我似乎无法弄清楚,我在纸上画了堆栈并进行了计算但没有任何效果,我也尝试删除对 printf 的调用,这样堆栈会更干净,但我无法找出正确的算法。这是到目前为止的代码,我的函数在第二次迭代时停止

#include <stdio.h>

char glo = 97;   // just for fun 97 is ascii lowercase 'a'
int copy_of_buried_arg;
char copy_of_buried_loc;

void rec(int arg) {
  char loc;

  loc = glo + arg * 2; // just for fun, some char arithmetic
  printf("inside rec() arg=%d loc='%c'\n", arg, loc);

  if (arg != 0) {
    // after this assembly code runs, the copy_of_buried_arg and
    // copy_of_buried_loc variables will have arg, loc values from
    // the frame of the previous call to rec().
    __asm__("\n\
            movl 28(%esp), %eax #moving stack pointer to old ebp (pointing it to old ebp)\n\
            addl $8, %eax       #now eax points to the first argument for the old ebp \n\
            movl (%eax), %ecx   #copy the value inside eax to ecx\n\ 
            movl %ecx, copy_of_buried_arg   # copies the old argument\n\
    \n\

");

    printf("copy_of_buried_arg=%u copy_of_buried_loc='%c'\n",
       copy_of_buried_arg, copy_of_buried_loc);
  } else {
      printf("there is no buried stack frame\n");// runs if argument = 0 so only the first time
  }

  if (arg < 10) {
    rec(arg + 1);
  }
}

int main (int argc, char **argv) {
  rec(0);

  return 0;
}

【问题讨论】:

  • 你不能把前一个调用的本地地址传递给下一个函数吗?乱用汇编语言在这里可能不会有什么好处。
  • 如何在调试器中运行它并从那里获取数字?
  • @GregHewgill 我们应该使用汇编从前一个堆栈中挖掘变量,这是一个要求
  • 我明白了。在问题中指出这类事情会很有用。
  • @Alex 我该怎么做。其实我也有这个问题,有没有程序可以图形化的显示堆栈?

标签: c gcc assembly x86


【解决方案1】:

我可以尝试提供帮助,但 GAS 中没有 Linux 或程序集。但是计算应该是类似的:

这是几次调用后的堆栈。典型的堆栈帧设置会创建堆栈帧的链表,其中 EBP 是当前堆栈帧,并指向其旧值用于前一个堆栈帧。

      +-------+
ESP-> |loc='c'|     <- ESP currently points here.
      +-------+
EBP-> |oldEBP |--+  <- rec(0)'s call frame
      +-------+  |
      |retaddr|  |  <- return value of rec(1)
      +-------+  |
      |arg=1  |  |  <- pushed argument of rec(1)
      +-------+  |
      |loc='a'|  |  <- local variable of rec(0)
      +-------+  |
   +--|oldEBP |<-+  <- main's call frame
   |  +-------+
   |  |retaddr|     <- return value of rec(0)
   |  +-------+ 
   |  |arg=0  |     <- pushed argument of rec(0)
   |  +-------+
  \|/ 
to main's call frame

这是由以下序列创建的:

  1. 首先推送参数最后一个 arg。
  2. 调用函数,推送返回地址。
  3. 推送即将成为旧的 EBP,保留之前的堆栈帧。
  4. 将 ESP(堆栈顶部,包含 oldEBP)移动到 EBP,创建新堆栈帧。
  5. 为局部变量减去空间。

这对 32 位堆栈有影响,EBP+8 将始终是调用的第一个参数,EBP+12 第二个参数等。EBP-n 始终是局部变量的偏移量。

获取之前的locarg 的代码是(在MASM 中):

mov ecx,[ebp]              // get previous stack frame
mov edx,[ecx]+8            // get first argument
mov copy_of_buried_arg,edx // save it
mov dl,[ecx]-1             // get first char-sized local variable.
mov copy_of_buried_loc,dl  // save it

或者我对 GAS 的最佳猜测(我不知道,但知道它倒退到 MASM):

movl (%ebp),ecx
movl 8(%ecx),edx
movl edx,copy_of_buried_arg
movb -1(%ecx),dl
movb dl,copy_of_buried_loc

在 Windows 上使用 VS2010 使用我的 MASM 输出您的代码:

inside rec() arg=0 loc='a'
there is no buried stack frame
inside rec() arg=1 loc='c'
copy_of_buried_arg=0 copy_of_buried_loc='a'
inside rec() arg=2 loc='e'
copy_of_buried_arg=1 copy_of_buried_loc='c'
inside rec() arg=3 loc='g'
copy_of_buried_arg=2 copy_of_buried_loc='e'
inside rec() arg=4 loc='i'
copy_of_buried_arg=3 copy_of_buried_loc='g'
inside rec() arg=5 loc='k'
copy_of_buried_arg=4 copy_of_buried_loc='i'
inside rec() arg=6 loc='m'
copy_of_buried_arg=5 copy_of_buried_loc='k'
inside rec() arg=7 loc='o'
copy_of_buried_arg=6 copy_of_buried_loc='m'
inside rec() arg=8 loc='q'
copy_of_buried_arg=7 copy_of_buried_loc='o'
inside rec() arg=9 loc='s'
copy_of_buried_arg=8 copy_of_buried_loc='q'
inside rec() arg=10 loc='u'
copy_of_buried_arg=9 copy_of_buried_loc='s'

【讨论】:

  • 我的猜测是,任何形式的堆栈保护都会使这种方法完全不可移植。
  • @tbert:堆栈保护与此有什么关系? OP 的代码是不可移植的,因为它会在使用不同的优化选项或不同版本的编译器编译时中断。
  • 我很轻,我只是在做噩梦,人们认为这是一种将信息传递给函数的进一步递归迭代的可行技术。如果讲师不能确保有与此 hack 相关的一长串警告,那么我会确保互联网上的某个地方有人说“caveat emptor”。
【解决方案2】:

使用我的编译器 (gcc 3.3.4) 我最终得到了这个:

#include <stdio.h>

char glo = 97;   // just for fun 97 is ascii lowercase 'a'
int copy_of_buried_arg;
char copy_of_buried_loc;

void rec(int arg) {
  char loc;

  loc = glo + arg * 2; // just for fun, some char arithmetic
  printf("inside rec() arg=%d loc='%c'\n", arg, loc);

  if (arg != 0) {
    // after this assembly code runs, the copy_of_buried_arg and
    // copy_of_buried_loc variables will have arg, loc values from
    // the frame of the previous call to rec().
    __asm__ __volatile__ (
            "movl 40(%%ebp), %%eax #\n"
            "movl %%eax, %0 #\n"
            "movb 31(%%ebp), %%al #\n"
            "movb %%al, %1 #\n"
            : "=m" (copy_of_buried_arg), "=m" (copy_of_buried_loc)
            :
            : "eax"
    );

    printf("copy_of_buried_arg=%u copy_of_buried_loc='%c'\n",
       copy_of_buried_arg, copy_of_buried_loc);
  } else {
      printf("there is no buried stack frame\n");// runs if argument = 0 so only the first time
  }

  if (arg < 10) {
    rec(arg + 1);
  }
}

int main (int argc, char **argv) {
  rec(0);

  return 0;
}

下面是相关部分的反汇编(用gcc file.c -S -o file.s获取):

_rec:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $8, %esp
        movl    8(%ebp), %eax
        addl    %eax, %eax
        addb    _glo, %al
        movb    %al, -1(%ebp)
        subl    $4, %esp
        movsbl  -1(%ebp),%eax
        pushl   %eax
        pushl   8(%ebp)
        pushl   $LC0
        call    _printf
        addl    $16, %esp
        cmpl    $0, 8(%ebp)
        je      L2
/APP
        movl 40(%ebp), %eax #
movl %eax, _copy_of_buried_arg #
movb 31(%ebp), %al #
movb %al, _copy_of_buried_loc #

/NO_APP
        subl    $4, %esp
        movsbl  _copy_of_buried_loc,%eax
        pushl   %eax
        pushl   _copy_of_buried_arg
        pushl   $LC1
        call    _printf
        addl    $16, %esp
        jmp     L3
L2:
        subl    $12, %esp
        pushl   $LC2
        call    _printf
        addl    $16, %esp
L3:
        cmpl    $9, 8(%ebp)
        jg      L1
        subl    $12, %esp
        movl    8(%ebp), %eax
        incl    %eax
        pushl   %eax
        call    _rec
        addl    $16, %esp
L1:
        leave
        ret

那些与ebp(40 和 31)的偏移量最初被设置为任意猜测值(例如 0),然后通过观察反汇编和一些简单的计算来改进。

请注意,该函数在递归调用自身时使用额外的 12+4=16 字节的堆栈进行对齐和参数:

        subl    $12, %esp
        movl    8(%ebp), %eax
        incl    %eax
        pushl   %eax
        call    _rec
        addl    $16, %esp

返回地址也有4个字节。

然后该函数使用 4+8=12 字节存储旧的 ebp 及其局部变量:

_rec:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $8, %esp

因此,每次递归调用,堆栈总共会增长 16+4+12=32 个字节。

现在,我们知道如何通过ebp 获取本地的argloc

        movl    8(%ebp), %eax ; <- arg
        addl    %eax, %eax
        addb    _glo, %al
        movb    %al, -1(%ebp) ; <- loc

所以,我们只需在偏移量 8 和 -1 上加上 32,就可以得到 40 和 31。

做同样的事情,你会得到你的“隐藏”变量。

【讨论】:

  • 请注意,至少在 x86-64 上,参数实际上到达寄存器(大部分),而不是在堆栈上,所以这是不可能的。
  • @perh 这个问题没有提到 64 位。此外,如果寄存器不足以将所有内容保存在其中,则不可避免地会将某些内容存储在堆栈中。
猜你喜欢
  • 1970-01-01
  • 2014-12-09
  • 1970-01-01
  • 2019-06-15
  • 2011-04-13
  • 2013-09-27
  • 2013-01-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多