TypeTreeDumper icon indicating copy to clipboard operation
TypeTreeDumper copied to clipboard

Export Stalls

Open spacehamster opened this issue 5 years ago • 102 comments

I've started to implement exporting here https://github.com/spacehamster/TypeTreeDumper/tree/working, and issues with code that I would have expected to cause a hard crash cause the process to stall. Closing the terminal leaves a zombie unity process alive. There are no stack traces logged in Editor.log either. I wonder if there are good ways to debug issues like that?

spacehamster avatar Aug 18 '20 04:08 spacehamster

I took a quick look through the code, and I saw an issue that could be causing stalls: Produce was changed to take in byte instead of in RuntimeTypeInfo. in can cause a copy to be made, which would truncate the data sent to the function to the size of a byte. It should use ref instead of in.

I also noticed that the managed delegate for GenerateTypeTree is wrong. The order for that one should be object, tree, flags instead of object, flags, tree. Unless you're testing with a version of Unity that lacks TypeTreeCache I don't think that's the cause of the hang though.

The zombie process could probably be dealt with by terminating the Unity process from the main console app when it closes. I'm not sure about how other issues could be debugged though.

DaZombieKiller avatar Aug 18 '20 07:08 DaZombieKiller

Thanks for the help with Produce and GenerateTypeTree. Debugging with logging to console had narrowed it down to something to do with object produce, I wasn't aware I had screwed up GenerateTypeTree too.

The question was mostly about debugging, I'm not very experienced with C# interop, so I expect to make a bunch of dumb mistakes like that, and debugging is a bit harder without the stacktraces unity was giving when it crashed. I'll look into it a bit more and see if I can come up with a good solution.

spacehamster avatar Aug 18 '20 08:08 spacehamster

I did some cursory searching, and apparently adding -logFile (with no file name listed afterwards) will cause Unity to route all logging to stdout, so you could potentially retrieve any stack traces in the main console program by reading from it.

Edit: According to Unity documentation, the parameter is -logfile - to log to stdout on Windows.

DaZombieKiller avatar Aug 18 '20 08:08 DaZombieKiller

Ah, I had assumed that it was automatically writing logs to C:\Users\username\AppData\Local\Unity\Editor\Editor.log, but it seems like you need to manually set a log path. I had been reading stale log files.

After fixing the log path and the Object::Produce signature, the log shows:

Cannot create on non-main thread without kCreateObjectFromNonMainThread

Assertion failed on expression: 'CurrentThread::IsMainThread()

and stalls while calling DestroyImmediate. It seems it doesn't like creating and destroying objects from outside the main thread.

Also passing ObjectCreationMode.FromNonMainThread to Object::Produce changes the log to this.

Cannot create on non-main thread without an instanceID

Assertion failed on expression: 'CurrentThread::IsMainThread()'

spacehamster avatar Aug 18 '20 11:08 spacehamster

Yeah, you need to create an instance ID yourself if you want to create an object outside of the main thread. I think there's a function for that somewhere in the code, I just don't know the signature. (Edit: It's ?AllocateNextLowestInstanceID@@YAHXZ, but I don't know if it's safe to call outside the main thread. It doesn't seem to check, at least. Also, I haven't checked Unity 4, but it's not present in Unity 3.)

For destroying objects outside of the main thread, we might be able to make use of the dummy project. Unity has an -executeMethod parameter that can be used to call a static method after loading the project, which could connect to the injected TypeTreeDumper.Client assembly somehow and provide an API for queuing work on the main thread.

Edit: Alternatively, we could hook the update loop with EasyHook, which would place us on the main thread. It looks like ?Update@SceneTracker@@QEAAXXZ is where the EditorApplication.update event is triggered.

DaZombieKiller avatar Aug 18 '20 11:08 DaZombieKiller

I was going to suggest trying to use -executeMethod to trigger the main thread to somehow call into TypeTreeDumper.Client to get it running on the main thread, but if EasyHook can hook into the update loop, that would be even better.

spacehamster avatar Aug 18 '20 13:08 spacehamster

