用匯編來實現OOP

用匯編來實現OOP

taowen


本人在OOP方面剛剛入門,只是看過一些國外這方面的好材料,才萌生了寫本文的念頭。希望能夠起到拋磚引玉的作用,引出高手們的批評和建議。

OOP和麪向過程都是編程中的思想,用學術一些的話是paradigm。曾經有人說過,既然cfront生成的是C代碼,那麼用C本身乃至彙編都可以實現OOP,只是太多東西需要自己手工來完成。確實是這樣的,面向過程早就用在彙編設計中了,OOP也早就和彙編有了交匯點(95年之前,TASM就引入了OOP的概念)。只是彙編實現OOP是沒有形式上的,無法提供C++這樣的Strong-typed和其他安全保證(比如存取權限)。封裝只是一種概念上的,自覺遵守的。

OOP有幾個關鍵,據我的粗淺理解即爲:封裝性,繼承與多態。具體表現就是把數據和操作數據的函數放在一起,數據放在對象中,提供接口實現存取。繼承性實現了語義或者實現的繼承,同時體現在概念層次與代碼重用兩個方面。多態則是利用指針實現使用pointer或reference來實現同一函數在不同繼承類中的多態表現。

OOP的對象模型有好幾種實現方式,在《inside C++ object model》中有極其詳盡的敘述:

  • 1.只把數據放在對象中,而通過name mangling技術把member-function與class關聯起來。
  • 2.單表模型,把member function的pointer放入到單獨的一個表格,把表格的入口地址放入對象中(一個類對應一個表格)。這在C++中表現爲Vtbl與Vptr,這種模型實現了運行時的動態靈活性,雖然多了兩次dereference。
  • 3.雙表模型,把數據與函數分列在兩個表格中,然後把兩個表格的入口地址存放在對象中,使得單個對象有了固定的大小。
  • 4.簡單模型,這個是彙編實做的時候用的模型。就是對象中即保存了數據也保存了函數地址。無論是TASM還是MASM,都是這麼做的。

從效能上來說,C++的做法是最優的。彙編使用第四種是迫不得已,是爲了實現的簡單性。一定程度上與彙編的高效的精神違背。

TASM已經不常用了,其OOP的做法和MASM的做法也是類似的。這裏主要討論MASM的OOP做法。作者是NaN 和Thomas Bleeker。其實現的辦法是用宏定義來達到本來應該是編譯器做的幕後工作。其中的宏的技巧很多。但是最終的使用是挺簡單的。宏的定義放在一個OBJECTS.INC的文件中,asm文件包含這個inc就能使用這個object model。

雖然宏做得很精巧,但是畢竟MASM缺少支持OOP的語法特性,在使用的很多方面都有麻煩或者在空間時間上有代價。比如覆蓋基類的虛函數必須每次手工的完成。也就是繼承的層次中所有父類以上的被覆蓋的虛函數都需要在子類中手工完成。雖然是有這樣那樣的缺點,但是OOP還是給彙編帶來了不少好處。比如:

  • 1.彙編更好的和COM,C++這樣的面向對象領域的東西互動。已經有用匯編+OOP調用com的例子。如果用匯編+OOP來寫com將可以產生適合高速度和小尺寸的組件。
  • 2.擴大了彙編能夠解決的問題範圍,使得彙編程序更加容易管理和合作編寫。這個object model的作者就用匯編+OOP寫了一個基於神經網絡的手寫字母識別的程序,不到200k(其中大部分是圖象文件佔用的空間)。

使用

定義一個基類的辦法。

;準備好函數原型
   Shape_Init    PROTO  :DWORD
   Shap_destructorPto    TYPEDEF  PROTO  :DWORD 
   Shap_getAreaPto    TYPEDEF  PROTO  :DWORD
   Shap_setColorPto    TYPEDEF  PROTO  :DWORD, :DWORD

