成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

結合實例深入理解C++對象的內存布局

開發
本篇文章試著從實際的例子出發,幫助大家對 C++ 類成員變量和函數在內存布局有個直觀的理解

作者 | daemonzhao

通過實例來深入理解 C++ 對象的內存布局,包括基礎數據類、帶方法的類、私有成員、靜態成員、類繼承等。通過 GDB 查看對象的內存布局,探討成員變量、成員方法、虛函數表等在內存中的存儲位置和實現細節,幫助大家對 C++ 類成員變量和函數在內存布局有個直觀的理解。

因為二進制使用了不同版本的 proto 對象,對象的內存布局不一致導致讀、寫成員的內存地址錯亂,進而導致進程 crash 掉。這之中會出現下面的問題:

  • 對象在內存中是怎么布局的?
  • 成員方法是如何拿到成員變量的地址?

這些其實涉及 C++ 的對象模型,《深度探索 C++對象模型:Inside the C++ Object Model》這本書全面聊了這個問題,非常值得一讀。不過這本書讀起來并不容易,有的內容讀過后如果沒有加以實踐,也很難完全理解。本篇文章試著從實際的例子出發,幫助大家對 C++ 類成員變量和函數在內存布局有個直觀的理解,后面再讀這本書也會容易理解些。

簡單對象內存分布

首先以一個最簡單的 Basic 類為例,來看看只含有基本數據類型的對象是怎么分配內存的。

#include <iostream>
using namespace std;

class Basic {
public:
    int a;
    double b;
};

int main() {
    Basic temp;
    temp.a = 10;
    return 0;
}

編譯運行后,可以用 GDB 來查看對象的內存分布。如下圖:

對象 temp 的起始地址是 0x7fffffffe3b0,這是整個對象在內存中的位置。成員變量 a 的地址也是 0x7fffffffe3b0,表明 int a 是對象 temp 中的第一個成員,位于對象的起始位置。成員變量 b 的類型為 double,其地址是 0x7fffffffe3b8(a 的地址+8),內存布局如下圖:

這里 int 類型在當前平臺上占用 4 個字節(可以用 sizeof(int)驗證),而這里 double 成員的起始地址與 int 成員的起始地址之間相差 8 個字節,說明在 a 之后存在內存對齊填充(具體取決于編譯器的實現細節和平臺的對齊要求)。內存對齊要求數據的起始地址在某個特定大小(比如 4、8)的倍數上,這樣可以優化硬件和操作系統訪問內存的效率。這是因為許多處理器訪問對齊的內存地址比訪問非對齊地址更快。

另外在不進行內存對齊的情況下,較大的數據結構可能會跨越多個緩存行或內存頁邊界,這會導致額外的緩存行或頁的加載,降低內存訪問效率。不過大多時候我們不需要手動管理內存對齊,編譯器和操作系統會自動處理這些問題。

帶方法的對象內存分布

帶有方法的類又是什么樣呢?接著上面的例子,在類中增加一個方法 setB,用來設置其中成員 b 的值。

#include <iostream>

class Basic {
public:
    int a;
    double b;

    void setB(double value) {
        b = value; // 直接訪問成員變量b
    }
};

int main() {
    Basic temp;
    temp.a = 10;
    temp.setB(3.14);
    return 0;
}

用 GDB 打印 temp 對象以及成員變量的地址,發現內存布局和前面不帶方法的完全一樣。整個對象 size 依然是 16,a 和 b 的內存地址分布也是一致的。那么新增加的成員方法存儲在什么位置?成員方法中又是如何拿到成員變量的地址呢?

1.成員方法內存布局

可以在 GDB 里面打印下成員方法的地址,如下圖所示。

回憶下 Linux 中進程的內存布局,其中文本段(也叫代碼段)是存儲程序執行代碼的內存區域,通常是只讀的,以防止程序在運行時意外或惡意修改其執行代碼。這里 setB 方法地址 0x5555555551d2 就是位于程序的文本段內,可以在 GDB 中用 info target 驗證一下:

其中 .text 段的地址范圍是 0x0000555555555060 - 0x0000555555555251,setB 剛好在這個范圍內。至此前面第一個問題有了答案,成員方法存儲在進程的文本段,添加成員方法不會改變類實例對象的內存布局大小,它們也不占用對象實例的內存空間。

2.成員變量尋址

