Flex / Yacc 程序在 VC++ 中的自由指令上导致断点

Flex / Yacc program causing breakpoint on free instruction in VC++

我的 Flex / Yacc 程序在 VC++ 2012 IDE 中的 运行 时导致断点。断点出现在指令上(在下面的 pre_lxr.l 中):

free(pre_fname);

该项目包含词法分析器(.l 文件)、yacc 文件 (.y) 和接口文件(位于程序的解析器和 C++ 部分之间)。该问题似乎与内存损坏/泄漏有关。请参阅下面的代码(我已经包含了我认为必要的尽可能多的相关代码)。我也不确定 yyterminate 调用。

/* pre_prs_ifc.h */
#define MAX_PRE_ERR 120
#define MAX_BANKS 16
#define MAX_PRES 512

typedef struct
{
    char *bank;
    char *name;
} preStruct;

void initPrePrs();
void appBank(int line_num, const char *name);
void appPre(int line_num, const char *bank, const char *name);
void freePrePrs();

char pre_err[MAX_PRE_ERR];
int num_banks;
int num_pres;
char* pre_fname;
int pre_lnum;
char* bank[MAX_BANKS];
preStruct pre[MAX_PRES];

/* pre_prs_ifc.c */
#include "pre_prs_ifc.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void initPrePrs()
// initialises parser variables
{
    pre_err[0] = 0;
    num_banks = 0;
    num_pres = 0;
    pre_lnum = 1;
}

void appBank(int line_num, const char *name)
{
    if (num_banks < MAX_BANKS)
        bank[num_banks++] = _strdup(name);
    else
        sprintf(pre_err, "Error on line %d: maximum number of banks     
exceeded.", line_num);
}

void appPre(int line_num, const char *bank, const char *name)
{
    if (num_pres < MAX_PRES)
    {
        pre[num_pres].bank = _strdup(bank);
        pre[num_pres++].name = _strdup(name);
    }
    else
        sprintf(pre_err, "Error on line %d: maximum number of presets     
exceeded.", line_num);
}

void freePrePrs()
// frees parser variables
{
    int i;
    for (i = 0; i < num_banks; i++)
        free(bank[i]);
    for (i = 0; i < num_pres; i++)
    {
        free(pre[i].bank);
        free(pre[i].name);
    }
}

/* pre_lxr.l */
%{
    #include "pre_prs_ifc.h"
    #include "pre_prs.h"
    #include <stdio.h>
%}

%option outfile="pre_lxr.c" header-file="pre_lxr.h"
%option prefix="pre"
%option warn reentrant noyywrap never-interactive nounistd bison-bridge
%option debug

%%

"banks"|"BANKS" { return banks; }
"presets"|"PRESETS" { return presets; }

"{" { return l_bracket; }
"}" { return r_bracket; }
";" { return semicolon; }

\"([^\\"\n]|\.)+\" {
    yylval->str = (char*)malloc(strlen(yytext) - 1);
    strncpy(yylval->str, &yytext[1], strlen(yytext) - 2);
    return quoted_str;
}

[ \t] { }
[\r\n] { pre_lnum++; }

<<EOF>> {
    free(pre_fname);
    if (!YY_CURRENT_BUFFER)
        yyterminate();
}

%%
int preerror(const char *msg)
{
    sprintf(pre_err, "Error in %s on line %d.", pre_fname, pre_lnum);
    return 0;
}

/* pre_prs.y */
%{
    #include "pre_prs_ifc.h"
    #include "pre_prs.h"
    #include "pre_lxr.h"
    int preerror(yyscan_t scanner, const char *msg);
%}

%code requires
{
    #ifndef YYSCAN_T
        #define YYSCAN_T
        typedef void* yyscan_t;
    #endif
}

%output  "pre_prs.c"
%defines "pre_prs.h"
%name-prefix "pre"
%define api.pure
%lex-param   { yyscan_t scanner }
%parse-param { yyscan_t scanner }
%error-verbose

%union
{
    char ch;
    char* str;
}

%destructor { free($$); } <str>

%token<ch> l_bracket r_bracket semicolon
%token<str> quoted_str
%token<num> banks presets

%%
cmds:
    | cmds cmd
    ;

cmd:
    bank_list
    |
    pre_list
    ;

bank_list:
    banks l_bracket bank_decls r_bracket
    ;

