IE DOM 樹概覽

[TOC]

一、DOM 流簡介

DOM (Document Object Model)作為現(xiàn)代瀏覽器的基礎(chǔ)栗菜,其設(shè)計和實(shí)現(xiàn)方式影響著整個瀏覽器的表現(xiàn)。對安全研究者而言夭谤,了解DOM 的結(jié)構(gòu)更是有著特殊的意義傍睹。在對 DOM 結(jié)構(gòu)有了了解之后進(jìn)行相關(guān)漏洞的分析隔盛,分析工作將會有一個清晰的脈絡(luò)和方向,使得分析工作變得更加簡單焰望,對漏洞的理解也會更加深刻和具體骚亿。

IE 作為一度占據(jù)很大市場份額的主流瀏覽器其 DOM 的組織和結(jié)構(gòu)值得我們?nèi)パ芯恳环E 瀏覽器在剛剛推出時還只是實(shí)現(xiàn)簡單的文本解析功能熊赖,隨著頁面功能的豐富和各種新特性的加入来屠,IE 瀏覽器的功能也變得越來越復(fù)雜,但是作為瀏覽器骨架的 DOM 結(jié)構(gòu)卻仍然延續(xù)了最初的設(shè)計,微軟嘗試通過不斷添加新的屬性以優(yōu)化其表現(xiàn)使其適應(yīng)現(xiàn)代瀏覽器的功能需要俱笛。

出于以上的歷史原因捆姜,IE 瀏覽器的 DOM 結(jié)構(gòu)并不是一個真正意義上的樹結(jié)構(gòu),而是被設(shè)計成為一個流結(jié)構(gòu)迎膜,頁面內(nèi)容解析出的節(jié)點(diǎn)在邏輯上以線性形式排列泥技,對于 DOM 的訪問和修改均以流的方式進(jìn)行。微軟在有關(guān)Markup 的設(shè)計文檔中描述了這一特點(diǎn) 磕仅。

以如下 Html 文檔舉例

My <B>dog</B> has fleas.

按照樹形結(jié)構(gòu)進(jìn)行構(gòu)建珊豹,上述頁面會被解析成如下的一棵樹

              ROOT
                |
          +-----+------+
          |     |      |
         "My"   B  "has fleas."
                |
              "dog"

但是如果將上述頁面進(jìn)行修改變成

Where do <B>you <I>want to</B> go</I> today?

在這個頁面中 <B> 標(biāo)簽和 <I> 標(biāo)簽相互嵌套,這樣一來頁面便無法被簡單的表示成為樹結(jié)構(gòu)榕订,此時便選擇使用流來表示頁面店茶,并通過范圍操作操作頁面內(nèi)容。

本文將通過對 IE 8 版本瀏覽器進(jìn)行分析和逆向劫恒,闡述 IE 中 DOM 的結(jié)構(gòu)贩幻,以及這種結(jié)構(gòu)如何在瀏覽器中發(fā)揮作用。(為了方便描述下文統(tǒng)一將 Html 文檔中的標(biāo)簽成為 tag两嘴,而將由 tag 解析而的到的對象稱為 Element丛楚。)

二、DOM 流的結(jié)構(gòu)

在 IE 的 DOM 流中憔辫,頁面元素 Element 并不直接相關(guān)聯(lián)趣些,而是間接通過一個叫做 CTreeNode 的類來完成這一功能。CTreeNode 類結(jié)構(gòu)如下(本文中所列出的數(shù)據(jù)結(jié)構(gòu)和函數(shù)均根據(jù) IE 8 逆向而來贰您,IE9喧务、IE10、IE11 中有所不同枉圃, 但是核心思想均是相同的)

class CTreeNode
{
public: 
    CElement * element;
    CTreeNode * parent;
    BYTE        _etag;                              // 0-7:     element tag
    BYTE        _fFirstCommonAncestorNode : 1;    // 8:       for finding common ancestor
    BYTE        _fInMarkup : 1;    // 9:       this node is in a markup and shouldn't die
    BYTE        _fInMarkupDestruction : 1;    // 10:      Used by CMarkup::DestroySplayTree
    BYTE        _fHasLookasidePtr : 2;    // 11-12    Lookaside flags
    BYTE        _fBlockNess : 1;    // 13:      Cached from format -- valid if _iFF != -1
    BYTE        _fHasLayout : 1;    // 14:      Cached from format -- valid if _iFF != -1
    BYTE        _fUnused : 1;    // 15:      Unused
    SHORT       _iPF;                               // 16-31:   Paragraph Format
                                                // DWORD 2
    SHORT       _iCF;                               // 0-15:    Char Format
    SHORT       _iFF;

    CTreePos    _tpBegin;
    CTreePos    _tpEnd;
    DWORD      unknow1;
    DWORD      unknow2;
    DWORD      unknow3;
};

每個 CTreeNode 都與一個 Element 相關(guān)聯(lián)功茴。一個 CTreeNode 中包含了兩個 CTreePos 結(jié)構(gòu),分別在邏輯上代表著一個 Element 對應(yīng)的 tag 的頭標(biāo)簽和尾標(biāo)簽孽亲,這些 CTreePos 按照 tag 在 html 文檔中的排布順序依次前后相連坎穿,DOM 流即是這些 CTreePos 對象鏈接而成的雙向鏈表。

CTreePos 對象結(jié)構(gòu)如下返劲,其中 _pLeft玲昧、_pRight 用于構(gòu)建 DOM 流;_cchLeft 表示當(dāng)前 CTreePos 代表的 tag 在頁面中左邊有多少個字符篮绿,可以看做 CTreePos 在 DOM 流中的 index(這里需要經(jīng)過計算)孵延。