;實際上就是STRUC的定義
  CLASS Shape, Shap
      CMETHOD destructor
      CMETHOD getArea
      CMETHOD setColor
      Color     dd    ?
   Shape ENDS

.data
;初始化
   BEGIN_INIT 
      dd offset Shap_destructor_Funct
      dd offset Shap_getArea_Funct
      dd offset Shap_setColor_Funct
      dd NULL
   END_INIT

.code
Shape_Init  PROC uses edi esi lpTHIS:DWORD
;實際調用初始化
   
;把edi assmue 爲Shape類型
   SetObject edi, Shape
;額外定義的DPrint宏,不用細究
     DPrint "Shape Created (Code in Shape.asm)"
;取消assmue
   ReleaseObject edi
   ret
Shape_Init ENDP
Shap_destructor_Funct  PROC uses edi lpTHIS:DWORD 
   SetObject edi, Shape
     DPrint "Shape Destroyed (Code in Shape.asm)"
     
   ReleaseObject edi
   ret
Shap_destructor_Funct  ENDP
Shap_setColor_Funct  PROC uses edi lpTHIS:DWORD, DATA:DWORD
   SetObject edi, Shape
   mov eax, DATA
   mov [edi].Color, eax
     
     DPrint "Shape Color Set!! (Code in Shape.asm)"
     
   ReleaseObject edi
   ret
Shap_setColor_Funct  ENDP
Shap_getArea_Funct  PROC uses edi lpTHIS:DWORD
   SetObject edi, Shape
     DPrint " "
     DPrint "   SuperClassing!!!!! This allows code re-use if you use this method!!"
     DPrint "   Shape's getArea Method! (Code in Shape.asm)"
     
     mov eax, [edi].Color
     DPrint "   Called from Shape.getArea, (Code in Shape.asm)"
     DPrintValH eax, "   This objects color val is"
     DPrint " "
     
   ReleaseObject edi
   ret
Shap_getArea_Funct  ENDP

繼承這個類

include Shape.asm  ; Inherited class info file
   Circle_Init    PROTO  :DWORD
   Circ_destructorPto    TYPEDEF  PROTO  :DWORD 
   Circ_setRadiusPto    TYPEDEF  PROTO  :DWORD, :DWORD
   Circ_getAreaPto  TYPEDEF PROTO :DWORD
   
   CLASS Circle, Circ
;繼承原有的數據和函數
      
      CMETHOD setRadius
      Radius     dd    ?
   Circle ENDS

.data
   BEGIN_INIT 
      dd offset Circ_destructor_Funct
      dd offset Circ_setRadius_Funct
      dd NULL
   END_INIT

.code
Circle_Init  PROC uses edi esi lpTHIS:DWORD
;初始化並實現繼承
   
   SetObject edi, Circle
;相當於構造函數重置vptr
     
     DPrint "Circle Created (Code in Circle.asm)"
   ReleaseObject edi
   ret
Circle_Init ENDP
Circ_destructor_Funct  PROC uses edi lpTHIS:DWORD 
   SetObject edi, Circle
     DPrint "Circle Destroyed (Code in Circle.asm)"
;實現了基類函數的調用     
     SUPER destructor
   ReleaseObject edi
   ret
Circ_destructor_Funct  ENDP
Circ_setRadius_Funct  PROC uses edi lpTHIS:DWORD, DATA:DWORD
   SetObject edi, Circle
   mov eax, DATA
   mov [edi].Radius, eax
   
     DPrint "Circle Radius Set (Code in Circle.asm)"
   ReleaseObject edi
   ret
Circ_setRadius_Funct  ENDP
CirleAreaProc PROC uses edi lpTHIS:DWORD
  LOCAL TEMP
  SetObject edi, Circle
  
  SUPER getArea
  
  
  mov eax, [edi].Radius
  mov TEMP, eax
  
  finit
  fild TEMP
  fimul TEMP
  fldpi
  fmul
  fistp TEMP
  
  mov eax, TEMP
  DPrint "Circle Area (integer Rounded) (Code in Circle.asm)"
  
  ReleaseObject edi 
  ret 
   
