gpt4 book ai didi

c - 访问不带参数的函数变量值

转载 作者:行者123 更新时间:2023-11-30 15:23:46 25 4
gpt4 key购买 nike

我有一个问题,其中包括使用另一个函数(RECOVER)打印一个函数(FOO)的某些值,但是recovery是一个没有参数的无效函数。如何访问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 ++中的引用)。

如当前所写,该问题有些不适,因为没有要求x,y,z实际不存在于程序中任何地方的内存中。一个体面的优化编译器会注意到这一点,不会释放冗余代码,并警告你,在这个过程中。例如,x86上的gcc为foo生成以下代码:

foo:
rep ret


即该函数所做的全部就是返回。 (它甚至都没有调用恢复,因为调用可恢复的调用尚无任何作用)。

让我们假设我们完全禁用了所有优化功能,并且拥有一个真正幼稚的编译器。我们还将不得不无视标准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))寻址。

从上面的代码中,我们可以得出以下几点结论:


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


如果我们在Windows上,使用fastcall并且只有2个参数要恢复,我们可以通过读取ecx和edx来实现,只要内部没有被覆盖。在x86_64上,要使用的寄存器更多,并且有不同的参数传递约定,那么即使在这种情况下,从寄存器中读取数据也是可行的。

因此,从到目前为止我们已经阅读的代码中,我们可以看到我们所要访问的变量存储在堆栈中,即ebp之后(因为它变小了)。

因此,对于我们的恢复功能,我们想使用从生成的汇编代码中学到的内容直接读取堆栈,以找到x,y和z。有两种方法可以实现这两种方法,它们都是完全不可移植的,它们对堆栈布局和编译器的假设远远超出了标准C语言中的任何标准。

方法1:

对于此方法,我们将使用一些内联asm直接像上一个函数中那样“捕获” 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。这是因为在我编写堆栈布局上方的代码时,与我最初显示的带注释的代码有所不同。那应该给人一个很好的提示,那就是找到这样的变量确实是个坏主意。

方法2

无需显式读取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


这使用“信息寄存器”(i r),“打印”(p),“断点”(b)停在正确的位置。

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

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

方法3:

我们还可以使用另一种技巧来从先前的函数中读取本地信息。回想一下我们函数的参数相对于函数入口处的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并且不接受任何参数,但是定义与该假设不符。

结论

这完全取决于编译器/平台,您确实不想这样做。即使对于给定的情况,编译器实际上也不必做出任何承诺。

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


将指针/引用传递到 recover(推荐)
foo中使用全局变量而不是局部变量
使用来自编译器的调试信息(如果需要,可以通过编程方式)来详细说明“此变量在哪里?”编译器的问题又一次出现了,不必担心除了DWARF / PDB以外的问题。

关于c - 访问不带参数的函数变量值,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28652809/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com