class CTreePos
{
public:
    DWORD       _cElemLeftAndFlags; 
    DWORD       _cchLeft;           // 左子樹中的字符數(shù)量
    CTreePos*   _pFirstChild;      
    CTreePos*   _pNext;           
    CTreePos*   _pLeft;              //  當(dāng)前 CTreePos 在 DOM 流中的左邊
    CTreePos*   _pRight;            //  當(dāng)前 CTreePos 在 DOM 流中的右邊
}

以具體的例子來說明 IE 中 DOM 流的情況∏着洌考慮如下頁面

<html>
    <body>
    <textarea>This is a Test</textarea>
    </body>
<html>

上述頁面經(jīng)過 IE 解析之后構(gòu)成如下所示的 DOM 流尘应,每個 Element 與一個 CTreeNode 相關(guān)聯(lián)惶凝,CTreeNode 的兩個 CTreePos 分別按照其 tag 在頁面中的排布前后相連,html 的 tpBegin 與 body 的 tpBegin 之間省略的部分為頁面解析時自動補(bǔ)全的 Head Element

 <html>   <body>    <textarea>    This is a Test   </textarea>    </body>    <html>
   |         |          |                |              |             |         |
tpBegin...tpBegin    tpBegin        DataTreePos       tpEnd        tpEnd      tpEnd
   |         |          |                               |             |         |
   |         |          ------------CTreeNode------------             |         | 
   |         |                       textarea                         |         |
   |         |                                                        |         | 
   |         ----------------------------body--------------------------         | 
   |                                                                            |
   --------------------------------------html -----------------------------------
 

三犬钢、DOM 流的遍歷

為了將頁面內(nèi)容與解析出 DOM 流關(guān)聯(lián)起來苍鲜,IE 需要使用一個容器類來對 DOM 流進(jìn)行管理,這個容器類稱為 CMarkup 玷犹。在頁面開始解析時混滔,IE 會默認(rèn)創(chuàng)建一個 CMarkup 用于管理解析過程中產(chǎn)生的一系列 DOM 節(jié)點(diǎn)(CTreeNode)和 DOM 操作,此后每一次針對 DOM 的操作均需要為其指定一個 CMarkup 歹颓,該次操作的 DOM 流即為這個 CMarkup 管理的 DOM坯屿。

IE 瀏覽器在訪問頁面 DOM 流時也按照流的規(guī)范進(jìn)行,為此 IE 使用一個名叫 CMarkupPointer 的對象來作為 DOM 流的迭代器巍扛。CMarkupPointer 顧名思義是用于指示 CMarkup 即DOM 流中位置的指針愿伴。CMarkupPointer 并不是DOM 流的一部分,多數(shù)情況下它都是作為臨時變量來使用电湘。CMarkupPointer 可以被放置于頁面的這些位置:element 的開始、element 的結(jié)束或者 text 之中鹅经。由于 CMarkupPointer 本身不包含內(nèi)容寂呛,因此如果兩個 CMarkupPointer 指向了同一個位置便會難以區(qū)分。CMarkupPointer 的放置工作通過 MoveAdjacentToElement 函數(shù)完成瘾晃。函數(shù)原型如下

HRESULT MoveAdjacentToElement(
    CElement *elementTarget,
    ELEMENT_ADJACENCY
);

    enum ELEMENT_ADJACENCY {
         ELEMENT_ADJ_BeforeBegin
         ELEMENT_ADJ_AfterBegin
         ELEMENT_ADJ_BeforeEnd
         ELEMENT_ADJ_AfterEnd
    };

一旦 CMarkupPointer 被放置在了文檔中贷痪, 瀏覽器便可以通過它來獲取其所指向位置中 DOM 流的相關(guān)信息。這個功能使用 There 函數(shù)實(shí)現(xiàn)蹦误,函數(shù)原型如下

HRESULT There(
    BOOL fLeft,
    BOOL fMove,
    MARKUP_CONTEXT_TYPE pContextType,
    CElement **ppElement,
    long *plCch,
    OLE_CHAR *pch
);
  • 第一個參數(shù)指定獲取指針右邊還是左邊的信息
  • 第二個參數(shù)指定指針是否可移動劫拢,若不可移動,則函數(shù)僅僅會返回指針周圍內(nèi)容的描述强胰;否則舱沧,函數(shù)- 在返回周圍內(nèi)容描述的同時還會移動過去。
  • 第三個參數(shù)為返回值偶洋,返回pointer周圍的內(nèi)容類型熟吏。
Value Are Example
CONTEXT_TYPE_None pointer左邊或者右邊沒有東西 [p1]<HTML></HTML>[p2]
CONTEXT_TYPE_Text pointer左邊或者右邊是一個text tex[p]t
CONTEXT_TYPE_EnterScope 如果是Left,則point左邊是一個End tag玄窝;如果是Right牵寺,pointer的右邊是一個Begin tag 。 </B>[p]<B>
CONTEXT_TYPE_ExitScope 如果是Left恩脂,則point左邊是一個Begin tag帽氓;如果是Right,pointer的右邊是一個End tag 俩块。 <B>[p]</B>
CONTEXT_TYPE_NoScope pointer的左邊或者右邊不是一個可以成對的標(biāo)簽 <BR>[p]<BR>
  • 第四個參數(shù)返回 pointer 左邊或者右邊的element
  • 第五個參數(shù)用來限定讀取的text范圍黎休,同時也用來返回獲取的text 的大小
  • 第六個參數(shù)返回pointer 左邊或者右邊的 text

