設(shè)為首頁收藏本站Access中國

Office中國論壇/Access中國論壇

 找回密碼
 注冊

QQ登錄

只需一步,快速開始

返回列表 發(fā)新帖
查看: 2899|回復: 6
打印 上一主題 下一主題

[模塊/函數(shù)] 【轉(zhuǎn)載 / 資料】Matthew Curland的VB函數(shù)指針調(diào)用

[復制鏈接]
跳轉(zhuǎn)到指定樓層
1#
發(fā)表于 2005-8-28 08:51:00 | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
Matthew Curland簡介:

    Visual Studio開發(fā)小組成員,參與開發(fā)了VB的IntelliSense和Object Browser。他是VB資深專家,對VB有非常深入的研究,堪稱VB大師。所著《Advanced Visual Basice》是闡述VB高級編程技巧的一本好書。

    本文英文原著可見2000年2月份《Visual Basic Programmer's Journal》(VB程序員月刊)里的《Call Function Pointers》,這是他發(fā)表的妙文之一,他的書里的第11章和本文同名,本文應(yīng)該是這一章節(jié)的精華。



    之所以推薦此文,是因為它綜合運用了VB里的不少技術(shù)。我們可從中看到Matt大師對VB的深刻理解,而各位技術(shù)的綜合運用正體現(xiàn)了他深厚的功力。



本文原文:http://www.devx.com/premier/mgznarch/vbpj/2000/02feb00/mc0200/mc0200.asp

(要先注冊成premier用戶)

本文配套代碼:

http://www.devx.com/free/mgznarch/vbpj/code/2000/02feb00/vb0002mc_p.zip





關(guān)鍵字:函數(shù)指針,COM、對象、接口,vTalbe,VB匯編,動態(tài)DLL調(diào)用。

級別:高級

要求:了解VB對象編程,了解匯編。



                          調(diào)用函數(shù)指針

    通過使用函數(shù)指針,我們能夠動態(tài)地在代碼中插入不同行為的函數(shù),從而使代碼擁有動態(tài)改變自身行為的能力。



作者:Matther Curland

要求:使用本文的示例代碼,你需要VB5或VB6的專業(yè)版或企業(yè)版。

    從Visual Basic 5.0開始Basic語言引入了一個重要的特性:AddressOf運算符。這個運算符能夠讓VB程序員直接體會到將自己的函數(shù)指針送出去的快感。比如我們在VB里就能夠得到系統(tǒng)字體的列表,我們能夠通過標準的API調(diào)用來進行子類化。一句話,我們終于可以象文檔里所說的那樣來使用Win32 API了。

    不過,這個新玩具只能給我們帶來短暫的快感,因為這個禮物并不完整。我們可以送出函數(shù)指針,但卻沒人能將函數(shù)指針送給我們。事實上,我們甚至不能給我們自己送函數(shù)指針,這使我們不能夠體驗送禮的真正樂趣(譯者:呵呵,光送禮卻不能收禮的確沒趣)。AddressOf讓我們看到了廣袤天地的一角,但是VB卻不讓我們?nèi)娴靥剿魉驗閂B根本就不讓我們調(diào)用函數(shù)指針,我們只能提供函數(shù)指針(譯者:可以先將函數(shù)指針送給API,然后讓API回調(diào)自已的函數(shù)指針來完成函數(shù)指針調(diào)用的功能,但這還是要先把禮物送給別人)。其實,我們能夠自己來實現(xiàn)調(diào)用函數(shù)指針的功能,我們可以手工將一個對COM接口的vTable綁定調(diào)用變成一個函數(shù)指針調(diào)用。最妙的是:我們能夠在純VB里寫出調(diào)用函數(shù)指針的代碼,不需要任何輔助的DLL。



    告訴編譯器函數(shù)指針是什么樣子,是使VB能夠調(diào)用任何函數(shù)的關(guān)鍵。將參數(shù)類型和返回值類型交給VB編譯器,讓編譯器將我們的函數(shù)調(diào)用編譯到我們的程序里,這樣程序才能在運行時知道怎樣去定位函數(shù)。在程序被編譯后,一個函數(shù)就是內(nèi)存里一串匯編字節(jié)流,通過CPU解釋執(zhí)行而形成我們的程序。調(diào)用一個函數(shù)指針,首先需要程序獲得指向這個函數(shù)字節(jié)流的指針,再通過x86匯編指令call將當前指令指針(譯注:即x86匯編里的IP寄存器)轉(zhuǎn)到函數(shù)所在的字節(jié)流上。在函數(shù)完成后,再用ret指令返回給調(diào)用此函數(shù)的程序來繼續(xù)操作。



    我下面將要提到的方法,利用了VB自己的函數(shù)調(diào)用方式,所以我先來解釋一下VB是怎樣來實現(xiàn)函數(shù)調(diào)用的。VB內(nèi)部使用三種函數(shù)指針,但是,在本質(zhì)上,不論VB是如何來定位這幾類函數(shù)指針,調(diào)用它們的方法卻是一樣的。VB編譯器必須知道準確的函數(shù)原型才能生成調(diào)用函數(shù)的代碼。



    第一類,最常見的函數(shù)指針類型,就是VB用來調(diào)用函數(shù)的普通指針,這樣的函數(shù)定義在標準模塊內(nèi)(或類模塊里的友元函數(shù)和私有函數(shù))。調(diào)用友元函數(shù)和私有函數(shù)時,調(diào)用指令定位在當前指令指針的一個偏移地址處,或者先跳到一個記錄著函數(shù)位置的查找表里,再跳到函數(shù)內(nèi)(譯者:即先"Call 絕對地址"跳到一個跳轉(zhuǎn)表內(nèi),表里的每個入口都是一個"Jmp"到函數(shù))。這些函數(shù)都在同一個工程內(nèi),聯(lián)結(jié)器總是將所有的模塊聯(lián)結(jié)在一起,所以總是知道在內(nèi)存何處能夠找到VB內(nèi)部函數(shù),因此轉(zhuǎn)移控制到內(nèi)部函數(shù)時,其運行時開銷是很少的。