bank_decls:
    | bank_decls bank_decl
    ;

bank_decl:
    quoted_str semicolon
    {
        fprintf(stderr, "append bank %s\n", );
        appBank(pre_lnum, );
        free();
    }
    ;

pre_list:
    presets l_bracket pre_decls r_bracket
    ;

pre_decls:
    | pre_decls pre_decl
    ;

pre_decl:
    quoted_str quoted_str semicolon
    {
        fprintf(stderr, "append preset, bank %s, name %s\n", , );
        appPre(pre_lnum, , );
        free();
        free();
    }
    ;

%%

/* C++ using the above code */
extern "C"
{
    #include "pre_prs_ifc.h"
    #include "pre_prs.h"
    #include "pre_lxr.h"
}
int yyparse(yyscan_t scanner);
...
bool CMainDlg::loadBankPre(std::string fname)
// load bank and preset lists
{
    int err, i;
    yyscan_t scanner;
    FILE *src;
    std::string name_str;
    CListBox* bank_lb;
    bool ret_val;
    pre_fname = _strdup(fname.c_str());
    if (prelex_init(&scanner))
    {
        m_err = "Error initialising scanner.";
        return false;
    }
    src = fopen(fname.c_str(), "r");
    if (src == NULL)
    {
        m_err = "Could not open file: " + fname;
        ret_val = false;
    }
    else
    {
        preset_in(src, scanner);
        initPrePrs();
        err = preparse(scanner);
        if (err)
        {
            m_err = pre_err;
            ret_val = false;
        }
        else
        {
            if (pre_err[0] == 0)
            {
                bank_lb = (CListBox*)GetDlgItem(IDC_LI_BANK);
                for (i = 0; i < num_banks; i++)
                    bank_lb->AddString(bank[i]);
                ret_val = true;
            }
            else
            {
                m_err = pre_err;
                ret_val = false;
            }
        }
        fclose(src);
    }
    pre_delete_buffer(0, scanner);
    prelex_destroy(scanner);
    return ret_val;
}
CMainDlg::~CMainDlg()
// destructor
{
    freePrePrs();
}

您应该永远不要在头文件中定义全局变量。头文件必须声明它们为extern,并且它们需要恰好在一个翻译单元中定义 .

正如所写,每个翻译单元都有自己的全局变量定义,包括pre_fname。这是未定义的行为,不能保证不同翻译单元中使用的名称指的是相同的存储位置。比修复声明更好的方法是将它们传递给解析器和扫描器,以避免使用全局变量。

无论如何,pre_fname 是在 CMainDlg::loadBankPre 中创建的(使用 strdup)并且在同一函数中也 free 它最有意义,理想情况下使用一个智能指针。在你这样做的过程中,你根本不需要 <<EOF>> 规则。

此外,strncpy 在这里真的不是一个好主意。正如@PaulOgilvie 在评论中指出的那样,它使复制的字符串未终止,结果 appBank 中的 strdup 将复制不确定数量的额外字节,可能引用无效内存。 (顺便说一句,您不需要在 yytext 上调用 strlen。Flex 正是为此目的提供了变量 yyleng,这节省了对令牌的额外扫描。)

另一方面,当您格式化为固定长度的缓冲区 (sprintf(pre_err...)) 时,您应该改用 snprintf。否则你可能会溢出 pre_err,最终可能会覆盖 pre_fname.

此外,您 preerror 的原型与其定义不符。所以如果它被调用,就会发生一些不好的事情。

最后,您的 <<EOF>> 规则(这是不必要的,见上文)不是 return 0,因此词法分析器将继续尝试扫描。 <<EOF>> 规则必须 return 0 或提供新的输入缓冲区。 (否则退出。)在这种情况下,由于 <<EOF>> 规则调用 free,规则的第二次(和后续)激活将多次释放相同的存储空间。

我发现 EOF 部分被反复调用导致无限循环,可以通过调用 yypop_buffer_state.

来停止该循环
<<EOF>> {
    yypop_buffer_state(yyscanner);
    free(pre_fname);
    if (!YY_CURRENT_BUFFER)
        yyterminate();
}

因此,免费在 EOF 部分附近第二次导致崩溃。正如 rici 所指出的(在这种情况下),如果将空闲部分移至函数 loadBankPre,则根本不需要 EOF 部分。