CirleAreaProc ENDP

根據類來生成對象,並使用

DEBUGC equ 1
.586
.model flat,stdcall
option casemap:none
include /masm32/include/windows.inc
include /masm32/include/masm32.inc
include /masm32/include/kernel32.inc
include /masm32/include/user32.inc
includelib /masm32/lib/kernel32.lib
includelib /masm32/lib/user32.lib
includelib /masm32/lib/masm32.lib
include Dmacros.inc
include Objects.inc
include Circle.asm
.data
.data?
hCircle dd ?
.code
start:
  ; Recuse all inherited constructors.. and do all inits
  DPrint " "
  DPrint " >>> main.asm <<< [ mov hCircle, $NEW( Circle ) ]"
  
  DPrint " "
  DPrint " >>> main.asm <<< [ METHOD hCircle, Circle, setColor, 7 ]"
  
  
  DPrint " "
  DPrint " >>> main.asm <<< [ METHOD hCircle, Circle, setRadius, 2 ]"
   
  
  DPrint " "
  DPrint " ------------ TEST POLYMORPHIC METHOD hCircle.getArea ------------- "
  DPrint " "
  DPrint " >>> main.asm <<< [ DPrintValD $EAX( hCircle, Circle, getArea ) , 'Area of hCircle' ]"
  
  DPrint " "
  DPrint " ------------ TEST POLYMORPHIC METHOD hCircle.getArea ------------- "
  DPrint " "
  DPrint " >>> main.asm <<< [   DPrintValD $EAX( hCircle, Shape, getArea ) , 'Area of hCircle' ]"
  DPrint " Typing calling this Ojbect Instance as a SHAPE type only! This is the true value"
  DPrint " of Polymorphism.  We dont need to know its a Circle object in order to get the"
  DPrint " proper area of this instance object, that is inherited from Shape."
  DPrint " "
  
  DPrint " "
  
  
  DPrint " "
  DPrint " >>> main.asm <<< [ DESTROY hCircle ]"
  
  DPrint " "
  DPrint " " 
  DPrint " NOTE: superclassing here, as each destructor call's the SUPER destructor"
  DPrint "       To properly clean up after each class.  To see SUPER classing in"
  DPrint "       in the Polymorphic getArea Function.  Uncomment the SUPER code in"
  DPrint "       CircleAreaProc, and re-compile"

     call ExitProcess
end start

看起來挺雜亂的,其實還是挺整齊的。由四部分組成。

  • 第一部分是各個成員函數的聲明。特別的一定要有一個“類名_Init”的函數,這個函數是類的構造函數,名字就是這個,不能改動。
  • 第二部分是由class引導的函數聲明,其實就是定一個STRUC,也就是結構體。其中通過內含基類的定義來達到結構上的繼承。(數據上的繼承在構造函數中調用SET_CLASS完成)。
  • 第三部分是放在.data中的初始化序列(BEGIN_INIT,END_INIT)。相當於C++的vtbl,但又包括了對象的數據成員的初始值。
  • 第四部分是各個成員函數的實現。特別的是構造函數中要調用的SET_CLASS和有可能調用的OVERRIDE,完成了數據的繼承和虛函數的改寫。

實際使用中參考一些已經有的例子就可以蠻舒暢的使用了。確實可以帶來很大的方便。


原理

所有的奧祕都在Object.inc中,其中定義瞭如下的宏

