Fr0sT.Brutal 900 Posted August 25, 2022 (edited) 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 August 25, 2022 by Fr0sT.Brutal 2 Share this post Link to post
pudnivec 1 Posted December 19, 2022 (edited) 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 December 19, 2022 by pudnivec Share this post Link to post
Anders Melander 1784 Posted December 19, 2022 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
Fr0sT.Brutal 900 Posted December 19, 2022 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
Fr0sT.Brutal 900 Posted December 19, 2022 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 1 Share this post Link to post
pudnivec 1 Posted December 19, 2022 (edited) 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 December 19, 2022 by pudnivec Share this post Link to post
pudnivec 1 Posted December 19, 2022 (edited) 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 December 19, 2022 by pudnivec Share this post Link to post
Fr0sT.Brutal 900 Posted December 19, 2022 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
pudnivec 1 Posted December 19, 2022 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
Fr0sT.Brutal 900 Posted December 19, 2022 (edited) Glad to help! Check out this topic for MAP file reader routines Edited December 19, 2022 by Fr0sT.Brutal Share this post Link to post
pudnivec 1 Posted December 19, 2022 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
pudnivec 1 Posted December 20, 2022 Can I ask you, why this solution does not work for windows service app? Share this post Link to post
pudnivec 1 Posted December 20, 2022 (edited) 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 December 20, 2022 by pudnivec Share this post Link to post
pudnivec 1 Posted December 20, 2022 (edited) ASLR is turned off. Booth app and service are on the same machine. Edited December 20, 2022 by pudnivec Share this post Link to post
Fr0sT.Brutal 900 Posted December 20, 2022 No idea currently but I'll take a look Share this post Link to post
pudnivec 1 Posted December 20, 2022 (edited) 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 December 20, 2022 by pudnivec Share this post Link to post
pudnivec 1 Posted December 20, 2022 3 hours ago, Fr0sT.Brutal said: No idea currently but I'll take a look Thank you. Share this post Link to post
pudnivec 1 Posted December 21, 2022 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
pudnivec 1 Posted December 24, 2022 (edited) 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 December 24, 2022 by pudnivec 1 Share this post Link to post