索引到调用堆栈 - 未定义的行为?

Indexing into call stack - Undefined behaviour?

所以我的朋友对如何在程序的调用堆栈中创建一个列表有这个相当,嗯,反常的想法。这个想法是,如果您可以计算递归调用中相同堆栈变量之间的偏移量,则可以访问调用堆栈中更上层的任意元素。这听起来令人困惑,所以我决定实施它,并且它起作用了,但当然警钟大声响起:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

// Reads all of stdin and prints it in reverse order
void read(char* last)
{
    char current;
    if (last==NULL)
    {
        // null terminator
        current = '[=10=]';
        read(&current);
    }
    else
    {
        int ip = getchar();
        if (ip==EOF)
        {
            // end of stdin found, loop back over the stack to find all read characters

            // Calculate offset between the stack frames
            int offset = &current - last;
            
            for (char* c = last; *c != '[=10=]'; c -= offset)
            {
                printf("%c", *c);
            }
        }
        else
        {
            current = (char) ip;
            read(&current);
        }
        
    }
    
}

int main(int c, char* argv[])
{
    read(NULL);
    return 0;
}

问题是:

Is this UB?

是的。

If so, why?

因为它是通过不相关的句柄访问内存的。一般见pointer provenance n2263.

并且因为标准不保证对象将被分配为彼此相邻且连续递减的内存地址。无法保证 c 指针的值有效。因为没有这样的保证,所以没有定义行为。

Can something simular be used without UB?

没有

Can something simular be used without UB?

是的,你只需要明确地建立一个链表:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

struct chain {
    struct chain *prev;
    char    ch;
}

// Reads all of stdin and prints it in reverse order
void read(struct chain *last)
{
    int ip = getchar();
    if (ip==EOF)
    {
        // end of stdin found, loop back over the stack to find all read characters
        
        for (struct chain *p = last; p; p = p->prev) {            {
            printf("%c", p->ch);
        }
    }
    else
    {
        struct chain current = { last, (char)ip };
        read(&current);
    }    
}

int main(int c, char* argv[])
{
    read(NULL);
    return 0;
}

然而,堆栈溢出的可能性很大...

一些实现相当详细地记录了它们将如何布置堆栈帧,至少在使用某些选项调用时是这样的[这通常会限制编译器将执行的优化类型]。如果这样的实现以与其文档一致的方式运行,则依赖编译器的代码将无法移植,但在编译器上可能具有正确定义的行为,如所描述的那样运行。如果标准不要求实施记录某些内容,但实施无论如何都会这样做,则此类文档的准确性将成为标准管辖范围之外的实施质量问题。符合规范的实现可以自由地记录几乎任何它喜欢的东西,不管它的行为是否与它记录的内容有任何关系。

请注意,按照标准的编写方式,在极少数情况下,符合标准的实现可以对某些特定的非人为设计的程序执行任何操作,但会导致其不符合标准。编写一个程序,如果一个人无法设计出一个低质量但合规的实现,而该实现在输入时会表现得毫无意义,这实际上是不可能的[事实上,我们可以设计出一对这样的实现,这样就没有源文本有意义由他们两个处理。

我认为质量实施应该只指定在标准不强加任何要求的情况下以某种方式处理某些构造是合理的,如果他们将始终如一地以这种方式处理此类构造,即使标准不会要求他们这样做。记录其将以某种方式处理某事但有时会做其他事情的实现可能符合要求,但仍应被视为质量较差。虽然有时程序员为低质量的实现做出让步可能是有用的或必要的,但此类实现的维护者不应将此类让步视为对其实现质量的任何形式的认可。

可以通过利用栈帧布局的详细知识来执行的任务相对较少,如果不依赖此类知识,则无法更好地完成这些任务。此外,依赖于堆栈框架布局的代码可能 运行 仅在相当小的实现中有用。尽管如此,应该期望这样的构造在质量实现上有用,这些实现足够详细地记录其堆栈框架布局以指定程序员需要的一切。