Export Stalls
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?
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.
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.
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.
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()'
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.
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.
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.
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.
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.
I just pushed two commits that fix cross-thread usage of DiaSymbolResolver, it was actually much simpler than the solution I just proposed above.
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
I tried calling
?IsMainThread@CurrentThread@@YA_NXZand it returns true when called inside the AfterEverythingLoaded callback and when called fromEntryPoint.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.
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.
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.
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).
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@")
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@*"));
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
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.
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?
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.
I can confirm that a wrapper TextWriter fixes the issue. Fixed in https://github.com/DaZombieKiller/TypeTreeDumper/commit/81d2e56c4f1be16a041fede1c55e9927ccf32a46.
Do you know why it effects UnresolvedSymbolException but not System.Exception?
That's because UnresolvedSymbolException is a custom exception that doesn't implement serialization, which would be necessary for it to travel between processes.
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.
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.
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.
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.
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.
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