The AfterEverythingLoaded callback might actually be running on the main thread too, so it's worth experimenting with running more code in that.

Edit: I tried printing the result of CurrentThread::IsMainThread in that callback and got True.

DaZombieKiller avatar Aug 18 '20 13:08 DaZombieKiller

Something interesting to note is that using SymbolResolver.Resolve/SymbolResolver.ResolveFunction within that function can cause a hang. I think the cause is DIA, because if I resolve that symbol in the Run function first, there is no hang (DiaSymbolResolver caches previously resolved symbols, so DIA wouldn't run the second time). I have no idea why that would happen though.

Edit: It seems DIA does not like being used from multiple threads. If I create a completely new DIA session on the main thread, there are no hangs. So it might be worth turning the symbol resolver and DIA-related fields into ThreadLocal<T> fields.

DaZombieKiller avatar Aug 18 '20 14:08 DaZombieKiller

Hmm, I wonder if dbghelp.dll has the same issue. The thread issue probably isn't big enough problem to switch over even if it doesn't.

spacehamster avatar Aug 18 '20 14:08 spacehamster

I just pushed two commits that fix cross-thread usage of DiaSymbolResolver, it was actually much simpler than the solution I just proposed above.

DaZombieKiller avatar Aug 18 '20 14:08 DaZombieKiller

The AfterEverythingLoaded callback might actually be running on the main thread too, so it's worth experimenting with running more code in that. Edit: I tried printing the result of CurrentThread::IsMainThread in that callback and got True.

I tried calling ?IsMainThread@CurrentThread@@YA_NXZ and it returns true when called inside the AfterEverythingLoaded callback and when called from EntryPoint.Run, there seems to be something wrong with that call.

Moving the dumping logic to the AfterEverythingLoaded callback fixes the stall.

The output is not valid, but i'm still looking into that

spacehamster avatar Sep 01 '20 00:09 spacehamster

I tried calling ?IsMainThread@CurrentThread@@YA_NXZ and it returns true when called inside the AfterEverythingLoaded callback and when called from EntryPoint.Run, there seems to be something wrong with that call.

This is caused by an incorrect P/Invoke signature. The default behaviour for bool is to marshal a WinAPI BOOL type, instead of a C++ bool. To fix this, use [return: MarshalAs(UnmanagedType.U1)]:

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.U1)]
delegate bool IsMainThreadDelegate();

I think I've actually made this mistake on the TypeTreeCache delegates (and any other bool-returning delegates), since I don't think they return a WinAPI BOOL but a C++ one. They should probably also specify U1 for return marshalling.

DaZombieKiller avatar Sep 01 '20 06:09 DaZombieKiller

Regarding the export stall with Unity 5.6.7, I created a c++ dumper to help with debugging https://github.com/spacehamster/NativeTypeTreeDumper. When using TypeTreeDumper, the editor log doesn't have a stacktrace, while the c++ dumper does.

TypeTreeDumper Editor.log C++ Dumper Editor.log

The stacktrace looks like this

0x000000014104B685 (Unity) MemoryProfiler::RegisterRootAllocation
0x0000000140784077 (Unity) assign_allocation_root
0x000000014013E511 (Unity) BaseObjectInternal::NewObject<AssetMetaData>
0x00000001401450B2 (Unity) ProduceHelper<AssetMetaData,0>::Produce
0x0000000140927067 (Unity) Object::Produce

and some testing shows that setting MemLabelId.Identifier to 0x32 stops the stall in TypeTreeDumper and stops the crash in the c++ dumper. An identifier of 0x32 is equivalent to ?kMemBaseObject@@3UMemLabelId@@A in 5.6.7, but I have not checked more recent versions.

spacehamster avatar Sep 13 '20 02:09 spacehamster

When using TypeTreeDumper, the editor log doesn't have a stacktrace, while the c++ dumper does.

I wonder if there's a way for us to still have a native stack trace in managed code. I can't imagine it's easy though, and probably requires a bunch of managed<->unmanaged hopping around.

DaZombieKiller avatar Sep 13 '20 02:09 DaZombieKiller