那么成員方法中又是如何拿到成員變量的地址呢?在解決這個疑問前,先來仔細看下 setB 的函數原型(void (*)(Basic * const, double)),這里函數的第一個參數是Basic* 指針,而在代碼中的調用是這樣:temp.setB(3.14)。這種用法其實是一種語法糖,編譯器在調用成員函數時自動將當前對象的地址作為 this 指針傳遞給了函數的。

(gdb) p &Basic::setB(double)
$7 = (void (*)(Basic * const, double)) 0x5555555551d2 <Basic::setB(double)>

這里參數傳遞了對象的地址,但是在函數里面是怎么拿到成員變量 b 的地址呢?我們在調用 setB 的地方打斷點,執行到斷點后,用 step 進入到函數,然后查看相應寄存器的值和匯編代碼。整個過程如下圖:

這里的匯編代碼展示了如何通過 this 指針和偏移量訪問 b??梢苑譃閮刹糠?,第一部分是處理 this 指針和參數,第二部分是找到成員 b 的內存位置然后進行賦值。

  • 參數傳遞部分。這里mov %rdi,-0x8(%rbp)將 this 指針(通過 rdi 寄存器傳入)保存到棧上。將 double 類型的參數 value 通過 xmm0 寄存器傳入保存到棧上。這是 x86_64 機器下 GCC 編譯器的傳參規定,我們可以通過打印 $rdi 保存的地址來驗證確實是 temp 對象的開始地址。
  • 對象賦值部分。mov -0x8(%rbp),%rax 將 this 指針從棧上加載到 rax 寄存器中。類似的,movsd -0x10(%rbp),%xmm0 將參數 value 從棧上重新加載到 xmm0 寄存器中。movsd %xmm0,0x8(%rax) 將 value 寫入到 this 對象的 b 成員。這里 0x8(%rax) 表示 rax(即 this 指針)加上 8 字節的偏移,這個偏移正是成員變量 b 在 Basic 對象中的位置。

這個偏移是什么時候,怎么算出來的呢?其實成員變量的地址相對于對象地址是固定的,對象的地址加上成員變量在對象內的偏移量就是成員變量的實際地址。編譯器在編譯時,基于類定義中成員變量的聲明順序和編譯器的內存布局規則,計算每個成員變量相對于對象起始地址的偏移量。然后在運行時,通過基地址(即對象的地址)加上偏移量,就能夠計算出每個成員變量的準確地址。這個過程對于程序員來說是透明的,由編譯器和運行時系統自動處理。

3.函數調用約定與優化

上面的匯編代碼中,setB 的兩個參數,都是從寄存器先放到棧上,接著又從棧上放到寄存器進行操作,為什么要移來移去多此一舉呢?要回答這個問題,需要先了解函數的調用約定和寄存器使用。在 x86_64 架構的系統調用約定中,前幾個整數或指針參數通常通過寄存器(如 rdi, rsi, rdx, 等)傳遞,而浮點參數通過 xmm0 到 xmm7 寄存器傳遞。這種約定目的是為了提高函數調用的效率,因為使用寄存器傳遞參數比使用棧更快。

而將寄存器上的參數又移動到棧上,是為了保證寄存器中的值不被覆蓋。因為寄存器是有限的資源,在函數中可能會被多次用于不同的目的。將值保存到棧上可以讓函數內部自由地使用寄存器,而不必擔心覆蓋調用者的數據。

接著又將-0x8(%rbp) 放到 rax 寄存器,然后再通過movsd %xmm0,0x8(%rax)寫入成員變量 b 的值,為啥不直接從xmm0寄存器寫到基于 rbp 的偏移地址呢?這是因為 x86_64 的指令集和其操作模式通常支持使用寄存器間接尋址方式訪問數據。使用rax等通用寄存器作為中間步驟,是一種更通用和兼容的方法。

當然上面編譯過程沒有開啟編譯優化,所以編譯器采用了直接但效率不高的代碼生成策略,包括將參數和局部變量頻繁地在棧與寄存器間移動。而編譯器的優化策略可能會影響參數的處理方式。如果我們開啟編譯優化,如下:

$ g++ basic_method.cpp -o basic_method_O2 -O2 -g -std=c++11

生成的 main 函數匯編部分如下:

