src/Microsoft.Diagnostics.Runtime/Linux/LinuxLiveDataReader.cs (350 lines of code) (raw):
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Microsoft.Diagnostics.Runtime.DataReaders.Implementation;
using Microsoft.Diagnostics.Runtime.Implementation;
namespace Microsoft.Diagnostics.Runtime.Utilities
{
/// <summary>
/// A data reader that targets a Linux process.
/// The current process must have ptrace access to the target process.
/// </summary>
internal sealed class LinuxLiveDataReader : CommonMemoryReader, IDataReader, IDisposable, IThreadReader
{
private ImmutableArray<MemoryMapEntry>.Builder _memoryMapEntries;
private readonly List<uint> _threadIDs = new();
private bool _suspended;
private bool _disposed;
private FileStream? _fileStream;
public string DisplayName => $"pid:{ProcessId:x}";
public OSPlatform TargetPlatform => OSPlatform.Linux;
public LinuxLiveDataReader(int processId, bool suspend)
{
int status = kill(processId, 0);
if (status < 0 && Marshal.GetLastWin32Error() != EPERM)
throw new ArgumentException("The process is not running");
ProcessId = processId;
_memoryMapEntries = LoadMemoryMaps();
if (suspend)
{
LoadThreadsAndAttach();
_suspended = true;
}
Architecture = RuntimeInformation.ProcessArchitecture;
}
~LinuxLiveDataReader() => Dispose(false);
public int ProcessId { get; }
public bool IsThreadSafe => false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool _)
{
if (_disposed)
return;
_fileStream?.Dispose();
if (_suspended)
{
foreach (uint tid in _threadIDs)
{
// no point in handling errors here as the user can do nothing with them
// also if Dispose is called from the finalizer we could crash the process
ptrace(PTRACE_DETACH, (int)tid, IntPtr.Zero, IntPtr.Zero);
}
_suspended = false;
}
_disposed = true;
}
public void FlushCachedData()
{
_threadIDs.Clear();
_memoryMapEntries = LoadMemoryMaps();
}
public Architecture Architecture { get; }
public IEnumerable<ModuleInfo> EnumerateModules() =>
from entry in _memoryMapEntries
where !string.IsNullOrEmpty(entry.FilePath)
group entry by entry.FilePath into image
let filePath = image.Key
let containsExecutable = image.Any(entry => entry.IsExecutable)
let beginAddress = image.Min(entry => entry.BeginAddress)
select GetModuleInfo(this, beginAddress, filePath, containsExecutable);
private ModuleInfo GetModuleInfo(IDataReader reader, ulong baseAddress, string filePath, bool isVirtual)
{
if (reader.Read<ushort>(baseAddress) == 0x5a4d)
return new PEModuleInfo(reader, baseAddress, filePath, isVirtual);
long size = 0;
FileInfo fileInfo = new(filePath);
if (fileInfo.Exists)
size = fileInfo.Length;
return new ElfModuleInfo(reader, GetElfFile(baseAddress), baseAddress, size, filePath);
}
private ElfFile? GetElfFile(ulong baseAddress)
{
try
{
return new ElfFile(this, baseAddress);
}
catch (InvalidDataException)
{
return null;
}
}
public override int Read(ulong address, Span<byte> buffer)
{
DebugOnly.Assert(!buffer.IsEmpty);
if (_fileStream == null)
{
try
{
return ReadMemoryReadv(address, buffer);
}
catch (UnauthorizedAccessException)
{
// process_vm_readv failed with EPERM, fallback to /proc/pid/mem
_fileStream = new FileStream($"/proc/{ProcessId}/mem", FileMode.Open, FileAccess.Read, FileShare.Read);
}
}
return ReadMemoryProc(address, buffer);
}
private unsafe int ReadMemoryProc(ulong address, Span<byte> buffer)
{
int readableBytesCount = this.GetReadableBytesCount(this._memoryMapEntries, address, buffer.Length);
if (readableBytesCount <= 0)
{
return 0;
}
try
{
_fileStream!.Seek((long)address, SeekOrigin.Begin);
return _fileStream.Read(buffer.Slice(0, readableBytesCount));
}
catch (IOException)
{
return 0;
}
}
private unsafe int ReadMemoryReadv(ulong address, Span<byte> buffer)
{
int readableBytesCount = this.GetReadableBytesCount(this._memoryMapEntries, address, buffer.Length);
if (readableBytesCount <= 0)
{
return 0;
}
fixed (byte* ptr = buffer)
{
IOVEC local = new()
{
iov_base = ptr,
iov_len = (IntPtr)readableBytesCount
};
IOVEC remote = new()
{
iov_base = (void*)address,
iov_len = (IntPtr)readableBytesCount
};
int read = (int)process_vm_readv(ProcessId, &local, (UIntPtr)1, &remote, (UIntPtr)1, UIntPtr.Zero).ToInt64();
if (read < 0)
{
return Marshal.GetLastWin32Error() switch
{
EPERM => throw new UnauthorizedAccessException(),
ESRCH => throw new InvalidOperationException("The process has exited"),
_ => 0
};
}
return read;
}
}
public IEnumerable<uint> EnumerateOSThreadIds()
{
LoadThreads();
return _threadIDs;
}
public ulong GetThreadTeb(uint _) => 0;
public unsafe bool GetThreadContext(uint threadID, uint contextFlags, Span<byte> context)
{
LoadThreads();
if (!_threadIDs.Contains(threadID) || Architecture == Architecture.X86)
return false;
int regSize = Architecture switch
{
Architecture.Arm => sizeof(RegSetArm),
Architecture.Arm64 => sizeof(RegSetArm64),
(Architecture)9 /* Architecture.RiscV64 */ => sizeof(RegSetRiscV64),
(Architecture)6 /* Architecture.LoongArch64 */ => sizeof(RegSetLoongArch64),
Architecture.X64 => sizeof(RegSetX64),
_ => sizeof(RegSetX86),
};
byte[] buffer = ArrayPool<byte>.Shared.Rent(regSize);
try
{
fixed (byte* data = buffer)
{
ptrace(PTRACE_GETREGS, (int)threadID, IntPtr.Zero, new IntPtr(data));
}
switch (Architecture)
{
case Architecture.Arm:
Unsafe.As<byte, RegSetArm>(ref MemoryMarshal.GetReference(buffer.AsSpan())).CopyContext(context);
break;
case Architecture.Arm64:
Unsafe.As<byte, RegSetArm64>(ref MemoryMarshal.GetReference(buffer.AsSpan())).CopyContext(context);
break;
case (Architecture)9 /* Architecture.RiscV64 */:
Unsafe.As<byte, RegSetRiscV64>(ref MemoryMarshal.GetReference(buffer.AsSpan())).CopyContext(context);
break;
case (Architecture)6 /* Architecture.LoongArch64 */:
Unsafe.As<byte, RegSetLoongArch64>(ref MemoryMarshal.GetReference(buffer.AsSpan())).CopyContext(context);
break;
case Architecture.X64:
Unsafe.As<byte, RegSetX64>(ref MemoryMarshal.GetReference(buffer.AsSpan())).CopyContext(context);
break;
default:
Unsafe.As<byte, RegSetX86>(ref MemoryMarshal.GetReference(buffer.AsSpan())).CopyContext(context);
break;
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
return true;
}
private void LoadThreadsAndAttach()
{
const int maxPasses = 100;
HashSet<uint> tracees = new();
bool makesProgress = true;
// Make up to maxPasses to be sure to attach to the threads that could have been created in the meantime
for (int i = 0; makesProgress && i < maxPasses; i++)
{
makesProgress = false;
// GetThreads could throw during enumeration. It means the process was killed so no cleanup is needed.
IEnumerable<uint> threads = GetThreads(ProcessId);
foreach (uint tid in threads)
{
if (tracees.Contains(tid))
{
// We have already attached successfully to this thread
continue;
}
int status = (int)ptrace(PTRACE_ATTACH, (int)tid, IntPtr.Zero, IntPtr.Zero);
if (status >= 0)
{
status = waitpid((int)tid, IntPtr.Zero, 0);
}
if (status >= 0)
{
tracees.Add(tid);
makesProgress = true;
}
if (status < 0)
{
// We failed to attach. It could mean multiple things:
// 1. The tid exited: it's ok we won't see it at the next iteration.
// 2. We don't have permissions: attach to other threads will likely fail, too, and we won't make progress
// 3. Something is weird with this particular thread. We'll keep it as is and try to attach to everything else
continue;
}
}
}
if (tracees.Count == 0)
{
throw new ClrDiagnosticsException($"Could not PTRACE_ATTACH to any thread of the process {ProcessId}. Either the process has exited or you don't have permission.");
}
_threadIDs.AddRange(tracees);
}
private void LoadThreads()
{
if (_threadIDs.Count == 0)
{
_threadIDs.AddRange(GetThreads(ProcessId));
}
}
private static IEnumerable<uint> GetThreads(int pid)
{
string taskDirPath = $"/proc/{pid}/task";
foreach (string taskDir in Directory.EnumerateDirectories(taskDirPath))
{
string dirName = Path.GetFileName(taskDir);
if (uint.TryParse(dirName, out uint taskId))
{
yield return taskId;
}
}
}
private ImmutableArray<MemoryMapEntry>.Builder LoadMemoryMaps()
{
ImmutableArray<MemoryMapEntry>.Builder result = ImmutableArray.CreateBuilder<MemoryMapEntry>();
string mapsFilePath = $"/proc/{ProcessId}/maps";
using StreamReader reader = new(mapsFilePath);
while (true)
{
string? line = reader.ReadLine();
if (string.IsNullOrEmpty(line))
{
break;
}
string address, permission, path;
string[] parts = line.Split(new char[] { ' ' }, 6, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 5)
{
path = string.Empty;
}
else if (parts.Length == 6)
{
path = parts[5].StartsWith("[", StringComparison.Ordinal) ? string.Empty : parts[5];
}
else
{
DebugOnly.Fail("Unknown data format");
continue;
}
address = parts[0];
permission = parts[1];
string[] addressBeginEnd = address.Split('-');
MemoryMapEntry entry = new()
{
BeginAddress = Convert.ToUInt64(addressBeginEnd[0], 16),
EndAddress = Convert.ToUInt64(addressBeginEnd[1], 16),
FilePath = path,
Permission = ParsePermission(permission)
};
result.Add(entry);
}
return result;
}
private static int ParsePermission(string permission)
{
DebugOnly.Assert(permission.Length == 4);
// r = read
// w = write
// x = execute
// s = shared
// p = private (copy on write)
int r = permission[0] == 'r' ? 8 : 0;
int w = permission[1] == 'w' ? 4 : 0;
int x = permission[2] == 'x' ? 2 : 0;
int p = permission[3] == 'p' ? 1 : 0;
return r | w | x | p;
}
private const int EPERM = 1;
private const int ESRCH = 3;
private const string LibC = "libc";
[DllImport(LibC, SetLastError = true)]
private static extern int kill(int pid, int sig);
[DllImport(LibC, SetLastError = true)]
private static extern IntPtr ptrace(int request, int pid, IntPtr addr, IntPtr data);
[DllImport(LibC, SetLastError = true)]
private static extern unsafe IntPtr process_vm_readv(int pid, IOVEC* local_iov, UIntPtr liovcnt, IOVEC* remote_iov, UIntPtr riovcnt, UIntPtr flags);
[DllImport(LibC)]
private static extern int waitpid(int pid, IntPtr status, int options);
private unsafe struct IOVEC
{
public void* iov_base;
public IntPtr iov_len;
}
private const int PTRACE_GETREGS = 12;
private const int PTRACE_ATTACH = 16;
private const int PTRACE_DETACH = 17;
}
internal struct MemoryMapEntry : IRegion
{
public ulong BeginAddress { get; set; }
public ulong EndAddress { get; set; }
public string? FilePath { get; set; }
public int Permission { get; set; }
public bool IsReadable => (Permission & 8) != 0;
public bool IsExecutable => (Permission & 2) != 0;
}
}