下面以具體的頁面舉例說明

[p1]Where [p2]<I>do </I>[p3]<B>you <BR>[p4]want</B> to go today[p5]?

對于頁面上的五個 CMarkupPointer 分別調(diào)用 There()函數(shù)浓领,調(diào)用的參數(shù)及結(jié)果如下表

Ptr Derection Type Element cch in cch out Text
p1 left None - - - -
p1 right Text - 2 2 Wh
p1 right Text - -1 6 -
p1 right Text - 345 6 Where
p2 left Text - NULL - -
p2 right EnterScope I - - -
p3 left ExitScope I - - -
p4 left NoScope BR - - -
p5 left Text I 100 12 NULL

下面通過實(shí)際的 js 操作來說明 IE 是如何通過 CMarkupPointer 來對流結(jié)構(gòu)進(jìn)行遍歷的

previousSibling,nextSibling奋渔,firstChild镊逝,lastChild

js 中可以通過 Node 的這四個函數(shù)獲取一個 Element 周圍 DOM 的信息,這里以 previousSibling 作為典型來說明嫉鲸,previousSibling 在 IE 8 中的函數(shù)邏輯如下所示

HRESULT CElement::GetpreviousSiblingHelper(CElement *this, CElement **previousSibling)
{
  CMarkupPointer * markupPointer;
  CDoc* cDoc;
  HRESULT result;

  cDoc = CElement::Doc(this);
  CMarkupPointer::CMarkupPointer(markupPointer, cDoc);   // 創(chuàng)建 CMarkupPointer

  result = markupPointer->MoveAdjacentToElement( this, ELEMENT_ADJ_BeforeBegin);    // 放置 CMarkupPointer
    if ( result == S_OK )
    {
      cDoc = CElement::Doc(this);
      result = sub_74D4A0B3(cDoc , markupPointer, &nextSibling);       // 通過 CMarkupPointer 獲取 Element
    }

  result = CBase::SetErrorInfo(markupPointer, result);
  CMarkupPointer::~CMarkupPointer(markupPointer);
  return result;
}

函數(shù)首先新建一個 CMarkupPointer 對象撑蒜,接著將該 CMarkupPointer 放置于目標(biāo)節(jié)點(diǎn)的 ELEMENT_ADJ_BeforeBegin 位置,而后通過該 CMarkupPointer 來檢查周圍的內(nèi)容玄渗,這里使用 CMarkupPointer::There 函數(shù)來獲取 CMarkupPointer 所指的位置的對應(yīng)節(jié)點(diǎn)信息座菠。邏輯上來說即 Element->_tpBegin->_pLeft->There(left)

nextSiblingfirstChild 藤树、lastChild 其函數(shù)邏輯也大體相同浴滴,有所區(qū)別的即是在調(diào)用 MoveAdjacentToElement 時的傳入?yún)?shù),分別為 ELEMENT_ADJ_AfterEnd岁钓、ELEMENT_ADJ_AfterBegin升略、ELEMENT_ADJ_BeforeEnd

這里也非常容易理解,以如下頁面流為例子屡限。調(diào)用 div.nextSibling 品嚣,首先使用一個 CMarkupPointer 對象指向在 div 的 ELEMENT_ADJ_AfterEnd位置,即下面頁面中[ae]的位置钧大,再調(diào)用 There 獲取 [ae] 右邊的內(nèi)容翰撑,即為下面頁面中的 <a> 標(biāo)簽,其他同理啊央。

<p></p>[bb]<div>[ab]<image/><input/>[be]</div>[ae]<a></a>

childNodes

childNodes 節(jié)點(diǎn)屬性則是通過 CMarkupPointer 遍歷對應(yīng) Element 節(jié)點(diǎn)而實(shí)現(xiàn)眶诈,在 IE 8 中其主要的功能函數(shù)為 CElement::DOMEnumerateChildren ,該函數(shù)逆向后主要功能代碼如下

CElement::DOMEnumerateChildren(CPtrAry<CTreeNode__> children)
{
     cDoc = CElement::Doc(this);
     CMarkupPointer::CMarkupPointer(markupPtrBegin, cDoc);   
     CMarkupPointer::CMarkupPointer(markupPtrEnd, cDoc);    
     ......
     result = markupPtrBegin->MoveAdjacentToElement( this, ELEMENT_ADJ_AfterBegin);    // 放置 MarkupPointer
     result = markupPtrEnd->MoveAdjacentToElement( this, ELEMENT_ADJ_AfterEnd);    // 放置 MarkupPointer
     do{
        ......
        child = markupPointer->There()
        children.Append(child); 
        result = markupPtrBegin->MoveAdjacentToElement( child, ELEMENT_ADJ_AfterBegin);    // 放置 MarkupPointer
        ......
        }while( !markupPtrBegin->isLeftOf(markupPtrEnd) )
    ......
}

通過兩個 CMarkupPointer 指針分別指向 Element 的開始位置 [ab] 和結(jié)尾位置 [ae]瓜饥,并從其開始位置開始依次遍歷 逝撬,其間所有的節(jié)點(diǎn)均為 Element 的子節(jié)點(diǎn)。

<div>[ab]<p></p><image/><input/><a></a></div>[ae]

四乓土、DOM 流的修改