(gdb) disassemble /m main
=> 0x0000555555555060 <+0>: xor    %eax,%eax
   0x0000555555555062 <+2>: ret
   0x0000555555555063: data16 nopw %cs:0x0(%rax,%rax,1)
   0x000055555555506e: xchg   %ax,%ax

在 O2 優化級別下,編譯器認定 main 函數中的所有操作(包括創建 Basic 對象和對其成員變量的賦值操作)對程序的最終結果沒有影響,因此它們都被優化掉了。這是編譯器的“死代碼消除”,直接移除那些不影響程序輸出的代碼部分。

特殊成員內存分布

上面的成員都是 public 的,如果是 private(私有) 變量,私有方法呢?另外,靜態成員變量或者靜態成員方法,在內存中又是怎么布局呢?

1.私有成員

先來看私有成員,接著上面的例子,增加私有成員變量和方法。整體代碼如下:

#include <iostream>

class Basic {
public:
    int a;
    double b;

    void setB(double value) {
        b = value; // 直接訪問成員變量b
        secret(b);
    }
private:
    int c;
    double d;

    void secret(int temp) {
        d = temp + c;
    }
};

int main() {
    Basic temp;
    temp.a = 10;
    temp.setB(3.14);
    return 0;
}

編譯之后,通過 GDB,可以打印出所有成員變量的地址,發現這里私有變量的內存布局并沒有什么特殊地方,也是依次順序存儲在對象中。私有的方法也沒有特殊地方,一樣存儲在文本段。整體布局如下如:

那么 private 怎么進行可見性控制的呢?首先編譯期肯定是有保護的,這個很容易驗證,我們無法直接訪問 temp.c ,或者調用 secret 方法,因為直接會編譯出錯。

那么運行期是否有保護呢?我們來驗證下。前面已經驗證 private 成員變量也是根據偏移來找到內存位置的,我們可以在代碼中直接根據偏移找到內存位置并更改里面的值。

int* pC = reinterpret_cast<int*>(reinterpret_cast<char*>(&temp) + 16);
*pC = 12; // 直接修改c的值

這里修改后,可以增加一個 show 方法打印所有成員的值,發現這里 temp.c 確實被改為了 12??梢姵蓡T變量在運行期并沒有做限制,知道地址就可以繞過編譯器的限制進行讀寫了。那么私有的方法呢?

私有方法和普通成員方法一樣存儲在文本段,我們拿到其地址后,可以通過這個地址調用嗎?這里需要一些騷操作,我們在類定義中添加額外的接口來暴露私有成員方法的地址,然后通過成員函數指針來調用私有成員函數。整體代碼如下:

class Basic {
...
public:
    // 暴露私有成員方法的地址
    static void (Basic::*getSecretPtr())(int) {
        return &Basic::secret;
    }

...
}

int main() {
    // ...
   void (Basic::*funcPtr)(int) = Basic::getSecretPtr();
    // 調用私有成員函數
    (temp.*funcPtr)(10);
    // ...
}

上面代碼正常運行,你可以通過 print 打印調用前后成員變量的值來驗證。看來對于成員函數來說,只是編譯期不讓直接調用,運行期并沒有保護,我們可以繞過編譯限制在對象外部調用。

當然實際開發中,千萬不要直接通過地址偏移來訪問私有成員變量,也不要通過各種騷操作來訪問私有成員方法,這樣不僅破壞了類的封裝性,而且是不安全的。

2.靜態成員

每個熟悉 c++ 類靜態成員的人都知道,靜態成員變量在類的所有實例之間共享,不管你創建了多少個類的對象,靜態成員變量只有一份數據。靜態成員變量的生命周期從它們被定義的時刻開始,直到程序結束。靜態成員方法不依賴于類的任何實例來執行,主要用在工廠方法、單例模式的實例獲取方法、或其他與類的特定實例無關的工具函數。

下面以一個具體的例子,來看看靜態成員變量和靜態成員方法的內存布局以及實現特點。繼續接著前面代碼例子,這里省略掉其他無關代碼了。

#include <iostream>

class Basic {
// ...
public:
    static float alias;
    static void show() {
        std::cout << alias << std::endl;
    }
};

float Basic::alias = 0.233;
int main() {
    // ...
    temp.show();
    return 0;
}

