Bison:$1、$2 等的交替顺序给出错误的输出

Bison: alternate order of $1, $2 etc gives wrong output

我正在使用 bison 构建用于教育目的的解析器。这是我非常简单的语法:

program: KW_VAR ident {printf("var %s\n", );} ;

ident:
 | IDENTIFIER OP_PLUS IDENTIFIER {sprintf($$, "%s + %s\n", , );}
 ;

其中 KW_VAR 表示单词 'var',OP_PLUS 表示运算符“+”。

var hello + hi 是这个语法可接受的短语。所以当我使用上面的代码时,一切正常并且 printf 给出: var hello + hi 正如预期的那样。但是当我尝试更改 sprintf 中 $1、$3 的顺序时,如下所示,printf 给出:var hi + hi +。我期望的是 var hi + hello.

program: KW_VAR ident {printf("var %s\n", );} ;

ident:
 | IDENTIFIER OP_PLUS IDENTIFIER {sprintf($$, "%s + %s\n", , );}
 ;

为什么会这样?我的代码有问题吗?

让我们考虑以下代码:

const char* greeting = "Hello";
const char* greeted = "world";
char* message;
sprintf(message, "%s, %s!", greeting, greeted);

有效吗? No! message 从未初始化,所以它指向外部 space。您当然不能将它传递给 sprintf 并期望一切顺利。

那么我们对以下内容有何期待?

sprintf($$, "%s + %s\n", , );

我们还没有初始化 $$,所以我们再次套印了一些随机内存。除了在这种情况下,它不是完全随机的,因为就在 bison-generated 解析器执行任何操作之前,它首先执行此操作:

$$ = ;

如此有效,sprintf 调用是:

sprintf(, "%s + %s\n", , );

这是另一种形式的未定义行为。在 Ubuntu 系统上引用 man 3 sprintf

the standards explicitly note that the results are undefined if source and destination buffers overlap when calling sprintf()

联机帮助页指出,尽管标准不允许,但对于某些 gcc 和 glibc 版本,如果源缓冲区覆盖自身(作为附加到缓冲区的一种方式),它似乎可以工作。

当然,这假设指向 </code> 的字符串中实际上有足够的 space 来保存 <code>sprintf 的结果。在那儿?谁知道?我们看不到 </code> 来自哪里。</p> <p>语义值 <code> 已由词法扫描器填充。在扫描仪中执行此操作的正确方法如下所示(尽管真实模式可能包含下划线):

[[:alpha:]][[:alnum:]]*   {  yylval = strdup(yytext); return IDENTIFIER; }

在那种情况下,语义值 </code> 将不够长,因为它与复制标识符所需的长度一样长,不再需要。即使<code>sprintf"seems to work"也会缓冲区溢出,随机内存会被覆盖。 [注1]


那么,怎么办?简单的解决方案(如果有的话)是使用 asprintf,它类似于 sprintf,只是它分配了一个新的缓冲区。使用该功能,您可以编写野牛动作:

asprintf(&$$, "%s + %s\n", , );

(注意 &asprintf 需要一个指向 char* 的指针,并且 returns 分配的内存地址进入 pointed-to 参数。所以在此调用结束时,$$ 将指向具有正确字符串的 newly-allocated 缓冲区。)

如果您的系统没有 asprintf,或者您想为没有的系统做好准备,请查看 concatf 的实现


备注

  1. 很多时候,你会在同学们写的词法扫描器中看到如下内容:

    [[:alpha:]][[:alnum:]]*   {  yylval = yytext;  /* DON'T DO THIS!!! */
                                 return IDENTIFIER;
                              }
    

    这是不正确的,因为 yytext 指向一个属于词法扫描器本身的临时缓冲区。不能保证当解析器开始查看指针时,它仍然指向相同的数据。或者,事实上,在任何事情上;扫描器很可能会释放该缓冲区并开始使用另一个缓冲区。所以这已经是个问题了。如果这还不够,sprintf 将覆盖 扫描器的输入缓冲区 ,这可能会在读取下一个标记时产生有趣的结果。