C 野牛:1美元、2美元等的交替顺序给出了错误的输出
我正在使用bison构建一个解析器,用于教育目的。以下是我非常简单的语法:C 野牛:1美元、2美元等的交替顺序给出了错误的输出,c,bison,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_加上运算符“+” var hello+hi是该语法可接受的短语。因此,当我使用上面的代码时,一切正常,printf给出:
program: KW_VAR ident {printf("var %s\n", $2);} ;
ident:
| IDENTIFIER OP_PLUS IDENTIFIER {sprintf($$, "%s + %s\n", $1, $3);}
;
其中KW_VAR表示单词“VAR”,OP_加上运算符“+”
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);}
;
为什么会这样?我的代码有问题吗我们考虑下面的代码:
const char* greeting = "Hello";
const char* greeted = "world";
char* message;
sprintf(message, "%s, %s!", greeting, greeted);
这有效吗<斯特朗>不消息
从未初始化,因此它指向外层空间。您当然不能将其传递给sprintf,并期望一切顺利
那么,我们对以下内容有何期待
sprintf($$, "%s + %s\n", $3, $1);
我们还没有初始化$$
,所以我们再次套印了一些随机内存。除此之外,它不是完全随机的,因为就在bison生成的解析器执行任何操作之前,它首先执行以下操作:
$$ = $1;
因此,sprintf
调用实际上是:
sprintf($1, "%s + %s\n", $3, $1);
这是另一种形式的未定义行为。引用Ubuntu系统上的Man3Sprintf
标准明确指出,如果调用sprintf()
手册页注意到,尽管该标准不允许这样做,但在某些gcc和glibc版本中,如果源缓冲区覆盖自身(作为附加到缓冲区的一种方式),那么它可能会正常工作
当然,这假设在指向$1
的字符串中有足够的空间来保存sprintf
的结果。有?谁知道呢?我们不知道1美元是从哪里来的
词法扫描程序填写语义值$1
。在扫描仪中这样做的正确方法如下(尽管实际模式可能包括下划线):
在这种情况下,语义值$1
将不够长,因为它将与复制标识符所需的长度一样长,不再需要。即使sprintf“似乎正常工作”,也会导致缓冲区溢出,随机内存将被覆盖。[注1]
那么,该怎么办?简单的解决方案是使用
asprintf
,它类似于sprintf
,只是它分配了一个新的缓冲区。使用该函数,您可以编写bison操作:
asprintf(&$$, "%s + %s\n", $3, $1);
(注意,&
:asprintf
需要一个指向char*
的指针,并将分配的内存地址返回到指向的参数中。因此,在该调用结束时,$$
将使用正确的字符串指向新分配的缓冲区。)
如果您的系统没有asprintf
,或者您希望为没有的系统做好准备,请查看中的concatf
的实现
笔记
[[:alpha:]][[:alnum:]]* { yylval = yytext; /* DON'T DO THIS!!! */
return IDENTIFIER;
}
这是不正确的,因为yytext
指向属于词法扫描程序本身的临时缓冲区。无法保证在解析器开始查看指针时,它仍然指向相同的数据。或者,事实上,在任何事情上;扫描仪很可能已经释放了该缓冲区,并开始使用另一个缓冲区。这已经是个问题了。如果这还不够,那么sprintf
将覆盖扫描仪的输入缓冲区,这可能会在读取下一个令牌时产生有趣的结果让我们考虑下面的代码:
const char* greeting = "Hello";
const char* greeted = "world";
char* message;
sprintf(message, "%s, %s!", greeting, greeted);
这有效吗<斯特朗>不消息
从未初始化,因此它指向外层空间。您当然不能将其传递给sprintf,并期望一切顺利
那么,我们对以下内容有何期待
sprintf($$, "%s + %s\n", $3, $1);
我们还没有初始化$$
,所以我们再次套印了一些随机内存。除此之外,它不是完全随机的,因为就在bison生成的解析器执行任何操作之前,它首先执行以下操作:
$$ = $1;
因此,sprintf
调用实际上是:
sprintf($1, "%s + %s\n", $3, $1);
这是另一种形式的未定义行为。引用Ubuntu系统上的Man3Sprintf
标准明确指出,如果调用sprintf()
手册页注意到,尽管该标准不允许这样做,但在某些gcc和glibc版本中,如果源缓冲区覆盖自身(作为附加到缓冲区的一种方式),那么它可能会正常工作
当然,这假设在指向$1
的字符串中有足够的空间来保存sprintf
的结果。有?谁知道呢?我们不知道1美元是从哪里来的
词法扫描程序填写语义值$1
。在扫描仪中这样做的正确方法如下(尽管实际模式可能包括下划线):
在这种情况下,语义值$1
将不够长,因为它将与复制标识符所需的长度一样长,不再需要。即使sprintf“似乎正常工作”,也会导致缓冲区溢出,随机内存将被覆盖。[注1]
那么,该怎么办?简单的解决方案是使用
asprintf
,它类似于sprintf
,只是它分配了一个新的缓冲区。使用该函数,您可以编写bison操作:
asprintf(&$$, "%s + %s\n", $3, $1);
(注意,&
:asprintf
需要指向char*
的指针,并返回分配的备忘录。)