【问题标题】:Access function variable values without arguments访问不带参数的函数变量值
【发布时间】:2015-02-21 23:49:37
【问题描述】:

我遇到了一个问题,即用另一个函数 (RECOVER) 打印函数 (FOO) 的某些值,但恢复是一个没有参数的 void 函数。我怎样才能访问 foo 变量?

例子:

int foo(int a, short b, char c){
   int x, y, z;
   x = a;
   y = b;
   z = c;
   recover();
}

void recover()
{
    How can I print the x y z values here ?
}

PS:我可以创建另一个函数来帮助我做到这一点。我可以使用寄存器值访问这些值吗?还是指针?

谢谢大家

【问题讨论】:

  • 将其标记为 C 和 C++11 是没有意义的。您使用哪种语言?也就是说,我不认为这真的是一个编程问题,编程答案是“不要那样做,如果你需要 x、y 和 z 的值,请将它们传递给参数”。这基本上只是一个谜题,不是吗?
  • 我使用的是 C 语言,但“谜题”基本上是取 x、y 和 z 的值,不带参数。
  • 好的,然后删除 C++11 标签。有可能的答案,但几乎可以肯定它们是高度系统特定的,实际上是无效的(标准)C,这就是为什么我自己不会发布这样的答案。
  • @Augusto:您可以将foo 的变量序列化到某个文件中,然后在recover 中反序列化。所以没有通过任何论据,但恕我直言,这在实际意义中没有多大意义。正如@hvd 已经指出的那样,其他“hackish”方式是特定于系统的。
  • @GrzegorzSzpetkowski Ugly hack 像 #define recover() globalx=x,globaly=y,globalz=z,recover()foo 的身体之前和#undef recover 之后。 :) 但我不认为这是允许的,问题只提到允许创建另一个函数,而不是宏。

标签: c


【解决方案1】:

如果您想明智地解决这个问题,唯一的方法就是使用指针(或 C++ 中的引用)。

正如目前所写的那样,这个问题有点不妥,因为没有要求x,y,z 实际存在于程序的内存中任何地方,因为它们未被使用。一个体面的优化编译器会注意到这一点,不会发出冗余代码并在此过程中警告您。例如,x86 上的 gcc 为 foo 生成以下代码:

foo:
    rep ret

即该函数所做的就是返回。 (它甚至不调用recover,因为可证明调用recover 还没有做任何事情)。

让我们假设我们已经禁用了所有优化并且有一个非常幼稚的编译器。我们也将不得不忽略标准 C 和可移植性的所有概念,而是依靠我们对特定编译器实现细节、调用约定和特定平台的了解才能走得更远。