現(xiàn)代瀏覽器中球拦,為了更好的用戶體驗(yàn),頁面經(jīng)常需要根據(jù)不同情況動態(tài)進(jìn)行變化帐我,DOM 流也需要相應(yīng)的進(jìn)行修改坎炼。為了提高對于流的訪問效率,IE 瀏覽器采用 Splay tree 來對這個流進(jìn)行操作拦键。SplayTree 雖然名義上稱作 Tree谣光,其實(shí)并不是一個真正意義上樹結(jié)構(gòu),其本質(zhì)是為了高效操作流結(jié)構(gòu)而產(chǎn)生的一套改進(jìn)算法芬为。

IE 中SpalyTree 的基本數(shù)據(jù)結(jié)構(gòu)為 CTreePos萄金,用于表示流中各數(shù)據(jù)在 SplayTree 中的邏輯關(guān)系蟀悦,再看一遍 CTreePos 的數(shù)據(jù)結(jié)構(gòu),其_pFirstChild氧敢、_pNext 指針便是用于描述當(dāng)前節(jié)點(diǎn)在 SplayTree 中的邏輯關(guān)系日戈。

class CTreePos
{
public:
    DWORD       _cElemLeftAndFlags;  // 左子樹中的 Begin Element 數(shù)量以及一些與 tag 有關(guān)的屬性
    DWORD       _cchLeft;           // 左子樹中的字符數(shù)量
    CTreePos*   _pFirstChild;      //  指向第一個子節(jié)點(diǎn)
    CTreePos*   _pNext;             //  若當(dāng)前節(jié)點(diǎn)為父節(jié)點(diǎn)的最后一個節(jié)點(diǎn),則該指針指向父節(jié)點(diǎn)孙乖,否則指向兄弟節(jié)點(diǎn)
    CTreePos*   _pLeft;
    CTreePos*   _pRight;
}

SplayTree 的主要功能既是在保證流結(jié)構(gòu)順序的情況下浙炼,使得最后訪問的節(jié)點(diǎn)處在樹的最頂層,從而提升訪問效率唯袄。以如下頁面舉例想要在在頁面[p1]弯屈、[p2] 位置插入標(biāo)簽 <p>

<html>[p1]<textarea></textarea>[p2]<html>

首先訪問 [p1] 位置,通過 Splay 操作將 [p1] 所指節(jié)點(diǎn)旋轉(zhuǎn)置樹頂恋拷,此時 SplayTree 如下左樹所示资厉;接著訪問 [p2] 位置,SplayTree 變?yōu)槿缦掠覉D蔬顾,此時針對DOM 的操作只需要發(fā)生在 [p1] 的右子樹上即可

      p1                                 p2
    /    \                             /    \
   hh    th                           p1    he
           \                         /  \
            te            =>       hh    th
              \                            \
               p2                           te 
                 \
                  he
// hh 表示 html tag 的頭結(jié)點(diǎn)宴偿,he表示html tag 的尾節(jié)點(diǎn)。依次類推

SplayTree 的核心函數(shù) Splay() 逆向如下诀豁,部分冗余代碼沒有給出

void CTreePos::Splay(CTreePos * t_node)
{
    DWORD t1;
    CTreePos *v1;
    CTreePos *v2;
    CTreePos *v3;
    CTreePos *v5;
    CTreePos *v6;
    CTreePos *v7;
    // v2 = t_node->parent->parent
    v1 = t_node->Parent();
    v2 = v1->Parent();

    while (v2)   // while grandparent
    {
        v3 = v2->Parent();
        // 如果 v3 沒有(當(dāng)前節(jié)點(diǎn)已經(jīng)旋轉(zhuǎn)到第三層窄刘,沒有曾祖父節(jié)點(diǎn)了)則只進(jìn)行一次單旋;否則進(jìn)行一次之字旋或者一字旋;
        if (v3)
        {
            t1 = t_node->_cElemLeftAndFlags ^ v1->_cElemLeftAndFlags;
            if (t1 & TPF_LEFT_CHILD)  // 如果 t_node 與其父節(jié)點(diǎn)形成左右且叁、或者右左的關(guān)系, 則進(jìn)行之字形旋轉(zhuǎn)
            {
                //           v3                         v3                               v3
                //          /  \                       /  \                            /    \
                //         v2   E                     v2   E                          t      E
                //        /  \                       /  \                           /    \ 
                //       D    v1          =>        D    t             =>         v2      v1
                //           /  \                       /  \                     /  \    /  \
                //          t    C                     A    v1                  D    A  B    C
                //         / \                             /  \
                //        A   B                           B    C
                v1->CTreePos::RotateUp(t_node, v2);   //RotateUp(t_node<eax>,v1<ecx>,v2);
                v5 = v2;
            }
            else           // 如果 t_node 與 其父節(jié)點(diǎn)均為左節(jié)點(diǎn)秩伞、或均為右節(jié)點(diǎn)逞带,則進(jìn)行一字型旋轉(zhuǎn)
            {
                //               v3                            v3                              v3
                //              /  \                         /    \                           /  \
                //             v2   E                       v1     E                         t    E
                //            /  \                        /    \                            / \
                //           v1   D           =>         t      v2            =>           A   v1
                //          /  \                        / \    /  \                           /  \
                //         t    C                      A   B  C    D                         B    v2
                //        / \                                                                    /  \
                //       A   B                                                                  C    D
                v2->CTreePos::RotateUp(v1, v3);      //RotateUp(v1<eax>,v2<ecx>,v3);
                v5 = v1;
            }
            v6 = v3;
        }
        else
        {
            v5 = v1;
            v6 = v2;
        }
        v5->RotateUp(t_node, v6);     //RotateUp(t_node<eax>,v5<ecx>,v6);
        // v2 = t_node->parent->parent
        v1 = t_node->Parent();
        v2 = v1->Parent();
    }
    return;
}