; --=====================================================================================--
; MACRO LIST INDEX:
; --=====================================================================================--
;   
;   METHOD          調用實體中的函數
;   
;   SetObject       把寄存器中的指針“認爲”某種結構的指針
;   ReleaseObject   取消這種“認爲”
;   
;   
;   
;
;   $EAX()           Accelerated METHOD, returns in eax
;   $EBX()           Accelerated METHOD, returns in ebx
;   $ESI()           Accelerated METHOD, returns in esi
;   $EDI()           Accelerated METHOD, returns in edi
;   $NEW()           Accelerated NEWOBJECT, returns in eax
;   $SUPER()         Accelerated SUPER, returns in eax
;   $DESTROY()       Accelerated DESTROY, returns in eax
;   $invoke()        Accelerated invoke, returns in eax
;
;   
;   
;
;   
;   SET_INTERFACE   To Declair Abbreviated Interface and Abv Name (MUST)
;   CMETHOD         聲明類或者接口中的函數
;
; --=====================================================================================--

宏的數量也不是很多,但是的確完成了編譯器爲我們完成的幕後工作。我只列出宏展開前後的代碼,並加以解釋。對於宏的具體實現由於牽涉到諸多語法和技巧,不方便詳細講解(其實我也是剛剛通過查手冊,一點點讀懂的)。

先來看CLASS吧,這個是想當然的入口部分。

其實很簡單就是把Class換成STRUC。

CLASS Shape, Shap
 CMETHOD destructor
 CMETHOD getArea
 CMETHOD setColor
 Color     dd    ?
Shape ENDS

替換之後就是

Shape STRUC
 CMETHOD destructor
 CMETHOD getArea
 CMETHOD setColor
 Color     dd    ?
Shape ENDS

很自然,看看CMETHOD是怎麼做的

CMETHOD destructor

就變成了

destructor PTR Circ_destructorPto ?

整個就展開爲:

Shape STRUC
  destructor PTR Circ_destructorPto ?
  getArea PTR Circ_getAreaPto ?
  setColor PTR Circ_setColor ?
  Color     dd    ?
Shape ENDS

^_^,結構體中就是函數指針和數據嘛。 然後,線索就斷了。光這樣定義一個結構是肯定不行的。那麼就從對象的產生開始吧,new是怎麼做的。

NEWOBJECT Circle

-->

invoke GetProcessHeap
invoke HeapAlloc, eax, NULL, SIZEOF Circle
push   eax
invoke Circle_Init, eax
pop    eax

這裏顯示了一個很明顯的缺陷,就是一定要在win32下使用,因爲win32api的使用。可以把api替換成一個外部的函數。然後放在不同的平臺上使用只要改變這個動態分配內存的函數就可以了。

產生的代碼很樸實,就是分配內存,然後調用對象的構造函數。這裏,強制的要求類的構造函數要以“類名_Init”的形式。雖然不是什麼大的限制,但是也不是很爽。這樣做也是有道理的,通過在編寫上對接上名字可以避免用指針這樣的東西來實現靈活性所帶來的overhead,下面可以看到析構函數用了指針的形式,這是因爲這裏默認了virtual desturctor。

好,下面前進到構造函數,我們看構造函數是怎麼寫的:

Shape_Init  PROC uses edi esi lpTHIS:DWORD
   SET_CLASS Shape
   SetObject edi, Shape
     DPrint "Shape Created (Code in Shape.asm)"
   ReleaseObject edi
   ret
Shape_Init ENDP

lpTHIS應該不會陌生,就是指向對象的指針。這裏一個對象也就是一個sturct啦。 第一行就是關鍵所在,SET_CLASS是最麻煩和富有技巧的一個宏。我們來看看是怎麼做的

SET_CLASS Shape

-->

push    esi
push    edi
cld   
mov  esi, offset @InitValLabel
mov  edi, lpTHIS
mov  ecx, @InitValSizeLabel
shr  ecx, 2
rep  movsd
mov  ecx, @InitValSizeLabel
and  ecx, 3
rep  movsb
pop  edi
pop  esi

