Flex & Bison

1. 文件結構

Flex和Bison的源代碼文件都是由定義規則用戶自定義函數三部分組成的,結構如下

{ definitions }
%%
{ rules }
%%
{ user subroutes }

2. 聯合使用Flex和Bison

Flex和Bison各有一份自己的源代碼,比如lexical.lsyntax.y,分別定義詞法規則語法規則,這兩份源代碼經過各自的處理後,分別生成一份詞法分析器語法分析器的源代碼。詞法分析器對文本進行初步處理後,向語法分析器返回一個個的詞法單元(token),語法分析器再做後續處理。

引用邏輯是這樣的,詞法分析器返回的詞法單元的定義,位於語法分析器中,而語法分析器需要使用詞法分析器中提供的接口。Flex生成的詞法分析器源代碼叫做lex.yy.c,爲了讓語法分析器能夠使用它提供的函數,我們直接在syntax.y的定義部分加上

%{ 
    #include "lex.yy.c"
%}

位於%{%}之間的代碼會被直接拷貝到生成的語法分析器中。然後我們使用bison -d syntax.y指令,會生成syntax.tab.hsyntax.tab.c,前者包含了詞法單元的定義,而後者即爲語法分析器的源代碼。爲了能在詞法分析器中使用這些詞法單元,我們在lexical.l的定義部分加上

%{
    #include "syntax.tab.h"
%}

再使用flex lexical.l即可生成詞法分析器的源代碼lex.yy.c。這份源代碼其實已經被syntax.tab.c包含了,所以需要編譯的文件只有syntax.tab.c和新添加的main.c。完整的源碼示例如下(MacOS,需要用-L指出fl庫文件的位置)

// lexical.l
%{
    #include <stdio.h>
    #include <stdlib.h>
    #include "syntax.tab.h" 
%}

%%

"+"     { return ADD; }
"-"     { return SUB; }
"*"     { return MUL; }
"/"     { return DIV; }
"|"     { return ABS; }
[0-9]+  { yylval = atoi(yytext); return NUM; }
\n      { return EOL; }
[ \t]   {  }
.       { printf("Invalid Character %c\n", *yytext); }

%%
void yyerror (char const *s)
{
	fprintf(stderr, "%s\n", s);
}

// syntax.y
%{
    #include <stdio.h>        
    #include "lex.yy.c"
%}

%token ADD SUB MUL DIV
%token ABS
%token NUM
%token EOL

%%

calclist:
    | calclist exp EOL { printf("= %d\n", $2); }
    ;

exp: factor { $$ = $1 }
    | exp ADD factor { $$ = $1 + $3; }
    | exp SUB factor { $$ = $1 - $3; }
    ;

factor: term { $$ = $1 }
    | factor MUL term { $$ = $1 * $3; }
    | factor DIV term { $$ = $1 / $3; }
    ;

term: NUM { $$ = $1 }
    | ABS term { $$ = $2 >= 0 ? $2 : -$2; }
    ;

%%

// main.c
int yyparse();

int main()
{
    yyparse();
}

# Makefile
build:
	bison -d syntax.y
	flex lexical.l
	gcc -L /usr/local/opt/flex/lib/ -lfl syntax.tab.c main.c
	rm syntax.tab.h syntax.tab.c lex.yy.c
clean:
	rm syntax.tab.h syntax.tab.c lex.yy.c

3. 問題

