C程序找到堆栈增长的方向

如何在C中找到堆栈正向或反向进展? 这项工作?

int j = 0; int k = 0; if (&k > &j) printf ("Stack is growing in forward direction"); else if (&k < &j) printf ("Stack is growing in reverse direction"); 

为了可靠,人们必须找到两个函数调用之间的区别。

 void func(int *p) { int i; if (!p) func(&i); else if (p < &i) printf("Stack grows upward\n"); else printf("Stack grows downward\n"); } func(NULL); 

请注意,这不会给出关于C的答案,而是关于编译器的答案。

你不能。 在您的代码中, (&k > &j)调用未定义的行为行为。 除非指针指向同一数组中的对象(或超出数组末尾的一个对象),否则不会定义与关系运算符的指针比较。

堆栈是否存在取决于您的实现。 未定义的行为无法预测实现细节。

ISO C标准甚至没有提到“堆栈”一词。 堆栈可能甚至不存在。 函数调用用于保存局部变量的内存可能甚至不是连续的。

这不是一个易于在C中单独确定的特性,因为您的编译器可能会执行各种可能会破坏此类测试的优化。 使用汇编function可能会更好。

换句话说,你的function可以工作,但它不确定。 如果它不起作用,它将不会报告错误:相反,您将得到不正确的结果,并且无法分辨。 堆栈和调用约定的处理是C设法隐藏的唯一两个低级事物。

我的x86汇编程序很生疏,但在我的脑海中,这个(Intel语法)汇编函数可以给出正确的结果。 它的C原型将是int getGrowthDirection() ; 如果堆栈向前增长,则返回正数;如果堆栈反向增长,则返回负数。

 getGrowthDirection: mov ebx, esp push esp sub ebx, esp xor eax, eax sub eax, ebx pop esp ret 

请注意,此function几乎没用,因为程序集要求您知道要定位的平台,如果您知道要定位的平台,那么您应该知道堆栈增长方向。

已经指出C执行环境不一定使用堆栈(可以在堆上分配function激活帧)。 因此,让我们假设我们有一个系统确实使用堆栈来自动变量。 然后我们可以通过比较来自两个不同激活帧的变量的地址来确定堆栈方向。 但是,这种方法存在两个问题:

  1. 比较是非法的。 如果编译器可以判断比较是非法的,或者比较(如果它是合法的)必须具有特定结果,那么它可能不会生成执行比较的代码。 例如,如果比较两个指向类型T的指针并且程序不包含长度大于1的T []类型的数组,那么编译器可能会推断出指针必须比较相等。
  2. 我们怎样才能确定变量确实在不同的激活框架中? 编译器可以将一些自动变量转换为静态变量,甚至可以内联递归函数(GCC内联一个简单的递归因子函数)。

如果我们有一个符号执行环境可以在运行时检测非法指针比较,那么第一个问题是不可解决的。 因此,让我们假设我们有一个传统的优化编译器,它表示具有裸机器地址的指针(当它们无法被优化时)。

考虑到这一切,我最初被指针转换为整数的想法分散了注意力(C99的uintptr_t)。 但我认为这是一个红鲱鱼。 首先,比较整数可能不会得到与比较原始指针相同的结果,因此您无论如何都必须将它们转换回来。 其次,我们不是试图从编译器中隐藏我们正在比较指针; 我们只是试图从编译器隐藏我们正在比较的指针。

我发现它首先考虑了第二个问题:我们如何确保在不同的激活帧中有变量指针?

让我们拒绝将一个函数放在一个单独的库或动态加载的模块中的想法:这将是不可移植的,如果我们将是不可移植的,那么我们也可以用printf打印出指针(“%p \ n“,p)并将它们与shell实用程序进行比较。 除了不便携之外,根本不会有趣。

为了强制编译器在激活帧中生成带有局部变量的代码,我们可以使用一个递归到深度的函数,该函数在编译时无法使用可能存在于递归调用中的局部变量来确定,依此类推。 简而言之,我们希望编译器确定在运行时会发生什么,这非常困难,最好是不可能。

