国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频

400 行 C 代碼實現(xiàn)一個虛擬機

共 16110字,需瀏覽 33分鐘

 ·

2022-02-26 00:12

鏈接:https://arthurchiao.art/blog/write-your-own-virtual-machine-zh/

1. 引言

本文將教你編寫一個自己的虛擬機(VM),這個虛擬機能夠運行匯編語言編寫的程序, 例如我朋友編寫的 2048 或者我自己的 Roguelike。如果你會編程,但希望 更深入地了解計算機的內(nèi)部原理以及編程語言是如何工作的,那本文很適合你。從零開始 寫一個虛擬機聽起來可能讓人有點望而生畏,但讀完本文之后你會驚訝于這件事原來如此簡 單,并從中深受啟發(fā)。

本文所說的虛擬機最終由 400 行左右 C 代碼組成。理解這些代碼只需要基本的 C/C++ 知識和二進制運算。這個虛擬機可以在 Unix 系統(tǒng)(包括 macOS)上執(zhí)行。代碼中包含少 量平臺相關的配置終端(terminal)和顯示(display)的代碼,但這些并不是本項目的核 心。(歡迎大家添加對 Windows 的支持。)

注意:這個虛擬機是Literate Programming 的產(chǎn)物。本文會解釋每段代碼的原理,最終的實現(xiàn)就是將這些代碼片段連起來。

什么是虛擬機?

虛擬機就像計算機(computer),它模擬包括 CPU 在內(nèi)的幾個硬件組件,能夠執(zhí)行 算術運算、讀寫內(nèi)存、與 I/O 設備交互。最重要的是,它能理解機器語言(machine language),因此可以用相應的語言來對它進行編程。

一個虛擬機需要模擬哪些硬件要看它的使用場景。有些虛擬機是設計用來模擬特定類型的計算設備 的,例如視頻游戲模擬器?,F(xiàn)在 NES 已經(jīng)不常見了,但我們還是可以用 NES 硬件模擬器來玩 NES 游戲。這些模擬器必須能忠實地 重建每一個細節(jié),以及原硬件的每個主要組件。

另外一些虛擬機則完全是虛構的,而非用來模擬硬件。這類虛擬機的主要用途是使軟件開發(fā) 更容易。例如,要開發(fā)一個能運行在不同計算架構上的程序,你無需使用每種架構特定的匯 編方言來實現(xiàn)一遍自己的程序,而只需要使用一個跨平臺的虛擬機提供的匯編語言。

注:編譯器也解決了類似的跨平臺問題,它將標準的高級語言編寫的程序編譯成能在不同 CPU 架構上執(zhí)行的程序。

相比之下,虛擬機的跨平臺方式是自己創(chuàng)建一個標準的 CPU 架 構,然后在不同的物理設備上模擬這個 CPU 架構。編譯器方式的優(yōu)點是沒有運行時開銷 (runtime overhead),但實現(xiàn)一個支持多平臺的編譯器是非常困難的,但實現(xiàn)一個虛擬 機就簡單多了。

在實際中,人們會根據(jù)需求的不同混合使用虛擬機和編譯器,因為二者工 作在不同的層次。

Java Virtual Machine (JVM) 就是一個非常成功的例子。JVM 本身是一個中等大小、程序員完全能夠看懂的程序,因此很 容易將它移植到包括手機在內(nèi)的上千種設備上。只要在設備上實現(xiàn)了 JVM,接下來任何 Java、Kotlin 或 Clojure 程序都無需任何修改就可以直接運行在這個設備上。唯一的開銷 來自虛擬機自身以及機器之上的 進一步抽象。大部分情況下,這完全是可以接受的。

虛擬機不必很大或者能適應各種場景,老式的視頻游戲 經(jīng)常使用很小的虛擬機來提 供簡單的腳本系統(tǒng)(scripting systems)。

虛擬機還適用于在一個安全的或隔離的環(huán)境中執(zhí)行代碼。一個例子就是垃圾回收(GC)。要 在 C 或 C++ 之上實現(xiàn)一個自動垃圾回收機制并不容易 ,因為程序無法看到它自身的?;蜃兞?。但是,虛擬機是在它運行的程序“之外”的,因此它能夠看到棧上所有的內(nèi)存引用 。

另一個例子是以太坊智能合約 (Ethereum smart contracts)。智能合約是在區(qū)塊鏈網(wǎng)絡中被驗證節(jié)點(validating node)執(zhí)行的小段程序。這就要求 人們在無法提前審查這些由陌生人編寫的代碼的情況下,直接他們的機器上執(zhí)行這些代碼。

為避免合約執(zhí)行一些惡意行為,智能合約將它們放到一個 虛擬機 內(nèi)執(zhí)行,這個虛擬機沒有權限訪問文件系統(tǒng)、網(wǎng)絡、磁盤等等資源。以太坊也很好地展現(xiàn)了虛擬機的可移植性特性,因為以太坊節(jié)點可以運行在多種計算機和操作系統(tǒng)上。使用虛擬機 使得智能合約的編寫無需考慮將在什么平臺運行。

2. LC-3 架構

我們的虛擬機將會模擬一個虛構的稱為 LC-3 的計算機。LC-3 在學校中比較流行,用于教學生如何用匯編編程。與 x86 相比 ,LC-3 的指令集更 加簡化,但現(xiàn)代 CPU 的主要思想其中都包括了。

我們首先需要模擬機器最基礎的硬件組件,嘗試來理解每個組件是做什么的,如果 現(xiàn)在無法將這些組件拼成一張完整的圖也不要著急。

2.1 內(nèi)存

LC-3 有 65,536 個內(nèi)存位置(16 bit 無符號整形能尋址的最大值),每個位置可以存儲一 個 16-bit 的值。這意味著它總共可以存儲 128KB 數(shù)據(jù)(64K * 2 Byte),比我們平時接觸 的計算機內(nèi)存小多了!在我們的程序中,這個內(nèi)存會以簡單數(shù)組的形式存放數(shù)據(jù):

/*?65536?locations?*/
uint16_t?memory[UINT16_MAX];

2.2 寄存器

一個寄存器就是 CPU 上一個能夠存儲單個數(shù)據(jù)的槽(slot)。寄存器就像是 CPU 的 “工作臺”(workbench),CPU 要對一段數(shù)據(jù)進行處理,必須先將數(shù)據(jù)放到某個寄存器中。但 因為寄存器的數(shù)量很少,因此在任意時刻只能有很少的數(shù)據(jù)加載到寄存器。計算機的解決辦 法是:首先將數(shù)據(jù)從內(nèi)存加載到寄存器,然后將計算結果放到其他寄存器,最后將最終結果 再寫回內(nèi)存。

LC-3 總共有 10 個寄存器,每個都是 16 比特。其中大部分都是通用目的寄存器,少數(shù)幾 個用于特定目的。

  • 8 個通用目的寄存器(R0-R7)
  • 1 個程序計數(shù)器(program counter, PC)寄存器
  • 1 個條件標志位(condition flags,COND)寄存器

通用目的寄存器可以用于執(zhí)行任何程序計算。程序計數(shù)器(PC)是一個無符號整數(shù),表示內(nèi) 存中將要執(zhí)行的下一條指令的地址。條件標記寄存器記錄前一次計算結果的正負符號。

enum?{
????R_R0?=?0,
????R_R1,
????R_R2,
????R_R3,
????R_R4,
????R_R5,
????R_R6,
????R_R7,
????R_PC,?/*?program?counter?*/
????R_COND,
????R_COUNT
};

和內(nèi)存一樣,我們也用數(shù)組來表示這些寄存器:

uint16_t?reg[R_COUNT];

2.3 指令集

一條指令就是一條 CPU 命令,它告訴 CPU 執(zhí)行什么任務,例如將兩個數(shù)相加。一條指令包 含兩部分:

  • 操作碼(opcode):表示任務的類型
  • 執(zhí)行任務所需的參數(shù)

每個操作碼代表 CPU “知道”的一種任務。在 LC-3 中只有 16 個操作碼。計算機能夠完成 的所有計算,都是這些簡單指令組成的指令流。每條指令 16 比特長,其中最左邊的 4 個 比特存儲的是操作碼,其余的比特存儲的是參數(shù)。

我們稍后會詳細介紹每條指令是做什么的,現(xiàn)在先定義下面的這些操作碼,確保它們 是按如下順序定義的,這樣每條指令就可以獲得正確的枚舉值:

enum?{
????OP_BR?=?0,?/*?branch?*/
????OP_ADD,????/*?add??*/
????OP_LD,?????/*?load?*/
????OP_ST,?????/*?store?*/
????OP_JSR,????/*?jump?register?*/
????OP_AND,????/*?bitwise?and?*/
????OP_LDR,????/*?load?register?*/
????OP_STR,????/*?store?register?*/
????OP_RTI,????/*?unused?*/
????OP_NOT,????/*?bitwise?not?*/
????OP_LDI,????/*?load?indirect?*/
????OP_STI,????/*?store?indirect?*/
????OP_JMP,????/*?jump?*/
????OP_RES,????/*?reserved?(unused)?*/
????OP_LEA,????/*?load?effective?address?*/
????OP_TRAP????/*?execute?trap?*/
};