VB對某些函數(shù)指針的調(diào)用卻困難得多

    對于另兩類函數(shù)指針,VB必須在運行時進行額外的工作才能夠找出它們。

    第二類,VB調(diào)用一個COM對象接口里的方法。我們可能認為建立COM對象的工作是相當復雜的,如果完全用VB來為我們建造COM的所有組成部分的話,但事實上并不是這樣。按照COM的二進制標準,一個COM對象是一個指針,這個指針指向一個結(jié)構(gòu),這個特定結(jié)構(gòu)的第一個元素是一個指向函數(shù)指針數(shù)組的指針。這個函數(shù)指針數(shù)組(又叫虛擬函數(shù)表,簡稱vTable)里的前三個指針,一定是標準QueryInterface,AddRef,Release函數(shù)。vTable里接下來的函數(shù)符合給定的COM對象接口定義里的函數(shù)定義(見圖一)









圖一:

函數(shù)指針代理是怎么工作的?click here



    當VB通過一個對象類型的變量來調(diào)用一個COM對象的方法或?qū)傩詴r,這個變量里存放著對這個COM對象接口的引用。VB要定位函數(shù)時,首先要通過COM引用的第一個元素來獲得指向vTalbe的指針,然后才能在vTable里定位函數(shù)指針。對一個vTable調(diào)用來說,編譯器提供了COM
分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享分享 分享淘帖 訂閱訂閱
2#
 樓主| 發(fā)表于 2005-8-28 08:51:00 | 只看該作者