簡單的打印 temp 和 alias 地址,發現兩者之間差異挺大。temp 地址是 0x7fffffffe380,Basic::alias 是 0x555555558048,用 info target 可以看到 alias 在程序的 .data 內存空間范圍 0x0000555555558038 - 0x000055555555804c 內。進一步驗證了下,.data段用于存儲已初始化的全局變量和靜態變量,注意這里需要是非零初始值。

對于沒有初始化,或者初始化為零的全局變量或者靜態變量,是存儲在 .bss 段內的。這個也很好驗證,把上面 alias 的值設為 0,重新查看內存位置,就能看到確實在 .bss 段內了。對于全局變量或者靜態變量,為啥需要分為這兩個段來存儲,而不是合并為一個段來存儲呢?

這里主要是考慮到二進制文件磁盤空間大小以及加載效率。在磁盤上,.data 占用實際的磁盤空間,因為它需要存儲具體的初始值數據。.bss段不占用實際的存儲空間,只需要在程序加載時由操作系統分配并清零相應的內存即可,這樣可以減少可執行文件的大小。在程序啟動時,操作系統可以快速地為.bss段分配內存并將其初始化為零,而無需從磁盤讀取大量的零值數據,可以提高程序的加載速度。這里詳細的解釋也可以參考 Why is the .bss segment required?。

靜態方法又是怎么實現呢?我們先輸出內存地址,發現在 .text 代碼段,這點和其他成員方法是一樣的。不過和成員方法不同的是,第一個參數并不是 this 指針了。在實現上它與普通的全局函數類似,主要區別在于它們的作用域是限定在其所屬的類中。

類繼承的內存布局

當然,既然是在聊面向對象的類,那就少不了繼承了。我們還是從具體例子來看看,在繼承情況下,類的內存布局情況。

1.不帶虛函數的繼承

先來看看不帶虛函數的繼承,示例代碼如下:

#include <iostream>

class Basic {
public:
    int a;
    double b;

    void setB(double value) {
        b = value; // 直接訪問成員變量b
    }
};

class Derived : public Basic {
public:
    int c;
    void setC(int value) {
        c = value; // 直接訪問成員變量c
    }
};

int main() {
    Derived temp;
    temp.a = 10;
    temp.setB(3.14);
    temp.c = 1;
    temp.setC(2);
    return 0;
}

編譯運行后,用 GDB 打印成員變量的內存分布,發現 Derived 類的對象在內存中的布局首先包含其基類Basic的所有成員變量,緊接著是 Derived 類自己的成員變量。整體布局如下圖:

其實 C++ 標準并沒有規定在繼承中,基類和派生類的成員變量之間的排列順序,編譯器可以自由發揮的。但是大部分編譯器在實現中,都是基類的成員變量在派生類的成員變量之前,為什么這么做呢?因為這樣實現,使對象模型變得更簡單和直觀。不論是基類還是派生類,對象的內存布局都是連續的,簡化了對象創建、復制和銷毀等操作的實現。我們通過派生類對象訪問基類成員與直接使用基類對象訪問時完全一致,一個派生類對象的前半部分就是一個完整的基類對象。

對于成員函數(包括普通函數和靜態函數),它們不占用對象實例的內存空間。不論是基類的成員函數還是派生類的成員函數,它們都存儲在程序的代碼段中(.text 段)。

2.帶有虛函數的繼承

帶有虛函數的繼承,稍微有點復雜了。在前面繼承例子基礎上,增加一個虛函數,然后在 main 中用多態的方式調用。

#include <iostream>

class Basic {
public:
    int a;
    double b;

    virtual void printInfo() {
        std::cout << "Basic: a = " << a << ", b = " << b << std::endl;
    }

    virtual void printB() {
        std::cout << "Basic in B" << std::endl;
    }

    void setB(double value) {
        b = value; // 直接訪問成員變量b
    }
};

class Derived : public Basic {
public:
    int c;

    void printInfo() override {
        std::cout << "Derived: a = " << a << ", b = " << b << ", c = " << c << std::endl;
    }

    void setC(int value) {
        c = value; // 直接訪問成員變量c
    }
};

int main() {
    Derived derivedObj;
    derivedObj.a = 10;
    derivedObj.setB(3.14);
    derivedObj.c = 1;
    derivedObj.setC(2);

    Basic* ptr = &derivedObj; // 基類指針指向派生類對象
    ptr->printInfo(); // 多態調用
    ptr->printB(); // 調用

    Basic  basicObj;
    basicObj.a = 10;
    basicObj.setB(3.14);

    Basic* anotherPtr = &basicObj;
    anotherPtr->printInfo();
    anotherPtr->printB();
    return 0;
}