push和pop是很普通的保存現場的做法。而mov esi, offset @InitValLabel則是和後面的BEGIN_INIT有關。offset @InitValLabel也就是BEGIN_INIT所標記的地址。這一段程序其實沒有做什麼特別的事情。也就是把BEGIN_INIT和END_INIT之間的初始化的數據賦給剛剛new出來的對象。lpTHIS就是這個對象的地址。由於SET_CLASS總是假定你在構造函數中調用它,所以lpTHIS當然是存在的(作爲構造函數的參數)。cld, rep movvsd 等都是彙編的快速搬移數據的技巧。查一下手冊就知道是幹什麼的了。也就是一開始儘量一個dword一個dword的搬移,然後就一個byte一個byte的移,直到全部都搬過去了。

如果帶上了繼承,則要麻煩許多

SET_CLASS Circle INHERITS Shape

-->

push    esi
push    edi
cld
mov  edi, lpTHIS
mov  esi, offset @InitValLabel
mov     eax, [esi]
mov     [edi], eax
add     esi, 4     
add     edi, Inher
mov  ecx, (@InitValSizeLabel - 4)
shr  ecx, 2
rep  movsd
mov  ecx, (@InitValSizeLabel - 4)
and  ecx, 3
rep  movsb
pop  edi
pop  esi

由於繼承了,所以要重置析構函數。mov eax, [esi]和mov [edi], eax做了這樣的工作。而由於析構函數的地址已經改變了,所以只需要 也只能繼承後面的數據成員包括虛函數的指針

接下來是對象的destroy

DESTROY hCircle

-->

mov eax, hCircle
push eax
call dword ptr [hCircle]
          
push eax
invoke GetProcessHeap
invoke HeapFree, eax, NULL, hCircle
pop eax

由於析構函數的地址是對象(結構體)的第一個成員,所以call就是調用析構函數。調用了之後就用win32api把申請的內存釋放掉

接下來是析構函數

Circ_destructor_Funct  PROC uses edi lpTHIS:DWORD 
   SetObject edi, Circle
     DPrint "Circle Destroyed (Code in Circle.asm)"
     
     SUPER destructor
   ReleaseObject edi
   ret
Circ_destructor_Funct  ENDP

這個是shape的繼承類circle的析構函數。裏面有一個SUPER,實現了調用基類中的函數。我們來繼續看它的實現。

SUPER destructor

-->

invoke Circ_destructorPto  PTR [ (INHER_initdata+INHER.MethodName) ], lpTHIS

Circ_destructorPto指定這個地址的類型是一個什麼樣的函數。INHER是宏內部的一個全局的東西,表示該類的基類名稱。INHER_initdata+INHER.MethodName的結構就是這個類在基類中的實際地址。

剩下的就是實際的使用對象中的函數了(你是“無權”操作對象中的數據的,雖然是概念上的。實際上你可以肆意的破壞這裏體現的OOP思想。因爲彙編不提供這樣的保護)。

METHOD hCircle, Shape, getArea

-->

mov edx, hCircle
invoke (Shape PTR [edx]).getArea, edx

收穫的季節了。這一句體現了多態的思想。hCircle指向的是一個Circle類的對象,但是調用的時候解釋爲Shape類。自己去理解吧。哪裏體現了多態。


我的看法

從全局來看這個對象模型,我們可以發現是這樣的。

  • 對象數據和虛函數指針放在同一個表格中
  • 所有的函數都是虛的
  • 繼承類改寫基類的虛函數需要在初始化數據之後手工完成(構造函數中)
  • 僅提供對上一層基類中被改寫的虛函數的訪問
  • 內存的分配和釋放使用win32api

OOP的三個特性的支持,如下

  • 封裝性:並沒有提供對於其中數據的特別保護(沒有Private)。數據和函數指針置於同一個結構體中成爲一個對象。訪問數據通過提供的接口完全靠自覺。
  • 繼承:通過結構體定義的嵌套(定義中包含已經定義的結構體),完成結構上的繼承。通過SET_CLASS完成數據意義上的繼承。所有的繼承都是Public的。
  • 多態:多態的狹義理解是對於同一個函數的調用將有不同的行爲。我們通過以下比較觀察,爲什麼這個對象模型支持了多態(因爲它支持了派生類對於基類函數的改寫)。
