C++對象模型:g++的實現(五)

来源:https://www.cnblogs.com/lycpp/archive/2022/11/05/16861358.html
-Advertisement-
Play Games

前後端分離開發,後端需要編寫接⼝說明⽂檔,會耗費⽐較多的時間。 swagger 是⼀個⽤於⽣成伺服器接⼝的規範性⽂檔,並且能夠對接⼝進⾏測試的⼯具。 作用 ⽣成接⼝說明⽂檔 對接⼝進⾏測試 使用步驟 添加依賴 <!--swagger--> <dependency> <groupId>io.sprin ...


這篇博客來講一下g++實現的C++對象模型中的虛函數的實現,包括:單一繼承體系下的虛函數,多繼承下的虛函數和虛繼承下的虛函數。其中虛繼承下的虛函數在《深度探索C++對象模型》中只是說很複雜,受限於技術力和查到的資料,這裡我只是對於g++的部分實現進行觀察。

單一繼承體系下的虛函數

在前面的博客中我們已經通過對虛表的探索講了虛函數的一般實現,大體上來說就是編譯器會在適當的時候(在單一繼承體系中就是當類中第一次出現虛函數的時候)添加一個虛表指針,指向屬於該類的虛函數表,而所有虛函數的地址會出現在虛表指針的固定表項,也就是說在繼承體系下的一個虛函數會被賦予固定的虛表下標。當派生類覆寫(override)了基類的虛函數時,新的虛函數的地址會出現在基類虛函數在虛表中的位置,在多態調用虛函數時從虛表中取出虛函數地址來調用,從而實現多態。
一般而言,在單一繼承體系下每一個類都只有一個虛表,在這個虛表中存有所有active virtual functions(中文版《深度探索C++對象模型》沒有翻譯,我這裡也直接使用了,在我的理解里就是派生類所有有效的、能用的虛函數)的地址。這些active virtual functions包括:

  • 該類所定義的所有虛函數,包括其覆寫(override)的基類的虛函數;
  • 繼承自基類的虛函數,如果派生類不覆寫這些虛函數的話;
  • 一個pure_vairtual_called()函數實體,她既可以扮演pure virtual function的空間保衛者角色,也可以當作異常處理函數(有時候會用到)【《深度探索C++對象模型》原話】
// test23.cpp

class Base {
public:
    Base(int i)
        : m_i(i)
    {}

    virtual
    ~Base() {
        m_i = 0;
    }

    virtual
    int getInt() {
        return m_i;
    }

    virtual
    void increaseInt() {
        m_i++;
    }

    virtual
    long getLong() = 0;
private:
    int m_i;
};

class Derived: public Base {
public:
    Derived(int i, long l)
        : Base(i),
          m_l(l)
    {}

    virtual
    ~Derived() {
        m_l = 0;
    }

    virtual
    int getInt() override {     // overrid Base::getInt()
        return Base::getInt() + 1;
    }

    virtual
    long getLong() override {   // overrid Base::getLong(),在Base中是一個純虛函數
        return m_l;
    }

    virtual
    void increaseLong() {       // new virtual function
        ++m_l;
    }
private:
    long m_l;
};

int main() {
    Derived* pd = new Derived(1, 2L);
    int i = pd->getInt();
    pd->increaseInt();
    long l = pd->getLong();
    pd->increaseLong();
    pd->~Derived();
    delete pd;
}

gdb調試test23.cpp編譯出的程式
另外,在這裡我們可以註意到一個問題,虛表指針指向的空間,前兩個表項都顯示是Derived::~Derived(),也就是都是析構函數,而且地址不一樣,這是怎麼回事?我們看一下這兩處地方的彙編代碼:
Derived::~Derived()反彙編
可以看到,第一個析構函數就是普通的析構函數它先調用了我們自己定義的析構函數,再調用了基類的析構函數Base::~Base;而第二個虛構函數則是先調用了第一個析構函數,再調用了::operator delete(_ZdlPvm使用c++filt工具查看可知其就是operator delete(void*, unsigned long))。
那是不是就是當我們自己調用Derived::~Derived時調用第一個,使用delete操作符時調用的就是第二個呢?我們看到反彙編:
兩個調用析構函數的方法的反彙編
可以看到確實是這樣的。同時,我們還有一個小發現,就是當delete操作符操作的指針是nullptr時,是不會調用析構函數的,編譯器真是相當費心了(在我的測試下好像是只有delete一個指向有虛析構函數的對象的指針時才會檢查,否則就直接不檢查調用::operator delete)。
關於最後一個,因為我們無法實例化抽象基類,所以使用-fdump-class-hierarchy選項查看類信息:

