野牛:$ 1,$ 2等的替代订单给出了错误的输出

时间:2016-01-22 03:12:18

标签: c bison

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

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

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

其中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", $2);} ;

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

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

1 个答案:

答案 0 :(得分:4)

让我们考虑以下代码:

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

这有效吗? 不! message从未初始化,因此它指向外太空。你当然不能将它传递给sprintf并期望一切都能解决。

那么我们对以下内容有什么期望?

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

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

$$ = $1;

如此有效,sprintf来电是:

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

这是不同形式的未定义行为。在Ubuntu系统上从man 3 sprintf引用,

  

标准明确指出,如果在调用sprintf()

时源和目标缓冲区重叠,则结果是不确定的

该联机帮助页指出,虽然标准不允许使用某些gcc和glibc版本,但如果源缓冲区覆盖自身(作为附加到缓冲区的一种方式),它可能看起来有效。

当然,假设字符串中有足够的空间指向$1来保存sprintf的结果。在那儿?谁知道?我们没有看到$1来自哪里。

语义值$1由词法扫描程序填充。在扫描仪中执行此操作的正确方法如下(尽管实际模式可能包含下划线):

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

在这种情况下,语义值$1将不够长,因为它将完全复制标识符所需的时间,而不再是。即使sprintf“似乎工作”,它也将是缓冲区溢出,随机内存将被覆盖。 [注1]

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

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

(注意&asprintf需要一个指向char*的指针,并将分配的内存地址返回到指向的参数。所以在这个调用结束时,{ {1}}将使用正确的字符串指向新分配的缓冲区。)

如果您的系统没有$$,或者您希望为不支持的系统做好准备,请查看this answerasprintf的实施情况

注释

  1. 很多时候,你会在学生写的词汇扫描仪中看到以下内容:

    concatf

    这是不正确的,因为[[:alpha:]][[:alnum:]]* { yylval = yytext; /* DON'T DO THIS!!! */ return IDENTIFIER; } 指向属于词法扫描程序本身的临时缓冲区。无法保证在解析器查看指针时,它仍将指向相同的数据。或者,确实,在任何事情上;扫描仪很可能已释放缓冲区并开始使用不同的缓冲区。所以这已经成了问题。如果这还不够,那么yytext将覆盖扫描仪的输入缓冲区,这可能会在读取下一个标记时产生有趣的后果。