i. multiple definition of `main’

在CentOS上測試時候,發現Flex提供的libfl.a靜態庫文件中已經有main函數了,這時,將-l選項放在c源代碼之後就行了。即,編譯指令變爲

gcc syntax.tab.c main.c -lfl

而在MacOS下編譯時即使把-lfl放在源代碼之前也不會出現這種問題,可見它們鏈接時的處理模式是有差異的。

ii. libfl和liby

libflliby分別是flex和bison提供的庫文件,我們可以用objdump看一下里面的內容,先是libfl

In archive /usr/lib64/libfl.a:

libmain.o:     file format elf64-x86-64


Disassembly of section .text.startup:

0000000000000000 <main>:
   0:	48 83 ec 08          	sub    $0x8,%rsp
   4:	0f 1f 40 00          	nopl   0x0(%rax)
   8:	31 c0                	xor    %eax,%eax
   a:	e8 00 00 00 00       	callq  f <main+0xf>
   f:	85 c0                	test   %eax,%eax
  11:	75 f5                	jne    8 <main+0x8>
  13:	48 83 c4 08          	add    $0x8,%rsp
  17:	c3                   	retq   

libyywrap.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <yywrap>:
   0:	b8 01 00 00 00       	mov    $0x1,%eax
   5:	c3                   	retq  

然後是liby

In archive /usr/lib64/liby.a:

main.o:     file format elf64-x86-64


Disassembly of section .text.startup:

0000000000000000 <main>:
   0:	48 83 ec 08          	sub    $0x8,%rsp
   4:	be 00 00 00 00       	mov    $0x0,%esi
   9:	bf 06 00 00 00       	mov    $0x6,%edi
   e:	e8 00 00 00 00       	callq  13 <main+0x13>
  13:	48 83 c4 08          	add    $0x8,%rsp
  17:	e9 00 00 00 00       	jmpq   1c <main+0x1c>

yyerror.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <yyerror>:
   0:	48 83 ec 08          	sub    $0x8,%rsp
   4:	48 8b 35 00 00 00 00 	mov    0x0(%rip),%rsi        # b <yyerror+0xb>
   b:	e8 00 00 00 00       	callq  10 <yyerror+0x10>
  10:	48 8b 35 00 00 00 00 	mov    0x0(%rip),%rsi        # 17 <yyerror+0x17>
  17:	bf 0a 00 00 00       	mov    $0xa,%edi
  1c:	e8 00 00 00 00       	callq  21 <yyerror+0x21>
  21:	31 c0                	xor    %eax,%eax
  23:	48 83 c4 08          	add    $0x8,%rsp
  27:	c3                   	retq 

可見,而libfl.a這個靜態庫打包了libmain.o和libyywrap.o這兩個對象文件,裏面分別是main函數和yywrap函數;liby.a這個靜態庫打包了main.o和yyerror.o這兩個對象文件,裏面分別是main函數和yyerror這個函數。

yywrap是爲了切換文件使用的。詞法分析器達到文件末尾時,調用yywrap(),通過其返回值來判斷是否繼續分析——它返回0則繼續分析,返回1則結束——但是在返回0之前,yyin必須被指向一個新的文件。不幸的是,庫文件中的yywrap是一個naive的實現,做的事情不過是每次返回1。也就是說,如果我們使用yywrap來實現切換文件,這個函數也是要自己重新定義的。當然我們也可以在lexical.l的定義部分加上下面一行代碼,移除對於yywrap的調用,用其他方式實現文件的切換。

%option noyywrap

而yyerror是用來打印錯誤信息的,一般我們使用時都是自己重新定義,從而定製錯誤信息的打印方式。剩下的兩個main函數,除了簡單的測試,我們幾乎不會使用。也就是說,這兩個庫文件,我們完全可以不使用 😛

在上文中,我們的處理方式是,使用了naive版本的yywrap,而自定義了yyerror。自然地我們會想,也可以在編譯時加上-ly選項,使用默認的yyerror。但是這樣在C99中會有一個warning——問題就在於,在flex生成的lex.yy.c中,使用前先聲明瞭yywrap,而在bison生成的syntax.tab.c中直接使用了yyerror,使用前並未聲明。還有一個在lex.yy.c中使用的函數int fileno(FILE *stream)在CentOS7.4需要定義一些POSIX的參數,預編譯時纔會包含它的聲明(見下圖stdio.h中的代碼),否則也會報warning。

// stdio.h
#ifdef  __USE_POSIX
/* Return the system file descriptor for STREAM.  */
extern int fileno (FILE *__stream) __THROW __wur;
#endif /* Use POSIX.  */
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章