Vtable for Base
Base::_ZTV4Base: 7 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI4Base)
16    0
24    0
32    (int (*)(...))Base::getInt
40    (int (*)(...))Base::increaseInt
48    (int (*)(...))__cxa_pure_virtual

Class Base
   size=16 align=8
   base size=12 base align=8
Base (0x0x7f24b28e7960) 0
    vptr=((& Base::_ZTV4Base) + 16)

Vtable for Derived
Derived::_ZTV7Derived: 8 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI7Derived)
16    (int (*)(...))Derived::~Derived
24    (int (*)(...))Derived::~Derived
32    (int (*)(...))Derived::getInt
40    (int (*)(...))Base::increaseInt
48    (int (*)(...))Derived::getLong
56    (int (*)(...))Derived::increaseLong

Class Derived
   size=24 align=8
   base size=24 base align=8
Derived (0x0x7f24b277d1a0) 0
    vptr=((& Derived::_ZTV7Derived) + 16)
  Base (0x0x7f24b28e7de0) 0
      primary-for Derived (0x0x7f24b277d1a0)

我們可以看到在Base類的48偏移處確實有一個__cxa_pure_virtual表項,應該就是所謂的pure_vairtual_called,在結合Derived類的虛表,在對應位置是Derived::getLong,說明正是使用該函數占位了Base::getLong這個虛函數。

多重繼承下的虛函數

在單一繼承體系下一切都顯得那麼美好,完全不涉及到指針的調整,因為所有的指針轉化都不需要做底層的調整,始終指向類的開頭。你可能現在還不能理解,在看完這一部分後再來看上面這一句話就會感慨:啊,單一繼承是這麼簡單的事!
但在多重繼承下事情開始變得複雜,看下麵的例子:

// test24.cpp

class Base1 {
public:
    Base1(int i)
        : m_i(i)
     {}
    
    virtual
    int getInt() {
        return m_i;
    }

    virtual 
    Base1* clone() {
        return new Base1(m_i);
    }
private:
    int m_i;
};

class Base2 {
public:
    Base2(long l)
        : m_l(l)
    {}

    virtual
    long getLong() {
        return m_l;
    }

    virtual
    Base2* clone() {
        return new Base2(m_l);
    }
private:
    long m_l;
};

class Derived: public Base1, public Base2 {
public:
    Derived(int i, long l)
        : Base1(i),
          Base2(l)
    {}

    virtual
    long getLong() {    // override Base2::getLong()
        return Base2::getLong() + 1L;
    }

    virtual
    Derived* clone() {  // override Base1::clone 和 Base2::clone
        return new Derived(getInt(), getLong());
    }
private:
};

int main() {
    Derived* pd = new Derived(1, 2L);
    Base2* pb2 = pd;
    long l = pb2->getLong();   // (1)
    Base2* p = pb2->clone();   // (2)
}

試想,在(1)這一語句上,我們使用pb2調用getLong這一虛函數,雖然pb2類型是Base2*,但它實際上指向的是類Derived的對象。由前面的知識我們知道,在指針由Dervied*轉化為Base2*時,會加上Base2在類Derived內的偏移(為0x10)。那就出問題了,pd2 = (pd2 == nullptr ? nullptr : pd + 0x10),在執行pb2->getLong()時,傳入的是pd2,但實際上調用的是Derived::getLong(),需要的是派生類Derived的指針,怎麼辦?
同時,在(2)這一語句上,返回的是Derived*指針,但接收的是Base2*指針,如何在運行時知道對指針進行處理?
解決這兩個問題的方法就是一個被稱為"thunk"的技術。
所謂"thunk",就是在代碼的前面或後面添加一段小的代碼段。
比如在Derived::getLong(),為了調整指針,編譯器會生成這樣一段代碼:

// 偽碼
// thunk for Derived::getLong()
this = this - 0x10
jmp Derived::getLong(this)

而在虛函數表中Derived::getLong()應該在的位置,便由上述thunk的地址代替了。
至於在pb2的clone()函數,則被調整為:

// 偽碼
// thunk for Derived::clone()
this = this - 0x10
Derived* pd =  Derived::clone()
pd = (pd == nullptr ? nullptr : pd + 0x10)
return pd

我們看一下反彙編,驗證一下:
Derived::getLong()的thunk
可以看到Derived::getLong()確實是這樣的。再看一下Derived::clone():
Derived::clone()的thunk第一部分:調整this指針
Derived::clone()的thunk第二部分:調整返回值
確實是前面描述的那樣,只不過編譯器將其分為了兩部分,一部分調整this指針,一部分調整返回值。
其實在《深度探索C++對象模型》中,還提到了一種情況,那就是基類指針調用派生類的虛函數,而在派生類的虛函數中又調用基類的虛函數。在這種情況下,在派生類的虛函數中調用基類的虛函數時又要調整this指針。
我覺得這種其實不是問題,因為在派生類中this指針明確是Derived*類型,既然要調用基類的虛函數,肯定是要將Derived*類型轉化為Base1*或者Base2*類型,自然要進行this指針的調整,這是自然而然的,不需要添加額外的東西。
總的來說,一個派生自n個基類的派生類,除了原本要生成的一個虛函數表外,還要生成n-1個額外的虛函數表。在本例中,有兩個虛函數表被編譯出來:

  • 一個主要實體,與Base1(最左側的基類)共用
  • 一個次要實體,與Base2(第二個基類)共用
    在g++中,這兩個表是緊貼在一起的。我們使用-fdump-class-hierarchy參數看一下類信息:
Vtable for Derived
Derived::_ZTV7Derived: 9 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI7Derived)
16    (int (*)(...))Base1::getInt   // 第一個虛表指針指向的地方
24    (int (*)(...))Derived::clone
32    (int (*)(...))Derived::getLong
40    (int (*)(...))-16
48    (int (*)(...))(& _ZTI7Derived)
56    (int (*)(...))Derived::_ZThn16_N7Derived7getLongEv    // 第二個虛表指針指向的地方
64    (int (*)(...))Derived::_ZTchn16_h16_N7Derived5cloneEv

Class Derived
   size=32 align=8
   base size=32 base align=8
Derived (0x0x7fa4a118e5b0) 0
    vptr=((& Derived::_ZTV7Derived) + 16)
  Base1 (0x0x7fa4a12e7ea0) 0
      primary-for Derived (0x0x7fa4a118e5b0)
  Base2 (0x0x7fa4a12e7f00) 16
      vptr=((& Derived::_ZTV7Derived) + 56)

虛繼承下的虛函數

《深度探索C++對象模型》中對於虛函數的實現並無講解,只是說再虛繼承體系下虛函數的實現非常複雜,其建議不要在虛基類中定義非靜態的數據成員。所以下麵只是我對於g++對虛繼承下虛函數的實現的觀察,並沒有形成總結。

// test25.cpp

class Point2D {
public:
    Point2D(int x, int y)
        : m_x(x),
          m_y(y)
    {}

    virtual
    ~Point2D() {
        m_x = m_y = 0;
    }

    virtual
    void allAddOne() {
        m_x += 1;
        m_y += 1;
    }

    virtual
    int z() const  {
        return 0;
    }

private:
    int m_x;
    int m_y;
};

class Point3D: virtual public Point2D {
public:
    Point3D(int x, int y, int z)
        : Point2D(x, y),
          m_z(z)
    {}

    virtual
    ~Point3D() {
        m_z = 0;
    }

    virtual
    void allAddOne() override {
        Point2D::allAddOne();
        m_z += 1;
    }

    virtual
    int z() const override {
        return m_z;
    }
private:
    int m_z;
};

int main () {
    Point3D* p3d = new Point3D(1, 2, 3);
    Point2D* p2d = p3d;
    p2d->allAddOne();
    int z = p2d->z();
}

我們先使用-fdump-class-hierarchy查看類的信息:

Vtable for Point2D
Point2D::_ZTV7Point2D: 6 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI7Point2D)
16    (int (*)(...))Point2D::~Point2D
24    (int (*)(...))Point2D::~Point2D
32    (int (*)(...))Point2D::allAddOne
40    (int (*)(...))Point2D::z

Class Point2D
   size=16 align=8
   base size=16 base align=8
Point2D (0x0x7ff517ae7960) 0
    vptr=((& Point2D::_ZTV7Point2D) + 16)