class Shape
{
 virtual float getArea();
 ……
};
class Cicle: public Shape
{
 float getArea();
 ……
};

當你通過對象指針調用一個對象中的虛函數的時候。其實你在編譯的時候已經指定了該指針的類型。比如:

float getArea(Shape* shp)
{
 return shp->getArea();
}

所以,編譯器可以通過查詢編譯時的信息來確定你所調用的函數在vtbl中的索引位置。然後這個調用就會被一個查詢vtbl,然後call所代替。而運行的時候,傳來的指針shp並不一定就是Shape類型,而是他的繼承類型。這樣兩個類的vtbl內容可能不一樣(派生類改寫了其中的某些slot的地址)。所以,這樣就可以實現在不知道派生類是什麼的情況下調用派生類的函數。奧祕就在於派生類和基類都把各自的實現版本放在了vtbl的相同位置。編譯期確定了位置,運行時確定了該位置的內容。

而這個彙編版的object model呢?其實差不多。mov edx, hCircle和 invoke (Shape PTR [edx]).getArea, edx就是一個多態的調用。hCircle實際指向一個Circle類型的對象(在這裏對象即有數據又承擔了vtbl的任務)。而調用的時候設計是把這個指針作爲Shape類性解釋的。也就是按照shape類型中getArea所在的index來調用。相同的index索引到不同的函數,多態就產生了。


可能的改進

關於虛函數

其實說實話,這個對象模式做得實在是很不錯,將宏的功能發揮到了極致。不過它強制的要求所有的類都有一個virtual destructor和所有的函數都是virtual。在宏的能力所及的範圍內,已經將定義和調用都做得儘可能的簡單,使用起來也的確有賞心悅目的感覺。不過我覺得不要把所有的函數都放在對象中(即強制性的作爲虛函數),那樣會增加一定的成本。

C++把非虛的成員函數看作是普通的函數,放在對象之外。其實我覺得這個對象模型也可以採用。而現有的強制性的將一些成員函數原型名稱限定爲“類型名_函數名Pto”,不如提供一個宏來這麼做好了。

我的建議,將部分函數放入對象中(就是用CMETHOD來聲明於類中)。而其他則不放入,只是寫在同一個文件中。然後用METHOD調用對象的member-function時,它會在彙編時決定這個member-function是否存在於虛函數表(也就是對象本身存放的一系列函數指針)中。如果不是則按照普通函數的一樣調用。如果是則按照現在這個模式調用。

METHOD這個宏也是編寫得出來的。在SUPER這個宏中就有檢驗該method是否在基類中第一次出現(而且還實現了檢驗是否是上一層而不是多層基類的檢驗)。這麼METHOD也可以檢驗調用的方法是否在class中出現過,然後分別使用不同的函數調用辦法。

關於SUPER

這個model中只能調用上一層(也就是父類的)的被覆蓋的函數,對於上上一層的被覆蓋的函數則無法SUPER了。其中的障礙在於無法知道那個函數是在哪個類型中第一次出現。我想如果手工提供這個類名,則可以SUPER任意層次的被覆蓋了的函數。像這樣:

SUPER getArea

SUPER getArea, Shape

沒有提供具體的類名,則認爲是SUPER上一層的,否則使用具體的類名來SUPER。SUPER的奧祕也就是查詢放在.data段的“類型名_initdata”中數據來達到“恢復”被改寫的函數的功能。

隨便講一句,TASM中的TABLE卻可以彌補OVERRIDE的缺陷,是一種更好的STRUC。不過那是Borland加強語法的結果。無論如何Thomas做到這一步也算是相當厲害。


鏈接

本文中所提到的Object Model的出處

我的個人主頁,提供了一個win32asm的基礎教程

發佈了61 篇原創文章 · 獲贊 1 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章