Jump to content
Fr0sT.Brutal

Get method's name as string from the code inside that method

Recommended Posts

Playing with ReturnAddress I discovered that it will be pretty easy to implement retrieval of name of an object's method without any RTTI or debug info. Could be useful for logging.

Alas, it relies on class layout internals but that's the only way to do. Code bases on TObject.MethodAddress

// Get address of currently executed code
function GetCurrentAddress: Pointer;
begin
  Result := ReturnAddress;
end;

// Get name of class method that contains the given address.
// Note that it has to utilize some internals
function GetMethodName(AClass: TClass; Address: Pointer): string; overload;
type   // copy declaration from System's impl section
  PMethRec = ^MethRec;
  MethRec = packed record
    recSize: Word;
    methAddr: Pointer;
    nameLen: Byte;
    { nameChars[nameLen]: AnsiChar }
  end;
var
  LMethTablePtr: Pointer;
  LMethCount: Word;
  LMethEntry, LResultMethEntry: PMethRec;
begin
  Result := '';

  { Obtain the method table and count }
  LMethTablePtr := PPointer(PByte(AClass) + vmtMethodTable)^;
  if LMethTablePtr = nil then // no methods...
    Exit;
  LMethCount := PWord(LMethTablePtr)^;
  if LMethCount = 0 then // no methods...
    Exit;

  Inc(PWord(LMethTablePtr));
  // Get all method entries and find max method entry addr that is less (or equal - very unlikely tho) than Address
  LMethEntry := LMethTablePtr;
  LResultMethEntry := nil;
  while LMethCount > 0 do
  begin
    // Only consider methods starting before the Address
    if PByte(LMethEntry.methAddr) <= PByte(Address) then
    begin
      // Not assigned yet
      if (LResultMethEntry = nil) or
        // Current entry is closer to Address, reassign the variable
        (PByte(LMethEntry.methAddr) > PByte(LResultMethEntry.methAddr)) then
        LResultMethEntry := LMethEntry;
    end;
    Dec(LMethCount);
    LMethEntry := Pointer(PByte(LMethEntry) + LMethEntry.recSize); // get next
  end;

  if LResultMethEntry <> nil then
    Result := string(PShortString(@LResultMethEntry.nameLen)^);
end;

// Get name of object's method that contains the given address
function GetMethodName(AObject: TObject; Address: Pointer): string; overload;
begin
  Result := GetMethodName(AObject.ClassType, Address);
end;

Test cases:

program Project2;

{$APPTYPE CONSOLE}

{$R *.res}

type
  TBaseClass = class
    procedure method; virtual;
  end;

  TTestClass = class(TBaseClass)
    procedure foo;
    procedure method; override;
    procedure method1; inline;
    procedure bar;
    class procedure classMethod;
  end;

procedure TBaseClass.method;
begin
end;

procedure TTestClass.method;
var s: string;
begin
  Assert(GetMethodName(Self, GetCurrentAddress) = 'method', 'override');
  // do some stuff to get another address
  str(123, s);
  Assert(GetMethodName(Self, GetCurrentAddress) = 'method', 'override');
end;

procedure TTestClass.foo;
begin
  Assert(GetMethodName(Self, GetCurrentAddress) = 'foo', 'usual - 1st');
end;

procedure TTestClass.bar;
begin
  Assert(GetMethodName(Self, GetCurrentAddress) = 'bar', 'usual - last');
end;

procedure TTestClass.method1;
begin
  Assert(GetMethodName(Self, GetCurrentAddress) <> 'method1', 'inline');
end;

class procedure TTestClass.classMethod;
begin
  Assert(GetMethodName(Self, GetCurrentAddress) = 'classMethod', 'class method');
end;

var
  cl: TTestClass;

begin
  cl := TTestClass.Create;
  cl.foo;
  cl.method;
  cl.method1; // ! inlined methods won't be detected !
  cl.bar;
  cl.classMethod;
  TTestClass.classMethod;
  Writeln('All tests OK');
  readln;

end.

 

Edited by Fr0sT.Brutal
  • Thanks 2

Share this post


Link to post

Hello,

this method (GetMethodName) works only for main class.

It does not display methods in the private and public section.

 

Is there any solution to show methods in private and public section?

Edited by pudnivec

Share this post


Link to post
1 hour ago, pudnivec said:

It does not display methods in the private and public section.

Only published members (properties, methods) have their names stored in the exe since that is required for DFM streaming to work. There's no reason have private/protected/public member names stored there, so they're not. It would just waste space in the exe file.

 

