日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

自己写编译器学习总结

發布時間:2025/4/14 55 豆豆
生活随笔 收集整理的這篇文章主要介紹了 自己写编译器学习总结 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

如何寫一個簡單的編譯器?

https://www.zhihu.com/question/36756224
初學編譯原理,想寫一個簡單的編譯器。

是時候亮出我的 LL 語言了,全稱:Lambda Lite Js。

LL 是一個類似 haskell 的函數式語言,使用 Javascript 實現。在線demo:Lambda-lite-js。項目地址:GitHub - moevis/lambda-lite-js: a tiny FUNCITONAL LANGUAGE implemented by javascript. 一個函數式語言,使用 js 實現。。

大概寫了兩周多,有了很多很有趣的功能了,比如可以玩 lambda 演算,可以柯里化,可以玩閉包,可以玩模式匹配,可以使用 Point-Free 風格,具有局部 lazy-evaluation 特性等等。

先給幾個階乘的示例代碼。

普通階乘
let fact = \n ->
? ? if n == 1 then 1 else n * (fact n - 1);
print $ fact 5;

利用模式匹配寫階乘:
let fact n@1 = 1;
let fact n@Number = n * (fact n - 1);
print $ fact 5;

不動點組合子階乘:
let z = \f -> (\x -> f (\y -> x x y)) (\x -> f (\y -> x x y));
let makeFact = \g -> \n -> if n < 2
? ? then 1
? ? else n * (g n - 1);
let fact = z makeFact;
print $ fact 5;

網頁版運行截圖:

是的,就是和 haskell 這么像。

=====說正事=====

之前我寫過一個 js 代碼高亮插件,用于高亮 html、js、css 代碼,GitHub - moevis/code-lighter: a lightweight code highlighting tool, 代碼高亮工具。原理是將代碼切分成一系列 tokens, 然后給這些 tokens 添加著色就好了。

至于如何切分 tokens 呢?Parser 就是負責這個事情的。我們先將輸入的代碼先分成以下幾個基本元素:
數字 Number
字面量 Literial
標點 Punctuar
空白符 White
文件結束符 EOF

我們的任務就是要區分它們,這個目標還是蠻容易的,我們使用遞歸下降法來解析,這就是一個狀態機的模型:

首先,我們的初始狀態機 State 處于默認狀態。

讀入待解析代碼,讀第一個字符 c ,這時候有五種可能:
c 屬于 '9' ~ '0',State 轉為數字模式。
c 屬于 'a' ~ 'z' 或者 'A' ~ 'Z',State 轉為字面量模式。
c 屬于 '*,;"+'... 的標點,State 轉為標點模式。
c 屬于 '\t' 或者 '\n' 或者空格,State 轉為空白符模式。
c 為空,則到了代碼的最后,State 為文件結束模式。

進入另一個模式后,我們會用不同的策略來接收下一個字符。比如,在數字模式假如下一個字符是 '0' ~ '9',那么讀入數字,并拼接到之前的數字后;直到接收到一個非數字的字符(這里我們就不考慮小數了)。當退出數字模式,我們就能得到一個數字的 token 啦。同理我們可以得到標點,字面量等等。

關于字面量,我們還需要做一些分類,字面量包括變量名,關鍵字,所以我們實現要有一個關鍵字表,當字面量不再關鍵字表里面,我們視為一個變量或者是函數名,否則就是有語法意義的字符比如 if else 。

初步的 tokens 切分結束后,去除空白 tokens,我們就得到一個 token 數組。

這時候我們要建立抽象語法樹 (Abstract syntax tree)了。先設計幾個基本語法,比如聲明一個函數或者變量時,使用`let`關鍵字,`if...else...then...`做分支判斷,聲明匿名函數使用類似'\n -> n + 1'的語法(有且只有一個參數 n ,n + 1 為返回值),表達式采用中綴 `1 + 2 + a`。

這時候我們可以定義幾個樹節點類型了:
聲明節點, defineNode (let x = 5)
匿名函數節點, lambdaNode (\n -> n + 1)
分支節點,conditionNode (if ... then ... else ...)
引用節點,代表某一個已聲明的變量值或者函數名 objectNode
數字節點,代表一個數字,numberNode
表達式節點, 代表某一中綴表達式,其本身儲存運算符,比如 a + b,節點儲存 `+`,expressNode
函數調用節點,表示對函數的調用,比如 print x,callNode

我們的任務就是將 tokens 轉為這些節點的相互關系。