void CTreePos::RotateUp(CTreePos* childNode, CTreePos* parentNode)
{
    CTreePos *v1;
    CTreePos *v2;
    CTreePos *v3;
    CTreePos *v4;
    CTreePos *v5;
    CTreePos *v6;
    DWORD v7;
    DWORD v8;

    if (childNode->IsLeftChild())
    {
        // 右旋
        //        this               child
        //       /    \              /    \
        //    child     c     =>    a     this
        //    /   \                       /   \
        //   a     b                     b     c 
        //
        // 得到 childNode 的左節(jié)點(diǎn) ,通過 v2 指示出來
        v1 = childNode->_pFirstChild; 
        if (v1 && v1->IsLeftChild())
            v2 = v1;
        else
            v2 = NULL
        //得到 childNode 的右節(jié)點(diǎn)纱新,通過 v3 表示
        v1 = childNode->_pFirstChild;
        v3 = 0;
        if (v1)
        {
            if (v1->IsLeftChild())
            {
                // 如果其左節(jié)點(diǎn)有兄弟節(jié)點(diǎn),則該兄弟節(jié)點(diǎn)為右節(jié)點(diǎn)
                if (!v1->IsLastChild()) 
                    v3 = v1->_pNext;
            }
            else
            {
                v3 = v1;
            }
        }
        //得到 this 的右節(jié)點(diǎn)展氓,通過 v5 表示
        v4 = this->_pFirstChild;
        v5 = 0;
        if (v4)
        {
            if (v4->IsLeftChild())
            {
                // 如果其左節(jié)點(diǎn)有兄弟節(jié)點(diǎn),則該兄弟節(jié)點(diǎn)為右節(jié)點(diǎn)
                if (!v4->IsLastChild())
                    v5 = v4->_pNext;
            }
            else
            {
                v5 = v4;
            }
        }
        //替換 this 節(jié)點(diǎn)和 childNode 節(jié)點(diǎn)的上下關(guān)系
        this->ReplaceChild(childNode, parentNode);
        // 如果 childNode 有左節(jié)點(diǎn),則該節(jié)點(diǎn)為 childNode 的第一個子節(jié)點(diǎn)脸爱,且該節(jié)點(diǎn)的兄弟節(jié)點(diǎn)應(yīng)為 this
        // 如果 childNode 沒有左節(jié)點(diǎn)遇汞,則 childNode 的第一個子節(jié)點(diǎn)為 this
        if (v2)
        {
            v2->MarkFirst();
            v2->_pNext = this;
        }
        else
        {
            childNode->_pFirstChild = this;
        }
        // 如果 childNode 有右節(jié)點(diǎn),則 this 節(jié)點(diǎn)的第一個節(jié)點(diǎn)為該節(jié)點(diǎn)
        // 如果 childNode 沒有右節(jié)點(diǎn)簿废,則 this 節(jié)點(diǎn)的第一個節(jié)點(diǎn)為其右節(jié)點(diǎn)
        if (v3)
        {
            this->_pFirstChild = v3;
            v3->MarkLeft();
            // 如果 this 節(jié)點(diǎn)也有右節(jié)點(diǎn)空入,則此節(jié)點(diǎn)為原 childNode 右節(jié)點(diǎn)的兄弟節(jié)點(diǎn)
            // 如果 this 節(jié)點(diǎn)沒有右節(jié)點(diǎn),則此節(jié)點(diǎn)變?yōu)?this 最后一節(jié)點(diǎn)族檬,需要為其設(shè)置父節(jié)點(diǎn)指針
            if (v5)
            {
                v3->MarkFirst();
                v3->_pNext = v5;
            }
            else
            {
                v3->MarkLast();
                v3->_pNext = this;
            }
        }
        else
        {
            this->_pFirstChild = v5;
        }
        //this 節(jié)點(diǎn)變?yōu)?childNode 節(jié)點(diǎn)的右節(jié)點(diǎn)歪赢,也即最后一個節(jié)點(diǎn),將其父節(jié)點(diǎn)指針設(shè)置為 childNode
        this->MarkRight();
        this->MarkLast();
        this->_pNext = childNode;
        // 調(diào)整 this 節(jié)點(diǎn)和 childNode 節(jié)點(diǎn)的 subtree num
        v7 = ((childNode->_cElemLeftAndFlags >> TPF_FLAGS_SHIFT) << TPF_FLAGS_SHIFT);   //GetElemLeft : 清除flag 位的干擾
        this->_cElemLeftAndFlags - v7;
        this->SetFlag(_cElemLeftAndFlags);
        this->_cchLeft = this->_cchLeft - childNode->_cchLeft;
        // childNode 節(jié)點(diǎn)
        v8 = this->_cchLeft;
        if (childNode->IsNode())  // Begin,End
        {
            if (childNode->IsData2Pos())
            {
                this->_cchLeft = v8 - 1;
                if (childNode->IsBeginNode())  // NodeBeg
                    this->SetFlag(_cElemLeftAndFlags - 0x100);   // ElemLeft 減一
            }
        }
        else if (childNode->IsText())
        {
            v8 = v8 - (childNode->GetInterNode()->_tpEnd._cchLeft) & 0x3FFFFFFF;
            this->_cchLeft = v8;
        }
        return;
    }
    else
    {
        // 左旋
        //        child                 this
        //       /     \               /    \
        //      a     this    =>    child     c
        //           /   \          /   \
        //          b     c        a     b
        //代碼總體和右旋差異不大单料,這里不再逆向
    }
}

