Delphi Assembler / RTTI guru: can I get the memory address and enter information about the proposed Result variable in the function?

Consider this typical method trace code (simplified to illustrate):

type IMethodTracer = interface end; TMethodTracer = class(TInterfacedObject, IMethodTracer) private FName: String; FResultAddr: Pointer; FResultType: PTypeInfo; public constructor Create( const AName: String; const AResultAddr: Pointer = nil; const AResultType: PTypeInfo = nil); destructor Destroy; override; end; constructor TMethodTracer.Create( const AName: String; const AResultAddr: Pointer; const AResultType: PTypeInfo); begin inherited Create(); FName := AName; FResultAddr := AResultAddr; FResultType := AResultType; Writeln('Entering ' + FName); end; destructor TMethodTracer.Destroy; var lSuffix: String; lResVal: TValue; begin lSuffix := ''; if FResultAddr <> nil then begin //there probably a more straight-forward to doing this, without involving TValue: TValue.Make(FResultAddr, FResultType, lResVal); lSuffix := ' - Result = ' + lResVal.AsString; end; Writeln('Leaving ' + FName + lSuffix); inherited Destroy; end; function TraceMethod( const AName: String; const AResultAddr: Pointer; const AResultType: PTypeInfo): IMethodTracer; begin Result := TMethodTracer.Create(AName, AResultAddr, AResultType); end; ////// function F1: String; begin TraceMethod('F1', @Result, TypeInfo(String)); Writeln('Doing some stuff...'); Result := 'Booyah!'; end; F1(); 

It works as intended. Exit:

Enter F1
Doing some things ...
Exit F1 - Result = Booyah!

Now I'm looking for a way to minimize the number of parameters required to call TraceMethod() , which ideally allows me to skip Result arguments altogether. I have no experience with assembly or stack layout, but if I’m not mistaken, judging by the “magic”, I saw other people, at least the memory address of the implied magic Result -variable must be somehow obtained, Isn't it ? And maybe you can work from there to get type information too?

Of course, if you could even define the name of the "surrounding" function itself, which would eliminate the need to pass arguments to TraceMethod completely ...

I use Delphi XE2, so you can use all the recently introduced language / structure functions.

And before anyone mentions this: my actual code already uses CodeSite.EnterMethod / ExitMethod instead of Writeln -calls. I also know that this simplified example cannot handle complex types and does not make any errors.

+4
source share
2 answers

It's best to just go to @Result . If you do not, then there is no guarantee that Result even has an address. Functions that return simple types, such as Integer and Boolean , put the result in the EAX register. If there is no reason to get the address, then the compiler will not allocate any memory for it. Using the @Result expression forces the compiler to give it an address.

Just knowing the address, you will not receive a return type. There may be a way to detect this using RTTI. It will be three stages:

  • Extract class name from method name. Then you can get RTTI for this type . This will require the method name to include a unique name for the class (including the name of the element).

  • Using a list of methods of this type, find the RTTI for this method. This will be complicated by the fact that the name does not necessarily uniquely identify the method. All overloads will be displayed with the same name. (Rruz showed how to deal with RTTI overloaded methods in the context of Invoke .) Also, the name of the method that you get from the debug information will not necessarily match the name of the RTTI.

    Instead of trying to match the name, you can instead CodeAddress over all the methods of the class, looking for one whose CodeAddress property matches the address of the caller. However, determining how to get the start address of the call (instead of the return address) turned out to be more difficult to find than I expected.

  • Get the return type method and use Handle to get the desired PTypeInfo value.

+3
source

Some of these features are already included in our TSynLog class , which runs from Delphi 5 to XE2.

This is a complete Open Source and works with Delphi XE2, so you have all the necessary source code. And it contains an interception exception and a stack trace.

It allows you to track the trace as follows:

 procedure TestPeopleProc; var People: TSQLRecordPeople; Log: ISynLog; begin Log := TSQLLog.Enter; People := TSQLRecordPeople.Create; try People.ID := 16; People.FirstName := 'Louis'; People.LastName := 'Croivébaton'; People.YearOfBirth := 1754; People.YearOfDeath := 1793; Log.Log(sllInfo,People); finally People.Free; end; end; 

It will be registered as such:

 20120520 13172261 + 000E9F67 SynSelfTests.TestPeopleProc (784) 20120520 13172261 info {"TSQLRecordPeople(00AB92E0)":{"ID":16,"FirstName":"Louis","LastName":"Croivébaton","Data":"","YearOfBirth":1754,"YearOfDeath":1793}} 20120520 13172261 - 000EA005 SynSelfTests.TestPeopleProc (794) 00.002.229 

I.e:

  • You have line numbers and a method name;
  • You have the exact time spent on the method (00.002.229);
  • It is very easy to show any result (here the class is even serialized as JSON directly, and you can do the same with any data, even records);
  • Methods can be nested, and there is also a profiling tool available in our log viewer - that is, you can get in which methods most of the time is spent on the client side from the log file;
  • You can easily add a stack trace or any other necessary information;
  • Our code is very optimized for speed: for example. it uses the interface for the auto-vacation function, just like you, but it will not allocate memory, because it uses the trick "fake reference counter",
  • Of course, the definition of the Log variable on the stack is optional: if you only need to trace the input / TSQLLog.Enter method, just write TSQLLog.Enter . The local variable Log used to easily embed additional information into the method ( Log.Log(sllInfo,People) ).

Note that debugging information (i.e. method name and line numbers) is extracted from the proprietary highly optimized binary conversion of the .map file generated at compile time. It will be much smaller than .map itself (for example, 900 KB .map → 70 KB .mab, which can be easily embedded in exe), therefore it is smaller than the format used by JCL or MadExcept, and also smaller than the information embedded in Delphi compilation time.

I don’t think having a “result” hardcoded in the “Enter” method is worth it. This will add a lot of coding (for example, switching to TValue takes a lot of time), for little benefit - most of the time you need to know much more than the result.

+1
source

Source: https://habr.com/ru/post/1415202/


All Articles