我们可以通过各种方式为我们预测执行,但编译器不清楚。 我们可以使用复数数学或伪随机数生成器。 但是,它可能足以让它可能依赖于命令行参数,我们希望的行为是没有参数的默认行为(希望没有真实编译器通过假设进行符号解释来优化程序它将在没有参数的情况下执行)。 因此,我们可以在argv [1]中明确指定要执行的操作序列,程序将是一种迷你解释器。 通过这种方法,我想我可以使用以下程序回答原始问题,该程序试图通过不使用头文件或库函数来移植:

 // Program to determine stack direction by Edmund Grimley Evans void *mem[99]; void **p = mem; char *pc; void run(void) { void *a[2]; for (;;) { switch (*pc++) { case '+': ++p; break; case '-': --p; break; case 't': { void *t = p[0]; p[0] = p[1]; p[1] = t; } break; case 'a': p[0] = &a[0]; p[1] = &a[1]; break; case 'p': *p = p; break; case 'l': *p = *(void **)*p; break; case 's': *(void **)p[0] = p[1]; break; case '<': *p = (p[0] < p[1]) ? p : 0; break; case 'c': run(); break; case 'r': return; } } } int main(int argc, char *argv[]) { pc = argc == 2 ? argv[1] : "ac+ac+ac- 

这是一个包含注释和跟踪输出的更长版本,用于解释它的工作原理:

 // Program to determine stack direction by Edmund Grimley Evans #include  #include  void *mem[99]; // memory void **p = mem; // pointer to memory char *pc; // program counter int depth = 0; // number of nested calls, only for debug // An interpreter for a strange programming language. // There are 10 instructions in the instruction set: "+-tapls p && !*e) --e; printf(" %d:", depth); for (t = mem; t <= e; t++) printf(t == p ? " [%p]" : " %p", *t); printf("\n%c\n", *pc); } switch (*pc++) { // increment memory pointer: case '+': ++p; break; // decrement memory pointer: case '-': --p; break; // swap contents of adjacent memory cells: case 't': { void *t = p[0]; p[0] = p[1]; p[1] = t; } break; // save addresses of local array in memory: case 'a': p[0] = &a[0]; p[1] = &a[1]; break; // save address of memory itself in memory: case 'p': *p = p; break; // load: case 'l': *p = *(void **)*p; break; // store: case 's': *(void **)p[0] = p[1]; break; // compare two pointers: case '<': *p = (p[0] < p[1]) ? p : 0; break; // recursive call to interpreter: case 'c': ++depth; run(); --depth; break; // return: case 'r': return; default: printf(" Error!\n"); exit(1); } } } int main(int argc, char *argv[]) { // The default program does three recursive calls and compares // addresses from the last two frames: pc = argc == 2 ? argv[1] : "ac+ac+ac- 

请注意,我几乎没有测试过这个程序!

我最初是在Debian“librep”软件包中通过失败的autoconf测试来解决这个问题的。 但是,我会毫不犹豫地推荐一个尚未经过测试的程序,用于autoconf测试。 在实践中,我认为假设所有堆栈都在下降是更安全的,除非我们有一个公认的exception,例如Debian的“hppa”架构。

在调用子例程的Linux(或其他操作系统)进程中,局部变量的内存来自进程的堆栈区域。 任何动态分配的内存(使用malloc,new等)都来自进程的堆区域。 在递归期间,在函数调用期间从堆栈区域分配本地存储器,并在函数执行完成时清除。

存储器的最低地址位于底部,最高地址位于顶部。 以下是使用快速C代码查找递归中堆栈增长方向的步骤。

 #include  void test_stack_growth_direction(recursion_depth) { int local_int1; printf("%p\n", &local_int1); if (recursion_depth < 10) { test_stack_growth_direction(recursion_depth + 1); } } main () { test_stack_growth_direction(0); } 

出来MAC

 0x7fff6e9e19ac 0x7fff6f9e89a8 0x7fff6f9e8988 0x7fff6f9e8968 0x7fff6f9e8948 0x7fff6f9e8928 0x7fff6f9e8908 0x7fff6f9e88e8 0x7fff6f9e88c8 0x7fff6f9e88a8 0x7fff6f9e8888 

在ubuntu上输出

 0x7ffffeec790c 0x7ffffeec78dc 0x7ffffeec78ac 0x7ffffeec787c 0x7ffffeec784c 0x7ffffeec781c 0x7ffffeec77ec 0x7ffffeec77bc 0x7ffffeec778c 0x7ffffeec775c 0x7ffffeec772c 

随着内存地址的减少,堆栈在这些特定设置上向下增长。 这取决于系统的体系结构,并且可能对其他体系结构具有不同的行为。 0x7fff6f9e8868