上面代碼中,Basic* ptr = &derivedObj; 這一行用一個基類指針指向派生類對象,當通過基類指針調用虛函數 ptr->printInfo();時,將在運行時解析為 Derived::printInfo() 方法,這是就是運行時多態。對于 ptr->printB(); 調用,由于派生類中沒有定義 printB() 方法,所以會調用基類的 printB() 方法。

那么在有虛函數繼承的情況下,對象的內存布局是什么樣?虛函數的多態調用又是怎么實現的呢?實踐出真知,我們可以通過 GDB 來查看對象的內存布局,在此基礎上可以驗證虛函數表指針,虛函數表以及多態調用的實現細節。這里先看下 Derived 類對象的內存布局,如下圖:

可以看到派生類對象的開始部分(地址 0x7fffffffe370 處)有一個 8 字節的虛函數表指針 vptr(指針地址 0x555555557d80),這個指針指向一個虛函數表(vtable),虛函數表中存儲了虛函數的地址,一共有兩個地址 0x55555555538c 和 0x555555555336,分別對應Derived 類中的兩個虛函數 printInfo 和 printB?;惖那闆r類似,下面畫一個圖來描述更清晰些:

現在搞清楚了虛函數在類對象中的內存布局。在編譯器實現中,虛函數表指針是每個對象實例的一部分,占用對象實例的內存空間。對于一個實例對象,通過其地址就能找到對應的虛函數表,然后通過虛函數表找到具體的虛函數地址,實現多態調用。那么為什么必須通過引用或者指針才能實現多態調用呢?看下面 3 個調用,最后一個沒法多態調用。

Basic& ref = derivedObj;
Basic* ptr = &derivedObj;
Basic dup = derivedObj; // 沒法實現多態調用

我們用 GDB 來看下這三種對象的內存布局,如下圖:

指針和引用在編譯器底層沒有區別,ref 和 ptr 的地址一樣,就是原來派生類 derivedObj 的地址0x7fffffffe360,里面的虛函數表指針指向派生類的虛函數表,所以可以調用到派生類的 printInfo。而這里的 dup 是通過拷貝構造函數生成的,編譯器執行了隱式類型轉換,從派生類截斷了基類部分,生成了一個基類對象。dup 中的虛函數表指針指向的是基類的虛函數表,所以調用的是基類的 printInfo。

從上面 dup 虛函數表指針的輸出也可以看到,虛函數表不用每個實例一份,所有對象實例共享同一個虛函數表即可。虛函數表是每個多態類一份,由編譯器在編譯時創建。

當然,這里是 Mac 平臺下 Clang 編譯器對于多態的實現。C++ 標準本身沒有規定多態的實現細節,沒有說一定要有虛函數表(vtable)和虛函數表指針(vptr)來實現。這是因為 C++標準關注的是行為和語義,確保我們使用多態特性時能夠得到正確的行為,但它不規定底層的內存布局或具體的實現機制,這些細節通常由編譯器的實現來決定。

不同編譯器的實現也可能不一樣,許多編譯器為了訪問效率,將虛函數表指針放在對象內存布局的開始位置。這樣,虛函數的調用可以快速定位到虛函數表,然后找到對應的函數指針。如果類有多重繼承,情況可能更復雜,某些編譯器可能會采取不同的策略來安排虛函數表指針的位置,或者一個對象可能有多個虛函數表指針。

地址空間布局隨機化

前面的例子中,如果用 GDB 多次運行程序,對象的虛擬內存地址每次都一樣,這是為什么呢?

我們知道現代操作系統中,每個運行的程序都使用虛擬內存地址空間,通過操作系統的內存管理單元(MMU)映射到物理內存的。虛擬內存有很多優勢,包括提高安全性、允許更靈活的內存管理等。為了防止緩沖區溢出攻擊等安全漏洞,操作系統還會在每次程序啟動時隨機化進程的地址空間布局,這就是地址空間布局隨機化(ASLR,Address Space Layout Randomization)。

在 Linux 操作系統上,可以通過 cat /proc/sys/kernel/randomize_va_space 查看當前系統的 ASLR 是否啟用,基本上默認都是開啟狀態(值為 2),如果是 0,則是禁用狀態。