動態(tài)指定函數(shù)指針

    無論是Declare還是庫型庫,當函數(shù)載入后,VB調(diào)用函數(shù)指針的方式是一樣的。指針已經(jīng)因為先前的調(diào)用而被載入了,所以第二次調(diào)用會更快,并且速度接近調(diào)用靜態(tài)聯(lián)結(jié)的函數(shù)。Declare語句是VB調(diào)用動態(tài)載入的函數(shù)指針的最自然的方法。但是,函數(shù)指針由VB決定而不是由我們來指定(譯者:此為原文直譯,意思應(yīng)該是:函數(shù)指針只能在編譯前指定,由VB來載入,而不能在運行時指定由我們自己動態(tài)載入的函數(shù)指針),所以我們不能用Declare語句來調(diào)用任意的函數(shù)指針。Declare語句的限制使我們只能載入在設(shè)計時通過Lib和Alias字句指定的函數(shù)。



    到這里,我已經(jīng)解釋了VB是怎么樣來調(diào)用自己的函數(shù)指針的。對VB本身沒有的功能進行擴展都應(yīng)該通過VB本身提供的工具來實現(xiàn)(譯者:看來作者Matt是一位VB純粹論支持者)。靜態(tài)聯(lián)結(jié)不用考慮——如果你喜歡自己修改PE文件頭的話,請自便(譯者:關(guān)于修改PE頭來Hook輸入函數(shù)的方法,在1998年2月MSJ專欄Bugslayer里,John Robbins大師就用純VB實現(xiàn)了HookImportedFunctionsByName,不過用來調(diào)用函數(shù)指針那是殺雞用牛刀)。我們不可能靜態(tài)地指定函數(shù)指針,所以Declare語句也不用考慮。但是,我們能夠在VB里自己用LoadLibaray和GetProcAddress這兩個API來從外部DLL里獲取函數(shù)指針,就象Declare為我們做的那樣。vTable調(diào)用是唯一一種讓VB自已綁定函數(shù)的調(diào)用方式。我們的任務(wù)是建一個符合COM二進制標準的結(jié)構(gòu),再將這個手工建立的COM對象的引用放到一個對象類型的變量里,然后調(diào)用手工建立的vTable入口。通過調(diào)用這個vTable里的函數(shù),就能夠直接代理到要調(diào)用的函數(shù)指針。我稱這個對象為FunctionDelegotor(函數(shù)代理者)。



    這個方法需要我們解決三個特有的問題。第一,vTalbe調(diào)用有額外的參數(shù)(this指針),我們不想將它也傳給我們的函數(shù)指針。所以我們需要一個通用的代理函數(shù)來將這個額外的this指針處理掉,然后才能進行調(diào)用。第二,我們需要建立一個vTable里有這個代理函數(shù)的COM對象。第三,我們需要一個接口定義才能讓VB編譯器知道我們的函數(shù)指針的樣子。接口定義應(yīng)該將函數(shù)原型也包括在vTable里,并且和代理函數(shù)在對象vTable里的位置一樣(譯者:當通過接口調(diào)用函數(shù)指針時,只有這樣才能夠讓代理函數(shù)處理掉做為函數(shù)參數(shù)壓在棧里的this指針)。



    我們可以用匯編代碼很容易地的寫出代理函數(shù)(譯者:對作者Matt來說的確很容易,因為他對在VB里插入線內(nèi)匯編代碼有相當深入的研究。其實作者這里的容易也是相對于Alpha平臺來說的)。在Intel平臺,所有傳遞給COM對象或標準API調(diào)用的參數(shù)都是通過堆棧來傳的。不幸的是,對Alpha平臺的VB來說不是這樣,它不能提供一種簡單的方法來寫出同樣功能的匯編代碼(譯注:Alpha平臺是一個RISC精簡指令集系統(tǒng),其參數(shù)傳遞多直接使用寄存器,要在這個平臺上手工寫匯編代碼要難得,從他的書的目錄里知道他在書里專門拿出一節(jié)介紹Alpha平臺下的匯編代碼)。



壓棧

    只要我們知道棧是什么樣子,我們就可以很清楚的知道匯編代碼需要做什么。VB僅僅支持符合stdcall調(diào)用規(guī)范的函數(shù)。這種調(diào)用規(guī)范,參數(shù)總是從右向左壓入棧中,并且是由調(diào)用者來負責棧的清理。清理的義務(wù)跟本文沒什么關(guān)系,但是壓棧的順序卻很重要。尤其要注意的是COM類里的this指針(在VB類里稱為Me),它總是作為最左邊的參數(shù)壓棧的。當函數(shù)被調(diào)用時,函數(shù)返回地址(函數(shù)返回后程序繼續(xù)執(zhí)行的地方)也被call指令本身壓入棧中。在任何COM接口輸出函數(shù)被執(zhí)行前,棧的樣子如下:



parameter n (第n個參數(shù),最右邊的參數(shù))

...

parameter 2

parameter 1 (第1個參數(shù))

this pointer(暗藏的this指針才是最左前的參數(shù))

return address (返回地址)



    但是,我們只想調(diào)用函數(shù)指針,并不需要暗藏的相關(guān)聯(lián)的this指針。調(diào)用一個符合vTable調(diào)用卻沒有額外參數(shù)的函數(shù),需要我們將this指針從棧里擠出來,然后才能將控制轉(zhuǎn)移到目標函數(shù)指針。讓this指針在棧里放著的好處是因為它指向結(jié)構(gòu)?紤]我們定義了一個結(jié)構(gòu),它的第二個成員是一個函數(shù)指針。這個成員距結(jié)構(gòu)開始位置的偏移是4個字節(jié)。那么將這個函數(shù)指出擠出來并通過代理函數(shù)調(diào)用它的匯編代碼如下:



;彈出返回地址到臨時的ecx寄存器,

;后面還要將它恢復。

pop ecx



;從棧里彈掉this指針(譯注:做為后面跳轉(zhuǎn)的基址)

pop eax



;重新將ecx寄存器里保存的返回地址壓棧

;以使得函數(shù)指針調(diào)用后知道返回到哪兒

push ecx