I'm looking at how to handle STL strings for versions 5.4 and lower. basic_string::c_str has a slightly different signature in each version. 5.4:

PublicSymbol: [0000EAB0][0001:0000DAB0] 
?c_str@?$basic_string@DU?$char_traits@D@std@@V?$stl_allocator@D$0EC@$0BA@@@@std@@QEBAPEBDXZ
public: char const * __cdecl std::basic_string<char,struct std::char_traits<char>,class stl_allocator<char,66,16> >::c_str(void)const

5.3:

PublicSymbol: [00D11560][0001:00D10560] 
?c_str@?$basic_string@DU?$char_traits@D@std@@V?$stl_allocator@D$0EB@$0BA@@@@std@@QEBAPEBDXZ
public: char const * __cdecl std::basic_string<char,struct std::char_traits<char>,class stl_allocator<char,65,16> >::c_str(void)const

5.2:

PublicSymbol: [0034B010][0001:0034A010] 
?c_str@?$basic_string@DU?$char_traits@D@std@@V?$stl_allocator@D$0CP@$0BA@@@@std@@QEBAPEBDXZ
public: char const * __cdecl std::basic_string<char,struct std::char_traits<char>,class stl_allocator<char,47,16> >::c_str(void)const

PublicSymbol: [0034B010][0001:0034A010]
?c_str@?$basic_string@DU?$char_traits@D@std@@V?$stl_allocator@D$0DL@$0BA@@@@std@@QEBAPEBDXZ
public: char const * __cdecl std::basic_string<char,struct std::char_traits<char>,class stl_allocator<char,59,16> >::c_str(void)const 

Etcetera, so I think symbol resolver needs to be extended to support finding symbols that match a prefix (starting with ?c_str@?$basic_string).

spacehamster avatar Sep 14 '20 05:09 spacehamster

public partial class SymbolResolver
{
    public abstract string[] FindSymbolsWithPrefix(string prefix);
}

I think an API like this would be fine. There can also be a few helper methods such as:

public partial class SymbolResolver
{
    public T* ResolveFirstWithPrefix<T>(string prefix) where T : unmanaged;
    public T  ResolveFirstFunctionWithPrefix<T>(string prefix) where T : Delegate;
}

So then we could just do:

resolver.ResolveFirstFunctionWithPrefix<CStrDelegate>("?c_str@?$basic_string@")

DaZombieKiller avatar Sep 14 '20 05:09 DaZombieKiller

Just pushed support for this on master. Since DIA supports Regex, I changed the API design a little bit to reflect that.

c_str can be found with:

resolver.ResolveFirstFunctionMatching<CStrDelegate>(new Regex(@"\?c_str@\?\$basic_string@*"));

DaZombieKiller avatar Sep 14 '20 08:09 DaZombieKiller

This line doesn't print out the exception https://github.com/DaZombieKiller/TypeTreeDumper/blob/fe16fd931a4d78331fa8a5de180cd9ae9e045592/TypeTreeDumper.Client/EntryPoint.cs#L80

but if you change it to Console.Error.WriteLine(ex.ToString());, it does. i'm not sure why that would be the case

spacehamster avatar Sep 14 '20 10:09 spacehamster

That's strange, I wonder if it's because it's being set to IpcInterface.Error which is tied to the server, and thus it can only take certain types. We might have to make a wrapper TextWriter to work around that. If what I'm thinking is correct, then Console.WriteLine(ex); should work, but Console.Out.WriteLine(ex); shouldn't.

DaZombieKiller avatar Sep 14 '20 10:09 DaZombieKiller

Hm, I still see exceptions printed out even with Console.Error.WriteLine(ex);. Do you have an example of a situation where it fails so I can experiment with it?

DaZombieKiller avatar Sep 14 '20 10:09 DaZombieKiller

I just add throw new UnresolvedSymbolException("Missing Symbol Name Here"); to the top of ExecuteDumper like this

