Skip to content

Commit fd2ebfd

Browse files
authored
Dynamically detect rbp offset used in GetRbp (#716)
In case the detection fails, fallback to the previous value of 1 long as the previous code used + 1 to actually mean 8 bytes. If something does go wrong with the detection, dump the code of the whole method to help debug.
1 parent 0de1a26 commit fd2ebfd

File tree

1 file changed

+93
-7
lines changed

1 file changed

+93
-7
lines changed

Source/Common/DeferredStackTracingImpl.cs

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using System;
22
using System.Runtime.CompilerServices;
3+
using System.Text;
4+
using HarmonyLib;
5+
using Multiplayer.Common;
36

47
namespace 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

Comments
 (0)