Vtable for Point3D
Point3D::_ZTV7Point3D: 16 entries
0     16
8     (int (*)(...))0
16    (int (*)(...))(& _ZTI7Point3D)
24    (int (*)(...))Point3D::~Point3D
32    (int (*)(...))Point3D::~Point3D
40    (int (*)(...))Point3D::allAddOne
48    (int (*)(...))Point3D::z
56    18446744073709551600
64    18446744073709551600
72    18446744073709551600
80    (int (*)(...))-16
88    (int (*)(...))(& _ZTI7Point3D)
96    (int (*)(...))Point3D::_ZTv0_n24_N7Point3DD1Ev
104   (int (*)(...))Point3D::_ZTv0_n24_N7Point3DD0Ev
112   (int (*)(...))Point3D::_ZTv0_n32_N7Point3D9allAddOneEv
120   (int (*)(...))Point3D::_ZTv0_n40_NK7Point3D1zEv

VTT for Point3D
Point3D::_ZTT7Point3D: 2 entries
0     ((& Point3D::_ZTV7Point3D) + 24)
8     ((& Point3D::_ZTV7Point3D) + 96)

Class Point3D
   size=32 align=8
   base size=12 base align=8
Point3D (0x0x7ff51797d1a0) 0
    vptridx=0 vptr=((& Point3D::_ZTV7Point3D) + 24)
  Point2D (0x0x7ff517ae7de0) 16 virtual
      vptridx=8 vbaseoffset=-24 vptr=((& Point3D::_ZTV7Point3D) + 96)

Point3D對象的結構還是比較簡單的,如下:

(gdb) x/8xw p3d
0x8414e70:      0x08201cd0      0x00000000      0x00000003      0x00000000
0x8414e80:      0x08201d18      0x00000000      0x00000001      0x00000002

很明顯0x08201cd0是Point3D新增的虛表指針,結合類信息,我們知道其指向了((& Point3D::_ZTV7Point3D) + 24);而0x08201d18是繼承自虛基類的放虛表指針的地方,只不過這裡放了Derived類自己的虛表指針,其指向了((& Point3D::_ZTV7Point3D) + 96)。
我們關註的重點是虛基類的虛函數表和其中虛函數的實現:虛函數表中放的是什麼的地址?不像沒有虛基類的多重繼承那樣各個對象的偏移是一定的,(在只有指針或引用的情況下)虛繼承下虛基類的偏移是運行時才能知道的,其中的虛函數又是如何調整this指針的呢?

(gdb) x/4ag 0x08201d18
0x8201d18 <_ZTV7Point3D+96>:    0x8000ba6 <_ZTv0_n24_N7Point3DD1Ev>     0x8000bdb <_ZTv0_n24_N7Point3DD0Ev>
0x8201d28 <_ZTV7Point3D+112>:   0x8000c24 <_ZTv0_n32_N7Point3D9allAddOneEv>     0x8000c3f <_ZTv0_n40_NK7Point3D1zEv>

正如類信息中展示的那樣,虛基類的虛表中放置的正是這幾個函數名字,但這幾個函數是什麼呢?我們使用c++filt看一下:

$ c++filt _ZTv0_n24_N7Point3DD1Ev
virtual thunk to Point3D::~Point3D()
$ c++filt _ZTv0_n24_N7Point3DD0Ev
virtual thunk to Point3D::~Point3D()
$ c++filt _ZTv0_n32_N7Point3D9allAddOneEv
virtual thunk to Point3D::allAddOne()
$ c++filt _ZTv0_n40_NK7Point3D1zEv
virtual thunk to Point3D::z() const

