C 野牛:1美元、2美元等的交替顺序给出了错误的输出

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给出:

我正在使用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给出: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*
    的指针,并返回分配的备忘录。)