The reason the test case in the OP works is that the classes omit the scope specifier (private/protected/public/published) from the class declaration which means that all members use the default scope = published.

 

1 hour ago, pudnivec said:

Is there any solution to show methods in private and public section?

Look them up in the map file. This is what stack tracers do. Search for that.

Share this post


Link to post
1 hour ago, pudnivec said:

Is there any solution to show methods in private and public section? 

Compile the class with {M+} option. Sorry to misinform, I must have had type info in my project option enabled so I thought this works by default. Actually this will work for published members and for public members if a class is compiled inside {$M+} (== TYPEINFO ON) directive.

For other members, as Anders said, MAP file or debug info should be used

Share this post


Link to post
19 minutes ago, Anders Melander said:

The reason the test case in the OP works is that the classes omit the scope specifier (private/protected/public/published) from the class declaration which means that all members use the default scope = published.

Not exactly. They're = published when compiled with typeinfo, otherwise = public

  • Like 1

Share this post


Link to post

Thank you for your answers.

The problem is that when I use the MAP file (detailed mode), different method addresses are listed: in APP the address is [007B6B90: frmMain->Button9Click] (own generated string with IntToHex(NativeInt(Address: Pointer - generated with ReturnAddress), 8)) and in MAP [003B5B90 frmMain.TFormMain.Button9Click] - 007B6B90 vs 003B5B90   :-(

Edited by pudnivec

Share this post


Link to post

I found that the difference between the address in APP and MAP is still constant (even with other methods) 4.198.400 (DEC) or 401000 (HEX).. Do you know why?

Edited by pudnivec

Share this post


Link to post
Just now, pudnivec said:

I found that the difference between the address in APP and MAP is still constant (even with other methods) 4198400 (DEC) or 401000 (HEX).. Do you know why?

Sure
 

Quote

 

 Start         Length     Name                   Class
 0001:00401000 0004B984H .text                   CODE


 

Addresses in MAP file are given as offsets to segments

Share this post


Link to post

Perfect. You're the man for the job.

Thank you again for all the information. It helped me a lot. I will now use the MAP file for detailed analysis of the application's behavior and errors.

Share this post


Link to post

Glad to help! Check out this topic for MAP file reader routines

 

Edited by Fr0sT.Brutal

Share this post


Link to post

Thank you for the link. The code is inspiring, but for our needs I will use a slightly different solution.

Share this post


Link to post

The generated method address does not match (after or without 401000 HEX subtraction) adress of method in MAP file. Interestingly, the difference is different for each method (procedure).

 

The MAP file service contains the same offset as the classic 401000 HEX application.

 Start	       Length    Name   Class
 0001:00401000 0036EB7CH .text  CODE
 0002:00770000 0004E840H .data  DATA
 0003:007BF000 0000D4CCH .bss   BSS
 0004:00000000 00000280H .tls   TLS
 0005:00804000 00031710H .pdata PDATA

Could it be that the MAP file is generated incorrectly?

 

PS: The settings for both applications are exactly the same, including the compilation settings

Edited by pudnivec

Share this post


Link to post

ASLR is turned off. Booth app and service are on the same machine.

Edited by pudnivec

Share this post


Link to post

I may have misspoken in my last post. 

The address of the method (procedure) does not change for the compiled service. It is still the same.

What does change is the difference in subtraction for different procedures - see the following examples:
 

 0001:003561A0       Unit1.TMainService.ServiceStart  (MAP file address: 003561A0, service address (used with MethodAddress('ServiceStart') function): 004C71A0, 004C71A0 - 003561A0 =  171000 HEX (1.511,424 DEC)
 0001:003576E0       Unit1.TMainService.ServiceStop   (MAP file address: 003576C0, service address (used with MethodAddress('ServiceStop') function):  004C86E0, 004C86E0 - 003576C0 =  171020 HEX (1.511.456 DEC)

* I used the MethodAddress function only for the purpose of this post, because it refers to the address of the beginning of the method (procedure).

Interestingly, for procedure Unit1.TMainService.ServiceStart the difference between MAP file and service is 171000 HEX (1.511.424 DEC), but for procedure Unit1.TMainService.ServiceStop the difference is 171020 HEX (1.511.456 DEC).
The difference in both cases should be 401000 HEX (4.198.400 DEC) as indicated in the service MAP file.
 

 Start	       Length    Name   Class
 0001:00401000 0036EB7CH .text  CODE
 0002:00770000 0004E840H .data  DATA
 0003:007BF000 0000D4CCH .bss   BSS
 0004:00000000 00000280H .tls   TLS
 0005:00804000 00031710H .pdata PDATA

For the classic application on the same machine the difference is 401000 HEX (4.198.400 DEC) for all mathods (procedures) as shown in the MAP file.

Edited by pudnivec

Share this post


Link to post

Interestingly, if I use your function

function GetMethodName(AClass: TClass; Address: Pointer): string;

which does not read data from the MAP file, with the same generated address pointer by MethodAddress, the correct method name is returned from the service app (but only for main class, not for private and public section).


This indicates that most likely a bad MAP file is generated for the service app.

Share this post


Link to post

Finally, I used your code from GitHub (https://github.com/Fr0sT-Brutal/Delphi_StackTraces), in which I took the liberty to make a few changes to account for dynamic address changes when using e.g. ASLR.

 

Added MapAddrOffset: Pointer to variables

var
  MapFileAvailable: Boolean;
  LineAddrs: TArray<TMapFileLineAddrInfo>;
  PublicAddrs: TArray<TMapFilePublicAddrInfo>;
  MapAddrOffset: Pointer;

 

Added the output for MapAddrOffest: Pointer to the ReadMapFile procedure.

procedure ReadMapFile(const MapFile: string; out LineAddrs: TArray<TMapFileLineAddrInfo>;
  out PublicAddrs: TArray<TMapFilePublicAddrInfo>; out MapAddrOffset: Pointer);
var
  arr: TStrArray;
  CurrIdx, HighArr: Integer;
  UnitName: string;
  SegmentInfo: TMapFileSegmentStartAddrs;
begin
  LineAddrs:= nil;
  PublicAddrs:= nil;

  arr:= Split(MapFile, NL);
  CurrIdx:= Low(arr);

  ReadSegments(arr, CurrIdx, SegmentInfo);
  ReadPublics(arr, CurrIdx, SegmentInfo, PublicAddrs);

  //Get MAP address offset
  MapAddrOffset:= SegmentInfo[1];
  .
  .
  .
  .
  end;

 

Added new function GetOrigMapProcAddress(AName: string): Pointer.

function GetOrigMapProcAddress(AName: string): Pointer;
var
  i: Integer;
  uRes: UInt64;
begin
  Result:= Pointer(0);

  for i:= Low(PublicAddrs) to High(PublicAddrs) do
    begin
      if PublicAddrs[i].Name.Equals(AName) then
        begin
          uRes:= UInt64(PublicAddrs[i].Addr) - UInt64(MapAddrOffset);

          if uRes > High(NativeUInt) then
            uRes:= 0;

          Result:= Pointer(uRes);

          Break;
        end;
    end;
end;


Added new overloaded function GetAddrInfo(Addr, RealAddrOffset: Pointer; out AddrInfo: TMapFileAddrInfo): Boolean.

function GetAddrInfo(Addr, RealAddrOffset: Pointer; out AddrInfo: TMapFileAddrInfo): Boolean; overload;
var
  uRes: UInt64;
begin
  uRes:= UInt64(Addr) - UInt64(RealAddrOffset) + UInt64(MapAddrOffset);

  if uRes > High(NativeUInt) then
    uRes:= 0;

  if not MapFileAvailable
    then Result:= False
    else Result:= GetAddrInfo(Pointer(uRes), LineAddrs, PublicAddrs, AddrInfo);
end;

 

Now it is enough to use this code in any application (including service app), which uses dynamic address change e.g. ASLR.

 

To demonstrate the functionality I used the code from the service app:

var
  RealAddressOffset: Pointer;

function TMainService.GetRealAddressOffset: Pointer;
var
  uRes: UInt64;
begin
  uRes:= UInt64(MethodAddress('ServiceStart')) - UInt64(GetOrigMapProcAddress(UnitName + '.' + ClassName + '.ServiceStart'));

  if uRes > High(NativeUInt) then
    uRes:= 0;
	  
  Result:= Pointer(uRes);	
end;

function TMainService.GetAddressInfo(AAddress: Pointer): string;
var
  tmpAddrInfo: TMapFileAddrInfo;
begin
  GetAddrInfo(AAddr, RealAddressOffset, tmpAddrInfo);
  Result:= AddrInfoToString(tmpAddrInfo);
end;

Then just call this function e.g. as follows:

GetAddressInfo(GetCurrentAddress);

 

Edited by pudnivec
  • Thanks 1

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×