首先還是讀入第一個 token,判斷 token 類型,如果是`let`,那么就按照聲明節點來解析; 如果是`\`則是一個匿名函數聲明節點,如果是`if`,則解析一個分支節點…… 解析表達式節點會難一點,因為有運算符的優先級,我參照了 llvm 教程中的 kaleidoscope 語言來寫(傳送門,Kaleidoscope: 實現解析器和抽象語法樹)。

總之這是需要你認真理解的一步,這一步完后,就可以根據語法樹來得到代碼運行結果了。每一種節點的結果計算方式都不同,但是有統一的接口 getValue()。比如聲明節點無返回值,匿名函數返回一個函數,分支節點先對 condition 求值,當 condition 為 true,返回第一個分支的 value,否則是第二個……

當這一切結束后,你的第一個語言就完成了。當然,現在我的語言已經比較復雜了,比如加入了模式匹配節點,聲明多參數函數的語法糖,改變結合方向等等。

你可以看我的 commit log 來看到我是如何一步一步添加這些功能的。

編譯原理這個方向,龍書虎書是經典,若要深入學習,一定要看。不過我也沒有時間啃下來,不過有一本書也是簡單有趣,叫做《計算的本質》,O‘REILY 出的,值得信賴。

說了那么多,我這個只能算是解釋器,并沒有真正編譯,當然對于初級已經夠了。你不滿足的話我再給你指條路,llvm 教程中有一個小語言叫做 kaleidoscope,你按照這個教程來,https://github.com/moevis/Kaleidoscope-LLVM-tutorial-zh-cn,這是我當年留下的中文翻譯坑,當時翻了一些后就去做實驗室任務了。這個從 parser 到 ast 到 ir 到各種優化都很全。

如果是用 Haskell 的話,三篇文章足矣。

prerequisite: 懂 state monad就行了

第一篇,《How to build a monadic interpreter in one day》http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.368.2522

跟著做,可以完成一個簡單的backend, 也就是架構基本的AST 并執行的步驟。

然后到frontend, 也就是parser的部分你就會發現你看不懂了, 這個時候看第二篇。(因為該文的 parser ?部分其實是 第二篇 的一個濃縮版,所以不看第二篇基本很難看懂這個部分)

第二篇,《Monadic Parser Combinator》 ,http://www.cs.nott.ac.uk/~pszgmh/monparsing.pdf

看了也就能對照第一篇,把 parser 寫出來了, 然后就能和之前的backend 組合,完成一個基本的,完全由自己手寫的,monadic的解釋器(parser 和 backend 分別由一個自定義的 state monad 實現)。順便加深對 monad 的理解。

看第二篇的時候,回過頭對照第一篇看效果會更高,雖然邏輯一樣,但第二篇是用 monad comprehension 的形式來寫, 第一篇是用 do notation 來寫。有的復雜的地方你兩種方式對照看一下,會有茅塞頓開的效果。

然后再看第三篇

第三篇,llvm的haskell 教程, Implementing a JIT Compiler with Haskell and LLVM ( Stephen Diehl ) ?, 把你的backend 換成llvm. (注:事先對 llvm 不熟的話,可以和 hackage 上面 llvm-general, llvm-general-pure 這兩個庫的 wiki, 以及 LLVM Language Reference Manual對照著看)

至于frontend, 可以換成Parsec之類,但也可以就不斷擴充之前自己寫的版本。

齊活兒~
------
今天閑著沒事兒,擼了一個 swift 的版本,GitHub - aaaron7/swift_monadic_parser: A simple haskell-style monadic combinator-based parser written in Swift. ? 僅包含最基本的 parser 和 interpreter 的功能,寫法完全按照前面兩篇文章的思路實現,有興趣可以看看。

這個題目有點久了,現在才想起來答,主要還是因為暑假才找到空閑的時間,把我的C11編譯器寫完了。

這個編譯器,(GitHub - wgtdkp/wgtcc: a tiny C compiler in C++) 一方面是為了學習 C11, 一方面為了練習C++。

在約11k代碼中實現了:

幾乎完整的C11語法解析(除去變長數組);
語義與類型檢查(仿gcc的錯誤提示)
預處理器
x86-64匯編代碼生成, 多謝?
@RednaxelaFX
?的回答寄存器分配問題? - 編譯器,把我從無休止的手動優化中拯救回來
精簡的x86-64 ABI
心形很流行嘛,wgtcc編譯的M大(?
@Milo Yip
?) 在 如何用C語言畫一個“心形”? - C(編程語言)回答里的代碼:

#include <stdio.h>
int main() {
? ? for (float y = 1.5f; y > -1.5f; y -= 0.1f) {
? ? ? ? for (float x = -1.5f; x < 1.5f; x += 0.05f) {
? ? ? ? ? ? float a = x * x + y * y - 1;
? ? ? ? ? ? putchar(a * a * a - x * x * y * y * y <= 0.0f ? '*' : ' ');
? ? ? ? } putchar('\n');
? ? }
}?

C11 中有一些非常實用或者好玩的新特性,如 compound literal. 一個典型的用途是當我們想獲得一個數據的另一種表示的時候, 我們可能會這么做:

float f = 1.5;
int i = *(int*)&f;?
然而gcc 在開 -O2 時會報 break strict-aliasing rules 的warning。 有了 compound literal, 我們可以這么做:

#define CAST(s_t, d_t, sv) \
? ? (union {s_t sv; d_t dv;}){sv}.dv
float f = 1.5;
int i = CAST(float, int, f);
而且這是一個模板呢~

C11 也支持在identifier 中使用unicode字符了,中文編程很exciting:

#define 整型 ? ?int
#define 輸出 ? ?printf
#define 面函數 ?main
#define 返回 ? ?return
#define 定義 ? ?typedef
#define 不可變 ?const
#define 字符 ? ?char
#define 指針 ? ?*
#define 為

定義 不可變 字符 指針 為 字面值;

整型 面函數() {
? 字面值 蛤蛤 = "\u82df\u5229\u56fd\u5bb6\u751f\u6b7b\u4ee5\uff0c"
? ? ? ? ? ? ? ?"\u5c82\u56e0\u7978\u798f\u907f\u8d8b\u4e4b";
? 輸出("%s\n", 蛤蛤);
? 返回 0;
}
這些例子在example/目錄下可以找到。

說說寫這個小編譯器總結的方法吧:

以最快的速度做到能夠解析下面這段代碼:
int main(int argc, char** argv) {
? ? ?int i;
? ? ?return 0;
}
以最快的速度看到hello,world。
開始對照語言標準一個一個實現特性,并同步做單元測試。因為已經看到hello world,這一步雖然工作量有點大,但是因為有了前面的經驗,可以很快。
解析聲明是最復雜的,所以先寫解析聲明。
龍書是必需的,一個好的參考也非常重要(如GitHub - rui314/8cc: A Small C Compiler)。
嘗試自舉(因為我用的C++,所以沒法自舉)。

寫一個編譯器的壞處是,以后寫一段代碼都試圖在腦子里面翻譯成匯編。。。

// Update_1

// Date: 09/20/2016

匆忙寫的回答,感覺僅僅是拋出結果和小結,實在是太不負責任了=-=。沒有實現過的同學可能還是不知如何入手,下面就寫寫我是如何一步一步做的吧(某些內容只限于C編譯器)

1. 初始狀態,你必須有一本第二版的龍書。其它的答案可能會推薦《編譯器實現》或者《編程語言實現模式》。《編譯器實現》因為中文翻譯的比較生硬,讀了幾章,發現還是龍書比較好,后來基本沒有看《編譯器實現》了。如果你是直接讀原版,那么還是推薦龍書,畢竟都有決心讀原版了,干脆徹底一點讀龍書原版好了^_^。其實龍書并沒有那么難,公式記不住,不理解,跳過就好了。《編程語言實現模式》其實很好的,各種實現方法都有所涉及。唯一不足的是,作者沒有向《代碼大全》的作者那樣,對我耳提面命 ----- 直接告訴我怎么做就好了(相信這是新手最需要的...)。

2. 你必須有C11 standard。open-std 上面的draft就夠了(http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1548.pdf)。(如果是其他語言的編譯器,對應之。如果是自己設計,那么應該學著standard那樣,將grammar,constraints等寫下來)

3. 一個簡單的不帶優化的編譯器,基本只需要3個步驟:詞法分析,語法分析,代碼生成;對應到代碼里面,就是3個class:Scanner, Parser, Generator;?

4. 對于C語言編譯器,支持預處理器,相當于又支持了一門新的簡單語言了。所以一開始不必去支持預處理器。在測試時,只使用不包含預處理器的代碼就好了。或者,用gcc進行預處理,將其輸出作為你的編譯器的輸入。

5. 在真的動手開始寫Scanner之前,有必要讀n1548的5.1節,對C源程序的編譯步驟有基本的了解。其實ad-hoc的詞法解析一點也不比寫一個split()函數更復雜,尤其是開始的時候不必去記錄 Source Location(當然后面如果寫錯誤提示,可以再加進來)。實現的時候,也不要吝惜內存了,直接將所有的token一次解析掉存起來就好。因為這對于后面Parser要回溯的時候會方便一點。

6. 已經得到Token List 之后,就可以開始做Parser部分了。暫時不做錯誤恢復,即遇到錯誤即exit。這里有一個很實用的設計建議:準備好下面這四個函數:

a. Peek() 返回下一個Token(只測試該Token,不向前移動Token List的offset指針)

b. Next() 消費下一個Token

c. Expect(expectedToken) , 這代替了下面的這段錯誤處理:

if (Peek() != expectedToken) {
? ? Error("expect %s, but got %s\n", expectedToken, Peek());
}?
d. Try(expectedToken), 這代替了下面這段代碼:

if (Peek() == expectedToken) {
? ? Next(); // 消費之
}?
? ? ?這很有用,因為Parser里面可能會有上百個這段代碼,在我的Parser里面,有84個Expect()調用,81個Peek()(Peek和Test), 39個Next(),62個Try()。相信我,這4個函數會讓你的代碼干凈一倍。

7. C的語言組成,大致可以分為 Expression, Declaration, Statement and Block 三個部分。這里面Statement and Block是最簡單的,Declaration難一點。按照我前面的心得體驗,應該從簡單的入手,但是我們非先做Declaration不可。因為Statements都是在函數內的啊,不搞定Declaration就沒法繼續了呢~ 其實,Declaration也好做,對著n1548的7.A.2.2節一個一個將grammar翻譯為Parser里面的邏輯就好了(是的,除去語義和類型檢查,Parser就是這么簡單)。做完Declaration,你還沒有往AST上添加任何node,是的,僅僅是Declaration,是沒有一行代碼需要生成的。所有的成就都在符號表里面。這里又有tip:暫時不要做Initializer,它有一點煩人的(C標準把它搞得好繁瑣)。

8. struct/union 類型;如果只是支持一個小小的子集,那么大可以跳過這一部分不做。struct會引入一些工作量,一方面,需要為tag(tag 和普通的identifier不在同一個命名空間)另開一個符號表(我使用一個小trick避免了這個麻煩);另一方面,它也是使Initializer變復雜的原因之一,尤其是匿名struct/union。tip:對struct/union的支持步驟是:普通嵌套的結構,匿名無tag的 struct成員,位域,union;這里把union放到最后是因為,它和struct除去存儲布局之外,別無二致(所以你甚至不必區分這兩個類型);你也可以在任何一步就開始支持union。

9. 數組和函數;除去作為sizeof關鍵字的操作數,數組總是會被cast成一個指針;你一定寫過這樣的代碼:

typedef void (*func_t)(void);
func_t f = func; f(); // 難道不應該是 (*f)(); ??
其實函數也是被cast成指針了,所以上面的調用方式都對,更微妙的是,任何一個函數調用都會被先cast成指針,再解引用(至少我的實現是這樣的);

10. storage 和 linkage;起初只實現所有的對象分配在棧空間;這會大大簡化代碼生成部分,因此對于“以最快速度看到hello world”是非常重要的;linkage對于前向聲明是重要的,如果你沒有打算支持,那么也可以跳過,等你看到hello world再回來支持,或者等你的函數和標準庫的沖突了。

11. expression;這個最簡單,該怎么樣就怎么樣=-=。tip:聯合賦值運算符:

a *= 5;
a = a * 5;?
是不是總是被告知效果和下面那行效果相等?那么不要害羞,就這么翻譯!(嗯,這么做是會產生bug(如果左值表達式有副作用),但是可以通過額外的檢查規避掉;對于帶優化的編譯器,這根本不是問題,因為它們怎么會對公共子表達式求兩遍值呢?)

12. statement;這是我最喜歡的部分,不僅僅是因為它簡單,而且讓我明白那些控制語句是怎么生成到匯編代碼的(對應請看龍書6.6和6.7節);如最簡單的while循環的展開:

/*
?* while (expression) statement ?
?* 展開后:?
?* cond: ?
?* ? ? ? if (expression) then?
?* ? ? ? ? ? empty?
?* ? ? ? else?
?* ? ? ? ? ? goto end?
?* statement?
?* goto cond?
?* end:?
?*/?
這里,我將 if 語句保留為基本的翻譯單元,因為將其他的控制結構翻譯為 if 語句會帶來很大的便利。tip:支持順序:if-else, while/do-while, for, switch-case;?

這些基本是一個C語言Parser的動手步驟了,現在你可以parse這段代碼了:

int main() {
? ? puts("hello world\n");
? ? return 0;
}
你可以手動將 puts插入到符號表以應付過去(某些builtin的函數還真就是這么干的),也可以在這個hello.c文件中聲明puts:

extern int puts(const char* str);?
或者你就要實現對struct/union的支持, 不然是沒有辦法 #include <stdio.h> 的了。這里沒有使用 printf,因為暫時沒有必要實現變參函數。

這樣,你與hello world只有一步之遙了:匯編代碼生成。

// TODO(wgtdkp): 匯編代碼生成

// End of update_1

// Update_2

// Date: 09/21/2016

因為按照"以最快的速度看到hello world"來做的話,語義檢查和類型檢查可以暫且放一放,或者只是實現parse過程中必不可少的一部分。下面是以x86-64 匯編代碼生成為例,說說代碼生成。這里又可以跳過中間代碼生成,直接由AST生成匯編代碼~

1. intel x86-64 手冊;顯然這是必需的,雖然我拿到這3000多頁的手冊時,也是虎軀一震的。不過,實實在在地講,我只看了大概30頁的內容;更多的時候,我是對照著gcc生成的匯編代碼照虎畫貓; tip:對于某些指令,如乘除法,移位,對照gcc進行翻譯是非常有效的;但你不應該企圖生成像gcc那么高效的代碼!(具體方法見下面)

2. System V x64 ABI;你必須至少看chapter 3(chapter 3就夠用了, 不過只有30多頁,放心吧);至少掌握stack frame的結構和對齊。注意x86-64的調用規約會稍微復雜一點,不過你可以做一些大膽的簡化:

?a. scalar type (除去struct/union,剩下的都是)按照 ABI里面的就好;

?b. struct/union 是比較復雜的,這里可以直接按照stack傳參(而不是寄存器傳參去做),畢竟又有多少代碼會直接傳遞struct/union 呢?等到你決意要做一個full featured的編譯器時,再來考慮它吧。

可以參考這里Introduction to X86-64 Assembly for Compiler Writers

3. visitor 模式;相信這個不必贅述,取決于怎么使用它,可以有很多作用;比如Parser中會需要做常量表達式求值,我們會用到它;獲得一個表達式的左值地址時,我們又需要用到它;(考慮你該如何翻譯賦值表達式)

4. 數據類型;在代碼生成這一步,我們的數據類型只有3種:整型,浮點型,struct/union(集合);我將能夠用通用寄存器存儲的數據稱為整型,包括指針;struct/union的處理比較特殊,對于一個類型為struct/union的表達式,visit該表達式,總是得到此struct/union對象的地址值(存儲于%rax寄存器中);只要我們在所有代碼中遵守這一規則,那么它確實很管用,即使會有一些冗余的拷貝;

5. 翻譯函數定義;一個函數的翻譯可以分為下面幾個步驟:

保存棧指針;
確定函數參數的位置(在哪個寄存器或者stack上的位置),將它們復制到當前stack frame的上,更新每個參數(object)的offset成員(object的地址)。更新當前stack frame的偏移量;
確定當前作用域內的object的地址, 這只需要掃描當前scope內的所有object,并線性地分配到stack frame上面就好;注意不包括內層scope內定義的object。這是一種優化,能夠充分利用棧空間,而且實現更簡單。更新當前的stack frame偏移量。
翻譯函數體;
在return 語句和函數結尾處,恢復棧指針并退出函數;

6. 翻譯函數調用;也可以分為下面幾個步驟:

確定函數地址;這可能是函數的名字,也可能是一個寄存器;這里就需要一個能夠計算表達式左值地址的 evaluator 了(后面會講到);
對函數參數求值,暫存之(push到stack上);
確定函數參數的位置,即,應該在哪個寄存器或stack的位置;拷貝參數值到對應位置;
調整棧指針以正確地對齊(這個很重要,不然會有segment fault的,都是淚);
調用之~

7. 翻譯賦值表達式;對左值表達式的賦值,需要獲得左值表達式的地址,而不是值;因此我們需要一個 LValGenerator 來取得左值表達式的地址,然后將右操作數的值load到該地址中;

8. 翻譯表達式;建議采用1-TOSCA方法,不懂的可以看看R大的回答寄存器分配問題? - 編譯器;這里的tip:不要被gcc生成的各種高效代碼蠱惑了而去做大量的手動優化,那是一個很大的坑,尤其是我們沒有生成中間代碼,是做不到全局寄存器分配的效果的。
========

第一個 C 語言編譯器是怎樣編寫的

http://www.csdn.net/article/2015-11-27/2826350
摘要:當今幾乎所有的實用的編譯器/解釋器(以下統稱編譯器)都是用C語言編寫的,有一些語言比如Clojure,Jython等是基于JVM或者說是用Java實現的,IronPython等是基于。
?
當今幾乎所有的實用的編譯器/解釋器(以下統稱編譯器)都是用C語言編寫的,有一些語言比如Clojure,Jython等是基于JVM或者說是用Java實現的,IronPython等是基于.NET實現的,但是Java和C#等本身也要依靠C/C++來實現,等于是間接調用了C。所以衡量某種高級語言的可移植性其實就是在討論ANSI/ISO C的移植性。


C語言是很低級的語言,很多方面都近似于匯編語言,在《Intel 32位匯編語言程序設計》一書中,甚至介紹了手工把簡單的C語言翻譯成匯編的方法。對于編譯器這種系統軟件,用C語言來編寫是很自然不過的,即使是像Python這樣的高級語言依然在底層依賴于C語言(舉Python的例子是因為Intel的黑客正在嘗試讓Python不需要操作系統就能運行——實際上是免去了BIOS上的一次性C代碼)。現在的學生,學過編譯原理后,只要有點編程能力的都可以實現一個功能簡單的類C語言編譯器。


可是問題來了,不知道你有沒有想過,大家都用C語言或基于C語言的語言來寫編譯器,那么世界上第一個C語言編譯器又是怎么編寫的呢?這不是一個“雞和蛋”的問題……


還是讓我們回顧一下C語言歷史:1970年Tomphson和Ritchie在BCPL(一種解釋型語言)的基礎上開發了B語言,1973年又在B語言的基礎上成功開發出了現在的C語言。在C語言被用作系統編程語言之前,Tomphson也用過B語言編寫過操作系統。可見在C語言實現以前,B語言已經可以投入實用了。因此第一個C語言編譯器的原型完全可能是用B語言或者混合B語言與PDP匯編語言編寫的。我們現在都知道,B語言的執行效率比較低,但是如果全部用匯編語言來編寫,不僅開發周期長、維護難度大,更可怕的是失去了高級程序設計語言必需的移植性。所以早期的C語言編譯器就采取了一個取巧的辦法:先用匯編語言編寫一個C語言的一個子集的編譯器,再通過這個子集去遞推完成完整的C語言編譯器。詳細的過程如下:


先創造一個只有C語言最基本功能的子集,記作C0語言,C0語言已經足夠簡單了,可以直接用匯編語言編寫出C0的編譯器。依靠C0已有的功能,設計比C0復雜,但仍然不完整的C語言的又一個子集C1語言,其中C0屬于C1,C1屬于C,用C0開發出C1語言的編譯器。在C1的基礎上設計C語言的又一個子集C2語言,C2語言比C1復雜,但是仍然不是完整的C語言,開發出C2語言的編譯器……如此直到CN,CN已經足夠強大了,這時候就足夠開發出完整的C語言編譯器的實現了。至于這里的N是多少,這取決于你的目標語言(這里是C語言)的復雜程度和程序員的編程能力——簡單地說,如果到了某個子集階段,可以很方便地利用現有功能實現C語言時,那么你就找到N了。下面的圖說明了這個抽象過程:


那么這種大膽的子集簡化的方法,是怎么實現的,又有什么理論依據呢?先介紹一個概念,“自編譯”Self-Compile,也就是對于某些具有明顯自舉性質的強類型(所謂強類型就是程序中的每個變量必須聲明類型后才能使用,比如C語言,相反有些腳本語言則根本沒有類型這一說法)編程語言,可以借助它們的一個有限小子集,通過有限次數的遞推來實現對它們自身的表述,這樣的語言有C、Pascal、Ada等等,至于為什么可以自編譯,可以參見清華大學出版社的《編譯原理》,書中實現了一個Pascal的子集的編譯器。總之,已經有計算機科學家證明了,C語言理論上是可以通過上面說的CVM的方法實現完整的編譯器的,那么實際上是怎樣做到簡化的呢?這張圖是不是有點熟悉?對了就是在講虛擬機的時候見到過,不過這里是CVM(C Language Virtual Machine),每種語言都是在每個虛擬層上可以獨立實現編譯的,并且除了C語言外,每一層的輸出都將作為下一層的輸入(最后一層的輸出就是應用程序了),這和滾雪球是一個道理。用手(匯編語言)把一小把雪結合在一起,一點點地滾下去就形成了一個大雪球,這大概就是所謂的0生1,1生C,C生萬物吧?


下面是C99的關鍵字:?
?
仔細看看,其實其中有很多關鍵字是為了幫助編譯器進行優化的,還有一些是用來限定變量、函數的作用域、鏈接性或者生存周期(函數沒有)的,這些在編譯器實現的早期根本不必加上,于是可以去掉auto, restrict, extern, volatile, const, sizeof, static, inline, register, typedef,這樣就形成了C的子集,C3語言,C3語言的關鍵字如下:?
?
再想一想,發現C3中其實有很多類型和類型修飾符是沒有必要一次性都加上去的,比如三種整型,只要實現int就行了,因此進一步去掉這些關鍵詞,它們是:unsigned, float, short, char(char 是 int), signed, _Bool, _Complex, _Imaginary, long,這樣就形成了我們的C2語言,C2語言關鍵字如下:
?
繼續思考,即使是只有18個關鍵字的C2語言,依然有很多高級的地方,比如基于基本數據類型的復合數據結構,另外我們的關鍵字表中是沒有寫運算符的,在C語言中的復合賦值運算符->、運算符的++、– 等過于靈活的表達方式此時也可以完全刪除掉,因此可以去掉的關鍵字有:enum, struct, union,這樣我們可以得到C1語言的關鍵字:?


接近完美了,不過最后一步手筆自然要大一點。這個時候數組和指針也要去掉了,另外C1語言其實仍然有很大的冗雜度,比如控制循環和分支的都有多種表述方法,其實都可簡化成一種,具體的來說,循環語句有while循環,do…while循環和for循環,只需要保留while循環就夠了;分支語句又有if…{}, if…{}…else, if…{}…else if…, switch,這四種形式,它們都可以通過兩個以上的if…{}來實現,因此只需要保留if,…{}就夠了。可是再一想,所謂的分支和循環不過是條件跳轉語句罷了,函數調用語句也不過是一個壓棧和跳轉語句罷了,因此只需要goto(未限制的goto)。因此大膽去掉所有結構化關鍵字,連函數也沒有,得到的C0語言關鍵字如下:
?
只有5個關鍵字,已經完全可以用匯編語言快速的實現了。通過逆向分析我們還原了第一個C語言編譯器的編寫過程,也感受到了前輩科學家們的智慧和勤勞!我們都不過是巨人肩膀上的灰塵罷了!0生1,1生C,C生萬物,實在巧妙!
========

學習較底層編程:動手寫一個C語言編譯器

http://blog.jobbole.com/77305/


動手編寫一個編譯器,學習一下較為底層的編程方式,是一種學習計算機到底是如何工作的非常有效方法。


編譯器通常被看作是十分復雜的工程。事實上,編寫一個產品級的編譯器也確實是一個龐大的任務。但是寫一個小巧可用的編譯器卻不是這么困難。


秘訣就是首先去找到一個最小的可用工程,然后把你想要的特性添加進去。這個方法也是Abdulaziz Ghuloum在他那篇著名的論文“一種構造編譯器的捷徑”里所提到的辦法。不過這個辦法確實可行。你只需要按照這篇論文中的第一步來操作,就可以得到一個真正可用的編譯器!當然,它只能編譯程序語言中的非常小的子集,但是它確實是一個真實可用的編譯器。你可以隨意的擴展這個編譯器,然后從中學到更多更深的知識。


受到這篇文章的鼓舞,我就寫了一個C編譯器。從某種意義上來說這比寫一個scheme的編譯器要困難一些(因為你必須去解析C那復雜的語法),但是在某些方面又很便利(你不需要去處理運行時類型)。要寫這樣一個編譯器,你只需要從你那個可用的最小的編譯器開始。


對于我寫的編譯器來說,我把它叫 babyc,我選了這段代碼來作為我需要運行的第一個程序:


C


int main() {
? ? return 2;
}


int main() {
? ? return 2;
}
沒有變量,沒有函數調用,沒有額外的依賴,甚至連if語句,循環語句都沒有,一切看起來是那么簡單。


我們首先需要解析這段代碼。我們將使用 Flex 和 Bison 來做到這點。這里有怎么用的例子可以參考,幸好我們的語法是如此簡單,下面就是詞法分析器:




"{" { return '{'; }
"}" { return '}'; }
"(" { return '('; }
")" { return ')'; }
";" { return ';'; }
[0-9]+ { return NUMBER; }
"return" { return RETURN; }
"int" { return TYPE; }
"main" { return IDENTIFIER; }


"{" { return '{'; }
"}" { return '}'; }
"(" { return '('; }
")" { return ')'; }
";" { return ';'; }
[0-9]+ { return NUMBER; }
"return" { return RETURN; }
"int" { return TYPE; }
"main" { return IDENTIFIER; }
這里是語法分析器:


function:
TYPE IDENTIFIER '(' ')' '{' expression '}'
;


expression:
RETURN NUMBER ';'
;


function:
TYPE IDENTIFIER '(' ')' '{' expression '}'
;
?
expression:
RETURN NUMBER ';'
;
最終,我們需要生成一些匯編代碼。我們將使用32位的X86匯編,因為它非常的通用而且可以很容易的運行在你的機器上。這里有X86匯編的相關網站。


下面就是我們需要生成的匯編代碼:


.text
? ? ? ? .global _start # Tell the loader we want to start at _start.


_start:
? ? ? ? movl ? ?$2,%ebx # The argument to our system call.
? ? ? ? movl ? ?$1,%eax # The system call number of sys_exit is 1.
? ? ? ? int ? ? $0x80 # Send an interrupt


.text
? ? ? ? .global _start # Tell the loader we want to start at _start.
?
_start:
? ? ? ? movl ? ?$2,%ebx # The argument to our system call.
? ? ? ? movl ? ?$1,%eax # The system call number of sys_exit is 1.
? ? ? ? int ? ? $0x80 # Send an interrupt
然后加上上面的詞法語法分析代碼,把這段匯編代碼寫進一個文件里。恭喜你!你已經是一個編譯器的編寫者了!


Babyc 就是這樣誕生的,你可以在這里看到它最開始的樣子。


當然,如果匯編代碼沒辦法運行也是枉然。讓我們來用編譯器生成我們所希望的真正的匯編代碼。


Shell


# Here's the file we want to compile.
$ cat return_two.c
#include <stdio.h>


int main() {
? ? return 2;
}


# Run the compiler with this file.
$ ./babyc return_two.c
Written out.s.


# Check the output looks sensible.
$ cat out.s
.text
? ? .global _start


_start:
? ? movl ? ?$2, %ebx
? ? movl ? ?$1, %eax
? ? int ? ? $0x80


# Here's the file we want to compile.
$ cat return_two.c
#include <stdio.h>
?
int main() {
? ? return 2;
}
?
# Run the compiler with this file.
$ ./babyc return_two.c
Written out.s.
?
# Check the output looks sensible.
$ cat out.s
.text
? ? .global _start
?
_start:
? ? movl ? ?$2, %ebx
? ? movl ? ?$1, %eax
? ? int ? ? $0x80
非常棒!接著讓我們來真正的運行一下編譯之后代碼來確保它能得到我們所想的結果。


Shell


# Assemble the file. We explicitly assemble as 32-bit
# to avoid confusion on x86_64 machines.
$ as out.s -o out.o --32


# Link the file, again specifying 32-bit.
$ ld -m elf_i386 -s -o out out.o


# Run it!
$ ./out


# What was the return code?
$ echo $?
2 # Woohoo!


# Assemble the file. We explicitly assemble as 32-bit
# to avoid confusion on x86_64 machines.
$ as out.s -o out.o --32
?
# Link the file, again specifying 32-bit.
$ ld -m elf_i386 -s -o out out.o
?
# Run it!
$ ./out
?
# What was the return code?
$ echo $?
2 # Woohoo!
我們踏出了第一步,接下去怎么做就全看你了。你可以按照那篇文章所指導的全部做一遍,然后制作一個更加復雜的編譯器。你需要去寫一個更加精巧的語法樹來生成匯編代碼。接下去的幾步分別是:(1)允許返回任意的值(比如,return 3; 一些可執行代碼);(2)添加對“非”的支持(比如,return ~1; 一些可執行代碼)。每一個額外的特性都可以教你關于C語言的更多知識,編譯器到底是怎么執行的,以及世界上其他編寫編譯器的人是如何想的。


這是構建 babyc 的方法。Babyc 現在已經擁有了if語句,循環,變量以及最基礎的數據結構。歡迎你來check out它的代碼,但是我希望看完我的文章你能夠自己動手寫一個。


不要害怕底層的一些事情。這是一個非常奇妙的世界。
========

Yacc 與 Lex 快速入門

https://www.ibm.com/developerworks/cn/linux/sdk/lex/
Lex 與 Yacc 介紹
Lex 和 Yacc 是 UNIX 兩個非常重要的、功能強大的工具。事實上,如果你熟練掌握 Lex 和 Yacc 的話,它們的強大功能使創建 FORTRAN 和 C 的編譯器如同兒戲。Ashish Bansal 為您詳細的討論了編寫自己的語言和編譯器所用到的這兩種工具,包括常規表達式、聲明、匹配模式、變量、Yacc 語法和解析器代碼。最后,他解釋了怎樣把 Lex 和 Yacc 結合起來。


Lex 代表 Lexical Analyzar。Yacc 代表 Yet Another Compiler Compiler。 讓我們從 Lex 開始吧。
Lex
Lex 是一種生成掃描器的工具。掃描器是一種識別文本中的詞匯模式的程序。 這些詞匯模式(或者常規表達式)在一種特殊的句子結構中定義,這個我們一會兒就要討論。
一種匹配的常規表達式可能會包含相關的動作。這一動作可能還包括返回一個標記。 當 Lex 接收到文件或文本形式的輸入時,它試圖將文本與常規表達式進行匹配。 它一次讀入一個輸入字符,直到找到一個匹配的模式。 如果能夠找到一個匹配的模式,Lex 就執行相關的動作(可能包括返回一個標記)。 另一方面,如果沒有可以匹配的常規表達式,將會停止進一步的處理,Lex 將顯示一個錯誤消息。
Lex 和 C 是強耦合的。一個 .lex 文件(Lex 文件具有 .lex 的擴展名)通過 lex 公用程序來傳遞,并生成 C 的輸出文件。這些文件被編譯為詞法分析器的可執行版本。


Lex 的常規表達式
常規表達式是一種使用元語言的模式描述。表達式由符號組成。符號一般是字符和數字,但是 Lex 中還有一些具有特殊含義的其他標記。 下面兩個表格定義了 Lex 中使用的一些標記并給出了幾個典型的例子。
用 Lex 定義常規表達式
字符 含義
A-Z, 0-9, a-z 構成了部分模式的字符和數字。
. 匹配任意字符,除了 \n。
- 用來指定范圍。例如:A-Z 指從 A 到 Z 之間的所有字符。
[ ] 一個字符集合。匹配括號內的 任意 字符。如果第一個字符是 ^ 那么它表示否定模式。例如: [abC] 匹配 a, b, 和 C中的任何一個。
* 匹配 0個或者多個上述的模式。
+ 匹配 1個或者多個上述模式。
? 匹配 0個或1個上述模式。
$ 作為模式的最后一個字符匹配一行的結尾。
{ } 指出一個模式可能出現的次數。 例如: A{1,3} 表示 A 可能出現1次或3次。
\ 用來轉義元字符。同樣用來覆蓋字符在此表中定義的特殊意義,只取字符的本意。
^ 否定。
| 表達式間的邏輯或。
"<一些符號>" 字符的字面含義。元字符具有。
/ 向前匹配。如果在匹配的模版中的“/”后跟有后續表達式,只匹配模版中“/”前 面的部分。如:如果輸入 A01,那么在模版 A0/1 中的 A0 是匹配的。
( ) 將一系列常規表達式分組。
常規表達式舉例
常規表達式 含義
joke[rs] 匹配 jokes 或 joker。
A{1,2}shis+ 匹配 AAshis, Ashis, AAshi, Ashi。
(A[b-e])+ 匹配在 A 出現位置后跟隨的從 b 到 e 的所有字符中的 0 個或 1個。
Lex 中的標記聲明類似 C 中的變量名。每個標記都有一個相關的表達式。 (下表中給出了標記和表達式的例子。) 使用這個表中的例子,我們就可以編一個字數統計的程序了。 我們的第一個任務就是說明如何聲明標記。
標記聲明舉例
標記 相關表達式 含義
數字(number) ([0-9])+ 1個或多個數字
字符(chars) [A-Za-z] 任意字符
空格(blank) " " 一個空格
字(word) (chars)+ 1個或多個 chars
變量(variable) (字符)+(數字)*(字符)*(數字)*
回頁首
Lex 編程
Lex 編程可以分為三步:
以 Lex 可以理解的格式指定模式相關的動作。
在這一文件上運行 Lex,生成掃描器的 C 代碼。
編譯和鏈接 C 代碼,生成可執行的掃描器。
注意: 如果掃描器是用 Yacc 開發的解析器的一部分,只需要進行第一步和第二步。 關于這一特殊問題的幫助請閱讀 Yacc和 將 Lex 和 Yacc 結合起來部分。
現在讓我們來看一看 Lex 可以理解的程序格式。一個 Lex 程序分為三個段:第一段是 C 和 Lex 的全局聲明,第二段包括模式(C 代碼),第三段是補充的 C 函數。 例如, 第三段中一般都有 main() 函數。這些段以%%來分界。 那么,回到字數統計的 Lex 程序,讓我們看一下程序不同段的構成。
回頁首
C 和 Lex 的全局聲明
這一段中我們可以增加 C 變量聲明。這里我們將為字數統計程序聲明一個整型變量,來保存程序統計出來的字數。 我們還將進行 Lex 的標記聲明。
字數統計程序的聲明
? ? ? ?%{
? ? ? ? int wordCount = 0;
? ? ? ? %}
? ? ? ? chars [A-za-z\_\'\.\"]
? ? ? ? numbers ([0-9])+
? ? ? ? delim [" "\n\t]
? ? ? ? whitespace {delim}+
? ? ? ? words {chars}+
? ? ? ? %%
兩個百分號標記指出了 Lex 程序中這一段的結束和三段中第二段的開始。
回頁首
Lex 的模式匹配規則
讓我們看一下 Lex 描述我們所要匹配的標記的規則。(我們將使用 C 來定義標記匹配后的動作。) 繼續看我們的字數統計程序,下面是標記匹配的規則。
字數統計程序中的 Lex 規則
? ? ? ?{words} { wordCount++; /*
? ? ? ? increase the word count by one*/ }
? ? ? ? {whitespace} { /* do
? ? ? ? nothing*/ }
? ? ? ? {numbers} { /* one may
? ? ? ? want to add some processing here*/ }
? ? ? ? %%


C 代碼
Lex 編程的第三段,也就是最后一段覆蓋了 C 的函數聲明(有時是主函數)。注意這一段必須包括 yywrap() 函數。 Lex 有一套可供使用的函數和變量。 其中之一就是 yywrap。 一般來說,yywrap() 的定義如下例。我們將在 高級 Lex 中探討這一問題。
字數統計程序的 C 代碼段
? ? ? ?void main()
? ? ? ? {
? ? ? ? yylex(); /* start the
? ? ? ? analysis*/
? ? ? ? printf(" No of words:
? ? ? ? %d\n", wordCount);
? ? ? ? }
? ? ? ? int yywrap()
? ? ? ? {
? ? ? ? return 1;
? ? ? ? }
上一節我們討論了 Lex 編程的基本元素,它將幫助你編寫簡單的詞法分析程序。 在 高級 Lex 這一節中我們將討論 Lex 提供的函數,這樣你就能編寫更加復雜的程序了。


將它們全部結合起來
.lex文件是 Lex 的掃描器。它在 Lex 程序中如下表示:
? ?$ lex <file name.lex>
這生成了 lex.yy.c 文件,它可以用 C 編譯器來進行編譯。它還可以用解析器來生成可執行程序,或者在鏈接步驟中通過選項 �ll 包含 Lex 庫。
這里是一些 Lex 的標志:
-c表示 C 動作,它是缺省的。
-t寫入 lex.yy.c 程序來代替標準輸出。
-v提供一個兩行的統計匯總。
-n不打印 -v 的匯總。


高級 Lex
Lex 有幾個函數和變量提供了不同的信息,可以用來編譯實現復雜函數的程序。 下表中列出了一些變量和函數,以及它們的使用。 詳盡的列表請參考 Lex 或 Flex 手冊(見后文的 資源)。
Lex 變量
yyin FILE* 類型。 它指向 lexer 正在解析的當前文件。
yyout FILE* 類型。 它指向記錄 lexer 輸出的位置。 缺省情況下,yyin 和 yyout 都指向標準輸入和輸出。
yytext 匹配模式的文本存儲在這一變量中(char*)。
yyleng 給出匹配模式的長度。
yylineno 提供當前的行數信息。 (lexer不一定支持。)
Lex 函數
yylex() 這一函數開始分析。 它由 Lex 自動生成。
yywrap() 這一函數在文件(或輸入)的末尾調用。 如果函數的返回值是1,就停止解析。 因此它可以用來解析多個文件。 代碼可以寫在第三段,這就能夠解析多個文件。 方法是使用 yyin 文件指針(見上表)指向不同的文件,直到所有的文件都被解析。 最后,yywrap() 可以返回 1 來表示解析的結束。
yyless(int n) 這一函數可以用來送回除了前�n? 個字符外的所有讀出標記。
yymore() 這一函數告訴 Lexer 將下一個標記附加到當前標記后。
對 Lex 的討論就到這里。下面我們來討論 Yacc...


Yacc
Yacc 代表 Yet Another Compiler Compiler。 Yacc 的 GNU 版叫做 Bison。它是一種工具,將任何一種編程語言的所有語法翻譯成針對此種語言的 Yacc 語 法解析器。它用巴科斯范式(BNF, Backus Naur Form)來書寫。按照慣例,Yacc 文件有 .y 后綴。編譯行如下調用 Yacc 編譯器:
? ? ? ?$ yacc <options>
? ? ? ? <filename ending with .y>
在進一步闡述以前,讓我們復習一下什么是語法。在上一節中,我們看到 Lex 從輸入序列中識別標記。 如果你在查看標記序列,你可能想在這一序列出現時執行某一動作。 這種情況下有效序列的規范稱為語法。Yacc 語法文件包括這一語法規范。 它還包含了序列匹配時你想要做的事。
為了更加說清這一概念,讓我們以英語為例。 這一套標記可能是:名詞, 動詞, 形容詞等等。為了使用這些標記造一個語法正確的句子,你的結構必須符合一定的規則。 一個簡單的句子可能是名詞+動詞或者名詞+動詞+名詞。(如 I care. See spot run.)
所以在我們這里,標記本身來自語言(Lex),并且標記序列允許用 Yacc 來指定這些標記(標記序列也叫語法)。
終端和非終端符號
終端符號 : 代表一類在語法結構上等效的標記。 終端符號有三種類型:
命名標記: 這些由 %token 標識符來定義。 按照慣例,它們都是大寫。
字符標記 : 字符常量的寫法與 C 相同。例如, -- 就是一個字符標記。
字符串標記 : 寫法與 C 的字符串常量相同。例如,"<<" 就是一個字符串標記。
lexer 返回命名標記。
非終端符號 : 是一組非終端符號和終端符號組成的符號。 按照慣例,它們都是小寫。 在例子中,file 是一個非終端標記而 NAME 是一個終端標記。
用 Yacc 來創建一個編譯器包括四個步驟:
通過在語法文件上運行 Yacc 生成一個解析器。
說明語法:
編寫一個 .y 的語法文件(同時說明 C 在這里要進行的動作)。
編寫一個詞法分析器來處理輸入并將標記傳遞給解析器。 這可以使用 Lex 來完成。
編寫一個函數,通過調用 yyparse() 來開始解析。
編寫錯誤處理例程(如 yyerror())。
編譯 Yacc 生成的代碼以及其他相關的源文件。
將目標文件鏈接到適當的可執行解析器庫。


用 Yacc 編寫語法
如同 Lex 一樣, 一個 Yacc 程序也用雙百分號分為三段。 它們是:聲明、語法規則和 C 代碼。 我們將解析一個格式為 姓名 = 年齡 的文件作為例子,來說明語法規則。 我們假設文件有多個姓名和年齡,它們以空格分隔。 在看 Yacc 程序的每一段時,我們將為我們的例子編寫一個語法文件。
回頁首
C 與 Yacc 的聲明
C 聲明可能會定義動作中使用的類型和變量,以及宏。 還可以包含頭文件。每個 Yacc 聲明段聲明了終端符號和非終端符號(標記)的名稱,還可能描述操作符優先級和針對不同符號的數據類型。 lexer (Lex) 一般返回這些標記。所有這些標記都必須在 Yacc 聲明中進行說明。
在文件解析的例子中我們感興趣的是這些標記:name, equal sign, 和 age。Name 是一個完全由字符組成的值。 Age 是數字。于是聲明段就會像這樣:
文件解析例子的聲明
? ? ? ?%
? ? ? ? #typedef char* string; /*
? ? ? ? to specify token types as char* */
? ? ? ? #define YYSTYPE string /*
? ? ? ? a Yacc variable which has the value of returned token */
? ? ? ? %}
? ? ? ? %token NAME EQ AGE
? ? ? ? %%
你可能會覺得 YYSTYPE 有點奇怪。但是類似 Lex, Yacc 也有一套變量和函數可供用戶來進行功能擴展。 YYSTYPE 定義了用來將值從 lexer 拷貝到解析器或者 Yacc 的 yylval (另一個 Yacc 變量)的類型。 默認的類型是 int。 由于字符串可以從 lexer 拷貝,類型被重定義為 char*。 關于 Yacc 變量的詳細討論,請參考 Yacc 手冊(見 資源)。


Yacc 語法規則
Yacc 語法規則具有以下一般格式:
? ? ? ?result: components { /*
? ? ? ? action to be taken in C */ }
? ? ? ? ;
在這個例子中,result 是規則描述的非終端符號。Components 是根據規則放在一起的不同的終端和非終端符號。 如果匹配特定序列的話 Components 后面可以跟隨要執行的動作。 考慮如下的例子:
? ? ? ?param : NAME EQ NAME {
? ? ? ? printf("\tName:%s\tValue(name):%s\n", $1,$3);}
? ? ? ? ? ? | NAME EQ VALUE{
? ? ? ? ? ? printf("\tName:%s\tValue(value):%s\n",$1,$3);}
? ? ? ? ;
如果上例中序列 NAME EQ NAME 被匹配,將執行相應的 { } 括號中的動作。 這里另一個有用的就是 $1 和 $3 的使用, 它們引用了標記 NAME 和 NAME(或者第二行的 VALUE)的值。 lexer 通過 Yacc 的變量 yylval 返回這些值。標記 NAME 的 Lex 代碼是這樣的:
? ? ? ?char [A-Za-z]
? ? ? ? name {char}+
? ? ? ? %%
? ? ? ? {name} { yylval = strdup(yytext);
? ? ? ? return NAME; }
文件解析例子的規則段是這樣的:
文件解析的語法
? ? ? ?file : record file
? ? ? ? | record
? ? ? ? ;
? ? ? ? record: NAME EQ AGE {
? ? ? ? printf("%s is now %s years old!!!", $1, $3);}
? ? ? ? ;
? ? ? ? %%


附加 C 代碼
現在讓我們看一下語法文件的最后一段,附加 C 代碼。 (這一段是可選的,如果有人想要略過它的話:)一個函數如 main() 調用 yyparse() 函數(Yacc 中 Lex 的 yylex() 等效函數)。 一般來說,Yacc 最好提供 yyerror(char msg) 函數的代碼。 當解析器遇到錯誤時調用 yyerror(char msg)。錯誤消息作為參數來傳遞。 一個簡單的 yyerror( char* ) 可能是這樣的:
? ? ? ?int yyerror(char* msg)
? ? ? ? {
? ? ? ? printf("Error: %s
? ? ? ? encountered at line number:%d\n", msg, yylineno);
? ? ? ? }
yylineno 提供了行數信息。
這一段還包括文件解析例子的主函數:
附加 C 代碼
? ? ? ?void main()
? ? ? ? {
? ? ? ? ? ? yyparse();
? ? ? ? }
? ? ? ? int yyerror(char* msg)
? ? ? ? {
? ? ? ? printf("Error: %s
? ? ? ? encountered \n", msg);
要生成代碼,可能用到以下命令:
? ? ? ?$ yacc _d <filename.y>
這生成了輸出文件 y.tab.h 和 y.tab.c,它們可以用 UNIX 上的任何標準 C 編譯器來編譯(如 gcc)。


命令行的其他常用選項
'-d' ,'--defines' : 編寫額外的輸出文件,它們包含這些宏定義:語法中定義的標記類型名稱,語義的取值類型 YYSTYPE, 以及一些外部變量聲明。如果解析器輸出文件名叫 'name.c', 那么 '-d' 文件就叫做 'name.h'。 如果你想將 yylex 定義放到獨立的源文件中,你需要 'name.h', 因為 yylex 必須能夠引用標記類型代碼和 yylval變量。
'-b file-prefix' ,'--file-prefix=prefix' : 指定一個所有Yacc輸出文件名都可以使用的前綴。選擇一個名字,就如輸入文件名叫 'prefix.c'.
'-o outfile' ,'--output-file=outfile' : 指定解析器文件的輸出文件名。其他輸出文件根據 '-d' 選項描述的輸出文件來命名。
Yacc 庫通常在編譯步驟中自動被包括。但是它也能被顯式的包括,以便在編譯步驟中指定 �ly選項。這種情況下的編譯命令行是:
? ? ? ?$ cc <source file
? ? ? ? names> -ly


將 Lex 與 Yacc 結合起來
到目前為止我們已經分別討論了 Lex 和 Yacc。現在讓我們來看一下他們是怎樣結合使用的。
一個程序通常在每次返回一個標記時都要調用 yylex() 函數。只有在文件結束或者出現錯誤標記時才會終止。
一個由 Yacc 生成的解析器調用 yylex() 函數來獲得標記。 yylex() 可以由 Lex 來生成或完全由自己來編寫。 對于由 Lex 生成的 lexer 來說,要和 Yacc 結合使用,每當 Lex 中匹配一個模式時都必須返回一個標記。 因此 Lex 中匹配模式時的動作一般格式為:
? ? ? ?{pattern} { /* do smthg*/
? ? ? ? return TOKEN_NAME; }
于是 Yacc 就會獲得返回的標記。當 Yacc 編譯一個帶有 _d 標記的 .y文件時,會生成一個頭文件,它對每個標記都有 #define 的定義。 如果 Lex 和 Yacc 一起使用的話,頭文件必須在相應的 Lex 文件 .lex中的 C 聲明段中包括。
讓我們回到名字和年齡的文件解析例子中,看一看 Lex 和 Yacc 文件的代碼。
Name.y - 語法文件
? ? ? ?%
? ? ? ? typedef char* string;
? ? ? ? #define YYSTYPE string
? ? ? ? %}
? ? ? ? %token NAME EQ AGE
? ? ? ? %%
? ? ? ? file : record file
? ? ? ? | record
? ? ? ? ;
? ? ? ? record : NAME EQ AGE {
? ? ? ? printf("%s is %s years old!!!\n", $1, $3); }
? ? ? ? ;
? ? ? ? %%
? ? ? ? int main()
? ? ? ? {
? ? ? ? yyparse();
? ? ? ? return 0;
? ? ? ? }
? ? ? ? int yyerror(char *msg)
? ? ? ? {
? ? ? ? printf("Error
? ? ? ? encountered: %s \n", msg);
? ? ? ? }
Name.lex - Lex 的解析器文件
? ? ? ?%{
? ? ? ? #include "y.tab.h"
? ? ? ??
? ? ? ? #include <stdio.h>
? ? ? ? #include <string.h>
? ? ? ? extern char* yylval;
? ? ? ? %}
? ? ? ? char [A-Za-z]
? ? ? ? num [0-9]
? ? ? ? eq [=]
? ? ? ? name {char}+
? ? ? ? age {num}+
? ? ? ? %%
? ? ? ? {name} { yylval = strdup(yytext);
? ? ? ? return NAME; }
? ? ? ? {eq} { return EQ; }
? ? ? ? {age} { yylval = strdup(yytext);
? ? ? ? return AGE; }
? ? ? ? %%
? ? ? ? int yywrap()
? ? ? ? {
? ? ? ? return 1;
? ? ? ? }
作為一個參考,我們列出了 y.tab.h, Yacc 生成的頭文件。
y.tab.h - Yacc 生成的頭文件
? ? ? ?# define NAME 257
? ? ? ? # define EQ 258
? ? ? ? # define AGE 259
我們對于 Lex 和 Yacc的討論到此為止。今天你想要編譯什么語言呢?
========

利用yacc和lex制作一個小的計算器

http://www.tuicool.com/articles/yqumyyi


由于本人是身處弱校,學校的課程沒有編譯原理這一門課,所以就想看這兩章,了解一下編譯原理,增加一下自己的軟實力。免得被別人鄙視。


一、安裝yacc和lex


我是在Windows下使用這兩個軟件的。所以使用bison代替yacc,用flex代替lex。兩者的下載地址是 http://sourceforge.net/projects/winflexbison/ 我的gcc環境是使用以前用過的mingw。我們吧解壓后的flex和bison放到mingw的bin目錄下。這一步就完成了。


二、編譯代碼


先編譯代碼,看一下結果,然后在分析。在這本書中提供的一個網址有書中代碼下載。下載地址 ?http://avnpc.com/pages/devlang ?下載后找到mycalc這個文件夾。然后執行下面進行編譯


1 bison --yacc -dv mycalc.y -o y.tab.c
2 flex mycalc.l
3 gcc -o mycalc y.tab.c lex.yy.c
三、yacc/lex是什么


一般編程語言的語法處理,都會有以下的過程。


1.詞法分析


將源代碼分割成若干個記號的處理。


2.語法分析


即從記號構建分析樹的處理。分析樹也叫作語法樹或抽象語法樹。


3.語義分析


經過語法分析生成的分析樹,并不包含數據類型等語義信息。因此在語義分析階段,會檢查程序中是否含有語法正確但是存在邏輯問題的錯誤。


4.生成代碼


如果是C語言等生成機器碼的編譯器或Java這樣生成字節碼的編譯器,在分析樹構建完畢后會進入代碼生成階段。


例如有下面源代碼


1 if(a==10)
2 {
3 ? ? printf("hoge\n");
4 }
5 else
6 {
7 ? ? printf("piyo\n");
8 }
執行詞法分析后,將被分割為如下的記號(每一塊就是一個記號)




對此進行語法分析后構建的分析樹,如下圖所示




執行詞法分析的程序稱為詞法分析器。lex的工作就是根據詞法規則自動生成詞法分析器。


執行語法分析的程序則稱為解析器。yacc就是根據語法規則自動生成解析器的程序。


四、分析計算器代碼


1.mycalc.l源代碼


?1 %{
?2 #include <stdio.h>
?3 #include "y.tab.h"
?4?
?5 int
?6 yywrap(void)
?7 {
?8 ? ? return 1;
?9 }
10 %}
11 %%
12 "+" ? ? ? ? ? ? return ADD;
13 "-" ? ? ? ? ? ? return SUB;
14 "*" ? ? ? ? ? ? return MUL;
15 "/" ? ? ? ? ? ? return DIV;
16 "\n" ? ? ? ? ? ?return CR;
17 ([1-9][0-9]*)|0|([0-9]+\.[0-9]*) {
18 ? ? double temp;
19 ? ? sscanf(yytext, "%lf", &temp);
20 ? ? yylval.double_value = temp;
21 ? ? return DOUBLE_LITERAL;
22 }
23 [ \t] ;
24 . {
25 ? ? fprintf(stderr, "lexical error.\n");
26 ? ? exit(1);
27 }
28 %%
第一行到第十行是一個定義區塊,lex中用 %{...}%定義,這里面代碼將原樣輸出。


第11行到第28行是一個規則區塊。語法大概就是前面一部分是使用正則表達式后面一部分是返回匹配到后這一部分是類型標記。大括號里面是動作。例如 ([1-9][0-9]*)|0|([0-9]+\.[0-9]*)是匹配小數,然后對這個小數進行sscanf處理后返回一個DOUBLE_LITERAL類型。


2.mycalc.y 源代碼


?1 %{
?2 #include <stdio.h>
?3 #include <stdlib.h>
?4 #define YYDEBUG 1
?5 %}
?6 %union {
?7 ? ? int ? ? ? ? ?int_value;
?8 ? ? double ? ? ? double_value;
?9 }
10 %token <double_value> ? ? ?DOUBLE_LITERAL
11 %token ADD SUB MUL DIV CR
12 %type <double_value> expression term primary_expression
13 %%
14 line_list
15 ? ? : line
16 ? ? | line_list line
17 ? ? ;
18 line
19 ? ? : expression CR
20 ? ? {
21 ? ? ? ? printf(">>%lf\n", $1);
22 ? ? }
23 expression
24 ? ? : term
25 ? ? | expression ADD term
26 ? ? {
27 ? ? ? ? $$ = $1 + $3;
28 ? ? }
29 ? ? | expression SUB term
30 ? ? {
31 ? ? ? ? $$ = $1 - $3;
32 ? ? }
33 ? ? ;
34 term
35 ? ? : primary_expression
36 ? ? | term MUL primary_expression?
37 ? ? {
38 ? ? ? ? $$ = $1 * $3;
39 ? ? }
40 ? ? | term DIV primary_expression
41 ? ? {
42 ? ? ? ? $$ = $1 / $3;
43 ? ? }
44 ? ? ;
45 primary_expression
46 ? ? : DOUBLE_LITERAL
47 ? ? ; ? ? ? ? ? ? ? ??
48 %%
49 int
50 yyerror(char const *str)
51 {
52 ? ? extern char *yytext;
53 ? ? fprintf(stderr, "parser error near %s\n", yytext);
54 ? ? return 0;
55 }
56?
57 int main(void)
58 {
59 ? ? extern int yyparse(void);
60 ? ? extern FILE *yyin;
61?
62 ? ? yyin = stdin;
63 ? ? if (yyparse()) {
64 ? ? ? ? fprintf(stderr, "Error ! Error ! Error !\n");
65 ? ? ? ? exit(1);
66 ? ? }
67 }
上面第13行到第48行,語法規則簡化為下面格式


A
? ? : B C
? ? | D
? ? ;
即A的定義是B與C的組合,或者為D。


上面的過程可以用一個游戲的方式解釋。就是一個數字是定位為DOUBLE_LITERAL類型,通過第45行的規則可以將DOUBLE_LITERAL升級成primary_expression類型,然后通過34行規則可以升級為term類型。又term類型可以升級為expression類型。所以 “2+4” 符合的規則是數字2升級到expression類型,而當數字4升級到term類型時,此時的狀態是 expression ADD term 通過第25行的規則可以得到兩者的結合,得到一個term類型。(PS:這個時候讓我想起了一個動漫,就是數碼寶貝,類型可以進行進化,進化,超進化,究極進化,還可以合體進化。<笑>)上面一個專業的叫法是叫做歸約。


由于歸約情況比較復雜和不好講,我就截書本上的原圖進行講解吧。


至于左結合或右結合是由寫的詞法分析器來決定的。例如給出的代碼,為什么是右結合呢,是因為用到了遞歸,所以會首先和低級的類型進行結合,這就是為什么MUL,DIV是term類型,ADD,SUB是expression類型,就是處理優先級的問題。


對于C或Java有這樣的一個問題


a+++++b;


我們可以分析為a++ + ++b 為什么編譯器還會報錯呢?是因為我們如果定義優先級的話,++優先級大于+。那么在代碼中就是實現為盡量使++在一起,而不是+優先,如果是+優先的話,那么每次都不會結合為++。所以代碼在詞法分析器階段代碼就會被分割成a ++ ++ + b ;這樣幾段。從而錯誤的。由于詞法分析器和解析器是各自獨立的。又因為詞法分析器先于語法分析器運行。


? 上面的過程就是這樣進行語法分析的。上面的過程雖然簡單,但是如果用代碼實現就有點困難了。我們使用yacc生成的執行文件就是對上面模擬的執行代碼,使用yacc自動生成的。如果我們要自制編程語言的話,那么這個過程就要自己寫了。因為有很多細節問題。不過不多說了,我們先了解這個就行。生成后的代碼文件是y.tab.c y.tab.h 。生成的代碼有幾十K呢,我們要了解這個過程還是比較難的。


五、用代碼實現詞法分析器


該代碼在calc/llparser目錄下


lexicalanalyzer.c




? 1 #include <stdio.h>
? 2 #include <stdlib.h>
? 3 #include <ctype.h>
? 4 #include "token.h"
? 5?
? 6 static char *st_line;
? 7 static int st_line_pos;
? 8?
? 9 typedef enum {
?10 ? ? INITIAL_STATUS,
?11 ? ? IN_INT_PART_STATUS,
?12 ? ? DOT_STATUS,
?13 ? ? IN_FRAC_PART_STATUS
?14 } LexerStatus;
?15?
?16 void
?17 get_token(Token *token)
?18 {
?19 ? ? int out_pos = 0;
?20 ? ? LexerStatus status = INITIAL_STATUS;
?21 ? ? char current_char;
?22?
?23 ? ? token->kind = BAD_TOKEN;
?24 ? ? while (st_line[st_line_pos] != '\0') {
?25 ? ? ? ? current_char = st_line[st_line_pos];
?26 ? ? ? ? if ((status == IN_INT_PART_STATUS || status == IN_FRAC_PART_STATUS)
?27 ? ? ? ? ? ? && !isdigit(current_char) && current_char != '.') {
?28 ? ? ? ? ? ? token->kind = NUMBER_TOKEN;
?29 ? ? ? ? ? ? sscanf(token->str, "%lf", &token->value);
?30 ? ? ? ? ? ? return;
?31 ? ? ? ? }
?32 ? ? ? ? if (isspace(current_char)) {
?33 ? ? ? ? ? ? if (current_char == '\n') {
?34 ? ? ? ? ? ? ? ? token->kind = END_OF_LINE_TOKEN;
?35 ? ? ? ? ? ? ? ? return;
?36 ? ? ? ? ? ? }
?37 ? ? ? ? ? ? st_line_pos++;
?38 ? ? ? ? ? ? continue;
?39 ? ? ? ? }
?40?
?41 ? ? ? ? if (out_pos >= MAX_TOKEN_SIZE-1) {
?42 ? ? ? ? ? ? fprintf(stderr, "token too long.\n");
?43 ? ? ? ? ? ? exit(1);
?44 ? ? ? ? }
?45 ? ? ? ? token->str[out_pos] = st_line[st_line_pos];
?46 ? ? ? ? st_line_pos++;
?47 ? ? ? ? out_pos++;
?48 ? ? ? ? token->str[out_pos] = '\0';
?49?
?50 ? ? ? ? if (current_char == '+') {
?51 ? ? ? ? ? ? token->kind = ADD_OPERATOR_TOKEN;
?52 ? ? ? ? ? ? return;
?53 ? ? ? ? } else if (current_char == '-') {
?54 ? ? ? ? ? ? token->kind = SUB_OPERATOR_TOKEN;
?55 ? ? ? ? ? ? return;
?56 ? ? ? ? } else if (current_char == '*') {
?57 ? ? ? ? ? ? token->kind = MUL_OPERATOR_TOKEN;
?58 ? ? ? ? ? ? return;
?59 ? ? ? ? } else if (current_char == '/') {
?60 ? ? ? ? ? ? token->kind = DIV_OPERATOR_TOKEN;
?61 ? ? ? ? ? ? return;
?62 ? ? ? ? } else if (isdigit(current_char)) {
?63 ? ? ? ? ? ? if (status == INITIAL_STATUS) {
?64 ? ? ? ? ? ? ? ? status = IN_INT_PART_STATUS;
?65 ? ? ? ? ? ? } else if (status == DOT_STATUS) {
?66 ? ? ? ? ? ? ? ? status = IN_FRAC_PART_STATUS;
?67 ? ? ? ? ? ? }
?68 ? ? ? ? } else if (current_char == '.') {
?69 ? ? ? ? ? ? if (status == IN_INT_PART_STATUS) {
?70 ? ? ? ? ? ? ? ? status = DOT_STATUS;
?71 ? ? ? ? ? ? } else {
?72 ? ? ? ? ? ? ? ? fprintf(stderr, "syntax error.\n");
?73 ? ? ? ? ? ? ? ? exit(1);
?74 ? ? ? ? ? ? }
?75 ? ? ? ? } else {
?76 ? ? ? ? ? ? fprintf(stderr, "bad character(%c)\n", current_char);
?77 ? ? ? ? ? ? exit(1);
?78 ? ? ? ? }
?79 ? ? }
?80 }
?81?
?82 void
?83 set_line(char *line)
?84 {
?85 ? ? st_line = line;
?86 ? ? st_line_pos = 0;
?87 }
?88?
?89 #if 1
?90 void
?91 parse_line(char *buf)
?92 {
?93 ? ? Token token;
?94?
?95 ? ? set_line(buf);
?96?
?97 ? ? for (;;) {
?98 ? ? ? ? get_token(&token);
?99 ? ? ? ? if (token.kind == END_OF_LINE_TOKEN) {
100 ? ? ? ? ? ? break;
101 ? ? ? ? } else {
102 ? ? ? ? ? ? printf("kind..%d, str..%s\n", token.kind, token.str);
103 ? ? ? ? }
104 ? ? }
105 }
106?
107 int
108 main(int argc, char **argv)
109 {
110 ? ? char buf[1024];
111?
112 ? ? while (fgets(buf, 1024, stdin) != NULL) {
113 ? ? ? ? parse_line(buf);
114 ? ? }
115?
116 ? ? return 0;
117 }
118 #endif
View Code
token.h


?1 #ifndef TOKEN_H_INCLUDED
?2 #define TOKEN_H_INCLUDED
?3?
?4 typedef enum {
?5 ? ? BAD_TOKEN,
?6 ? ? NUMBER_TOKEN,
?7 ? ? ADD_OPERATOR_TOKEN,
?8 ? ? SUB_OPERATOR_TOKEN,
?9 ? ? MUL_OPERATOR_TOKEN,
10 ? ? DIV_OPERATOR_TOKEN,
11 ? ? END_OF_LINE_TOKEN
12 } TokenKind;
13?
14 #define MAX_TOKEN_SIZE (100)
15?
16 typedef struct {
17 ? ? TokenKind kind;
18 ? ? double ? ? ?value;
19 ? ? char ? ? ? ?str[MAX_TOKEN_SIZE];
20 } Token;
21?
22 void set_line(char *line);
23 void get_token(Token *token);
24?
25 #endif /* TOKEN_H_INCLUDED */
View Code
上面使用的方法是DFA(確定有限狀態自動機)


上面的圖有些指向error的箭頭沒有標出,不過這個圖就大概描述了這個過程。可以自己baidu一些狀態機的知識。


有了這兩章的基礎就可以自己寫個分析器了(作用:以后寫應用程序時,要給程序一個配置文件時,可以自己寫個腳本進行解析,方便用戶書寫配置文件。不過現在都使用xml語法了,都還有解析的庫呢。都不知道學了以后還有沒有機會用到實際中呢)。不過循環和判斷就還不能實現。
========

總結

以上是生活随笔為你收集整理的自己写编译器学习总结的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

欧美国产亚洲精品久久久8v | 欧美美女激情18p | 国内视频在线观看 | 色婷五月天| 国内一区二区视频 | 成人一区电影 | 久久亚洲综合国产精品99麻豆的功能介绍 | 狠狠插狠狠干 | 一区二区视频电影在线观看 | 五月天综合网站 | 久久久www免费电影网 | 91视频麻豆视频 | 日本xxxxav| 中文字幕在线影院 | 国产手机在线观看 | 国产91学生粉嫩喷水 | 蜜臀久久99静品久久久久久 | 欧美三级在线播放 | 久色婷婷 | 国产福利一区二区三区在线观看 | 亚洲在线视频网站 | 国产在线不卡精品 | 国产 av 日韩| 成人黄性视频 | 精品96久久久久久中文字幕无 | 成人免费在线观看av | 欧美日韩一区二区三区不卡 | 蜜臀aⅴ精品一区二区三区 久久视屏网 | 伊人超碰在线 | 国产成人一区二区三区影院在线 | 免费男女羞羞的视频网站中文字幕 | 亚洲在线不卡 | 99爱这里只有精品 | 能在线观看的日韩av | www五月婷婷 | av在线一级| 超碰人人草 | 国产韩国日本高清视频 | 91完整版观看 | 免费黄色av电影 | 亚洲欧美视频网站 | 色综合久久精品 | 98精品国产自产在线观看 | 久久久久伊人 | 激情深爱 | 婷婷在线不卡 | 青青河边草免费视频 | 黄av免费| 色网影音先锋 | 日韩精品字幕 | 色干干| 日韩电影中文字幕在线 | 日本在线观看一区二区 | 国产精品九九热 | 亚洲欧美精品在线 | 国产又黄又爽无遮挡 | 久久五月激情 | 美女网站视频色 | 国产视频69 | 91在线视频观看 | 日韩网站在线 | 国产一级免费在线 | 国内视频一区二区 | 天堂av免费观看 | av大全在线看 | 五月婷婷视频在线 | av网站手机在线观看 | 久草在线免费电影 | 国产精品普通话 | 四虎影视久久久 | 日日摸日日碰 | www久| 亚洲精品在线看 | 在线日韩av | 精品国产一区二区三区不卡 | 香蕉视频国产在线 | 日韩成人精品一区二区三区 | 免费色视频网址 | 一区二区三区韩国免费中文网站 | 久久激情视频 久久 | 91av大全| 亚洲免费av一区二区 | 91九色国产视频 | 欧美日韩高清国产 | 国产精品美乳一区二区免费 | 亚洲视频综合 | 久久五月情影视 | 新版资源中文在线观看 | 日韩av一区二区三区四区 | 国产精品视频永久免费播放 | 成人av日韩 | 国产 欧美 在线 | 狠狠激情中文字幕 | 精品亚洲欧美一区 | 手机成人免费视频 | 免费观看黄色12片一级视频 | 国产在线999 | 久久精品2| 黄色大片入口 | 久久午夜免费视频 | 欧美日韩99 | 狠狠狠色 | 国产精品久久久久一区二区 | 五月天中文字幕mv在线 | 久久爱综合 | 五月婷婷国产 | 欧美少妇xxxxxx | 精品久久久成人 | 日韩精品欧美精品 | 开心婷婷色 | 91精品在线免费 | 97超碰超碰久久福利超碰 | 亚洲春色成人 | 精品国产中文字幕 | 久久久久久看片 | 国产综合在线观看视频 | 久青草影院 | 日韩欧美高清一区二区 | 久久国产精品二国产精品中国洋人 | 天天爽夜夜爽精品视频婷婷 | 欧美日韩视频免费 | 97av超碰 | 日韩在线网址 | 国产流白浆高潮在线观看 | 97看片吧| 亚洲做受高潮欧美裸体 | 最新中文在线视频 | 69精品 | 狠狠操狠狠干天天操 | 男女视频91 | 欧美久久久久久久久久 | 黄在线免费观看 | 91观看视频 | 日日夜夜天天射 | 97av精品| 亚洲精品乱码久久久久久高潮 | 亚洲无在线 | 国产精品免费一区二区三区 | 插婷婷| 97福利在线| 青草视频网 | 久久精品国产亚洲aⅴ | 国产精品久久久久久久婷婷 | 黄色av免费电影 | 国产黄色片网站 | 色综合久久久久综合体桃花网 | 91一区二区三区久久久久国产乱 | 日韩av午夜 | 久久久久欠精品国产毛片国产毛生 | 天天操天天射天天插 | 黄色中文字幕 | 粉嫩av一区二区三区免费 | 色妞久久福利网 | 麻豆视频在线免费看 | 在线观看中文字幕2021 | 又爽又黄又刺激的视频 | 精品国产伦一区二区三区观看体验 | 久久看视频| 国产99久久久国产精品成人免费 | 在线观看黄色av | 91欧美国产| 欧美日韩一区二区视频在线观看 | 69视频网站 | 麻豆精品传媒视频 | 婷婷av综合| 天天做天天爱天天综合网 | 成片免费观看视频大全 | 日韩av一区在线观看 | 国产丝袜制服在线 | 婷婷激情在线观看 | www.五月天 | 精品国产123| 久久久精品国产一区二区电影四季 | 69精品在线观看 | 久久国产91 | 国产精品久久久久免费 | 日韩精品一区二区不卡 | 色狠狠综合天天综合综合 | 欧洲一区二区三区精品 | 91视频88av| 五月婷婷激情 | 日韩精品视频在线免费观看 | 久久国产网| 综合网在线视频 | 久久免费看片 | 日韩免费在线网站 | 97电影网站 | 在线免费观看视频一区二区三区 | 波多野结衣电影一区 | 天天天综合 | 天天爱天天舔 | 香蕉视频4aa | 国产亚洲精品久久19p | 欧美激情精品久久久久 | 国产一卡久久电影永久 | 国产一级在线播放 | 国产精品大片在线观看 | 97视频播放 | 五月婷社区| 91av手机在线观看 | 国产成人在线播放 | 国产精品永久在线观看 | 久久69精品久久久久久久电影好 | 亚洲精品中文在线资源 | 久久久国产精品网站 | 国产精品九九视频 | 天天综合色网 | 久久久人人爽 | 成人黄色毛片视频 | 久久精品视频免费播放 | 欧美福利网站 | 国产精品小视频网站 | 日本色小说视频 | 国产91在线免费视频 | 日韩 在线观看 | 久久狠狠一本精品综合网 | 久久看看 | 欧美日韩亚洲在线观看 | 亚洲天堂视频在线 | 久保带人 | 国产高清一级 | 欧美在线视频一区二区三区 | 美女网站视频一区 | www黄色软件 | 人九九精品 | 亚洲情感电影大片 | 狠狠色丁婷婷日日 | 国产日韩在线一区 | 狠狠色狠狠色 | 国产主播大尺度精品福利免费 | 毛片无卡免费无播放器 | 国产精品美女视频网站 | 麻豆视频在线免费看 | 男女啪啪免费网站 | 欧美激情视频一区二区三区免费 | 午夜免费久久看 | 一区 二区电影免费在线观看 | 国产精品一区二区久久 | 天天搞天天干 | 国产精品视频大全 | 日韩av免费在线电影 | 日韩欧美在线观看 | 人人看97| 97在线免费视频 | 99精品国产福利在线观看免费 | 久久国产免 | 96国产精品| 亚洲 欧美日韩 国产 中文 | av在线在线| 九九久久精品视频 | 人人爱人人做人人爽 | 国产精品久久久精品 | 欧洲亚洲激情 | 久久综合久久综合这里只有精品 | 日韩性色 | 欧美一区日韩精品 | 黄色av在 | 激情欧美丁香 | 五月婷婷综合在线观看 | 97国产视频| 日韩视频在线播放 | www看片网站 | 日韩免费在线网站 | 不卡电影一区二区三区 | 亚洲精品乱码久久久久久 | 狠狠成人 | 国产精品久久久久久久久久久免费看 | 99人久久精品视频最新地址 | 亚洲综合狠狠干 | 国产精久久久久久妇女av | 久久精品国产亚洲aⅴ | 色 免费观看 | 91精品国产自产在线观看 | 黄色在线观看免费网站 | 超黄视频网站 | 中文字幕高清有码 | 2019中文字幕第一页 | 亚洲综合一区二区精品导航 | 日韩免费一二三区 | av东方在线 | 日日激情 | 美女网站一区 | 日韩一区精品 | 日韩一二三区不卡 | 九九在线国产视频 | 开心色激情网 | 日本久久精 | 91麻豆精品国产91久久久更新时间 | 中文字幕不卡在线88 | 亚洲japanese制服美女 | 成人亚洲精品国产www | 久久综合色播五月 | 看av免费 | 在线成人高清电影 | 最近中文字幕免费视频 | av中文天堂 | 婷婷亚洲激情 | 国产精品免费视频久久久 | 成人在线免费av | 欧美日韩综合在线观看 | 久久精品一级片 | 日韩精品aaa | 久久国内视频 | 久久激情五月丁香伊人 | 菠萝菠萝蜜在线播放 | 777视频在线观看 | 亚洲第一中文网 | 国产精品视频999 | 欧美日韩精品二区第二页 | 91成人免费观看视频 | 亚洲一区av | 狠狠躁日日躁夜夜躁av | 91在线91 | 国产日产欧美在线观看 | 狠狠躁日日躁狂躁夜夜躁av | av线上看 | 日本不卡视频 | 国产一区二区三区久久久 | 国产成人av在线影院 | 日日操日日插 | 91在线视频播放 | 国产精品女主播一区二区三区 | 中文字幕九九 | 国产麻豆精品久久一二三 | 国产视频资源在线观看 | 精品亚洲欧美一区 | 精品二区久久 | 国产高清在线精品 | 欧美色伊人 | 啪一啪在线 | www.色午夜.com | 色在线国产| 色福利网站 | 婷婷综合视频 | 国产精品网址在线观看 | 欧美色综合 | 播五月综合 | 激情欧美一区二区三区免费看 | 国产中文字幕免费 | 91麻豆精品国产 | 久久免费成人 | 色综合欧洲 | 国产精品视频最多的网站 | 国产精品永久在线观看 | 蜜桃视频日韩 | 久久99精品久久久久久秒播蜜臀 | 天天躁日日躁狠狠躁av中文 | a极黄色片| 久久精品欧美一区 | 91热爆视频 | 最近更新好看的中文字幕 | 99久久99视频只有精品 | 在线观看一区二区精品 | 一区二区三区日韩在线观看 | 亚洲国产av精品毛片鲁大师 | 成人在线播放网站 | 国产高清视频免费观看 | 亚洲国产大片 | 欧美日韩不卡在线观看 | 成人a级黄色片 | 69久久99精品久久久久婷婷 | 国产一二区在线观看 | 天堂av一区二区 | 日韩黄色免费在线观看 | 欧美嫩草影院 | 国内三级在线 | 国内精品久久久久久中文字幕 | 国产一级精品视频 | www.天天干.com | 韩国视频一区二区三区 | 91超在线 | 国内视频1区 | 五月香视频在线观看 | 在线免费观看的av网站 | 黄色av影院 | 中文字幕在线观看亚洲 | 97超碰免费在线 | 国产成人三级三级三级97 | av在线官网| 欧美成人精品三级在线观看播放 | 婷婷深爱五月 | 国产婷婷久久 | 国产美女视频黄a视频免费 久久综合九色欧美综合狠狠 | 天天操天天操天天操天天操 | 91免费观看网站 | 在线观看一区视频 | 国产精品激情在线观看 | 91禁在线观看 | 色橹橹欧美在线观看视频高清 | 精品在线视频一区 | 夜夜骑日日操 | 国产亚洲欧美精品久久久久久 | 免费在线观看av网站 | 91精品免费在线视频 | 日本成人中文字幕在线观看 | 伊人亚洲综合网 | 香蕉97视频观看在线观看 | 国产视频精品网 | 欧美一级黄色视屏 | 日本高清dvd | 亚洲黄色片在线 | 又黄又刺激| 日日夜夜添 | 欧美日韩3p | 国产精品成人久久 | 久草手机视频 | 欧美精品一区二区在线播放 | 午夜精品久久久久久久99无限制 | 久久免费黄色大片 | 探花视频免费在线观看 | 久久成人午夜视频 | 亚洲一区二区三区精品在线观看 | 国内成人综合 | 欧美日韩国产一区二 | 久久久天堂 | 五月天伊人 | 丝袜+亚洲+另类+欧美+变态 | 丁香激情综合久久伊人久久 | 亚洲国产精品500在线观看 | 91av视频 | 国产视频日韩 | 欧美日韩视频在线播放 | 黄色三级在线观看 | 96av在线视频| 久草在线中文视频 | 激情视频网页 | 中文久久精品 | 免费日韩高清 | 国产91亚洲精品 | 国产精品嫩草影院99网站 | 天天爽天天碰狠狠添 | 免费人成网 | 毛片www | 国产 视频 久久 | 伊人天堂av | 欧美少妇影院 | 久久综合欧美精品亚洲一区 | 五月激情久久 | 黄色av网站在线观看免费 | 丰满少妇高潮在线观看 | 天天射天天搞 | 欧美一区在线观看视频 | 亚洲h色精品 | 国内精品视频在线 | 久久国产精品99国产 | 久久久黄视频 | 日韩在线视频网 | 日韩av线观看 | 91正在播放 | 99久久99久久精品免费 | www.黄色| 亚洲成人精品久久 | 毛片网站在线看 | 国产精品网红直播 | 久久99中文字幕 | 日韩性xxx| 国产福利专区 | 日韩免费小视频 | 国产精品aⅴ | 精品亚洲二区 | 狠狠色丁香久久婷婷综合_中 | 不卡的一区二区三区 | 久久九九影视网 | 久久精品电影 | 91男人影院 | 丝袜网站在线观看 | 国产丝袜一区二区三区 | 亚洲性少妇性猛交wwww乱大交 | 亚洲精品91天天久久人人 | 亚洲人xxx | 久久不射电影院 | 国产免费久久av | 亚洲激情在线视频 | 久久6精品 | 又黄又刺激的视频 | 国产三级午夜理伦三级 | 久久免费视频99 | 婷婷精品 | 国产黄色在线看 | 日日日网 | 999久久久久 | 国产91学生粉嫩喷水 | 国产成人久久久77777 | 久久精品福利 | 亚洲免费不卡 | 久久国产免费视频 | 久久国产精品一区二区三区 | 狠狠干天天干 | 中文字幕免费播放 | 国产超碰在线 | 91资源在线视频 | 日韩av网址在线 | 97影视| 久久久久久免费 | 波多野结衣一区二区三区中文字幕 | 久久看毛片 | 天堂网一区二区 | 国产91九色蝌蚪 | 国产裸体无遮挡 | 欧美日韩一区二区三区免费视频 | 成人免费网站在线观看 | av在线免费观看网站 | 精品美女在线视频 | 国产精品一区二区免费在线观看 | av综合av| av中文字幕在线观看网站 | 国产一级黄色av | 人人爱人人做人人爽 | 一区二区中文字幕在线播放 | 日韩中字在线 | 中文字幕一区二区三区四区视频 | 亚洲精品美女在线 | 久久久久久美女 | 国产精品毛片久久久 | 国产精品综合在线观看 | 国产丝袜一区二区三区 | 中文字幕亚洲欧美 | 99在线精品免费视频九九视 | 狠狠的干狠狠的操 | 国产精品久久99综合免费观看尤物 | 香蕉视频在线看 | 国产一级一级国产 | 99热这里有 | 一区二区三区国产欧美 | av怡红院 | 激情久久综合网 | 成人黄大片 | 亚洲精品电影在线 | 黄色三级免费看 | 久久精品久久99精品久久 | 麻花豆传媒mv在线观看 | 狠狠干夜夜 | 国产小视频在线看 | 久草视频在线观 | 四虎永久网站 | 97电影在线观看 | 91热精品| 波多野结衣日韩 | 99久久日韩精品免费热麻豆美女 | 国产资源在线观看 | 欧美一区免费观看 | 一性一交视频 | 日韩欧美国产免费播放 | 精品亚洲一区二区三区 | 亚洲免费一级电影 | 在线观看视频三级 | 亚洲欧洲精品一区二区精品久久久 | 午夜婷婷网| 天天干天天操天天爱 | 久久国产精品一国产精品 | 日本性xxxxx 亚洲精品午夜久久久 | 免费情缘 | 美女国产精品 | 中文视频在线播放 | se视频网址 | 欧美a级在线免费观看 | 亚洲国产精品一区二区久久,亚洲午夜 | 91在线九色 | 亚洲九九九在线观看 | 亚洲无毛专区 | 欧美精品一区二区在线观看 | 日韩欧美一区二区三区免费观看 | 亚洲精品久久久蜜臀下载官网 | 在线导航福利 | 天天色天天 | 在线韩国电影免费观影完整版 | 欧美精品免费在线 | 亚洲欧美国内爽妇网 | 人人爽爽人人 | 蜜桃视频在线视频 | 99久久毛片| caobi视频 | 观看免费av| 免费黄色在线播放 | 国产精品久久久久久久久久久杏吧 | 国产精品免费观看视频 | 黄色在线看网站 | 九九导航| 国产色视频一区二区三区qq号 | 国产婷婷视频在线 | 亚洲六月丁香色婷婷综合久久 | 最新av在线免费观看 | 玖玖999 | 亚洲国产精品第一区二区 | 欧美福利网站 | 六月久久婷婷 | 亚洲无吗av | 91人人爽久久涩噜噜噜 | 精品国产视频一区 | 国产 一区二区三区 在线 | 成人av动漫在线观看 | 国产一卡久久电影永久 | 天天爱天天干天天爽 | 在线看免费 | 久久久91精品国产一区二区三区 | 国产精品国产亚洲精品看不卡15 | 日韩乱理 | 一区二区三区在线免费播放 | 亚州精品视频 | 欧美天天综合 | 欧美国产日韩一区 | 亚洲黄色高清 | 色婷婷88av视频一二三区 | 国产综合在线视频 | av在线免费观看不卡 | 日韩天堂网 | 色狠狠综合天天综合综合 | 欧美另类交在线观看 | 999久久国精品免费观看网站 | www.激情五月.com | 日韩区欠美精品av视频 | 插综合网 | 99久久精品国产欧美主题曲 | 欧美一级电影在线观看 | 日韩乱码在线 | 日韩欧美在线一区二区 | 成人福利av | 精品国产一二三 | 欧美a免费 | 麻豆传媒电影在线观看 | 欧美不卡视频在线 | 国产尤物在线观看 | 久久久久久久久影视 | 波多野结衣电影一区二区三区 | 综合色综合色 | 黄色国产精品 | 国产精品一二三 | 中文字幕在线高清 | 久草资源在线 | 日韩 在线 | 999视频在线播放 | 国产高清一 | 三级av在线免费观看 | 久久久久久网址 | 一区二区三区在线观看 | 国产理论一区二区三区 | 国产在线观看高清视频 | 亚洲理论电影网 | 五月黄色 | 992tv人人草 黄色国产区 | 国内精品中文字幕 | 欧美一二区视频 | 国产精品一区免费在线观看 | 热久久99这里有精品 | 91麻豆看国产在线紧急地址 | 日韩精品一区二区三区电影 | 九九在线精品视频 | 久久久久女教师免费一区 | 国产1区在线观看 | 日日夜夜骑 | 久久爽久久爽久久av东京爽 | 亚洲麻豆精品 | 五月婷亚洲 | 91网页版免费观看 | 在线观看的av网站 | 国产高清在线看 | 久久久久久免费毛片精品 | 国产精品综合久久久久久 | 日本电影黄色 | 欧美精品二 | 操操操操网 | 色五丁香| 日韩成片 | 色人久久 | 黄色影院在线免费观看 | 日一日干一干 | 九九激情视频 | 日本丶国产丶欧美色综合 | 久久国产精品一国产精品 | 精品二区久久 | 亚洲精品国产第一综合99久久 | 91中文字幕一区 | 天天色天天搞 | 天天天干天天射天天天操 | 日韩精品视频在线免费观看 | 日日夜夜噜噜噜 | 亚洲欧洲日韩在线观看 | www.国产在线视频 | 午夜18视频在线观看 | 996久久国产精品线观看 | 超碰99人人 | 国产在线一线 | 日韩在线中文字幕 | 欧美性免费 | 久久久久久久久久久国产精品 | 一区二区不卡高清 | 中文字幕日本在线 | 就要干b| 国产精品视频线看 | 国产精品亚洲视频 | 99在线热播 | 激情小说网站亚洲综合网 | 日本黄色一级电影 | 亚洲国产视频直播 | 日韩精品一区二区三区第95 | 国产麻豆果冻传媒在线观看 | 国产精品一区二区久久久 | 丁香五月亚洲综合在线 | 精品视频免费观看 | 字幕网av| 天天操天天添 | 十八岁免进欧美 | 在线精品一区二区 | 人人爽人人干 | 国产午夜精品一区二区三区在线观看 | 免费日韩精品 | 中文字幕色站 | 国产精品成久久久久三级 | 国产91免费观看 | 五月婷婷丁香色 | 久久久麻豆精品一区二区 | 久久精品日本啪啪涩涩 | 97碰碰碰| 午夜久久福利视频 | 少妇激情久久 | 91福利试看 | 开心婷婷色 | 超碰97人人爱 | 久久激情视频免费观看 | 片网址| 国产99久久久国产精品免费二区 | 天天撸夜夜操 | 婷婷六月天丁香 | 国产精品毛片一区二区 | av电影在线观看 | 狠狠色丁香婷婷综合视频 | 国产精品久久久久久久久久久久久久 | 人人爽人人乐 | 中文字幕在线观看免费高清电影 | 少妇激情久久 | 日韩欧美视频在线播放 | 中文字幕 在线 一 二 | 日韩免费成人 | 在线小视频 | 国产精品成人一区二区三区 | 久久久免费网站 | 人人爱人人爽 | 在线播放日韩av | 最新国产在线 | 9797在线看片亚洲精品 | av中文字幕av| 97视频在线观看播放 | 成人在线观看av | 欧美a级在线免费观看 | 国产小视频免费在线观看 | 欧洲激情在线 | 国产不卡视频在线播放 | 日韩免费电影一区二区 | 99国产视频| 国产精品一区电影 | 国产精品一区二区果冻传媒 | 亚洲日本va午夜在线电影 | 久av在线 | 91成人看片| 国产成人精品999在线观看 | 丁香五月缴情综合网 | 国产不卡精品 | 亚洲精品456在线播放乱码 | 久久久网| www.在线观看av | 亚洲综合在线发布 | 福利网址在线观看 | 西西大胆免费视频 | 国产做爰视频 | 久久精品福利 | 国产一区二区三区高清播放 | 国产自在线 | 911精品视频| 亚洲欧洲精品一区 | av一区二区三区在线观看 | 日p在线观看 | 色网站黄 | 日韩专区中文字幕 | 在线看av的网址 | 超碰个人在线 | 国产成人99久久亚洲综合精品 | 爱情影院aqdy鲁丝片二区 | 亚洲乱亚洲乱妇 | 国产裸体视频bbbbb | 亚洲资源在线观看 | 免费看污在线观看 | 国产又粗又硬又爽视频 | 国产一区二区在线精品 | 国产99久久久欧美黑人 | 亚洲精品在线网站 | 国内外成人在线 | 欧美最爽乱淫视频播放 | 亚洲精品视频二区 | 97国产在线视频 | 97成人精品区在线播放 | 国产视频在线观看一区 | 中文字幕亚洲精品日韩 | 天天碰天天操视频 | 国产日产精品一区二区三区四区的观看方式 | a级一a一级在线观看 | 黄网站免费久久 | 欧美激情第八页 | 超碰个人在线 | 黄色成人免费电影 | 亚洲精品在线视频播放 | 午夜婷婷网| 欧美男同视频网站 | 免费黄色a级毛片 | 国产高清视频免费在线观看 | 国产精品久久久久一区二区三区 | av超碰在线 | 亚洲精品国产电影 | 久草在线资源视频 | 欧美伦理电影一区二区 | 99爱这里只有精品 | 国内精品视频一区二区三区八戒 | 中文字幕在线视频免费播放 | 五月天婷婷在线观看视频 | 狠狠色丁香婷婷综合久小说久 | 久久精品一区二区三区视频 | 国产日韩精品一区二区三区在线 | 91网免费看 | 国产亚洲精品精品精品 | 日韩电影在线一区二区 | а天堂中文最新一区二区三区 | 美女黄频在线观看 | av无限看 | 久久久精品高清 | 国产精品色婷婷 | 亚洲一区日韩 | 中文字幕在线视频免费播放 | 91亚洲综合 | 国产一性一爱一乱一交 | 久久96国产精品久久99漫画 | 亚洲国产精品久久久久久 | 色视频国产直接看 | 99r在线精品| 日韩有码中文字幕在线 | 亚洲国产高清视频 | 九九热99视频 | 成人精品电影 | 美女国产 | 亚洲电影在线看 | 99精品视频在线免费观看 | 国产精品无av码在线观看 | 在线免费观看黄色av | 免费高清在线视频一区· | 干天天 | 欧美国产视频在线 | 国产久草在线观看 | 国产999精品久久久影片官网 | 精品999久久久 | 欧美午夜剧场 | 午夜av在线免费 | 九九九九九精品 | 欧美一区二区三区在线观看 | 国产成年免费视频 | 又粗又长又大又爽又黄少妇毛片 | 国产精品色视频 | 91麻豆精品国产91久久久使用方法 | 五月婷网| 视频二区 | 一区二区在线不卡 | 在线免费观看国产视频 | 中文字幕网站 | 色网站在线 | 在线视频中文字幕一区 | 色婷婷国产在线 | 欧美日韩观看 | 最近2019年日本中文免费字幕 | 久久久久久久久久福利 | 日本精品一区二区三区在线播放视频 | 四虎国产免费 | 99久久精品日本一区二区免费 | 国产我不卡 | 日本在线观看黄色 | 日韩在线视频国产 | 日韩av一卡二卡三卡 | 91在线永久 | av在线色| 国产正在播放 | 最近免费中文视频 | 国产精品国产亚洲精品看不卡15 | 夜色.com| 亚洲精品免费视频 | 久久刺激视频 | 91在线观看黄 | 激情av资源 | 国产精品69久久久久 | 中文字幕av免费观看 | 成人丝袜| 波多野结衣亚洲一区二区 | 综合五月 | 国产乱对白刺激视频在线观看女王 | 日韩欧美精品在线观看视频 | 久久激情小视频 | 91精品在线免费观看视频 | 精品久久五月天 | 欧美午夜激情网 | 伊人影院在线观看 | 国产一区二区在线免费播放 | 日本中文字幕在线视频 | av网址在线播放 | 精品视频99 | 韩国精品在线观看 | 又黄又爽又湿又无遮挡的在线视频 | 色射色 | 天天射天天 | 日日射天天射 | 国产精品欧美 | 欧美黑吊大战白妞欧美 | 在线激情网| 欧美韩国日本在线 | www婷婷| 成人国产精品 | 精品福利在线视频 | 综合色狠狠 | 欧美激情另类文学 | 日韩精品一区二区免费视频 | 欧美另类交在线观看 | 国产最新在线视频 | 91理论电影 | 国产99精品在线观看 | 91| 在线观看mv的中文字幕网站 | 综合久久婷婷 | 黄色软件在线观看 | 色噜噜狠狠狠狠色综合 | 日韩久久影院 | 二区精品视频 | 国产第一页福利影院 | 在线观看免费av网 | 在线观看黄色 | 天天干天天插 | 欧美日一级片 | 亚洲日本欧美在线 | 久久精品男人的天堂 | 国产精品久久电影网 | 欧美一二三四在线 | 国产精品永久免费视频 | 亚洲精品国产综合99久久夜夜嗨 | 亚洲有 在线 | 欧美孕妇与黑人孕交 | 日韩av免费在线电影 | 视频在线观看入口黄最新永久免费国产 | 国产精品一区二区白浆 | 精品国产美女在线 | 激情深爱 | 狠狠狠狠狠狠狠 | 91桃色在线观看视频 | 日本动漫做毛片一区二区 | 在线观看香蕉视频 | 欧美性大战久久久久 | 成人 亚洲 欧美 | 日本中文字幕在线一区 | 嫩草伊人久久精品少妇av | 在线观看爱爱视频 | 国产在线观看你懂的 | 毛片一级免费一级 | 日韩免费福利 | 国产三级久久久 | 欧美成年网站 | 国产婷婷一区二区 | 日韩av手机在线观看 | 草久在线观看视频 | 免费视频久久 | 国产品久精国精产拍 | 欧洲视频一区 | 久久99中文字幕 | 一区二区三区四区不卡 | 一区二区三区免费在线 | 97电影手机版| 中文不卡视频在线 | 国产二区视频在线观看 | 欧美久久99| 欧美污污网站 | 丁香六月婷婷 | 91日本在线播放 | 中文字幕在线中文 | 一区二区中文字幕在线观看 | 精品xxx| 欧洲精品在线视频 | 激情综合久久 | 天天干天天干天天 | 亚洲一区二区三区四区在线视频 | 国产首页| 视频三区| 久久免费在线观看视频 | 免费热情视频 | 玖玖玖在线观看 | www,黄视频| 色婷婷丁香 | 91精品视频导航 | 日本特黄一级片 | 99综合电影在线视频 | 国产精品久久久av | 人人超碰人人 | 99精品久久久久 | 国产精品免费看 | 精品一区二区久久久久久久网站 | 国产在线精品一区二区不卡了 | 玖玖视频国产 | 成人av影视在线 |