注:Intel x86 架構有幾百條指令,而其他的架構例如 ARM 和 LC-3 只有很少的指令 。較小的指令集稱為精簡指令集(RISC),較大 的指令集稱為復雜指令集(CISC)。更大 的指令集本質(zhì)上通常并沒有提供新特性,只是使得編寫 匯編更加方便 。一條 CISC 指令能做的事情可能需要好幾條 RISC 才能完成。

但是,對設計和制造工程 師來說,CISC 更加復雜和昂貴,設計和制造業(yè)更貴。包括這一點在內(nèi)的一些權衡使得指 令設計也在不斷變化。

2.4 條件標志位

R_COND 寄存器存儲條件標記,其中記錄了最近一次計算的執(zhí)行結果。這使得程序可以完成諸如?if (x > 0) { ... }?之類的邏輯條件。

每個 CPU 都有很多條件標志位來表示不同的情形。LC-3 只使用 3 個條件標記位,用來 表示前一次計算結果的符號:

enum?{
????FL_POS?=?1?<0,?/*?P?*/
????FL_ZRO?=?1?<1,?/*?Z?*/
????FL_NEG?=?1?<2,?/*?N?*/
};

注:<< 和 >> 表示移位操作。

至此,我們就完成了虛擬機的硬件組件的模擬。

3. 匯編示例

下面通過一個 LC-3 匯編程序先來感受一下這個虛擬機運行的是什么代碼。這里無需知 道如何編寫匯編程序或者理解背后的工作原理,只是先直觀感受一下。下面是 “Hello World” 例子:

.ORIG x3000                        ; this is the address in memory where the program will be loaded
LEA R0, HELLO_STR ; load the address of the HELLO_STR string into R0
PUTs ; output the string pointed to by R0 to the console
HALT ; halt the program
HELLO_STR .STRINGZ "Hello World!" ; store this string here in the program
.END ; mark the end of the file

和 C 類似,這段程序從最上面開始,每次執(zhí)行一條聲明(statement)。但和 C 不同的是, 這里沒有作用域符號 {} 或者控制結構(例如 if 和 while),僅僅是一個扁平的聲 明列表(a flat list of statements)。這樣的程序更容易執(zhí)行。

注意,其中一些聲明中的名字和我們前面的定義的操作碼(opcodes)是一樣的。前面 介紹到,每條指令都是 16 比特,但這里的匯編程序看起來每行的字符數(shù)都是不一樣的。為什么會有這種不一致呢?

這是因為這些匯編聲明都是以人類可讀寫的格式編寫的,以純文本的形式表示。一種稱為 匯編器(assembler)的工具會將這些文本格式的指令轉換成 16 比特的二進制指令, 后者是虛擬機可以理解的。這種二進制格式稱為機器碼(machine code),是虛擬機可以 執(zhí)行的格式,其本質(zhì)上就是一個 16 比特指令組成的數(shù)組。

注:雖然在開發(fā)中編譯器(compiler)和匯編器(assembler)的角色是類似的,但二者 是兩個不同的工具。匯編器只是簡單地將程序員編寫的文本編碼(encode)成二進制格式 ,將其中的符號替換成相應的二進制表示并打包到指令內(nèi)。

.ORIG?和?.STRINGZ?看起來像是指令,但其實不是,它們稱為匯編制導命令 (assembler directives),可以生成一段代碼或數(shù)據(jù)。例如,.STRINGZ?會在它所在的 位置插入一段字符串。

循環(huán)和條件判斷是通過類似 goto 的指令實現(xiàn)的。下面是一個如何計時到 10 的例子:

AND R0, R0, 0                      ; clear R0
LOOP ; label at the top of our loop
ADD R0, R0, 1 ; add 1 to R0 and store back in R0
ADD R1, R0, -10 ; subtract 10 from R0 and store back in R1
BRn LOOP ; go back to LOOP if the result was negative
... ; R0 is now 10!

注:本文不需要讀者會編寫匯編代碼。但如果你感興趣,你可以使用 LC-3 工具來編寫和匯編你自己寫的匯編程序。

4. 執(zhí)行程序

前面的例子是給大家一個直觀印象來理解虛擬機在做什么。實現(xiàn)一個虛擬機不必精通匯編編 程,只要遵循正確的流程來讀取和執(zhí)行指令,任何 LC-3 程序都能夠正確執(zhí)行,不管這些程 序有多么復雜。理論上,這樣的虛擬機甚至可以運行一個瀏覽器或者 Linux 這樣的操作系 統(tǒng)。

如果深入地思考這個特性,你就會意識到這是一個在哲學上非常奇特的現(xiàn)象:程序能完成各種智能的事情,其中一些我們甚至都很難想象;但同時,所有這些程序最終都是用我們編 寫的這些少量指令來執(zhí)行的!我們既了解 —— 又不了解 —— 那些和程序執(zhí)行相關的的事情。圖靈 曾經(jīng)討探討過這種令人驚嘆的思想:

“The view that machines cannot give rise to surprises is due, I believe, to a fallacy to which philosophers and mathematicians are particularly subject. This is the assumption that as soon as a fact is presented to a mind all consequences of that fact spring into the mind simultaneously with it. It is a very useful assumption under many circumstances, but one too easily forgets that it is false.” — Alan M. Turing

過程(Procedure)

我們將編寫的這個過程(procedure)描述如下:

  • 1. 從 PC 寄存器指向的內(nèi)存地址中加載一條指令


  • 2. 遞增 PC 寄存器


  • 3. 查看指令中的 opcode 字段,判斷指令類型


  • 4. 根據(jù)指令類型和指令中所帶的參數(shù)執(zhí)行該指令


  • 5. 跳轉到步驟 1


你可能會有疑問:“如果這個循環(huán)不斷遞增 PC,而我們沒有 if 或 while,那程序不會 很快運行到內(nèi)存外嗎?”答案是不會,我們前面提到過,有類似 goto 的指令會通過修改 PC 來改變執(zhí)行流。

下面是以上流程的大致代碼實現(xiàn):

int?main(int?argc,?const?char*?argv[])?{
????{Load?Arguments,?12}
????{Setup,?12}

????/*?set?the?PC?to?starting?position?*/
????enum?{?PC_START?=?0x3000?};?/*?0x3000?is?the?default?*/
????reg[R_PC]?=?PC_START;

????int?running?=?1;
????while?(running)?{
????????uint16_t?instr?=?mem_read(reg[R_PC]++);?/*?FETCH?*/
????????uint16_t?op?=?instr?>>?12;

????????switch?(op)?{
????????????case?OP_ADD:?{ADD,?6}?break;
????????????case?OP_AND:?{AND,?7}?break;
????????????case?OP_NOT:?{NOT,?7}?break;
????????????case?OP_BR:?{BR,?7}?break;
????????????case?OP_JMP:?{JMP,?7}?break;
????????????case?OP_JSR:?{JSR,?7}?break;
????????????case?OP_LD:?{LD,?7}?break;
????????????case?OP_LDI:?{LDI,?6}?break;
????????????case?OP_LDR:?{LDR,?7}?break;
????????????case?OP_LEA:?{LEA,?7}?break;
????????????case?OP_ST:?{ST,?7}?break;
????????????case?OP_STI:?{STI,?7}?break;
????????????case?OP_STR:?{STR,?7}?break;
????????????case?OP_TRAP:?{TRAP,?8}?break;
????????????case?OP_RES:
????????????case?OP_RTI:
????????????default:
????????????????{BAD?OPCODE,?7}
????????????????break;
????????}
????}
????{Shutdown,?12}
}

5. 指令實現(xiàn)

現(xiàn)在需要做的就是正確地實現(xiàn)每一條指令。每條指令的詳細描述見 GitHub Repo 中附錄的 PDF 文檔。你需要 照著文檔的描述自己實現(xiàn)這些指令。這項工作做起來其實比聽起來要容易。下面我會拿其中 的兩個作為例子來展示如何實現(xiàn),其余的見下一章。

5.1 ADD

ADD 指令將兩個數(shù)相加,然后將結果存到一個寄存器中。關于這條指令的描述見 526 頁。ADD 指令的編碼格式如下:

這里給出了兩張圖是因為 ADD 指令有兩種不同的“模式”。在解釋模式之前,先來看看兩張 圖的共同點:

  • 1. 兩者都是以 0001 這 4 個比特開始的,這是 OP_ADD 的操作碼(opcode)


  • 2. 后面 3 個比特名為 DR(destination register),即目的寄存器,相加的結果會放到 這里


  • 3. 再后面 3 個比特是 SR1,這個寄存器存放了第一個將要相加的數(shù)字


至此,我們知道了相加的結果應該存到哪里,以及相加的第一個數(shù)字。只要再知道第二個數(shù) 在哪里就可以執(zhí)行加法操作了。從這里開始,這兩者模式開始不同:注意第 5 比特 ,這個標志位表示的是操作模式是立即模式(immediate mode)還是寄存器模式 (register mode)。在寄存器模式中,第二個數(shù)是存儲在寄存器中的,和第一個數(shù)類似。這個寄存器稱為 SR2,保存在第 0-2 比特中。第 3 和 第 4 比特沒用到。用匯編代碼描 述就是:

ADD R2 R0 R1 ; add the contents of R0 to R1 and store in R2.