在带有 gcc 4.8.2 的 Linux x86 上,默认调用约定是将参数压入堆栈,这样最左边的参数 (a) 位于顶部,然后是 b,直到最右边的参数 (c) 最低在堆栈中。所以调用 `foo(1,2,3) 你会期望这样的代码:

push 3
push 2
push 1
call foo

要生成。请注意,尽管类型的大小不同,但生成的代码对于每个都是相同的,也就是说,char 获得的堆栈空间量与 int 在作为这样的参数传递时所获得的堆栈空间量相同。 (这是有利的,因为它允许堆栈的对齐保持可预测,从而确保不需要缓慢的未对齐加载,并且我们总是可以依靠对齐的指令就足够了)。

实际上,我的 GCC 版本实际生成的代码是:

subl    $12, %esp
movl    $3, 8(%esp)
movl    $2, 4(%esp)
movl    $1, (%esp)
call    foo

这很可能是对推送的优化,即使优化已关闭,但它具有完全相同的语义,只是使用显式堆栈指针寻址。 (可能它需要更少的字节来表示,或者至少在某些情况下它允许现代处理器快几个时钟周期)。

这里还有一点需要注意,那就是在 x86 上,堆栈会“向下”增长。 (这只是很久以前就存在的规则,它可能曾经允许有效使用有限的地址空间,并使加载/存储指令在所需字节方面更短)。

所以一旦我们输入函数foo我们的堆栈看起来像:

ESP + 0 ==> a 
ESP + 4   > b 
ESP + 8   > c 

在函数foo 内部,因为我们已经告诉编译器不要进行任何优化,所以代码看起来很粗略(我删除了一些与本次讨论无关的部分),例如:

foo:
        pushl   %ebp           ; Save EBP as it was when we were called
        movl    %esp, %ebp     ; Update EBP to be what the stack pointer was
        subl    $24, %esp      ; Reserve 24 bytes of stack space, for locals and to maintain 16 byte alignment
        movl    12(%ebp), %edx ; copy 'b' into register edx
        movl    16(%ebp), %eax ; copy 'c' into register eax
        movw    %dx, -20(%ebp) ; copy just the low 16 bytes of edx into a temporary variable on the stack
        movb    %al, -24(%ebp) ; copy just the low byte of eax into a temporary variable on the stack
        movl    8(%ebp), %eax  ; copy 'a' into register eax
        movl    %eax, -12(%ebp); copy eax into stack variable x
        movswl  -20(%ebp), %eax; copy first temporary into eax  (as short)
        movl    %eax, -8(%ebp) ; copy eax to y
        movsbl  -24(%ebp), %eax; copy second tempoary into eax (as char)
        movl    %eax, -4(%ebp) ; copy eax to z
        call    recover
        leave

Ebp 与 esp 的作用相似,只是它指向调用当前函数时栈顶的位置,而不是当前栈顶的位置。这在 x86 上很有用且很常见,因为我们可以使用这个基指针轻松地处理我们的参数,如 ebp+(4*n) 和局部变量如 ebp-(4*n)。

从上面的代码我们可以总结出几件事:

  1. 在禁用优化的情况下进行编译是一个糟糕的主意——即使是一些无用的冗余代码最终也会浪费堆栈空间和时间。 (这项额外的工作可能只是将所有内容始终存储在安全的地方,并避免试图弄清楚是否有任何其他代码可以以某种方式覆盖输入)。
  2. recover 内部,我们无法仅通过读取寄存器来恢复所有变量。 (在这种情况下,寄存器分配在内部重用了它们)。

如果我们在 Windows 上,使用 fastcall 并且只有 2 个参数来恢复,我们可以通过读取 ecx 和 edx 来做到这一点提供内部不会被覆盖。在 x86_64 上,有更多的寄存器可供使用,并且不同的参数传递约定然后从寄存器读取也可能是可行的,即使在这种情况下也是如此。

所以从我们目前阅读的代码中可以看出,我们所追求的变量存储在堆栈中,就在(因为它向下增长)ebp 之后。

因此,对于我们的恢复函数,我们希望直接从堆栈中读取,使用我们从生成的汇编代码中学到的内容来查找 x、y 和 z。有两种方法可以做到这一点,它们都是完全不可移植的,并且对堆栈布局和编译器的假设远远超出了标准 C 中的任何内容。

方法一:

对于这个方法,我们将使用一些内联汇编来直接“捕获” ebp 的值,就像在前一个函数中一样。除了被压入堆栈的参数之外,还有两个条目不太明显。首先,当call 指令发生时,它实际上保存了eip,即隐式指向堆栈的指令指针。 (这需要让返回指令找出实际返回的位置)。其次,大多数函数做的第一件事就是将 ebp 保存在堆栈中以便以后恢复。 (虽然请注意并非所有函数都这样做,但有些函数根本不使用 ebp 或将其用作通用寄存器,这基本上由编译器决定)。

所以在recover 内,最早我们实际上可以编写堆栈实际上看起来像的任何代码:

ESP    ==> 
ESP+4  ==> Old ebp value
ESP+8  ==> Old eip value
ESP+12 ==> Padding/local variables for previous function
.... More local variables, followed by padding and arguments to previous function

所以我们要做的是从堆栈中读取旧的 Ebp 值,然后分别找到相对于 -12、-8 和 -4 的 x、y、z 值:

#include <stdio.h>

void recover() {
  register void *old_ebp;
  asm("mov (%%ebp), %0"
      : "=g" (old_ebp));
  printf("old ebp was: %p\n", old_ebp);
  int *locals = ((int*)old_ebp) - 5; // Note 5*4
  printf("%d %d %d\n", locals[0], locals[1], locals[2]);
}

int foo(int a, short b, char c) {
   int x, y, z;
   x = a;
   y = b;
   z = c;
   recover();
}

int main() {
  foo(1,2,3);
  return 0;
}

运行时会打印如下内容:

old ebp was: 0xbfd51d38
1 2 3

请注意,在上面的代码中,我们用来查找局部变量开始的偏移量是 -5。这是因为当我在上面写代码时,堆栈布局从我开始展示的带注释的代码中改变了。这应该会给出另一个很好的暗示,说明找到这样的变量确实是一个坏主意。

方法二

我们可以相对于当前堆栈帧中的局部变量进行搜索,而不是显式读取 ebp 并找到旧的 ebp 值。我对这个方法的实现是:

void recover() {
  int tmp;
  int *locals = &tmp + 11;
  printf("%d %d %d\n", locals[0], locals[1], locals[2]);
}

我使用调试器凭经验找到了此处所需的特定值 (11) - 它是堆栈上从我的函数中的本地人到我关心的调用者中的本地人的测量距离。您可以在 GDB 中使用类似以下的方式来执行此操作:

Breakpoint 1, foo (a=1, b=2, c=3 '\003') at test.c:5
5      x = a;
(gdb) i r ebp
ebp            0xbffff528   0xbffff528
(gdb) p &x
$1 = (int *) 0xbffff514
(gdb) p &y
$2 = (int *) 0xbffff518
(gdb) p &z
$3 = (int *) 0xbffff51c
(gdb) c
Continuing.

Breakpoint 2, recover () at test.c:13
13    int local=0;
(gdb) i r ebp
ebp            0xbffff4f8   0xbffff4f8
(gdb) p &local
$4 = (int *) 0xbffff4f4

这使用 'info registers' (ir)、'print' (p)、'breakpoint' (b) 在正确的位置停止。

所以在示例调试会话中,堆栈上localx 之间的距离为 0xbffff514-0xbffff4f4 为 0x20。将其除以 4(因为我们需要指针运算,而不是所写表达式的字节数),我们得到给定编译器 + 程序 + 平台的堆栈距离。

请注意,使用此方法编译器可以很高兴地生成任何代码,因为根据 C 标准这是未定义的。至少对于方法 1,它符合 GCC 特定的 asm 语法。

方法三:

我们还可以使用另一个技巧从前一个函数中读取局部变量。回想一下,我们函数的参数在函数入口处相对于 ebp/esp 是正数。我们正在寻找的局部变量相对于 ebp/esp 也是正数,所以如果我们欺骗编译器认为我们的函数的参数比调用者提供的参数多,我们可以将旧的局部变量读取为参数:

#include <stdio.h>

int foo(int a, short b, char c) {
   int x, y, z;
   x = a;
   y = b;
   z = c;
   recover();
}

void recover(int l1, int l2, int l3, int l4, int l5, int l6, int l7, int l8, int l9, int l10, int l11, int l12, int l13, int l14, int l15) {
  printf("%d %d %d\n", l13, l14, l15);
}

int main() {
  foo(1,2,3);
  return 0;
}

在上面的代码中,我们简单地定义了足够的参数来跳过堆栈中我们不关心的东西,直到我们找到上一个函数调用的局部变量。

这依赖于这样一个事实,即 C 假设函数 recover 返回一个 int 并且不接受任何参数,但定义与该假设不匹配。

结论

这完全依赖于编译器/平台,您真的不想这样做。即使对于给定的情况,编译器也不需要做出任何承诺。

如果您真的想在实践中这样做,请不要这样做!您可以通过以下两种方式之一获得相同的行为:

  1. 将指针/引用传递到recover(推荐)
  2. foo 中使用全局变量而不是局部变量
  3. 使用来自编译器的调试信息(如果需要,以编程方式)详细说明“这个变量在哪里?”再次编译器的问题,不用担心的不仅仅是DWARF/PDB。

【讨论】:

  • 非常感谢您对 Flexo 的帮助。我会平静而专注地阅读所有内容。我知道这不切实际,但这是一项功课,所以我必须遵守规则。
  • Flexo,如何使用调试器找到经验值?非常感谢。
  • 您有可用的调试信息吗?你用什么调试器? (我可以为 windbg 或 gdb 解释)
  • 我正在使用 gdb 但是我没有很多使用这些工具的经验。
  • @Augusto GDB 示例会话已添加 - 它与之前的工作示例不太匹配,因为我没有完全使用相同的示例程序,但它显示了所需的方法。
【解决方案2】:

如果您更喜欢打印 x,y.z 在 C++ 的情况下将它们作为参考传递,在 c 中您可以依赖指针。下面给出了我们如何在 C++ 中实现它的示例:

#include<iostream>


void recover(int &x, int& y, int& z)
{
    std::cout<<"x:"<<x<<std::endl;
    std::cout<<"y:"<<y<<std::endl;
}

void  foo(){
   int x, y, z;
   x = 1;
   y = 2;
   z = 3;
   recover(x,y,z);
}

int main()
{

    foo();
    return 0;
}

【讨论】:

  • 我不能在恢复函数中使用参数。
猜你喜欢
  • 2021-01-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多