在通過 SplayTree 高效的實(shí)現(xiàn)了 DOM 流的訪問之后埋凯,IE 設(shè)計了一套專門用于操作 DOM 樹的機(jī)制稱為 CSpliceTreeEngine点楼,對于 DOM 流的一系列修改操作均通過它來進(jìn)行。SpliceTreeInternal() 函數(shù)部分功能逆向如下

CMarkup::SpliceTreeInternal( CTreePosGap *tpg_Begin,  CTreePosGap *tpg_End, CMarkup* target, CTreePosGap *tpg_tar, DWORD opt1白对,DWORD *opt2)
{

  CDoc *v1;
  CSpliceTreeEngine v2;
  HRESULT result;

  v1 = this->Doc();
  v2 = CSpliceTreeEngine::CSpliceTreeEngine(v1);
  EnsureTotalOrder(tpg1, tpg2);

  result = CSpliceTreeEngine::Init(this, tpg_Begin, tpg_End, target, tpg_tar, opt1,  opt2);
   // ...
   case copy:
    {
        result = v1->CSpliceTreeEngine::RecordSplice();
        result = v1->CSpliceTreeEngine::InsertSplice();
    }
   case move:
   {
    result = v1->CSpliceTreeEngine::RecordSplice();
    result = v1->CSpliceTreeEngine::RemoveSplice();
    result = v1->CSpliceTreeEngine::InsertSplice();
    }
   // ...
  CSpliceTreeEngine::~CSpliceTreeEngine(v1);
  return result;
}

函數(shù)首先調(diào)用 RecordSplice 函數(shù)將源 DOM 流中的節(jié)點(diǎn)信息備份一遍掠廓,接著根據(jù)操作要求決定是否將源 DOM 流中的節(jié)點(diǎn)信息刪除,最后將之前備份的節(jié)點(diǎn)信息插入目標(biāo) DOM 流中甩恼。

對 DOM 流結(jié)構(gòu)進(jìn)行操作還需要有一個重要的結(jié)構(gòu) CTreePosGap蟀瞧,該結(jié)構(gòu)用于指示兩個 CTreePos 之間的內(nèi)容,在對流進(jìn)行插入和刪除操作時都需要用到 CTreePosGap 結(jié)構(gòu)來指示需要操作的區(qū)間媳拴。CTreePosGap 數(shù)據(jù)結(jié)構(gòu)如下所示

class CTreePosGap{
    CElement    *_pElemRestrict; // 指定 CTreePosGap 所在的Element范圍
    CTreePos    *_ptpAttach;    // 指示與 CTreePosGap 相關(guān)聯(lián)的 CTreePos
    unsigned    _fLeft : 1;       // 當(dāng)前 Gap 是否在  CTreePos 的左邊
    unsigned    _eAttach : 2;
    unsigned    _fMoveLeft : 1;
}

當(dāng)然上述操作均要通過 CMarkupPointer 來作為 DOM 流的指針才能完成黄橘。

通常情況下,一個頁面內(nèi)容被修改之后屈溉, 頁面中的 CMarkupPointer 還會保留在之前未修改時的位置塞关。舉例來說

abc[p1]defg[p2]hij
abc[p1]deXYZfg[p2]hij

當(dāng)?shù)谝粋€頁面被修改為第二個頁面之后,雖然頁面的內(nèi)容發(fā)生了改變子巾,但是 CMarkupPointer 的相對位置仍然保持不變。但如果頁面的修改發(fā)生在 CMarkupPointer 指向的位置线梗,如上例中椰于,向c、d之間插入一個Z仪搔,p 的位置就會出現(xiàn)二義性瘾婿。

abcZ[p1]de  or  abc[p1]Zde

這時就需要引用另一個重要的概念gravity,每一個 CMarkupPointer 都有一個 gravity 值標(biāo)識著其左偏或右偏烤咧。仍以上述頁面為例

abc[p1,right]defg[p2,left]hij 

分別在p1,p2的位置插入一對 <B> 標(biāo)簽偏陪。這時由于gravity的存在,頁面會變成如下

abc<B>[p1,right]defg[p2,left]</B>hij 

默認(rèn)情況下 CMarkupPointergravity 值是 left煮嫌。下面的函數(shù)負(fù)責(zé)查看或者修改CMarkupPointergravity

enum POINTER_GRAVITY {
    POINTER_GRAVITY_Left,
    POINTER_GRAVITY_Right
};

HRESULT Gravity(
    POINTER_GRAVITY *pGravityOut
);

HRESULT SetGravity(
    POINTER_GRAVITY newGravity
);

再考慮如下例子

[p2]ab[p1]cdxy

當(dāng)bc 段被移動到 xy之間時p1的位置也出現(xiàn)了二義性笛谦,是應(yīng)該隨著bc移動,還是應(yīng)該繼續(xù)保持在原位呢

[p2]a[p1]dxbcy or [p2]adxb[p1]cy

這就需要 cling 的存在昌阿,如果p1指定了cling 屬性饥脑,那么頁面操作之后就會成為右邊所示的情況,否則就會出現(xiàn)左邊所示的情況

clinggravity 可以協(xié)同作用懦冰,考慮下面的例子

a[p1]bcxy

b移動到x灶轰、y之間,如果p1指定了 cling 屬性刷钢,并且 gravity 值為 right框往,那么p1便會跟隨b一起到xy之間。這種情況下如果b被刪除闯捎,那么p1也會跟著從DOM 流中移除椰弊,但并不會銷毀许溅,因?yàn)閜1還有可能重新被使用。cling相關(guān)的函數(shù)秉版,函數(shù)原型如下