在立即模式中,第二個數(shù)直接存儲在指令中,而不是寄存器中。這種模式更加方便,因 為程序不需要額外的指令來將數(shù)據(jù)從內(nèi)存加載到寄存器,直接從指令中就可以拿到這個值。這種方式的限制是存儲的數(shù)很小,不超過 2^5 = 32(無符號)。這種方式很適合對一個值 進行遞增。用匯編描述就是:

ADD R0 R0 1 ; add 1 to R0 and store back in R0

下面一段解釋來自 LC-3 規(guī)范:

If bit [5] is 0, the second source operand is obtained from SR2. If bit [5] is 1, the second source operand is obtained by sign-extending the imm5 field to 16 bits. In both cases, the second source operand is added to the contents of SR1 and the result stored in DR. (Pg. 526)

這段解釋也就是我們前面討論的內(nèi)容。但什么是 “sign-extending”(有符號擴展)?雖然立即 模式中存儲的值只有 5 比特,但這個值需要加到一個 16 比特的值上。因此,這些 5 比 特的數(shù)需要擴展到 16 比特才能和另一個數(shù)相匹配。對于正數(shù),我們可以在前面填充 0, 填充之后值是不變的。但是,對于負數(shù),這樣填充會導致問題。例如, -1 的 5 比特表示 是 11111。如果我們用 0 填充,那填充之后的 0000 0000 0001 1111 等于 32!這種 情況下就需要使用有符號擴展( sign extension),對于正數(shù)填充 0,對負數(shù)填充 1。

