我使用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);}
;
为什么会这样?我的代码中有什么问题吗?
答案 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 answer中asprintf
的实施情况
很多时候,你会在学生写的词汇扫描仪中看到以下内容:
concatf
这是不正确的,因为[[:alpha:]][[:alnum:]]* { yylval = yytext; /* DON'T DO THIS!!! */
return IDENTIFIER;
}
指向属于词法扫描程序本身的临时缓冲区。无法保证在解析器查看指针时,它仍将指向相同的数据。或者,确实,在任何事情上;扫描仪很可能已释放缓冲区并开始使用不同的缓冲区。所以这已经成了问题。如果这还不够,那么yytext
将覆盖扫描仪的输入缓冲区,这可能会在读取下一个标记时产生有趣的后果。