HRESULT Cling(
    BOOL *pClingOut
);

HRESULT SetCling(
    BOOL NewCling
);

下面通過實(shí)際的 js 操作來說明如何對 DOM 流進(jìn)行修改的

appendChild

appendChild 意為在目標(biāo) Element 的最后添加一個子節(jié)點(diǎn)贤重,其內(nèi)部其實(shí)是通過 InsertBefore 來實(shí)現(xiàn)的,

CElement::InsertBeforeHelper()
{
    cDoc = CElement::Doc(this);
     CMarkupPointer::CMarkupPointer(markupPtr, cDoc);   

      markupPointer->MoveAdjacentToElement( this, ELEMENT_ADJ_BeforeEnd); 
     CDoc::InsertElement();
}

函數(shù)首先通過 CMarkupPointer 指定到 parent 的 BeforeEnd 位置清焕,再調(diào)用 CDoc::InsertElement() -> CMarkup::InsertElementInternal 進(jìn)行實(shí)際的插入操作并蝗,一般而言標(biāo)簽都是成對出現(xiàn)的,因此這里需要使用兩個 CMarkupPointer 分別指定新插入標(biāo)簽的 Begin 和 End 位置

HRESULT CMarkup::InsertElementInternal(CMarkup *this, int a2, struct CElement *a3, struct CTreePosGap *a4, struct CTreePosGap *a5, unsigned __int32 a6)
{
    CTreePosGap::PartitionPointers(v10, a5, a3, v7);
    CTreePosGap::PartitionPointers(v12, v11, a3, v69);
    //......
    CTreePosGap::SetAttachPreference(((*((_BYTE *)a5 + 8) & 1) == 0) + 1, (int)&v78);
    CTreePosGap::MoveTo((CTreePosGap *)&v78, v62);
    CTreePosGap::SetAttachPreference(((*((_BYTE *)v11 + 8) & 1) == 0) + 1, (int)&v78);
    CTreePosGap::MoveTo((CTreePosGap *)&v75, v16);
    //.....
    CTreePosGap::MoveTo((CTreePosGap *)&v71, v63);
    v69 = (int)CTreePosGap::Branch(v19);
    v21 = CTreePosGap::Branch(v20);
    //......
      if ( CMarkup::SearchBranchForNodeInStory(v22, v21, v62) )
        v70 = 1;
      v23 = (CTreeNode *)HeapAlloc(g_hProcessHeap, 8u, 0x4Cu);
        //......
        v25 = CTreeNode::CTreeNode(v23, v66, 0, (int)v62);
        //......
        CElement::SetMarkupPtr(v24, v62);
        CElement::PrivateEnterTree(v26);
        //......
      v27 = CTreeNode::InitBeginPos(v24, v67 == 0);
      CMarkup::Insert(v28, a3, v27);
        //......
        v30 = CTreePos::GetCpAndMarkup(v29, 0, 0);
        //......
      v34 = CTreeNode::InitEndPos(v33, v70);
      CMarkup::Insert(v35, a3, v34);
      CTreePosGap::MoveImpl(v36, (int)&v71, 0, 0);
}

函數(shù)的主要邏輯為秸妥,首先通過一系列的 CTreePosGap 操作滚停,指定 Begin 和 End 的位置;接著新建一個 CTreeNode 并與 Element 關(guān)聯(lián)粥惧。調(diào)用 CTreeNode::InitBeginPos 初始化標(biāo)簽對象的 BeginPos 键畴;接著調(diào)用 CMarkup::Insert 將 BeginPos 插入 DOM 流中,同時也插入 SpalyTree 中突雪,并調(diào)用 CTreePos::GetCpAndMarkup 獲取cp 信息起惕,更新 SpalyTree 結(jié)構(gòu),同時觸發(fā) Notify 咏删,進(jìn)行響應(yīng)事件的分發(fā)惹想。完成了 BeginPos 的操作之后,對 EndPos 也執(zhí)行相同的操作督函,最終完成功能嘀粱。

replaceNode

replaceNode 用于將 DOM 流中一個節(jié)點(diǎn)替換為另一個節(jié)點(diǎn),其主要功能函數(shù)我這里顯示不出符號表辰狡,其逆向主要功能代碼如下

HRESULT sub_74D359BA(CDOMTextNode *a1, int a2, int a3, struct CMarkupPointer *a4)
{
// .....
  CMarkupPointer::CMarkupPointer(v6, v7);
  CMarkupPointer::CMarkupPointer(v8, v7);
  result = CElement::GetMarkupPtrRange(v9, (struct CMarkupPointer *)&v15, (struct CMarkupPointer *)&v16, v13);
  if ( result == SUCCESS )
    v11 = CDoc::Move(v10, (struct CMarkupPointer *)&v15, (struct CMarkupPointer *)&v16, (struct CMarkupPointer *)1, v14);
  CMarkupPointer::~CMarkupPointer((CMarkupPointer *)&v16);
  CMarkupPointer::~CMarkupPointer((CMarkupPointer *)&v15);
  return v11;
}

函數(shù)的主要邏輯為锋叨,通過兩個 CMarkupPointer 指針指定需要替換的目標(biāo)節(jié)點(diǎn)在 DOM 流中的 Begin 和 End 位置,接著調(diào)用 CDoc::Move() 函數(shù)完成功能搓译。CDoc::Move() 則直接通過調(diào)用 CDoc::CutCopyMove 來實(shí)現(xiàn)