可以看到他們被稱為virtual thunk,看來是和thunk相似的技術,用來調整this指針和返回值,我們來看看其內部是怎麼運行的:
虛表指針指向的第一個表項指向空間的反彙編
和我們前面討論的thunk非常像,都是調整this指針,只是前面的thunk里this指針調整的值是固定的,而這裡this指針調整的值是動態的放在vptr[-3]處,我們再看一下這裡放的是什麼,我們直接看g++生成的類信息,虛表指針是指向((& Point3D::_ZTV7Point3D) + 96),那vptr[-3]就應該是((& Point3D::_ZTV7Point3D) + 72)放的東西了,可以看到是18446744073709551600,把這個值當作一個long類型的值的話正好是-16,這不就是從Point2D*類型轉化為Point3D*類型需要減的值嘛(因為Point2D在Point3D類的實體中偏移為16)。我們再檢查一下其他的virtual thunk是不是也是一樣?
虛表指針指向的第二個表項指向空間的反彙編
嗯,沒問題,再看看下一個:
虛表指針指向的第三個表項指向空間的反彙編
不好,出現不一樣了,這次偏移是vptr[-4]這裡,也就是((& Point3D::_ZTV7Point3D) + 64)放的東西,可以看到是18446744073709551600。咦,和上面的是一樣的。考慮到虛表指針指向的前兩項其實是一個函數,只不過一個不調用::operator delete,一個調用而已。那是不是編譯器為每個虛函數都準備了一個this指針的調整量?我們繼續看最後剩下的那個virtual thunk:
虛表指針指向的第四個表項指向空間的反彙編
果然是這樣的,這次是vptr[-5]
我們可以稍微總結一下虛繼承下虛函數的實現:就是在虛表裡為每個虛函數增加了虛基類指針到override該虛函數的派生類的指針需要對this進行的偏移。
我能做的總結也就是這樣了,如果有大神知道詳細的規則可以評論一下,或者給一個鏈接,謝謝。
這一章後面還有成員函數指針的內容,就留在後面的博客里講吧。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 嵌入式系統安全的一些解決方法及缺陷 外部硬體安全模塊:在主SoC之外包含一個專用的硬體安全模塊或可信元件,e.g. 手機的SIM卡。==隔離僅限於可以從非易失性存儲器運行的相對靜態程式== 內部硬體安全模塊:管理加密操作和密鑰存儲的硬體模塊;通用的處理引擎,放在主處理器旁邊,它使用自定義硬體邏輯來防 ...
  • 2022-11-05 一、自增長的鍵值問題 1、說明: 將在資料庫表單中添加數據的自增長的欄位返回給用戶 2、使用方式: 在預編譯語句中,除了要傳入sql語句外,還要傳入一個參數“Statement.RETURN_GENERATED_KEYS”。其中“Statement”表示的是一個介面,“RETU ...
  • 11月4日,HDC2022華為開發者大會在東莞松山湖舉辦。在本次大會的HMS Core創新圖形能力分論壇中,HMS Core重點介紹了其在3D技術領域的創新應用方向,其中3D建模服務展示了創新的自動骨骼綁定功能,其具有高度自動化,超強魯棒性,優質的蒙皮效果等優勢,進一步助力開發者技術創新。 HMS ...
  • 1、HTML基本結構 HTML網頁基本結構 HTML標簽都以“< >”開始、“</ >”結束 網頁中所有的內容都放在之間 網頁基本信息 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>腹白的博客</title ...
  • 目標: 會使用HTML5的基本結構創建網頁 會使用文本相關標簽排版文本信息 會使用圖像相關標簽實現圖文並茂的頁面 會使用標簽創建超鏈接、錨鏈接及功能性鏈接 1、什麼是HTML HTML:Hyper Text Markup Language(超文本標記語言) 超文本包括:文字、圖片、音頻、視頻、動畫等 ...
  • Tomcat02 4.IDEA開發JavaWeb工程 4.1開發javaweb工程&配置Tomcat&啟動項目 需求:使用idea開發javaweb工程fishWeb,並將網頁部署到fishWeb工程 點擊File-New-Project 在彈出的框中點擊 Java,點擊next 寫入你的工程名字, ...
  • MD5 常用於密碼加密,例如,在註冊時可以將密碼轉為 MD5 再放入資料庫,在登錄時校驗登錄密碼和資料庫存放的加密密碼是否一致,來保證密碼在資料庫中存儲的安全性。 下麵介紹使用 Java 自帶的加密類 MessageDigest 來加密字元: /** * MD5生成器 * * 1.MD5(messa ...
  • 限流概述 系統存在服務上限,流量超過服務上限會導致系統卡死、崩潰。 限流:為了在高併發時系統穩定可用,犧牲或延遲部分請求流量以保證系統整體服務可用。 限流演算法 固定視窗計數 將時間劃分為多個視窗; 在每個視窗內每有一次請求就將計數器加一; 如果計數器超過了限制數量,則本視窗內所有的請求都被丟棄當時間 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...