void ExecuteDumper()
{
    throw new UnresolvedSymbolException("Missing Symbol Name Here");
    var GetUnityVersion   = resolver.ResolveFunction<GetUnityVersionDelegate>("?GameEngineVersion@PlatformWrapper@UnityEngine@@SAPEBDXZ");
    var ParseUnityVersion = resolver.ResolveFunction<UnityVersionDelegate>("??0UnityVersion@@QEAA@PEBD@Z");
    ParseUnityVersion(out UnityVersion version, Marshal.PtrToStringAnsi(GetUnityVersion()));
    Dumper.Execute(new UnityEngine(version, resolver), server.OutputDirectory);
}

It also appears that if I change UnresolvedSymbolException to System.Exception, it prints like normal.

spacehamster avatar Sep 14 '20 10:09 spacehamster

I can confirm that a wrapper TextWriter fixes the issue. Fixed in https://github.com/DaZombieKiller/TypeTreeDumper/commit/81d2e56c4f1be16a041fede1c55e9927ccf32a46.

DaZombieKiller avatar Sep 14 '20 10:09 DaZombieKiller

Do you know why it effects UnresolvedSymbolException but not System.Exception?

spacehamster avatar Sep 14 '20 10:09 spacehamster

That's because UnresolvedSymbolException is a custom exception that doesn't implement serialization, which would be necessary for it to travel between processes.

DaZombieKiller avatar Sep 14 '20 10:09 DaZombieKiller

Remote hooking Unity 4.7 doesn't work, a blank console pops up and nothing happens. I'm guessing it is because it is a 32 bit app? I believe 5.0 onwards are 64 bit.

spacehamster avatar Sep 15 '20 04:09 spacehamster

That's probably why, yeah. It would probably work if you enable Prefer 32 Bit for a build, and you'll need to register the 32-bit msdia140.dll as well.

DaZombieKiller avatar Sep 15 '20 05:09 DaZombieKiller

The most recent commit on master should now work ~~when the dumper is compiled for x86~~ (actually, just Any CPU should work fine too) with no further changes.

DaZombieKiller avatar Sep 15 '20 06:09 DaZombieKiller

WIth Unity 4.7, ClassIDToRTTI always returns null. I've not been able to figure out why.

Also, side note, AfterEverythingLoaded was changed to a __thiscall in 4.7.

spacehamster avatar Sep 16 '20 10:09 spacehamster

WIth Unity 4.7, ClassIDToRTTI always returns null. I've not been able to figure out why.

It might require some investigation in Ghidra.

Also, side note, AfterEverythingLoaded was changed to a __thiscall in 4.7.

Thanks for the heads up, I haven't verified most of the calling conventions since it only applies to x86 and not x64.

DaZombieKiller avatar Sep 16 '20 11:09 DaZombieKiller

The function seems really simple

/* public: static struct Object::RTTI * __cdecl Object::ClassIDToRTTI(int) */
RTTI * __cdecl ClassIDToRTTI(int param_1)
{
  _Tree<class_std::_Tmap_traits<int,struct_Object::RTTI,struct_std::less<int>,class_stl_allocator<struct_std::pair<int_const_,struct_Object::RTTI>,1,4>,0>_>
  *p_Var1;
    
  _Tree_iterator<std::_Tree_val<std::_Tmap_traits<int,Object::RTTI,std::less<int>,stl_allocator<std::pair<intconst,Object::RTTI>,1,4>,0>>>
  i;
  
  p_Var1 = gRTTI;
  find(gRTTI,(int *)&i);
  if (i == *(
             _Tree_iterator<std::_Tree_val<std::_Tmap_traits<int,Object::RTTI,std::less<int>,stl_allocator<std::pair<intconst,Object::RTTI>,1,4>,0>>>
             *)(p_Var1 + 4)) {
    return (RTTI *)0x0;
  }
  return (RTTI *)((int)i + 0x10);
}

(BTW i renamed gRTTI, it doesn't have a symbol associated with it)

My first guess was that gRTTI was not being initialized, so I tried calling RegisterAllClasses and InitializeAllClasses, but that does not help (it warns that it can't register classes multiple times). Changing the entry point to InitializeEngineNoGraphics also didn't help

spacehamster avatar Sep 16 '20 11:09 spacehamster