HRESULT  CDoc::CutCopyMove(CDoc *this, int a2, struct CMarkupPointer *a3, struct CMarkupPointer *a4, struct CMarkupPointer *a5, int a6, DWORD a7)
{
    //......
      CTreePosGap::MoveTo(v12, TPG_LEFT);
      CTreePosGap::MoveTo(v13, TPG_RIGHT);
      CTreePosGap::MoveTo(v14, TPG_LEFT);
    // ......
      if ( v7 )
        result = CMarkup::SpliceTreeInternal((CMarkup *)&v19,v15,(struct CTreePosGap *)&v19,(struct CTreePosGap *)&v22,*(struct CMarkup **)(v7 + 28),(struct CTreePosGap *)&v16,(int)a5,a6);
      else
        result = CMarkup::SpliceTreeInternal((CMarkup *)&v19,v15,(struct CTreePosGap *)&v19,(struct CTreePosGap *)&v22,0,0,(int)a5,a6);
 
  return result;
}

CDoc::CutCopyMove 根據(jù)傳入的 CMarkupPointer 位置信息構(gòu)造三個 CTreePosGap 對象悲柱,并根據(jù)調(diào)用者的要求锋喜,選擇是進(jìn)行 Copy 操作還是 進(jìn)行 Move 操作些己,最終將請求傳遞給 CSpliceTreeEngine

五嘿般、總結(jié)

IE 的這種 DOM 流結(jié)構(gòu)是由于歷史原因形成的一種特殊情況段标,隨著瀏覽器功能的越來越豐富,這種 DOM 組織方式出現(xiàn)越來越多的問題炉奴。2016 年 10 月份的補(bǔ)丁之后微軟在 Edge 中已經(jīng)拋棄了 DOM 流的設(shè)計逼庞,轉(zhuǎn)而構(gòu)建了一個真正意義上的 DOM 樹。 關(guān)于 Edge 中 DOM 樹的結(jié)構(gòu)將在以后的文檔中再進(jìn)行討論瞻赶。

IE 中與 DOM 相關(guān)的內(nèi)容還有很多赛糟,這里僅僅列出了一點(diǎn)微小的工作派任,還有很多復(fù)雜的結(jié)構(gòu)需要進(jìn)一步分析。

六璧南、Reference

[1] https://msdn.microsoft.com/en-us/library/bb508514(v=vs.85).aspx

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末掌逛,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子司倚,更是在濱河造成了極大的恐慌豆混,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件动知,死亡現(xiàn)場離奇詭異皿伺,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)盒粮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進(jìn)店門鸵鸥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拆讯,“玉大人,你說我怎么就攤上這事宰翅。” “怎么了爽室?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長嘿架。 經(jīng)常有香客問我,道長啸箫,這世上最難降的妖魔是什么耸彪? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任忘苛,我火速辦了婚禮,結(jié)果婚禮上召川,老公的妹妹穿的比我還像新娘胸遇。我一直安慰自己荧呐,他們只是感情好倍阐,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布峰搪。 她就那樣靜靜地躺著,像睡著了一般楣颠。 火紅的嫁衣襯著肌膚如雪咐蚯。 梳的紋絲不亂的頭發(fā)上春锋,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天,我揣著相機(jī)與錄音侧馅,去河邊找鬼馁痴。 笑死肺孤,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的小渊。 我是一名探鬼主播茫叭,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼揍愁,長吁一口氣:“原來是場噩夢啊……” “哼吗垮!你這毒婦竟也來了凹髓?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤锨络,失蹤者是張志新(化名)和其女友劉穎狼牺,沒想到半個月后是钥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡虏冻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年厨相,在試婚紗的時候發(fā)現(xiàn)自己被綠了蛮穿。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片毁渗。...
    茶點(diǎn)故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡灸异,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出细溅,到底是詐尸還是另有隱情喇聊,我是刑警寧澤蹦狂,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布凯楔,位于F島的核電站,受9級特大地震影響邻遏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜赎线,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一垂寥、第九天 我趴在偏房一處隱蔽的房頂上張望另锋。 院中可真熱鬧夭坪,春花似錦、人聲如沸律杠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽浑厚。三九已至,卻和暖如春物蝙,著一層夾襖步出監(jiān)牢的瞬間敢艰,已是汗流浹背钠导。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留票堵,地道東北人悴势。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓瞳浦,卻偏偏與公主長得像废士,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子矗蕊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 問答題47 /72 常見瀏覽器兼容性問題與解決方案傻咖? 參考答案 (1)瀏覽器兼容問題一:不同瀏覽器的標(biāo)簽?zāi)J(rèn)的外補(bǔ)...
    _Yfling閱讀 13,751評論 1 92
  • 本章內(nèi)容 理解包含不同層次節(jié)點(diǎn)的 DOM 使用不同的節(jié)點(diǎn)類型 克服瀏覽器兼容性問題及各種陷阱 DOM 是針對 HT...
    悶油瓶小張閱讀 638評論 0 1
  • 很多人知道婺源的油菜花美,也知道青海的油菜花很壯觀孙援,但你知道呼倫貝爾草原上的油菜花有多么的連片拓售,有多么的壯美嗎? ...
    草原上的某只羊閱讀 265評論 0 1
  • 白蓮花開滿城雨 一夜落花散盡葉 凌亂秋風(fēng)花已枯 凄寒相落獨(dú)白蓮
    白蓮花開滿城雨閱讀 232評論 0 2
  • 開心就好 對嗎?
    武小倩宇閱讀 99評論 0 1