using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Globalization; using System.Runtime.InteropServices; public enum Record { MustNotHoldAny, MustNotHoldOne, MustHoldOne, LockAcquired, LockReleased } public struct LockRecord { public SimLock lk; public string frame; public LockRecord (SimLock lk, string frame) { this.lk = lk; this.frame = frame; } } public class SimThread { int thread; List locks = new List (); public SimThread (int t) { this.thread = t; } public bool HoldsLock (SimLock lk) { foreach (var l in locks) { if (l.lk == lk) return true; } return false; } public int HoldCount (SimLock lk) { int res = 0; foreach (var l in locks) if (l.lk == lk) ++res; return res; } public void Lock (SimLock lk, string frame) { foreach (LockRecord lr in locks) { if (lk.WarnAbout (this, lr.lk)) Console.WriteLine ("WARNING: tried to acquire lock {0} at {1} while holding {2} at {3}: {4}", lk, frame, lr.lk, lr.frame, lk.GetWarningMessage (this, lr.lk)); else if (!lk.IsValid (this, lr.lk)) Console.WriteLine ("ERROR: tried to acquire lock {0} at {1} while holding {2} at {3}: {4}", lk, frame, lr.lk, lr.frame, lk.GetErrorMessage (this, lr.lk)); } locks.Add (new LockRecord (lk, frame)); } public void Release (SimLock lk, string frame) { if (locks.Count == 0) { Console.WriteLine ("ERROR: released lock {0} at {1} while holding no locks!", lk, frame); return; } LockRecord top = locks [locks.Count - 1]; if (top.lk != lk && !(lk.IsGlobalLock && HoldCount (lk) > 1)) { Console.WriteLine ("WARNING: released lock {0} at {1} out of order with {2} at {3}!", lk, frame, top.lk, top.frame); } for (int i = locks.Count -1; i >= 0; --i) { if (locks [i].lk == lk) { locks.RemoveAt (i); break; } } } } /* LOCK RULES Simple locks: Can be acquired at any point regardless of which locks are taken or not. No other locks can be acquired or released while holding a simple lock. Reentrancy is not recomended. (warning) Simple locks are leaf locks on the lock lattice. Complex locks: Must respect locking order, which form a lattice. IOW, to take a given lock, only it's parents might have been taken. Reentrancy is ok. Locks around resources count as separate instances of the hierarchy. Global locks: Must respect locking order. Must be the at the botton of the locking lattice. Can be taken out-of-order by other locks given that it was previously acquired. Adding global locks is not to be taken lightly. The current lock hierarchy: loader lock domain lock domain jit lock simple locks Examples: You can take the loader lock without holding a domain lock. You cannot take a domain lock if the loader lock is held. You cannot take a domain lock while holding the lock to another domain. */ public enum Lock { Invalid, LoaderLock, ImageDataLock, DomainLock, DomainAssembliesLock, DomainJitCodeHashLock, } public class SimLock { Lock kind; int id; public SimLock (Lock kind, int id) { this.kind = kind; this.id = id; } static int GetLockOrder (Lock kind) { switch (kind) { case Lock.LoaderLock: return 0; case Lock.DomainLock: return 1; case Lock.DomainJitCodeHashLock: return 2; default: return 3; } } bool IsParent (SimLock other) { return GetLockOrder (kind) > GetLockOrder (other.kind); } public bool IsSimpleLock { get { return GetLockOrder (kind) == 3; } } public bool IsGlobalLock { get { return kind == Lock.LoaderLock; } } /*locked is already owned by the thread, 'this' is the new one*/ bool Compare (SimThread thread, SimLock locked, out bool isWarning, out string msg) { isWarning = false; msg = null; if (locked != this) { if (!IsParent (locked)) { if (IsGlobalLock) { /*acquiring a global lock*/ if (!thread.HoldsLock (this)) { /*does the thread alread hold it?*/ msg = "Acquired a global lock after a regular lock without having it before."; return false; } } else { msg = "Hierarchy violation."; return false; } } } else if (IsSimpleLock) { msg = "Avoid taking simple locks recursively"; isWarning = true; return false; } return true; } public bool IsValid (SimThread thread, SimLock locked) { bool warn; string msg; return Compare (thread, locked, out warn, out msg); } public bool WarnAbout (SimThread thread, SimLock locked) { bool warn; string msg; Compare (thread, locked, out warn, out msg); return warn; } public string GetWarningMessage (SimThread thread, SimLock locked) { bool warn; string msg; Compare (thread, locked, out warn, out msg); return warn ? msg : null; } public string GetErrorMessage (SimThread thread, SimLock locked) { bool warn; string msg; bool res = Compare (thread, locked, out warn, out msg); return !res && !warn ? msg : null; } public override string ToString () { return String.Format ("{0}", kind); } } public class LockSimulator { static Dictionary threads = new Dictionary (); static Dictionary locks = new Dictionary (); SymbolTable syms; public LockSimulator (SymbolTable s) { this.syms = s; } SimLock GetLock (Trace t) { if (locks.ContainsKey (t.lockPtr)) return locks [t.lockPtr]; else { return locks [t.lockPtr] = new SimLock (t.lockKind, t.lockPtr); } } SimThread GetThread (Trace t) { if (threads.ContainsKey (t.thread)) return threads [t.thread]; else return threads [t.thread] = new SimThread (t.thread); } public void PlayBack (IEnumerable traces) { foreach (var t in traces) { SimThread thread = GetThread (t); SimLock lk = GetLock (t); string frame = t.GetUsefullTopTrace (this.syms); switch (t.record) { case Record.MustNotHoldAny: case Record.MustNotHoldOne: case Record.MustHoldOne: throw new Exception ("not supported"); case Record.LockAcquired: thread.Lock (lk, frame); break; case Record.LockReleased: thread.Release (lk, frame); break; default: throw new Exception ("Invalid trace record: "+t.record); } } } } public class Trace { public int thread; public Record record; public Lock lockKind; public int lockPtr; int[] frames; static readonly string[] BAD_FRAME_METHODS = new string[] { "mono_loader_lock", "mono_loader_unlock", "mono_image_lock", "mono_image_unlock", }; public Trace (string[] fields) { thread = fields [0].ParseHex (); record = (Record)fields [1].ParseDec (); lockKind = (Lock)fields [2].ParseDec (); lockPtr = fields [3].ParseHex (); frames = new int [fields.Length - 4]; for (int i = 0; i < frames.Length; ++i) frames [i] = fields [i + 4].ParseHex (); } public void Dump (SymbolTable table) { Console.WriteLine ("{0:x} {1} {2} {3:x}", thread, record, lockKind, lockPtr); for (int i = 0; i < frames.Length; ++i) Console.WriteLine ("\t{0}", table.Translate (frames [i])); } public string GetUsefullTopTrace (SymbolTable syms) { for (int i = 0; i < frames.Length; ++i) { string str = syms.Translate (frames [i]); bool ok = true; for (int j = 0; j < BAD_FRAME_METHODS.Length; ++j) { if (str.IndexOf (BAD_FRAME_METHODS [j]) >= 0) { ok = false; break; } } if (ok) return str; } return "[unknown]"; } } public class Symbol : IComparable { public int offset; public int size; public string name; public Symbol (int o, int size, string n) { this.offset = o; this.size = size; this.name = n; } public int CompareTo(Symbol other) { return offset - other.offset; } } public interface SymbolTable { string Translate (int offset); } public class OsxSymbolTable : SymbolTable { Symbol[] table; const int MAX_FUNC_SIZE = 0x20000; public OsxSymbolTable (string binary) { Load (binary); } void Load (string binary) { ProcessStartInfo psi = new ProcessStartInfo ("gobjdump", "-t "+binary); psi.UseShellExecute = false; psi.RedirectStandardOutput = true; var proc = Process.Start (psi); var list = new List (); string line; while ((line = proc.StandardOutput.ReadLine ()) != null) { string[] fields = line.Split(new char[] {' ', '\t'}, StringSplitOptions.RemoveEmptyEntries); if (fields.Length < 4) continue; if (!(fields [1].Equals ("g") || fields [1].Equals ("d"))) continue; if (!fields [2].Equals ("*UND*")) continue; int offset = fields [0].ParseHex (); string name = fields [3]; if (offset != 0) list.Add (new Symbol (offset, 0, name)); } table = new Symbol [list.Count]; list.CopyTo (table, 0); Array.Sort (table); } public string Translate (int offset) { Symbol sym = null; int res = Array.BinarySearch (table, new Symbol (offset, 0, null)); if (res >= 0) return table [res].name; res = ~res; if (res >= table.Length) sym = table [table.Length - 1]; else if (res != 0) sym = table [res - 1]; if (sym != null) { int size = Math.Max (sym.size, 10); if (offset - sym.offset < size) return sym.name; } return String.Format ("[{0:x}]", offset); } } public class LinuxSymbolTable : SymbolTable { Symbol[] table; const int MAX_FUNC_SIZE = 0x20000; public LinuxSymbolTable (string binary) { Load (binary); } void Load (string binary) { ProcessStartInfo psi = new ProcessStartInfo ("objdump", "-t "+binary); psi.UseShellExecute = false; psi.RedirectStandardOutput = true; var proc = Process.Start (psi); var list = new List (); string line; while ((line = proc.StandardOutput.ReadLine ()) != null) { string[] fields = line.Split(new char[] {' ', '\t'}, StringSplitOptions.RemoveEmptyEntries); if (fields.Length < 6) continue; if (fields [3] != ".text" || fields [2] != "F") continue; int offset = fields [0].ParseHex (); int size = fields [4].ParseHex (); string name = fields [fields.Length - 1]; if (offset != 0) list.Add (new Symbol (offset, size, name)); } table = new Symbol [list.Count]; list.CopyTo (table, 0); Array.Sort (table); } public string Translate (int offset) { Symbol sym = null; int res = Array.BinarySearch (table, new Symbol (offset, 0, null)); if (res >= 0) return table [res].name; res = ~res; if (res >= table.Length) sym = table [table.Length - 1]; else if (res != 0) sym = table [res - 1]; if (sym != null && offset - sym.offset < MAX_FUNC_SIZE) return sym.name; return String.Format ("[{0:x}]", offset); } } public class TraceDecoder { string file; public TraceDecoder (string file) { this.file = file; } public IEnumerable GetTraces () { using (StreamReader reader = new StreamReader (file)) { string line; while ((line = reader.ReadLine ()) != null) { string[] fields = line.Split(new char[] {','}, StringSplitOptions.RemoveEmptyEntries); if (fields.Length >= 7) { yield return new Trace (fields); } } } } } public class Driver { [DllImport ("libc")] static extern int uname (IntPtr buf); static bool IsOSX () { bool isOsx = false; IntPtr buf = Marshal.AllocHGlobal (8192); if (uname (buf) == 0) { string os = Marshal.PtrToStringAnsi (buf); isOsx = os == "Darwin"; } Marshal.FreeHGlobal (buf); return isOsx; } static void Main (string[] args) { SymbolTable syms; if (args.Length != 2) { Console.WriteLine ("usage: LockTracerDecoder.exe /path/to/mono /path/to/locks.pid"); return; } if (IsOSX ()) syms = new OsxSymbolTable (args [0]); else syms = new LinuxSymbolTable (args [0]); var decoder = new TraceDecoder (args [1]); var sim = new LockSimulator (syms); sim.PlayBack (decoder.GetTraces ()); } } public static class Utils { public static int ParseHex (this string number) { while (number.Length > 1 && (number [0] == '0' || number [0] == 'x' || number [0] == 'X')) number = number.Substring (1); return int.Parse (number, NumberStyles.HexNumber); } public static int ParseDec (this string number) { while (number.Length > 1 && number [0] == '0') number = number.Substring (1); return int.Parse (number); } }