;將控制轉(zhuǎn)移到函數(shù)指針,

;它在this指針后偏移4個字節(jié)處。

jmp DWORD PTR [eax + 4]



這四條指令的連在一起需要6個字節(jié):59 58 51 FF 60 04。我們在后面補兩個Int3指令(CC CC)以湊足8個字節(jié),這正好可以一個VB的Currency變量內(nèi)。這樣一個Currency變量的地址里會放著如下的magic number(幻數(shù))——368956918007638.6215@ ——這個Currency變量是指向代理函數(shù)的函數(shù)指針。這個代理函數(shù)擠掉this指針,并可跳到任何函數(shù),而不用考慮函數(shù)的參數(shù)。這就是說,我們可以用同樣的匯編代碼來代理任何函數(shù)指針。我們現(xiàn)在需要一個vTable來包含這個指向字節(jié)流的指針
3#
 樓主| 發(fā)表于 2005-8-28 08:52:00 | 只看該作者
Listing 1 這段代碼將一個FunctionDelegator轉(zhuǎn)換成一個支持特定函數(shù)指針的COM對象。這是一個特殊的COM對象,因為它不要求任何內(nèi)存分配并且對我們的接口請求總是盲目合作。請求僅有的正確接口是我們的責任。



'The magic number

Private Const cDelegateASM _

   As Currency = -368956918007638.6215@



'到處到用的輔助函數(shù)

Private Declare Sub CopyMemory _

   Lib "kernel32" Alias "RtlMoveMemory" _

   (pDest As Any, pSrc As Any, ByVal ByteLen As Long)



Private m_DelegateASM As Currency



'vTable的類型聲明

Private Type DelegatorVTables

   'OKQI vtable in 0 to 3, FailQI vtable in 4 to 7

   VTable(7) As Long

End Type



Private m_VTables As DelegatorVTables



'指向vtable的指針, 成功QI

Private m_pVTableOKQI As Long



'指向vtable的指針, 失敗QI

Private m_pVTableFailQI As Long



'函數(shù)指針代理的結(jié)構(gòu)聲明

Public Type FunctionDelegator

   pVTable As Long 'This has to stay at offset 0

   pfn As Long  'This has to stay at offset 4

End Type



'初始化FunctionDelegator結(jié)構(gòu),并將指向它的指針

'    作為一個COM對象返回.

Public Function InitDelegator( _

   Delegator As FunctionDelegator, _

   Optional ByVal pfn As Long) As IUnknown

   '第一次訪問時初始化vTable

   If m_pVTableOKQI = 0 Then InitVTables

   With Delegator

      .pVTable = m_pVTableOKQI

      .pfn = pfn

   End With

   CopyMemory InitDelegator, VarPtr(Delegator), 4

End Function



'初始化vTable

Private Sub InitVTables()

Dim pAddRefRelease As Long

   With m_VTables

      .VTable(0) = _

         FuncAddr(AddressOf QueryInterfaceOK)

      .VTable(4) = _

         FuncAddr(AddressOf QueryInterfaceFail)

      pAddRefRelease = FuncAddr(AddressOf AddRefRelease)

      .VTable(1) = pAddRefRelease

      .VTable(5) = pAddRefRelease

      .VTable(2) = pAddRefRelease

      .VTable(6) = pAddRefRelease

      m_DelegateASM = cDelegateASM

      .VTable(3) = VarPtr(m_DelegateASM)

      .VTable(7) = .VTable(3)

      m_pVTableOKQI = VarPtr(.VTable(0))

      m_pVTableFailQI = VarPtr(.VTable(4))

   End With

End Sub



'成功QI

Private Function QueryInterfaceOK( _

   This As FunctionDelegator, _

   riid As Long, pvObj As Long) As Long

   '對第一次請求總是盲目合作

   pvObj = VarPtr(This)

   '交換成失敗時vTable,僅在調(diào)用函數(shù)指針會返回HRESULT錯誤代碼

   '    時才需要這么做,當然這么做總是更安全。

   This.pVTable = m_pVTableFailQI

End Function



Private Function AddRefRelease( _

   ByVal This As Long) As Long

   '什么都不做,無需要引用計數(shù)。

End Function



'失敗QI

Private Function QueryInterfaceFail( _

   ByVal This As Long, _

   riid As Long, pvObj As Long) As Long

   '對任何請求都說:"不"

   pvObj = 0

   QueryInterfaceFail = &H80004002 'E_NOINTERFACE

End Function



'返回函數(shù)指針的輔助函數(shù)

Private Function FuncAddr (ByVal pfn As Long) As Long

   FuncAddr = pfn

