11using System ;
22using System . Runtime . CompilerServices ;
3+ using System . Text ;
4+ using HarmonyLib ;
5+ using Multiplayer . Common ;
36
47namespace Multiplayer . Client . Desyncs ;
58
@@ -184,7 +187,7 @@ private static void UpdateNewElement(ref AddrInfo info, long ret)
184187 StableStringHash ( normalizedMethodNameBetweenOS ) ;
185188 }
186189
187- static unsafe long GetStackUsage( long addr )
190+ private static unsafe long GetStackUsage( long addr )
188191 {
189192 var ji = Native. mono_jit_info_table_find ( Native . DomainPtr , ( IntPtr ) addr ) ;
190193
@@ -194,11 +197,16 @@ static unsafe long GetStackUsage(long addr)
194197 var start = ( uint * ) Native . mono_jit_info_get_code_start ( ji ) ;
195198 long usage = 0 ;
196199
200+ // Emitted at: https://github.com/Unity-Technologies/mono/blob/2022.3.35f1/mono/mini/mini-amd64.c#L7652
201+ // - diverges into: https://github.com/Unity-Technologies/mono/blob/2022.3.35f1/mono/arch/amd64/amd64-codegen.h#L190-L193
197202 if ( ( * start & 0xFFFFFF ) == 0xEC8348 ) // sub rsp,XX (4883EC XX)
198203 {
199204 usage = * start >> 24 ;
200205 start += 1 ;
201- } else if ( ( * start & 0xFFFFFF ) == 0xEC8148 ) // sub rsp,XXXXXXXX (4881EC XXXXXXXX)
206+ }
207+ // - diverges into: https://github.com/Unity-Technologies/mono/blob/2022.3.35f1/mono/arch/amd64/amd64-codegen.h#L199-L202
208+ // basically just a long form of the above branch.
209+ else if ( ( * start & 0xFFFFFF ) == 0xEC8148 ) // sub rsp,XXXXXXXX (4881EC XXXXXXXX)
202210 {
203211 usage = * ( uint * ) ( ( long ) start + 3 ) ;
204212 start = ( uint * ) ( ( long ) start + 7 ) ;
@@ -210,6 +218,7 @@ static unsafe long GetStackUsage(long addr)
210218 return usage;
211219 }
212220
221+ // https://github.com/Unity-Technologies/mono/blob/2022.3.35f1/mono/mini/mini-amd64.c#L7559
213222 // push rbp (55)
214223 if ( * ( byte * ) start == 0x55 )
215224 return RbpBased;
@@ -224,7 +233,7 @@ private static unsafe void CheckRbpUsage(uint* at, ref long stackUsage)
224233 // or:
225234 // mov [rsp],rbx (48891C24)
226235 // mov [rsp+8],rbp (48896C2408)
227- // (The calle saved registers are always in the same order
236+ // (The callee saved registers are always in the same order
228237 // and are saved at the bottom of the frame)
229238
230239 if ( * at == 0x242C8948 )
@@ -238,12 +247,89 @@ private static unsafe void CheckRbpUsage(uint* at, ref long stackUsage)
238247 }
239248 }
240249
241- [ MethodImpl ( MethodImplOptions . NoInlining ) ]
242- private static unsafe long GetRbp( )
250+ static DeferredStackTracingImpl ( )
251+ {
252+ // All of this code assumes that the rbp pointer offset stays the same throughout the method invocations.
253+ // Mono is generally allowed to recompile code, which could cause issues for us, but GetRpb is annotated as
254+ // NoInline and NoOptimization to heavily discourage any changes and avoid breaking.
255+
256+ var method = AccessTools. DeclaredMethod( typeof ( DeferredStackTracingImpl ) , nameof ( GetRbp ) ) ! ;
257+ var rbpCodeStart = Native. mono_compile_method( method . MethodHandle . Value ) ;
258+ // Add 1 byte because Mono recognizes the jit_info to be just after the code start address returned by
259+ // the compile method.
260+ var jitInfo = Native . mono_jit_info_table_find ( Native . DomainPtr , rbpCodeStart + 1 ) ;
261+ var instStart = Native. mono_jit_info_get_code_start ( jitInfo ) ;
262+ var instLen = Native. mono_jit_info_get_code_size ( jitInfo ) ;
263+ // Search for the following instruction:
264+ // mov rax, imm<MagicNumber> (48b8 <MagicNumber>)
265+ // It should directly precede:
266+ // mov [rbp-XX], rax
267+ // From which we can extract the offset from rbp.
268+ byte [ ] magicBytes = [ 0x48 , 0xb8 , ..BitConverter . GetBytes ( MagicNumber ) ] ;
269+ unsafe
270+ {
271+ // Make sure we don't access out-of-bounds memory.
272+ // magicBytes.Length -- mov rax, <MagicNumber>
273+ // sizeof(uint) -- mov [rbp-XX], rax
274+ var maxLen = instLen - magicBytes . Length - sizeof ( uint ) ;
275+ for ( int i = 0 ; i < maxLen ; i++ )
276+ {
277+ byte * at = ( byte * ) instStart + i ;
278+ var matches = new ReadOnlySpan < byte > ( at , magicBytes . Length ) . SequenceEqual ( magicBytes ) ;
279+ if ( ! matches ) continue;
280+
281+ uint * match = ( uint * ) ( at + magicBytes . Length ) ;
282+ // mov [rbp-XX], rax (488945XX)
283+ if ( ( * match & 0xFFFFFF ) == 0x458948 )
284+ {
285+ offsetFromRbp = ( sbyte ) ( * match >> 24 ) ;
286+ return ;
287+ }
288+ }
289+
290+ // To analyze the assembly dump, remove the offset prefixes at the start of each line and paste the hex to
291+ // a site like https://defuse.ca/online-x86-assembler.htm#disassembly2. Choose x64. Search for the
292+ // magic number and compare the code with the loop above.
293+ var asm = HexDump( new ReadOnlySpan < byte > ( ( void * ) instStart , instLen ) ) ;
294+ ServerLog. Error(
295+ $"Unexpected GetRpb asm structure. Couldn't find a magic bytes match. " +
296+ $ "Using fallback offset ({ offsetFromRbp } ). " +
297+ $ "Asm dump for the method: \n { asm } ") ;
298+ }
299+ }
300+
301+ private static string HexDump( ReadOnlySpan < byte > data , int bytesPerLine = 16 )
243302 {
244- long rbp = 0 ;
303+ var sb = new StringBuilder ( ) ;
245304
246- return * ( & rbp + 1 ) ;
305+ for ( int i = 0 ; i < data . Length ; i += bytesPerLine )
306+ {
307+ sb. Append( $"{ i: X4} : ") ;
308+
309+ for ( int j = 0 ; j < bytesPerLine && i + j < data. Length; j++ ) sb. Append( $"{ data[ i + j] : X2} ") ;
310+
311+ sb. AppendLine( ) ;
312+ }
313+
314+ return sb. ToString( ) ;
315+ }
316+
317+ // Magic number is used to locate the relevant method code.
318+ private const long MagicNumber = 0x0123456789ABCDEF ;
319+ private static sbyte offsetFromRbp = - 8 ;
320+
321+ [ MethodImpl( MethodImplOptions. NoInlining | MethodImplOptions. NoOptimization) ]
322+ private static unsafe long GetRbp( )
323+ {
324+ // This variable declaration compiles down to the following IL:
325+ // ldc.i8 <MagicNumber>
326+ // stloc.0
327+ // In turn, the second IL instruction compiles to amd64 as:
328+ // mov qword ptr [rbp-XX], rax
329+ // From which we can extract the offset (XX) to reliably calculate the rbp address
330+ long register = MagicNumber;
331+
332+ return * ( long * ) ( ( byte * ) & register - offsetFromRbp) ;
247333 }
248334
249335 private static int HashCombineInt( int seed, int value) =>
0 commit comments