1. 文件結構
Flex和Bison的源代碼文件都是由定義,規則,用戶自定義函數三部分組成的,結構如下
{ definitions }
%%
{ rules }
%%
{ user subroutes }
2. 聯合使用Flex和Bison
Flex和Bison各有一份自己的源代碼,比如lexical.l和syntax.y,分別定義詞法規則和語法規則,這兩份源代碼經過各自的處理後,分別生成一份詞法分析器和語法分析器的源代碼。詞法分析器對文本進行初步處理後,向語法分析器返回一個個的詞法單元(token),語法分析器再做後續處理。
引用邏輯是這樣的,詞法分析器返回的詞法單元的定義,位於語法分析器中,而語法分析器需要使用詞法分析器中提供的接口。Flex生成的詞法分析器源代碼叫做lex.yy.c,爲了讓語法分析器能夠使用它提供的函數,我們直接在syntax.y的定義部分加上
%{
#include "lex.yy.c"
%}
位於%{
和%}
之間的代碼會被直接拷貝到生成的語法分析器中。然後我們使用bison -d syntax.y
指令,會生成syntax.tab.h和syntax.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
libfl和liby分別是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. */