我遇到的问题包括用另一个函数(RECOVER)打印一个函数值(FOO),但是recover是一个没有参数的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:我可以创建另一个可以帮助我做的功能。我可以使用寄存器值访问这些值吗?还是指针?
谢谢你们
答案 0 :(得分:1)
如果你想明智地解决这个问题,唯一的方法就是使用指针(或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上,堆栈增长了#34; down"。 (这只是很久以前一直存在的规则,它可能曾经允许有效使用有限的地址空间,并且在所需的字节数方面保持较短的加载/存储指令。)
所以一旦我们输入函数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中的任何内容。
对于这种方法,我们将使用一些内联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。这是因为当我编写上面的代码时,堆栈布局从我显示的带注释的代码改变了。这应该给出另一个非常好的暗示:关于找到这样的变量有多大的坏主意。
我没有显式读取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
这使用&#39;信息寄存器&#39; (i r),&#39; print&#39; (p),&#39;断点&#39; (b)在适当的地方停留。
因此,示例调试会话中堆栈上local
和x
之间的距离为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并且不带参数,但该定义与该假设不匹配。
这完全完全依赖于编译器/平台,你真的不想这样做。即使对于给定的情况,编译器也不必做出任何承诺。
如果你真的想在实践中这样做,不要!您可以通过以下两种方式之一获得相同的行为:
recover
(推荐)foo
答案 1 :(得分:0)
如果你喜欢打印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;
}