uint16_t?sign_extend(uint16_t?x,?int?bit_count)?{
????if?((x?>>?(bit_count?-?1))?&?1)?{
????????x?|=?(0xFFFF?<????}
????return?x;
}

注:如果你如何用二進制表示負數(shù)感興趣,可以查閱二進制補碼(Two’s Complement) 相關的內(nèi)容。本文中只需要知道怎么進行有符號擴展就行了。

規(guī)范中還有一句:

The condition codes are set, based on whether the result is negative, zero, or positive. (Pg. 526)

前面我們定義的那個條件標記枚舉類型現(xiàn)在要派上用場了。每次有值寫到寄存器時,我們 需要更新這個標記,以標明這個值的符號。為了方便,我們用下面的函數(shù)來實現(xiàn)這個功能:

void?update_flags(uint16_t?r)?{
????if?(reg[r]?==?0)?{
????????reg[R_COND]?=?FL_ZRO;
????}
????else?if?(reg[r]?>>?15)?{?/*?a?1?in?the?left-most?bit?indicates?negative?*/
????????reg[R_COND]?=?FL_NEG;
????}?else?{
????????reg[R_COND]?=?FL_POS;
????}
}

現(xiàn)在我們就可以實現(xiàn) ADD 的邏輯了:

{
????uint16_t?r0?=?(instr?>>?9)?&?0x7;?/*?destination?register?(DR)?*/
????uint16_t?r1?=?(instr?>>?6)?&?0x7;?/*?first?operand?(SR1)?*/
????uint16_t?imm_flag?=?(instr?>>?5)?&?0x1;?/*?whether?we?are?in?immediate?mode?*/

????if?(imm_flag)?{
????????uint16_t?imm5?=?sign_extend(instr?&?0x1F,?5);
????????reg[r0]?=?reg[r1]?+?imm5;
????}?else?{
????????uint16_t?r2?=?instr?&?0x7;
????????reg[r0]?=?reg[r1]?+?reg[r2];
????}

????update_flags(r0);
}

本節(jié)包含了大量信息,這里再總結一下:

  • ADD 接受兩個值作為參數(shù),并將計算結果寫到一個寄存器中
  • 在寄存器模式中,第二個值存儲在某個寄存器中
  • 在立即模式中,第二個值存儲在指令最右邊的 5 個比特中
  • 短于 16 比特的值需要執(zhí)行有符號擴展
  • 每次指令修改了寄存器后,都需要更新條件標志位(condition flags)

以上就是 ADD 的實現(xiàn),你可能會覺得以這樣的方式實現(xiàn)另外 15 個指令將會是一件非常繁 瑣的事情。好消息是,前面的這些函數(shù)基本都是可以重用的,因為另外 15 條指令中,大部 分都會組合有符號擴展、不同的模式和更新條件標記等等。

5.2 LDI

LDI 是 load indirect 的縮寫,用于從內(nèi)存加載一個值到寄存器,規(guī)范見 532 頁。LDI 的二進制格式如下:

與 ADD 相比,LDI 只有一種模式,參數(shù)也更少。LDI 的操作碼是 1010,對應 OP_LDI 枚舉類型。和 ADD 類似,它包含一個 3 比特的 DR(destination register)寄存器,用 于存放加載的值。剩余的比特組成 PCoffset9 字段,這是該指令內(nèi)嵌的一個立即值( immediate value),和 imm5 類似。由于這個指令是從內(nèi)存加載值,因此我們可以猜測 ,PCoffset9 是一個加載值的內(nèi)存地址。LC-3 規(guī)范提供了更多細節(jié):

An address is computed by sign-extending bits [8:0] to 16 bits and adding this value to the incremented PC. What is stored in memory at this address is the address of the data to be loaded into DR. (Pg. 532)

和前面一樣,我們需要將這個 9 比特的 PCoffset9 以有符號的方式擴展到 16 比特,但 這次是將擴展之后的值加到當前的程序計數(shù)器 PC(如果回頭去看前面的 while 循 環(huán),就會發(fā)現(xiàn)這條指令加載之后 PC 就會遞增)。相加得到的結果(也就是 PC 加完之后的 值)表示一個內(nèi)存地址,這個地址中存儲的值表示另一個地址,后者中存儲的是需要加載到 DR 中的值。

這種方式聽上去非常繞,但它確是不可或缺的。LD 指令只能加載 offset 是 9 位的地址, 但整個內(nèi)存是 16 位的。LDI 適用于加載那些遠離當前 PC 的地址內(nèi)的值,但要加載這 些值,需要將這些最終地址存儲在離 PC 較近的位置??梢詫⑺胂氤?C 中有一個局部變 量,這變量是指向某些數(shù)據(jù)的指針:

//?the?value?of?far_data?is?an?address
//?of?course?far_data?itself?(the?location?in?memory?containing?the?address)?has?an?address

char*?far_data?=?"apple";

//?In?memory?it?may?be?layed?out?like?this:

//?Address?Label??????Value
//?0x123:??far_data?=?0x456
//?...
//?0x456:??string???=?'a'

//?if?PC?was?at?0x100
//?LDI?R0?0x023
//?would?load?'a'?into?R0

和 ADD 類似,將值放到 DR 之后需要更新條件標志位:

The condition codes are set based on whether the value loaded is negative, zero, or positive. (Pg. 532)

下面是我對 LDI 的實現(xiàn)(后面章節(jié)中會介紹 mem_read):

{
????uint16_t?r0?=?(instr?>>?9)?&?0x7;?/*?destination?register?(DR)?*/
????uint16_t?pc_offset?=?sign_extend(instr?&?0x1ff,?9);?/*?PCoffset?9*/

????/*?add?pc_offset?to?the?current?PC,?look?at?that?memory?location?to?get?the?final?address?*/
????reg[r0]?=?mem_read(mem_read(reg[R_PC]?+?pc_offset));
????update_flags(r0);
}

后面會看到,這些指令的實現(xiàn)中,大部分輔助功能函數(shù)都是可以復用的。

以上是兩個例子,接下來就可以參考這兩個例子實現(xiàn)其他的指令。注意本文中有兩個指令是 沒有用到的:OP_RTI 和 OP_RES。你可以忽略這兩個指令,如果執(zhí)行到它們直接報錯。將 main() 函數(shù)中未實現(xiàn)的 switch case 補全后,你的虛擬機主體就完成了!

6. 全部指令的參考實現(xiàn)

本節(jié)給出所有指令的實現(xiàn)。如果你自己的實現(xiàn)遇到問題,可以參考這里給出的版本。

6.1 RTI & RES

這兩個指令本文沒用到。

abort();

6.2 Bitwise and(按位與)

{
????uint16_t?r0?=?(instr?>>?9)?&?0x7;
????uint16_t?r1?=?(instr?>>?6)?&?0x7;
????uint16_t?imm_flag?=?(instr?>>?5)?&?0x1;

????if?(imm_flag)?{
????????uint16_t?imm5?=?sign_extend(instr?&?0x1F,?5);
????????reg[r0]?=?reg[r1]?&?imm5;
????}?else?{
????????uint16_t?r2?=?instr?&?0x7;
????????reg[r0]?=?reg[r1]?&?reg[r2];
????}
????update_flags(r0);
}

6.3 Bitwise not(按位非)

{
????uint16_t?r0?=?(instr?>>?9)?&?0x7;
????uint16_t?r1?=?(instr?>>?6)?&?0x7;

????reg[r0]?=?~reg[r1];
????update_flags(r0);
}

6.4 Branch(條件分支)

{
????uint16_t?pc_offset?=?sign_extend((instr)?&?0x1ff,?9);
????uint16_t?cond_flag?=?(instr?>>?9)?&?0x7;
????if?(cond_flag?&?reg[R_COND])?{
????????reg[R_PC]?+=?pc_offset;
????}
}

6.5 Jump(跳轉)

RET 在規(guī)范中作為一個單獨的指令列出,因為在匯編中它是一個獨立的關鍵字。但是,RET 本質(zhì)上是 JMP 的一個特殊情況。當 R1 為 7 時會執(zhí)行 RET。

{
????/*?Also?handles?RET?*/
????uint16_t?r1?=?(instr?>>?6)?&?0x7;
????reg[R_PC]?=?reg[r1];
}

6.6 Jump Register(跳轉寄存器)

{
????uint16_t?r1?=?(instr?>>?6)?&?0x7;
????uint16_t?long_pc_offset?=?sign_extend(instr?&?0x7ff,?11);
????uint16_t?long_flag?=?(instr?>>?11)?&?1;

????reg[R_R7]?=?reg[R_PC];
????if?(long_flag)?{
????????reg[R_PC]?+=?long_pc_offset;??/*?JSR?*/
????}?else?{
????????reg[R_PC]?=?reg[r1];?/*?JSRR?*/
????}
????break;
}

6.7 Load(加載)

{
????uint16_t?r0?=?(instr?>>?9)?&?0x7;
????uint16_t?pc_offset?=?sign_extend(instr?&?0x1ff,?9);
????reg[r0]?=?mem_read(reg[R_PC]?+?pc_offset);
????update_flags(r0);
}

6.8 Load Register(加載寄存器)

{
????uint16_t?r0?=?(instr?>>?9)?&?0x7;
????uint16_t?r1?=?(instr?>>?6)?&?0x7;
????uint16_t?offset?=?sign_extend(instr?&?0x3F,?6);
????reg[r0]?=?mem_read(reg[r1]?+?offset);
????update_flags(r0);
}

6.9 Load Effective Address(加載有效地址)

{
????uint16_t?r0?=?(instr?>>?9)?&?0x7;
????uint16_t?pc_offset?=?sign_extend(instr?&?0x1ff,?9);
????reg[r0]?=?reg[R_PC]?+?pc_offset;
????update_flags(r0);
}

6.10 Store(存儲)

{
????uint16_t?r0?=?(instr?>>?9)?&?0x7;
????uint16_t?pc_offset?=?sign_extend(instr?&?0x1ff,?9);
????mem_write(reg[R_PC]?+?pc_offset,?reg[r0]);
}

6.11 Store Indirect(間接存儲)

{
????uint16_t?r0?=?(instr?>>?9)?&?0x7;
????uint16_t?pc_offset?=?sign_extend(instr?&?0x1ff,?9);
????mem_write(mem_read(reg[R_PC]?+?pc_offset),?reg[r0]);
}

6.12 Store Register(存儲寄存器)

{
????uint16_t?r0?=?(instr?>>?9)?&?0x7;
????uint16_t?r1?=?(instr?>>?6)?&?0x7;
????uint16_t?offset?=?sign_extend(instr?&?0x3F,?6);
????mem_write(reg[r1]?+?offset,?reg[r0]);
}

7. Trap Routines(中斷陷入例程)

LC-3 提供了幾個預定于的函數(shù)(過程),用于執(zhí)行常規(guī)任務以及與 I/O 設備交換, 例如,用于從鍵盤接收輸入的函數(shù),在控制臺上顯示字符串的函數(shù)。這些都稱為 trap routines,你可以將它們當做操作系統(tǒng)或者是 LC-3 的 API。每個 trap routine 都有一個對應的 trap code(中斷號)。要執(zhí)行一次捕獲, 需要用相應的 trap code 執(zhí)行 TRAP 指令。

定義所有 trap code:

enum?{
????TRAP_GETC?=?0x20,??/*?get?character?from?keyboard,?not?echoed?onto?the?terminal?*/
????TRAP_OUT?=?0x21,???/*?output?a?character?*/
????TRAP_PUTS?=?0x22,??/*?output?a?word?string?*/
????TRAP_IN?=?0x23,????/*?get?character?from?keyboard,?echoed?onto?the?terminal?*/
????TRAP_PUTSP?=?0x24,?/*?output?a?byte?string?*/
????TRAP_HALT?=?0x25???/*?halt?the?program?*/
};

你可能會覺得奇怪:為什么 trap code 沒有包含在指令編碼中?這是因為它們沒有給 LC-3 帶來任何新功能,只是提供了一種方便地執(zhí)行任務的方式(和 C 中的系統(tǒng)函數(shù)類似 )。在官方 LC-3 模擬器中,trap routines 是用匯編實現(xiàn)的。當調(diào)用到 trap code 時,PC 會移動到 code 對應的地址。CPU 執(zhí)行這個函數(shù)( procedure)的指令流,函數(shù)結束后 PC 重置到 trap 調(diào)用之前的位置。

注:這就是為什么程序從 0x3000 而不是 0x0 開始的原因。低地址空間是特意留出來 給 trap routine 用的。

規(guī)范只定義了 trap routine 的行為,并沒有規(guī)定應該如何實現(xiàn)。在我們這個虛擬機中, 將會用 C 實現(xiàn)。當觸發(fā)某個 trap code 時,會調(diào)用一個相應的 C 函數(shù)。這個函數(shù)執(zhí)行 完成后,執(zhí)行過程會返回到原來的指令流。

雖然 trap routine 可以用匯編實現(xiàn),而且物理的 LC-3 計算機也確實是這樣做的,但對虛 擬機來說并不是非常合適。相比于實現(xiàn)自己的 primitive I/O routines,我們可以利用操 作系統(tǒng)上已有的。這樣可以使我們的虛擬機運行更良好,還簡化了代碼,提供了一個便于移 植的高層抽象。

注:從鍵盤獲取輸入就是一個例子。匯編版本使用一個循環(huán)來持續(xù)檢查鍵盤有沒有輸入 ,這會消耗大量 CPU 而實際上沒做多少事情!使用操作系統(tǒng)提供的某個合適的輸入函 數(shù)的話,程序可以在收到輸入之前一直 sleep。

TRAP 處理邏輯:

switch?(instr?&?0xFF)?{
????case?TRAP_GETC:?{TRAP?GETC,?9}?break;
????case?TRAP_OUT:?{TRAP?OUT,?9}?break;
????case?TRAP_PUTS:?{TRAP?PUTS,?8}?break;
????case?TRAP_IN:?{TRAP?IN,?9}?break;
????case?TRAP_PUTSP:?{TRAP?PUTSP,?9}?break;
????case?TRAP_HALT:?{TRAP?HALT,?9}?break;
}

和前面幾節(jié)類似,我會拿一個 trap routine 作為例子展示如何實現(xiàn),其他的留給讀者自己 完成。

7.1 PUTS

PUT trap code 用于輸出一個以空字符結尾的字符串(和 C 中的 printf 類似)。規(guī) 范見 543 頁。

顯示一個字符串需要將這個字符串的地址放到 R0 寄存器,然后觸發(fā) trap。規(guī)范中說:

Write a string of ASCII characters to the console display. The characters are contained in consecutive memory locations, one character per memory location, starting with the address specified in R0. Writing terminates with the occurrence of x0000 in a memory location. (Pg. 543)

意思是說字符串是存儲在一個連續(xù)的內(nèi)存區(qū)域。注意這里和 C 中的字符串有所不同:C 中每個字符占用一個 byte;LC-3 中內(nèi)存尋找是 16 位的,每個字符都是 16 位,占用 兩個 byte。因此要用 C 函數(shù)打印這些字符,需要將每個值先轉換成 char 類型再輸出:

{
????/*?one?char?per?word?*/
????uint16_t*?c?=?memory?+?reg[R_R0];
????while?(*c)?{
????????putc((char)*c,?stdout);
????????++c;
????}
????fflush(stdout);
}

這就是 PUTS trap routine 的實現(xiàn)了。如果熟悉 C 的話,這個函數(shù)應該很容易理解。現(xiàn) 在你可以按照 LC-3 規(guī)范,自己動手實現(xiàn)其他的 trap routine 了。

8. Trap Routine 參考實現(xiàn)

本節(jié)給出所有 trap routine 的一份參考實現(xiàn)。

8.1 輸入單個字符(Input Character)

/*?read?a?single?ASCII?char?*/
reg[R_R0]?=?(uint16_t)getchar();

8.2 輸出單個字符(Output Character)

putc((char)reg[R_R0],?stdout);
fflush(stdout);

8.3 打印輸入單個字符提示(Prompt for Input Character)

printf("Enter?a?character:?");
char?c?=?getchar();
putc(c,?stdout);
reg[R_R0]?=?(uint16_t)c;

8.4 輸出字符串(Output String)

{
????/*?one?char?per?byte?(two?bytes?per?word)?here?we?need?to?swap?back?to
???????big?endian?format?*/

????uint16_t*?c?=?memory?+?reg[R_R0];
????while?(*c)?{
????????char?char1?=?(*c)?&?0xFF;
????????putc(char1,?stdout);
????????char?char2?=?(*c)?>>?8;
????????if?(char2)?putc(char2,?stdout);
????????++c;
????}
????fflush(stdout);
}

8.5 暫停程序執(zhí)行(Halt Program)

puts("HALT");
fflush(stdout);
running?=?0;

9. 加載程序

前面提到了從內(nèi)存加載和執(zhí)行指令,但指令是如何進入內(nèi)存的呢?將匯編程序轉換為 機器碼時,得到的是一個文件,其中包含一個指令流和相應的數(shù)據(jù)。只需要將這個文件的內(nèi) 容復制到內(nèi)存就算完成加載了。

程序的前 16 比特規(guī)定了這個程序在內(nèi)存中的起始地址,這個地址稱為 origin。因此 加載時應該首先讀取這 16 比特,確定起始地址,然后才能依次讀取和放置后面的指令及數(shù) 據(jù)。

下面是將 LC-3 程序讀到內(nèi)存的代碼:

void?read_image_file(FILE*?file)?{
????uint16_t?origin;?/*?the?origin?tells?us?where?in?memory?to?place?the?image?*/
????fread(&origin,?sizeof(origin),?1,?file);
????origin?=?swap16(origin);

????/*?we?know?the?maximum?file?size?so?we?only?need?one?fread?*/
????uint16_t?max_read?=?UINT16_MAX?-?origin;
????uint16_t*?p?=?memory?+?origin;
????size_t?read?=?fread(p,?sizeof(uint16_t),?max_read,?file);

????/*?swap?to?little?endian?*/
????while?(read--?>?0)?{
????????*p?=?swap16(*p);
????????++p;
????}
}

注意讀取前 16 比特之后,對這個值執(zhí)行了 swap16()。這是因為 LC-3 程序是大端 (big-endian),但現(xiàn)在大部分計算機都是小端的(little-endian),因此需要做大小端 轉換。如果你是在某些特殊的機器 (例如 PPC)上運行,那就不 需要這些轉換了。

uint16_t?swap16(uint16_t?x)?{
????return?(x?<8)?|?(x?>>?8);
}

注:大小端(Endianness)是指對于 一個整型數(shù)據(jù),它的每個字節(jié)應該如何解釋。在小端中,第一個字節(jié)是最低位,而在大端 中剛好相反,第一個字節(jié)是最高位。據(jù)我所知,這個順序完全是人為規(guī)定的。不同的公司 做出的抉擇不同,因此我們這些后來人只能針對大小端做一些特殊處理。要理解本文中大 小端相關的內(nèi)容,知道這些就足夠了。

我們再封裝一下前面加載程序的函數(shù),接受一個文件路徑字符串作為參數(shù),這樣更加方便:

int?read_image(const?char*?image_path)?{
????FILE*?file?=?fopen(image_path,?"rb");
????if?(!file)?{?return?0;?};
????read_image_file(file);
????fclose(file);
????return?1;
}

10. 內(nèi)存映射寄存器(Memory Mapped Registers)

某些特殊類型的寄存器是無法從常規(guī)寄存器表(register table)中訪問的。因此,在內(nèi) 存中為這些寄存器預留了特殊的地址。要讀寫這些寄存器,只需要讀寫相應的內(nèi)存地址。這些稱為 內(nèi)存映射寄存器(MMR)。內(nèi)存映射寄存器通常用于處理與特殊硬件的交互。

LC-3 有兩個內(nèi)存映射寄存器需要實現(xiàn),分別是:

  • KBSR:鍵盤狀態(tài)寄存器(keyboard status register),表示是否有鍵按下
  • KBDR:鍵盤數(shù)據(jù)寄存器(keyboard data register),表示哪個鍵按下了 雖然可以用 GETC 來請求鍵盤輸入,但這個 trap routine 會阻塞執(zhí)行,知道從鍵盤獲得 了輸入。KBSR 和 KBDR 使得我們可以輪詢設備的狀態(tài)然后繼續(xù)執(zhí) 行,因此程序不會阻塞。
enum?{
????MR_KBSR?=?0xFE00,?/*?keyboard?status?*/
????MR_KBDR?=?0xFE02??/*?keyboard?data?*/
};

內(nèi)存映射寄存器使內(nèi)存訪問稍微復雜了一些。這種情況下不能直接讀寫內(nèi)存位置,而要使 用 setter 和 getter 輔助函數(shù)。當獲取輸入時,getter 會檢查鍵盤輸入并更新兩 個寄存器(也就是相應的內(nèi)存位置)。

void?mem_write(uint16_t?address,?uint16_t?val)?{
????memory[address]?=?val;
}

uint16_t?mem_read(uint16_t?address)
{
????if?(address?==?MR_KBSR)?{
????????if?(check_key())?{
????????????memory[MR_KBSR]?=?(1?<15);
????????????memory[MR_KBDR]?=?getchar();
????????}?else?{
????????????memory[MR_KBSR]?=?0;
????????}
????}
????return?memory[address];
}

這就是我們的虛擬機的最后一部分了!只要你實現(xiàn)了前面提到的 trap routine 和指令,你 的虛擬機就即將能夠運行了!

11. 平臺相關的細節(jié)

本節(jié)包含一些與鍵盤交互以及顯示相關的代碼。如果不感興趣可以直接復制粘貼。

如果不是在 Unix 類系統(tǒng)上運行本程序,例如 Windows,那本節(jié)內(nèi)容需要替換為相應的平臺 實現(xiàn)。

uint16_t?check_key()?{
????fd_set?readfds;
????FD_ZERO(&readfds);
????FD_SET(STDIN_FILENO,?&readfds);

????struct?timeval?timeout;
????timeout.tv_sec?=?0;
????timeout.tv_usec?=?0;
????return?select(1,?&readfds,?NULL,?NULL,?&timeout)?!=?0;
}

下面是特定于 Unix 的設置終端輸入的代碼:

struct?termios?original_tio;

void?disable_input_buffering()?{
????tcgetattr(STDIN_FILENO,?&original_tio);
????struct?termios?new_tio?=?original_tio;
????new_tio.c_lflag?&=?~ICANON?&?~ECHO;
????tcsetattr(STDIN_FILENO,?TCSANOW,?&new_tio);
}

void?restore_input_buffering()?{
????tcsetattr(STDIN_FILENO,?TCSANOW,?&original_tio);
}

當程序被中斷時,我們需要將終端的設置恢復到默認:

void?handle_interrupt(int?signal)?{
????restore_input_buffering();
????printf("\n");
????exit(-2);
}
signal(SIGINT,?handle_interrupt);
disable_input_buffering();

12. 運行虛擬機

現(xiàn)在你可以編譯和運行這個 LC-3 虛擬機了!

使用你喜歡的 C 編譯器編譯這個虛擬機( https://arthurchiao.art/assets/img/write-your-own-virtual-machine-zh/lc3-vm.c ),然后下載匯 編之后的兩個小游戲:

  • 2048?下載: https://arthurchiao.art/assets/img/write-your-own-virtual-machine-zh/2048.obj
  • Rogue?下載: https://justinmeiners.github.io/lc3-vm/supplies/rogue.obj

用如下命令執(zhí)行:lc3-vm path/to/2048.obj。

Play 2048!
{2048 Example 13}
Control the game using WASD keys.
Are you on an ANSI terminal (y/n)? y
+--------------------------+
| |
| |
| |
| 2 |
| |
| 2 |
| |
| |
| |
+--------------------------+

調(diào)試

如果程序不能正常工作,那可能是你的實現(xiàn)有問題。調(diào)試程序就有點麻煩了。我建議通讀 LC-3 程序的匯編源代碼,然后使用一個調(diào)試器單步執(zhí)行虛擬機指令,確保虛擬機執(zhí)行到 的指令是符合預期的。如果發(fā)現(xiàn)了不符合預期的行為,就需要重新查看 LC-3 規(guī)范,確認你 的實現(xiàn)是否有問題。

13. C++ 實現(xiàn)(可選)

使用 C++ 會使代碼更簡短。本節(jié)介紹 C++ 的一些實現(xiàn)技巧。

C++ 有強大的編譯時泛型(compile-time generics)機制,可以幫我們自動生成部分指令 的實現(xiàn)代碼。這里的基本思想是重用每個指令的公共部分。例如,好幾條指令都用到了間接 尋址或有符號擴展然后加到當前寄存器的功能。模板如下:

{Instruction?C++?14}
template?<unsigned?op>
void?ins(uint16_t?instr)?{
????uint16_t?r0,?r1,?r2,?imm5,?imm_flag;
????uint16_t?pc_plus_off,?base_plus_off;

????uint16_t?opbit?=?(1?<????if?(0x4EEE?&?opbit)?{?r0?=?(instr?>>?9)?&?0x7;?}
????if?(0x12E3?&?opbit)?{?r1?=?(instr?>>?6)?&?0x7;?}
????if?(0x0022?&?opbit)?{
????????r2?=?instr?&?0x7;
????????imm_flag?=?(instr?>>?5)?&?0x1;
????????imm5?=?sign_extend((instr)?&?0x1F,?5);
????}
????if?(0x00C0?&?opbit)?{???//?Base?+?offset
????????base_plus_off?=?reg[r1]?+?sign_extend(instr?&?0x3f,?6);
????}
????if?(0x4C0D?&?opbit)?{?//?Indirect?address
????????pc_plus_off?=?reg[R_PC]?+?sign_extend(instr?&?0x1ff,?9);
????}
????if?(0x0001?&?opbit)?{
????????//?BR
????????uint16_t?cond?=?(instr?>>?9)?&?0x7;
????????if?(cond?&?reg[R_COND])?{?reg[R_PC]?=?pc_plus_off;?}
????}
????if?(0x0002?&?opbit)?{??//?ADD
????????if?(imm_flag)?{
????????????reg[r0]?=?reg[r1]?+?imm5;
????????}?else?{
????????????reg[r0]?=?reg[r1]?+?reg[r2];
????????}
????}
????if?(0x0020?&?opbit)?{??//?AND
????????if?(imm_flag)?{
????????????reg[r0]?=?reg[r1]?&?imm5;
????????}?else?{
????????????reg[r0]?=?reg[r1]?&?reg[r2];
????????}
????}
????if?(0x0200?&?opbit)?{?reg[r0]?=?~reg[r1];?}?//?NOT
????if?(0x1000?&?opbit)?{?reg[R_PC]?=?reg[r1];?}?//?JMP
????if?(0x0010?&?opbit)?{??//?JSR
????????uint16_t?long_flag?=?(instr?>>?11)?&?1;
????????pc_plus_off?=?reg[R_PC]?+??sign_extend(instr?&?0x7ff,?11);
????????reg[R_R7]?=?reg[R_PC];
????????if?(long_flag)?{
????????????reg[R_PC]?=?pc_plus_off;
????????}?else?{
????????????reg[R_PC]?=?reg[r1];
????????}
????}

????if?(0x0004?&?opbit)?{?reg[r0]?=?mem_read(pc_plus_off);?}?//?LD
????if?(0x0400?&?opbit)?{?reg[r0]?=?mem_read(mem_read(pc_plus_off));?}?//?LDI
????if?(0x0040?&?opbit)?{?reg[r0]?=?mem_read(base_plus_off);?}??//?LDR
????if?(0x4000?&?opbit)?{?reg[r0]?=?pc_plus_off;?}?//?LEA
????if?(0x0008?&?opbit)?{?mem_write(pc_plus_off,?reg[r0]);?}?//?ST
????if?(0x0800?&?opbit)?{?mem_write(mem_read(pc_plus_off),?reg[r0]);?}?//?STI
????if?(0x0080?&?opbit)?{?mem_write(base_plus_off,?reg[r0]);?}?//?STR
????if?(0x8000?&?opbit)?{??//?TRAP
?????????{TRAP,?8}
????}
????//if?(0x0100?&?opbit)?{?}?//?RTI
????if?(0x4666?&?opbit)?{?update_flags(r0);?}
}
{Op?Table?14}
static?void?(*op_table[16])(uint16_t)?=?{
????ins<0>,?ins<1>,?ins<2>,?ins<3>,
????ins<4>,?ins<5>,?ins<6>,?ins<7>,
????NULL,?ins<9>,?ins<10>,?ins<11>,
????ins<12>,?NULL,?ins<14>,?ins<15>
};

這里的技巧是從 Bisqwit’s NES emulator 學來的。如果你對仿真或 NES 感興趣,強烈建議觀看他的視頻。

完整版 C++ 實現(xiàn)見:
https://justinmeiners.github.io/lc3-vm/src/lc3-alt.cpp


--- EOF ---

瀏覽 19
點贊
評論
收藏
分享

手機掃一掃分享

分享
舉報
評論
圖片
表情
推薦
點贊
評論
收藏
分享

手機掃一掃分享

分享
舉報

感谢您访问我们的网站,您可能还对以下资源感兴趣:

国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频 免费无码| 久久大鸡| 亚洲综合片| 激情婷婷av| 免费在线观看中文字幕| 久久精品免费| 小骚逼操死你| 黄色性爱小说| 最近2021中文字幕免费| 亚洲第一免费视频| 欧美成人午夜福利| 亚洲精品中文字幕在线观看| 亚洲精品成人无码AV在线| 可以免费看的黄色视频| 神马午夜三级| 欧美性爱A片| 欧美草比视频| 学生妹一级| 天堂网免费视频| 国产亚洲视频在线观看视频| 亚洲无码AV麻豆| 日逼视频| 99热最新| 麻豆三级片| 99在线精品视频| 日韩三级小说| 日韩视频在线观看免费| 亚洲无码视频免费看| jizz在线免费观看| 一级黄色电影网| 3344gc在线观看入口| 熟女91视频| 热99re69精品8在线播放| 影音先锋AV在线资源| 亚洲午夜免费视频| 国产91精品久久久天天| 国产精品黄色片| 日日躁夜夜躁| h亚洲| 中文字幕在线视频第一页| 99视频在线精品| 可以免费观看的毛片| 99免费在线视频| 天天爽夜夜爽夜夜爽精品| 狼友视频免费在线观看| 成人自拍视频在线| 综合伊人大香蕉| 中文无码不卡| 久久AA| 三级在线网站| 亚洲精品伦理| 中文字幕高清| 人妻AV无码| 色综合加勒比| 欧美国产三级| 无码人妻精品一区| 成人大香蕉网| 日韩欧美二区| 青娱乐久久| 一级黄色片在线观看| 大香伊人中文字幕精品| 亚洲无码网址| 在线观看国产黄色| 影音先锋男人站| 免费看操逼视频| 日韩精品区| 天a堂8在线www| 久久777| 嘿咻无码推油| 嫩草av在线| 91丨牛牛丨国产人妻| 免费看一级高潮毛片| 日韩免费Av| 日韩av中文字幕在线播放 | 欧美性性性| 成人永久免费视频| 影音先锋在线视频观看| 亚洲婷婷五月天| 亚洲视频网站在线观看| 成人精品三级麻豆| 国产精品香蕉| 天堂中文资源在线| 成人欧美一区二区三区黑人免费| 久久久久成人电影| 亚洲欧美成人| 久久久中文字幕| 日韩欧美国产黄色电影| 一区二区三区国产精品| 亚洲秘无码一区二区三区电影| 日韩无码av电影| 久久永久免费视频| 婷婷久久网| 第一福利成人AV导航| 黄色视频亚洲| 国产高潮视频| 最全av在线| 免费一级黄色毛片| 日韩AⅤ无码一区二区三区| 亚洲女同在线| 男女乱伦视频| 黄色理论片| 婷婷色色婷婷| 学生妹一级J人片内射视频| 嫩草av| 在线观看A片| 成人禁区| 久久久久久无码精品亚洲日韩麻豆| 日本黄在线看| 天天撸在线| 欧美一级久久| 三级片一区二区| 亚洲中文字幕码mv| 成人黄色免费| 久草视频99| www久草| 在线h网站| 伊人久久香蕉网| 天堂网AV在线| 久久久久免费| 日韩欧美色| 免费一级黄色片| 亚洲AV无码久| 日韩三级麻豆| 成人在线网址| 亚洲日韩视频在线播放| 日逼网站免费观看| 午夜性爱网| 91人妻视频| 日韩性爱视频在线播放| 成人免费啪啪视频| 欧美日韩精品一区二区三区视频播放 | 久久久久久久毛片| 91麻豆国产视频| 91人人精品| 十八毛片| 97激情| 亚洲无码第一页| 女人特级毛片18| 日韩在线小电影| 日韩群交视频| 黄色片视频| 中文字幕一区二区三区四区在线视频| a视频在线| 午夜艹 | 国产探花| 日批网站在线观看| 丁香五月激情婷婷| 国产传媒AV| 欧美一区在线视频| 亚洲AV播放| 亚洲视频中文字幕在线观看| 亚洲AV成人无码精在线| 特黄特色免费大片| 欧美精品三区| 国产无套进入免费| 2025天天干| 91精品久久久久久久| 搡BBBB搡BBB搡五十粉嫩| 亚洲精品日韩综合观看成人91| 午夜福利视频3000| 黄色网页在线免费观看| 日韩中文字幕av| 秋霞91| 日韩精品一区二区三区在线观看免费 | 久久无码在线观看| 欧美一级网站| 爱搞搞就要搞| 无码不卡视频在线| 天天舔天天射| 国产成人ab| 女人的天堂av| 久久日韩操| 免费观看黄片网站| 色五月在线视频| 国产亚洲精品久久久久动| 欧美天天干| 日本精品在线观看视频| 青青草小视频| 男女做爱视频网站| 高清无码黄片| 中字幕视频在线永久在线观看免费| 免费看的操逼视频| 黄片天堂| 五月天婷婷影院| 怕怕怕视频| 操B久久| 中文字幕在线观看不卡| 91久久爽久久爽爽久久片| 人人操人人透| 精品成人在线视频| 国产九九九九| 亚洲天堂网站| 中文字幕无码免费| 五月婷婷六月丁香综合| 免费黄色A片| 成人女人18女人毛片| 婷婷五月天色综合| 亚洲图片在线观看| 日韩一页| 中文字幕第一页亚洲| 欧美三级一级| A片在线视频| 天天插在线视频| 香蕉在线观看| 风流少妇一区二区三区91| 影音先锋成人网| 国产三级成人| 无码入口| 无码人妻一区二区三区线花季传件| 超碰超爽| 精品伊人久久| 人人草人人看| 中文字幕在线观看辣文| 久久久国产精品在线| 精品成人一区二区三区| 另类AV| 亚洲天堂在线免费观看视频| 色色播播| 黄视频免费在线观看| 四川少妇搡BBw搡BBBB搡| 中文字幕日本无码| 久久99精品视频| 这里视频很精彩免费观看电视剧最新 | 东京热第一页| 91午夜视频| 久久肏| 老熟女搡BBBB搡BBBB视频| 啊v视频在线| 777视频在线观看| 亚洲无码免费看| 开心五月色婷婷综合开心网| 午夜九九九| 欧美激情一区| 日韩极品在线观看| 97在线视频免费观看| 日日干夜夜撸| 99免费热视频| 91人妻人人澡人人爽人人精品乱 | 激情小说在线视频| 一级特黄AAAA片| 熊猫视频91| 四川少妇搡bbw搡bbbb| www.天天操| 午夜精品久久久久久久久久久久 | 猛男大粗猛爽H男人味| 亚洲欧美大香蕉视频网| 日日夜夜精选视频| 特黄aaaaaaaa真人毛片| 18禁裸体美女| 国产粉嫩在线观看| 色一区二区三区| 国产人妖av| 欧美黄片一区| 99热国产| ww亚洲ww| 亚洲A片在线观看| 人人操人人干人人| 97国产精品人人爽人人做| www深夜成人a√在线| 色综合99久久久无码国产精品| a视频| 亚洲福利视频97| 国产三级精品三级在线观看| 嫩草在线观看| 人妻懂色av粉嫩av浪潮av| 成年人在线观看视频网站| 国产一级在线观看| 97人人爽人人爽人人爽人人爽 | 人妻在线你懂的| 久久视频免费在线观看| 亚洲免费性爱视频| 国产噜噜噜噜噜久久久久久久久| 久久XX| 特黄特黄免费看| 亚洲无码色色| 欧美黄色A片| 中文人妻无码| 精品九九九九| 在线观看免费黄视频| 一区二区三区四区视频在线| 久久毛片人妻| 久久狼人| 日韩欧美一级| 翔田千里无码AV在线观看| 国产一区二区AV| 亚洲美女视频网| 久久99嫩草熟妇人妻蜜臀| 婷婷五月999| 无码中文字幕在线视频| 五月婷婷六月丁香| 美女靠逼视频| 五月婷婷丁香五月| 人人操人人操人人操人人操| 日韩免费A片| 欧美三级欧美一级| 久久久久99精品成人网站| 91一区二区在线观看| 成人一卡二卡| 国产操女人| 亚洲午夜精品久久久| 黑人av| 久久学生妹| 免费一区二区三区四区| 日韩精品在线视频观看| 无码专区一区二区三区| 精品人妻一二三区| 日本精品黄色| 亚洲无码免费| 成年人国产| 爱爱网址| 亚洲伊人成人| 性爱无码| 日韩精品一区在线| 亚洲麻豆| 久久精品在线播放| 大乳奶一级婬片A片| 亚洲伊人大香蕉| 99re在线观看| 精品国产毛片| 久久久一区二区| 操逼毛片视频| 久久机热| 免费观看高清无码| 人人操人人色| 亚洲中文字幕2019| 欧美人操逼一二区| 亚洲激情欧美| 黄色视频在线免费观看高清视频| 色婷婷婷| 中国一级黄色A片| 欧美夜夜骑| 午夜午夜福利理论片在线播放| 国产精品一区二区不卡| 国产老熟女高潮毛片A片仙踪林 | 91丨九色丨熟女新版| 亚洲综合p| 豆花av在线| 亚洲AV三级片| 91ThePorn国产在线观看| 国产亚洲99久久精品| 欧美国产日本| 欧美日韩一区二区三区视频| www中文字幕| 三级乱伦视频| 91亚洲视频在线观看| 国产18水真多18精品| 超碰麻豆| www.199麻豆在线观看网站| 99视频免费在线观看| 日韩黄色一级| 加勒比DVD手机在线播放观看视频 日韩精品一区二区三区四区蜜桃视频 | 黄色毛片视频| 精品中文在线| 日韩人妻丰满无码区A片| 青娱乐精品在线| 日韩A人人| 日本一区二区三区免费观看| 人人澡人人爽欧一区| 精品一二三| 蜜桃精品无码| 一本色道久久加勒比精品| 操操操无码| 亚洲黄色电影| 五月天激情导航| 少妇熟女视频| www.yw尤物| 免费黄色一级视频| 黄片视频大全| 婷婷五月开心五月| 久久亚洲中文| HEZ-502搭讪绝品人妻系列| 国产福利在线导航| 国产熟妇婬乱一区二区| 无码人妻在线播放| 一区二区操逼| 久久99嫩草熟妇人妻蜜臀| 日韩AV手机在线观看| 成人欧美| 无码H| 狠狠干高清成人二区三区| 欧美一级精品| 日韩72页| 99热网站| 少妇搡BBBB搡BBB搡造水多/| 亚洲AV秘一区二区色盗战流出| 99视频在线看| 色情五月婷婷| 韩国AV在线| www五月天| 91美女在线观看| 在线观看禁无码精品| 天天插天天拍| 最新va在线观看| 久久久久久性爱| 激情综合网五月婷婷| 欧美激情五月| 久操视频免费在线观看| 人人草超碰| 一本色道久久综合亚洲二区三区 | 国产黄h| 超碰天天爱| 日韩欧美天堂| 国产成人秘一区二区三区东京热 | 9I成人免费版| 欧美日韩免费在线| 国产一级a毛一级a做免费的视频 | 国产高清无码18| 在线看毛片网站| 91狠狠综合| 国产一区二区免费| 国产成人午夜福利视频| 中文视频在线观看| 日韩av小电影| 按摩性高湖婬AAA片A片中国| 人人爽人人干| 精品无码人妻一区二区媚黑| 国产成人AA| 2025中文字幕| 久久永久免费视频| 爱爱视频无码| 亚洲无码免费观看视频| 亚洲无码AV片| 高清无码一区二区在线| 欧美日韩卡一卡二在线播放视频| 欧美日韩成人在线观看| 精品吃奶一区二区三区视频| 欧美三级| av色色| 91就去干| 久久久成人电影| 久操人妻| 精品国产区一区二| 五月天亭亭.com| 在线视频日韩| 男人天堂新地址| 欧美操日本| 久久av电影| 欧美成人黄色小说| 成年人免费毛片| 日韩精品高清中文| 极品少妇AV| 欧美日韩一级毛| 人妻人人干| 91日韩欧美| 国产9熟妇视频网站| 午夜性爱网址| 青草中文娱乐网在线| 91九色视频| 黄在线免费观看| 91麻豆国产福利精品| 一级片在线观看视频| 欧美日韩一级电影| 日本在线免费视频| 一本到在线观看午夜剧场| 欧美性爱一区二区三区| 一级日逼视频| 日本一区二区精品| 日逼视频网站| 九九热超碰| 国产一区在线看| 在线观看免费高清无码| 久久er| 色逼综合| 97精品人妻一区二区三区香蕉| 欧美理伦| 亚洲av免费在线观看| 成人h视频| 国产三级精品三级在线观看| 西西人体大胆ww4444图片 | 色五月在线视频| 一本色道无码人妻精品| 青草社区在线观看| 精品一区二区视频| 国产毛片毛片毛片毛片毛片| 国产精品人人人人| 亚洲视频中文字母| 伊人久久久| 国产精品免费一区二区三区都可以 | 日本黄A三级三级三级| 免费操b视频| 影音先锋成人网| 91网站免费观看| 在线啊啊啊| 婷婷少妇激情| 五月色婷婷撸| 乱伦三区| 国产免费a片| 婷婷亚洲精品| 69精品免费视频| 中文字幕免费| 亚洲夜夜撸| 欧美第五页| 婷婷成人视频| 999热这里只有精品| 综合+++夜夜| 色哟哟一区二区三区| 亚州天堂网| 撸一撸免费视频| 在线高清无码不卡| 亚洲无码二区| 人善交精品一区二区三区| 伊人大香蕉综合在线| 欧美污视频在线观看| 欧美一区电影| 欧美777| 一本色道久久综合亚洲二区三区 | 在线不卡中文字幕| 麻豆免费版在线观看| 一级免费黄色电影| 爱搞搞就要搞搞| 免费成人三级片| 人妻丰满精品一区二区| 青青草视频免费看| 日韩日逼| 免费无码一区二区三区| 亚洲无码视频一区二区| 日韩欧美中文字幕公布| a日韩| 亚洲都市激情| 九九99久久| 国产偷拍网站| 加勒比无码在线播放| 青娱乐网站| 欧美一级免费视频| 国产精品三级在线| 人人干人人看| 韩国成人啪啪无码高潮| 日本色色网站| 无码AV免费观看| 久久尹人| AV大片免费看| 婷婷五月天在线电影| 亚洲视频无码| 亚洲黄色无码| 无码人妻一区二区三区免费n鬼沢| 自拍偷拍一区二区| 日本视频免费| 热久久久久久| 欧美日韩AV| 亚洲AV在线人妻| 国产熟女视频| 狠狠撸在线观看| 中文字幕乱码中文字幕电视剧| 欧美人操逼视频| 亚洲国产成人久久| 成人香蕉| 精品国产一区二区三区性色AV | 亚洲视频在线播放| 日韩欧美中文字幕在线观看| 亚洲高清无码中文字幕| 成年人黄色电影| 五月婷婷色色色| 性爱91视频| 三须三级久久三级久久18| 无码精品一区二区三区在线观看| 在线不卡免费Av| 欧美激情一区| 最新超碰| 欧美色就是色| 强开小嫩苞一区二区电影| 四虎影院在线| AV高清无码在线| 中文资源在线观看| 日本免费在线观看视频| 国产A级黄色片| 亚洲精品欧美久久婷婷| 插菊花综合网1| 囯产精品宾馆在线精品酒店| 国产成人精品电影| 日本精品中文字幕| 精品无码三级在线观看视频| 国产精品视频在线免费观看| 国产免费性爱| 999久久久精品| 久久性爱免费视频| 337P大胆粉嫩噜噜噜| 香蕉漫画在线观看18| 亚洲欧美在线视频| 日本人妻中文字幕| 日韩动态图| 青青草亚洲| 国产精品香蕉| 日本久久综合| 国产丝袜在线视频| 国产视频一二三| 亚洲成人电影一区| 国产淫荡视频| 国产成人激情| 久久动态图| 男人的天堂色琪琪| 国产无遮挡又黄又爽又| 亚洲视频二| 久久99精品国产.久久久久| 中文字幕久久播放| 久草视频在线播放| 亚洲精品三级| 亚洲操操操| 婷婷啪啪| 亚洲va国产va天堂va久久| 中文字幕在线不卡| 撸一撸在线| 国产一级A片久久久免费看快餐| 熟睡侵犯の奶水授乳在线| 露脸老熟女91集合| 天码人妻一区二区三区在线看| 久久久免费观看视频| 在线亚洲欧洲| 老司机精品| 成人午夜婬片A片| 日韩成人AV毛片| 99精品99| 波多野结衣在线无码视频| 一级片在线免费看| 亚洲日本中文字幕在线| 国产一级乱伦| 色婷婷欧美| 在线不卡中文字幕| 亚洲无码在线资源| 日韩18在线| 男人的天堂婷婷| 日韩精品免费一区二区在线观看| 伊人av网| 香蕉成人网站在线观看| 久久久久久久久成人| 成人视频黄片| 日日骚亚洲| 制服.丝袜.亚洲.中文.豆花 | 99久久婷婷| 国产无遮挡又黄又爽又色| 无码福利视频| 色噜噜人妻丝袜无码影院| 亚洲伊人综合| 西西人体44www大胆无码| 国产一级a毛一级a毛视频在线网站)| 黄色永久网站| 日日骚av一区二区三区| 日韩视频――中文字幕| 91久久免费视频| 国产草草| 亚洲人妻无码视频| 中文字幕777| 黄色三级毛片| 俺也去操| eeuss一区| 日本免费黄色片| 国产精成人品| 丰滿老婦BBwBBwBBw| 久操综合| 久久青草视频| 在线观看亚洲| 国产中文字幕AV在线播放| 2016av天堂网| 久热最新| 一本色道久久88综合无码| 亚洲一区二区三区免费视频| 无遮挡动态图| 91免费成人视频| 亚洲精品高清视频| 国产91探花精品一区二区| 一本色道综合久久欧美日韩精品| 182在线视频| 俺去俺来也www色官网黑人| 伊人综合视频| 日本特黄AA片免费视频| 在线免费黄| 成人做爰黄AAA片免费直播岛国| 在线中文av| 精品一区在线| 中文字幕一区二区三区四区| 精品视频一区二区三区四区| 久久久久久精品国产三级| 安徽少妇搡bbw搡bbbb| 免费操b视频| 国产欧美日韩综合精品| 无码高清视频在线观看| 夜间福利视频| 人妻啪啪视频| ThePorn-成人网站入口| 天天添| 99热超碰在线| 不卡中文字幕| 啪啪免费视频| 午夜大香蕉| 日本精品久久| 五月人妻| 天天干天天操天天拍| 狼色AV| 成人在线综合| 在线观看日韩av| 人人操人人摸人人爱| AV免费播放| 影音先锋成人资源站| 精品国产av| 淫一区二区| 久久久久久成人无码| 五月天婷婷小说| 中文字幕精品视频在线| 日韩精品一区在线| 国产精品成人在线观看| 狼友无码| 蜜桃av秘无码一区二区| 国产AV影视| 五月丁香综合久久| 96精品久久久久久久久久| 无码欧精品亚洲日韩一区| 91蜜桃视频在线观看| 免费一级大片| 中文字幕五月久久| 第四色大香蕉| 人妻第一页| 欧美激情视频在线| 91香蕉视频在线看| 校园春色亚洲色图| 超碰人人操人人摸| 成全在线观看高清的| 三级三级久久三级久久18| 欧美精品性爱| 亚洲vs天堂vs成人vs无码| 亚洲国产精品自| 大香蕉伊人视频在线观看| 日韩黄色一级| 欧美成人视屏| 日本色色色| 91热| 超碰在线观看2407| 蜜桃免费网站| 91污视频在线观看| 特级艺体西西444WWw| 日韩成人在线免费观看| www.蜜桃| 在线观看AV无码| 婷婷视频网| 日本天堂在线视频| 日韩AV中文| 国产色情在线| 久操大香蕉| 欧美精品18videosex性欧美| 亚洲三级视频在线播出| 婷婷丁香色五月| 国产美女啪啪视频| 九九这里有精品| 婷婷五月天丁香| 国产精品无码激情视频| 人妻18无码人伦一区二区三区精品 | 热久久精品| 欧美一卡二卡三卡| 中文字幕东京热加勒比| 日韩欧美偷拍| 无码电影免费观看| 欧美成人免费电影| 国产成人精品一区二区三区在线 | 亚洲精品天堂无码AV片| 国产99久久久精品| 天天天天日天天干| 亚洲中文免费视频| 狠狠干在线视频| 亚洲欧美日韩黑料吃瓜在线观看| 成人无码99| 蜜桃传媒一区二区亚洲A| 成人黄色视频免费| 无码视频免费看| 操你啦无码日韩| 国产午夜成人视频| 豆花视频久久| 69精品无码成人久久久久久| 国产一级二级三级| 日韩另类| 自慰一区二区| 欧美人妻中文字幕| 少妇嫩搡BBBB搡BBBB| 国产日韩欧美在线| 澳门午夜黄色在线| 真实白嫖91探花无码| 风流少妇一区二区三区91| 成人福利| 久久99精品国产.久久久久| 秋霞午夜久久| 精品无码三级在线观看视频| 日本激情网| 激情性爱婷婷色五月| 99视频+国产日韩欧美| 欧美亚洲图区| 丰满少妇在线观看网站| 青娱乐av| 亚洲AV永久无码国产精品久久| 亚洲视频91| 日韩综合久久| 亚洲中文字幕在线视频| 国产麻豆免费| 爱逼综合网| h片免费观看| 亚洲一区二区黄色电影视频网站| 蜜桃免费网站| 国产操逼大全| 青青草青娱乐| 五月六月丁香激情视频| 亚洲AV毛片成人精品网站| 午夜福利aaa| 久久av一区二区三区| 成人在线伊人| 老太老熟女城中层露脸60| 91调教视频| 性爱无码| 另类综合激情| 超碰在线精品| 婷婷视频在线| 无套进入无套内谢| 亚洲一区二区久久| 亚洲无码高清视频在线| 熟妇无码| 91精品91久久久中77777| 日本一区不卡| 青青草无码| 九九热播精品| 欧美日韩国产成人电影| 三根一起进菊眼| 91福利在线观看| jzzijzzij亚洲成熟少妇在线观看 九色蝌蚪9l视频蝌蚪9l视频成人熟妇 | 做爰视频毛片下载蜜桃视频。| 国产午夜视频在线观看| 蜜芽av在线观看| 亚洲国产欧美日韩在线| 国产成人精品一区二区三区| 西西掰穴| 影音先锋AV资源在线| 婷婷精品在线| 国产精品96久久久久久| 亚洲第一网站| 成人综合激情| 香蕉伊人视频| 97人妻人人澡人人爽人人精品 | 中文无码高清视频| 男人的天堂视频网站| 欧美AⅤ| 久久A级片| 日本在线| 亚洲综合网在线| 亚洲日韩欧美中在线| 荫蒂添出高潮A片视频| 色第一页| 中文字幕视频免费| 高潮无码视频| 大秀91视频| 97资源在线视频| 无码人妻精品一区二区蜜桃91| 国产操| 俺去也av| 日韩精品一区二区在线观看| 久久久久成人电影| 婷婷少妇激情| 搡女人视频国产一级午夜片| 亚洲中文无码视频| 超碰自拍97| 开心色情| 亚州成熟少妇视频在线观看| 思思精品在线| 日韩在线视频中文字幕| 人人妻人人澡人人爽人人| 成人毛片在线大全免费| 日韩av小说| 国产乱仑视频| 超碰成人免费| 成人亚洲性情网站www在线| 三级网站视频| 久热无码| 久久精品视频在线免费观看| 在线观看免费视频黄| 午夜av影院| 日韩和的一区二区| 国产视频激情| 中文字幕久久无码| 九九天堂网| 精品蜜桃秘一区二区三区在线播放| 91综合视频| 亚洲欧美在线一区| 日韩无码123| 国产99久久九九精品无码免费| 成人黄色A片| 亚洲AV永久无码国产精品久久| 青草国产视频| 无码高清视频| 米奇狠狠干| 欧美性爱91| 亚洲综合免费| 97久久一区二区| 日韩精品第一页| 黑人精品XXX一区一二区| 亚洲三级片在线| 免费中文资源在线观看| 久草免费福利| 91成人免费| 大香蕉精品一区| 日韩亚洲欧美在线| 内射视频网站| 色综合久久88色综合| 色综合久| 四虎影库男人天堂| 成人无码日韩精品| 高清一区二区三区| 亚洲美女网站免费观看网址|