End Function



譯者:上面的代碼在原文已經(jīng)發(fā)表后經(jīng)過了修改,因此原文沒有提到為什么上面的代碼需要兩個不同的vTable。Matt在更新的示例代碼的Readme文件里解釋這個原因。我下面將這個原因簡單的敘述如下:

    這是因為當調(diào)用的函數(shù)指針需要返回HRESULT錯誤代碼時,VB會用再次調(diào)用QI來向?qū)ο笳埱笠粋ISupportErrorInfo接口的引用。但是,由于原來代碼里的QI完全采用盲目合作的信任方式,它總是返回對象自身的接口指針,哪怕它并不支持所要求的接口。由于返回的接口引用并不支持ISupportErrorInfo,所以當VB試圖用ISupportErrorInfo的方法來搜集錯誤信息時程序就會崩潰。解決的辦法,就是提供兩個vTable。當?shù)谝淮握{(diào)用初始化后的vTable里的QI時,它采取信任方式返回接口指針,并在返回之前將包含失敗QI的vTable交換進來。這樣下一次訪問的QI將是失敗QI,而失敗QI拒絕所有接口請求,這樣就有效的阻塞了后繼的QI請求,包括VB對ISupportErrorInfo的請求。在后面的Listing3的代碼中我們可以看到,一旦我們增加引用就會有類型不匹配錯誤。

    還有VB在對Err對象的處理上有BUG,那就是當VB用QI向某個對象請求ISupportErrorInfo接口失敗后,Err對象內(nèi)總是保留著對這個對象的引用。由于我們的vTalbe會先于Err對象釋放,所以Err對象里有一個掛起的引用,當釋放Err對象時程序會崩潰。解決的方法是:在程序結(jié)束前自己用Err.Raise來引發(fā)一個新錯誤。具體做法,見源代碼。



   

Listing 2 用來告訴VB編譯怎樣調(diào)用我們的函數(shù)指針的外部ODL文件。沒有對這個接口的描述,我們雖仍能生成代理到正確函數(shù)指針的COM對象
4#
 樓主| 發(fā)表于 2005-8-28 08:54:00 | 只看該作者
How VB Allocates Fixed-Size Arrays <TR align=left bgColor=#ffffcc>You might think the entire array is being created as a local variable on the stack when you allocate a fixed-sized array in VB (I did). This is only a half-truth. The array descriptor, a structure that describes an array抯 size and type, is placed on the stack like any other local variable. However, the array抯 data is allocated on the heap. If you want a fully stack-allocated array, you need to embed your fixed-size array in a user-defined type (UDT) and use a local variable typed as the UDT, not as a fixed-size array. The reason for these allocation semantics is that the 64K size restriction on embedded fixed-size arrays does not apply to nonembedded fixed-size arrays, but the 64K limit does apply to the total size of local variables. Similar semantics apply to a module-level fixed-size array, except that embedded fixed-size arrays are allocated with the module instead of on the stack. You should take the precaution of defining all vtables as fixed-size arrays in a UDT to protect against the case where an object is still pointing at a function delegator structure when VB is tearing the process down. During teardown, VB clears all heap-allocated variables (objects, arrays, strings) in all modules before releasing the memory allocated for each specific module. Your tear-down code is dependent on the ordering of your modules in the VBP file if your vtable is heap-allocated, and you don抰 have to worry about custom vtables vaporizing beneath your objects or disappearing during tear-down if you define any custom vtable in an embedded fixed-size array.
5#
 樓主| 發(fā)表于 2005-8-28 08:55:00 | 只看該作者
本文的附圖:









我花了整整5個小時,從網(wǎng)上找到的本文的VB源碼:







[此貼子已經(jīng)被作者于2005-8-28 1:33:01編輯過]

本帖子中包含更多資源

您需要 登錄 才可以下載或查看,沒有帳號?注冊

x
6#
發(fā)表于 2005-8-29 16:06:00 | 只看該作者
提示: 作者被禁止或刪除 內(nèi)容自動屏蔽

點擊這里給我發(fā)消息

7#
發(fā)表于 2005-8-30 02:17:00 | 只看該作者
好文!!
您需要登錄后才可以回帖 登錄 | 注冊

本版積分規(guī)則

QQ|站長郵箱|小黑屋|手機版|Office中國/Access中國 ( 粵ICP備10043721號-1 )  

GMT+8, 2024-10-23 10:24 , Processed in 0.104656 second(s), 32 queries .

Powered by Discuz! X3.3

© 2001-2017 Comsenz Inc.

快速回復 返回頂部 返回列表