由接口获取对象的代码分析
读了《 JCL中由接口获得对象的方法》后,对于文中提到的“在代码执行过程中,接口和对象指针的偏移是硬编码的在汇编中的。转换的过程,就是解析这段汇编的过程”,不是很理解,当然,也代码是如何做到的也不是很清楚,我想很多人也会有同样的问题。在经过请教牛人后,把自己的一些疑难点和理解做一个总结。
JCL的源码如下:
- //==================================================================================================
- // Interface information
- //==================================================================================================
- function GetImplementorOfInterface(const I: IInterface): TObject;
- { TODO -cDOC : Original code by Hallvard Vassbotn }
- { TODO -cTesting : Check the implemetation for any further version of compiler }
- const
- AddByte = $04244483; // opcode for ADD DWORD PTR [ESP+4], Shortint
- AddLong = $04244481; // opcode for ADD DWORD PTR [ESP+4], Longint
- type
- PAdjustSelfThunk = ^TAdjustSelfThunk;
- TAdjustSelfThunk = packed record
- case AddInstruction: Longint of
- AddByte: (AdjustmentByte: ShortInt);
- AddLong: (AdjustmentLong: Longint);
- end;
- PInterfaceMT = ^TInterfaceMT;
- TInterfaceMT = packed record
- QueryInterfaceThunk: PAdjustSelfThunk;
- end;
- TInterfaceRef = ^PInterfaceMT;
- var
- QueryInterfaceThunk: PAdjustSelfThunk;
- begin
- try
- Result := Pointer(I);
- if Assigned(Result) then
- begin
- QueryInterfaceThunk := TInterfaceRef(I)^.QueryInterfaceThunk;
- case QueryInterfaceThunk.AddInstruction of
- AddByte:
- Inc(PChar(Result), QueryInterfaceThunk.AdjustmentByte);
- AddLong:
- Inc(PChar(Result), QueryInterfaceThunk.AdjustmentLong);
- else
- Result := nil;
- end;
- end;
- except
- Result := nil;
- end;
- end;
为了理解这段代码,先构造一个简单的例子:
- var
- iIntf: IInterface;
- pOut: Pointer;
- begin
- iIntf := TInterfacedObject.Create;
- iIntf.QueryInterface( IInterface , pOut);
- end;
跟踪调试其汇编代码,顺便学习下汇编:
- Unit1.pas.31: iIntf := TInterfacedObject.Create;
- 00453355 B201 mov dl,$01
- 00453357 A194114000 mov eax,[$00401194]
- 0045335C E8A704FBFF call TObject.Create
- 00453361 8BD0 mov edx,eax //对象的地址保存到edx,为00E64F40
- 00453363 85D2 test edx,edx //判断构造是否成功
- 00453365 7403 jz $0045336a
- 00453367 83EAF8 sub edx,-$08 //由对象地址获取接口的指针 self - $08
- 0045336A 8D45FC lea eax,[ebp-$04] //获取临时变量iIntf地址,在堆栈中
- 0045336D E86E2AFBFF call @IntfCopy //赋值
- Unit1.pas.32: iIntf.QueryInterface( IInterface , pOut);
- 00453372 8D45F8 lea eax,[ebp-$08] //从堆栈中获取临时变量pOut地址
- 00453375 50 push eax //参数入栈
- 00453376 68A4334500 push $004533a4 //
- 0045337B 8B45FC mov eax,[ebp-$04]
- 0045337E 50 push eax //iIntf入栈
- 0045337F 8B00 mov eax,[eax]
- 00453381 FF10 call dword ptr [eax] //调用函数
//看编译器是如何找到函数地址的
- 00401145 83442404F8 add dword ptr [esp+$04],-$08 //获取对象的地址
- 0040114A E9254D0000 jmp TInterfacedObject.QueryInterface //获取对象的方法
- 0040114F 83442404F8 add dword ptr [esp+$04],-$08
- 00401154 E9434D0000 jmp TInterfacedObject._AddRef
- 00401159 83442404F8 add dword ptr [esp+$04],-$08
- 0040115E E94D4D0000 jmp TInterfacedObject._Release
现在回头去看代码就比较清楚了:
const
AddByte = $04244483; // opcode for ADD DWORD PTR [ESP+4], Shortint
AddLong = $04244481; // opcode for ADD DWORD PTR [ESP+4], Longint
这两个变量实际上对应了
83442404F8 add dword ptr [esp+$04],-$08 //获取对象的地址
汇编码的前四个字节,不过顺序变为由低位到高位了,那么我们要得到的偏移则是最后一个字节F8,也就是-$08
再看转换代码:
- Result := Pointer(I); //接口指针
- if Assigned(Result) then
- begin
- QueryInterfaceThunk := TInterfaceRef(I)^.QueryInterfaceThunk; //实际指向接口所指内存
- case QueryInterfaceThunk.AddInstruction of //前四个字节
- AddByte:
- Inc(PChar(Result), QueryInterfaceThunk.AdjustmentByte); //最后一个字节,偏移接口指针指向对象
- AddLong:
- Inc(PChar(Result), QueryInterfaceThunk.AdjustmentLong);
- else
- Result := nil;
- end;
- end;
遗留的问题:
1,esp+4的内容是什么时候压栈的
2,怎样才能构造出AddLong情况