前面使用 GDB 進行調試時,之所以觀察到內存地址是固定不變的,這是因為 GDB 默認禁用了 ASLR,以便于調試過程中更容易重現問題??梢栽谑褂?GDB 時啟用 ASLR,從而讓調試環境更貼近實際運行環境。啟動 GDB 后,可以通過下面命令開啟地址空間的隨機化。

(gdb) set disable-randomization off

之后再多次運行,這里的地址就會變化了。

總結

C++ 的對象模型是一個復雜的話題,涉及到類的內存布局、成員變量和成員函數的訪問、繼承、多態等多個方面。本文從實際例子出發,幫助大家對 C++ 對象的內存布局有了一個直觀的認識。

簡單總結下本文的核心結論:

  • 對象的內存布局是連續的,成員變量按照聲明的順序存儲在對象中,編譯器會根據類定義計算每個成員變量相對于對象起始地址的偏移量。
  • 成員方法存儲在進程的文本段,不占用對象實例的內存空間,通過 this 指針和偏移量訪問成員變量。
  • 私有成員變量和方法在運行期并沒有保護,可以通過地址偏移繞過編譯器的限制進行讀寫,但是不推薦這樣做。
  • 靜態成員變量和靜態成員方法存儲在程序的數據段和代碼段,不占用對象實例的內存空間。
  • 繼承類的內存布局,編譯器一般會把基類的成員變量放在派生類的成員變量之前,使對象模型變得更簡單和直觀。
  • 帶有虛函數的繼承,對象的內存布局中包含虛函數表指針,多態調用通過虛函數表實現。虛函數實現比較復雜,這里只考慮簡單的單繼承。
  • 地址空間布局隨機化(ASLR)是現代操作系統的安全特性,可以有效防止緩沖區溢出攻擊等安全漏洞。GDB 默認禁用 ASLR,可以通過 set disable-randomization off 命令開啟地址空間的隨機化。
責任編輯:趙寧寧 來源: 騰訊技術工程
相關推薦

2022-07-06 08:05:52

Java對象JVM

2023-12-31 12:56:02

C++內存編程

2024-04-10 12:14:36

C++指針算術運算

2022-05-06 16:18:00

Block和 C++OC 類lambda

2024-04-30 08:38:31

C++

2023-09-12 11:44:02

C++數據對齊

2023-11-05 12:05:35

JVM內存

2017-03-27 09:36:20

Flex布局計算

2024-04-10 07:40:45

Java虛擬機內存

2019-10-22 08:11:43

Socket網絡通信網絡協議

2015-12-28 11:25:51

C++異常處理機制

2023-10-04 00:04:00

C++extern

2020-06-01 21:07:33

C11C++11內存

2022-02-16 12:52:22

C++項目編譯器

2021-11-26 00:00:48

JVM內存區域

2024-04-11 14:04:23

C++編程函數

2023-09-19 22:47:39

Java內存

2020-11-04 15:35:13

Golang內存程序員

2013-06-20 10:25:56

2024-01-03 13:38:00

C++面向對象編程OOP
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 久操伊人 | 欧美日韩视频一区二区 | 高清国产午夜精品久久久久久 | 久久精品亚洲精品国产欧美 | 国产在线观看一区二区 | 精品国产一区二区三区四区在线 | 精品啪啪 | 91影视| 综合精品 | 一级片免费视频 | 男女羞羞视频网站 | 亚洲精品久久久一区二区三区 | 午夜播放器在线观看 | 国产精品国产a级 | 天天干天天谢 | 欧美日韩久久久 | 黄色av网站在线免费观看 | 99久久久久 | 成人三级网址 | 亚洲视频免费观看 | 免费国产视频在线观看 | 成人黄色a| 国产成人免费视频网站视频社区 | 午夜国产一区 | www.日韩| 欧美三区视频 | av中文字幕在线观看 | 久久久久国产一区二区三区四区 | 日韩在线中文 | 色频| 亚洲性视频 | 精品久久久久国产免费第一页 | 欧美综合国产精品久久丁香 | 中文字幕成人在线 | 亚洲国产电影 | 一区二区三区在线观看免费视频 | 97久久久| 91在线观看免费 | 欧美中文字幕一区 | 国产亚洲一区二区三区 | 日本免费一区二区三区四区 |