diff --git a/src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.ParseMapModules.cs b/src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.ParseMapModules.cs new file mode 100644 index 0000000000000..eeb472a19e5d7 --- /dev/null +++ b/src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.ParseMapModules.cs @@ -0,0 +1,163 @@ +// 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.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Text; + +internal static partial class Interop +{ + internal static partial class procfs + { + private const string MapsFileName = "/maps"; + + private static string GetMapsFilePathForProcess(int pid) => + RootPath + pid.ToString(CultureInfo.InvariantCulture) + MapsFileName; + + internal static ProcessModuleCollection? ParseMapsModules(int pid) + { + try + { + return ParseMapsModulesCore(File.ReadLines(GetMapsFilePathForProcess(pid))); + } + catch (IOException) { } + catch (UnauthorizedAccessException) { } + + return null; + } + + private static ProcessModuleCollection ParseMapsModulesCore(IEnumerable lines) + { + Debug.Assert(lines != null); + + ProcessModule? module = null; + ProcessModuleCollection modules = new(capacity: 0); + bool moduleHasReadAndExecFlags = false; + + foreach (string line in lines) + { + if (!TryParseMapsEntry(line, out (long StartAddress, int Size, bool HasReadAndExecFlags, string Path) parsedLine)) + { + // Invalid entry for the purposes of ProcessModule parsing, + // discard flushing the current module if it exists. + CommitCurrentModule(); + continue; + } + + // Check if entry is a continuation of the current module. + if (module is not null && + module.FileName == parsedLine.Path && + (long)module.BaseAddress + module.ModuleMemorySize == parsedLine.StartAddress) + { + // Is continuation, update the current module. + module.ModuleMemorySize += parsedLine.Size; + moduleHasReadAndExecFlags |= parsedLine.HasReadAndExecFlags; + continue; + } + + // Not a continuation, commit any current modules and create a new one. + CommitCurrentModule(); + + module = new ProcessModule + { + FileName = parsedLine.Path, + ModuleName = Path.GetFileName(parsedLine.Path), + ModuleMemorySize = parsedLine.Size, + EntryPointAddress = IntPtr.Zero // unknown + }; + + // on 32-bit platforms, it throws System.OverflowException with IntPtr.ctor(Int64), + // so we use IntPtr.ctor(void*) to skip the overflow checking. + unsafe + { + module.BaseAddress = new IntPtr((void*)parsedLine.StartAddress); + } + + moduleHasReadAndExecFlags = parsedLine.HasReadAndExecFlags; + } + + // Commit any pending modules. + CommitCurrentModule(); + + return modules; + + void CommitCurrentModule() + { + // we only add module to collection, if at least one row had 'r' and 'x' set. + if (moduleHasReadAndExecFlags && module is not null) + { + modules.Add(module); + module = null; + } + } + } + + private static bool TryParseMapsEntry(string line, out (long StartAddress, int Size, bool HasReadAndExecFlags, string Path) parsedLine) + { + // Use a StringParser to avoid string.Split costs + var parser = new StringParser(line, separator: ' ', skipEmpty: true); + + // Parse the address start and size + (long start, int size) = parser.ParseRaw(TryParseAddressRange); + + if (size < 0) + { + parsedLine = default; + return false; + } + + // Parse the permissions + bool lineHasReadAndExecFlags = parser.ParseRaw(HasReadAndExecFlags); + + // Skip past the offset, dev, and inode fields + parser.MoveNext(); + parser.MoveNext(); + parser.MoveNext(); + + // we only care about the named modules + if (!parser.MoveNext()) + { + parsedLine = default; + return false; + } + + // Parse the pathname + string pathname = parser.ExtractCurrentToEnd(); + parsedLine = (start, size, lineHasReadAndExecFlags, pathname); + return true; + + static (long Start, int Size) TryParseAddressRange(string s, ref int start, ref int end) + { + int pos = s.IndexOf('-', start, end - start); + if (pos > 0) + { + if (long.TryParse(s.AsSpan(start, pos), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out long startingAddress) && + long.TryParse(s.AsSpan(pos + 1, end - (pos + 1)), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out long endingAddress)) + { + return (startingAddress, (int)(endingAddress - startingAddress)); + } + } + + return (0, -1); + } + + static bool HasReadAndExecFlags(string s, ref int start, ref int end) + { + bool sawRead = false, sawExec = false; + for (int i = start; i < end; i++) + { + if (s[i] == 'r') + sawRead = true; + else if (s[i] == 'x') + sawExec = true; + } + + return sawRead & sawExec; + } + } + } +} diff --git a/src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.cs b/src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.cs index 45d484eeac6b6..0ca179b736eca 100644 --- a/src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.cs +++ b/src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable enable using System; using System.Collections.Generic; using System.Diagnostics; @@ -17,7 +16,6 @@ internal static partial class procfs private const string ExeFileName = "/exe"; private const string CmdLineFileName = "/cmdline"; private const string StatFileName = "/stat"; - private const string MapsFileName = "/maps"; private const string FileDescriptorDirectoryName = "/fd/"; private const string TaskDirectoryName = "/task/"; @@ -78,12 +76,6 @@ internal struct ParsedStat //internal long cguest_time; } - internal struct ParsedMapsModule - { - internal string FileName; - internal KeyValuePair AddressRange; - } - internal static string GetExeFilePathForProcess(int pid) { return RootPath + pid.ToString(CultureInfo.InvariantCulture) + ExeFileName; @@ -99,11 +91,6 @@ internal static string GetStatFilePathForProcess(int pid) return RootPath + pid.ToString(CultureInfo.InvariantCulture) + StatFileName; } - internal static string GetMapsFilePathForProcess(int pid) - { - return RootPath + pid.ToString(CultureInfo.InvariantCulture) + MapsFileName; - } - internal static string GetTaskDirectoryPathForProcess(int pid) { return RootPath + pid.ToString(CultureInfo.InvariantCulture) + TaskDirectoryName; @@ -114,80 +101,6 @@ internal static string GetFileDescriptorDirectoryPathForProcess(int pid) return RootPath + pid.ToString(CultureInfo.InvariantCulture) + FileDescriptorDirectoryName; } - internal static IEnumerable ParseMapsModules(int pid) - { - try - { - return ParseMapsModulesCore(File.ReadLines(GetMapsFilePathForProcess(pid))); - } - catch (IOException) { } - catch (UnauthorizedAccessException) { } - - return Array.Empty(); - } - - private static IEnumerable ParseMapsModulesCore(IEnumerable lines) - { - Debug.Assert(lines != null); - - // Parse each line from the maps file into a ParsedMapsModule result - foreach (string line in lines) - { - // Use a StringParser to avoid string.Split costs - var parser = new StringParser(line, separator: ' ', skipEmpty: true); - - // Parse the address range - KeyValuePair addressRange = - parser.ParseRaw(delegate (string s, ref int start, ref int end) - { - long startingAddress = 0, endingAddress = 0; - int pos = s.IndexOf('-', start, end - start); - if (pos > 0) - { - if (long.TryParse(s.AsSpan(start, pos), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out startingAddress)) - { - long.TryParse(s.AsSpan(pos + 1, end - (pos + 1)), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out endingAddress); - } - } - return new KeyValuePair(startingAddress, endingAddress); - }); - - // Parse the permissions (we only care about entries with 'r' and 'x' set) - if (!parser.ParseRaw(delegate (string s, ref int start, ref int end) - { - bool sawRead = false, sawExec = false; - for (int i = start; i < end; i++) - { - if (s[i] == 'r') - sawRead = true; - else if (s[i] == 'x') - sawExec = true; - } - return sawRead & sawExec; - })) - { - continue; - } - - // Skip past the offset, dev, and inode fields - parser.MoveNext(); - parser.MoveNext(); - parser.MoveNext(); - - // Parse the pathname - if (!parser.MoveNext()) - { - continue; - } - string pathname = parser.ExtractCurrentToEnd(); - - // We only get here if a we have a non-empty pathname and - // the permissions included both readability and executability. - // Yield the result. - yield return new ParsedMapsModule { FileName = pathname, AddressRange = addressRange }; - } - } - private static string GetStatFilePathForThread(int pid, int tid) { // Perf note: Calling GetTaskDirectoryPathForProcess will allocate a string, diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj index 3c41c1027ffc5..28e419f230f0e 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -279,6 +279,8 @@ Link="Common\Interop\Linux\Interop.cgroups.cs" /> + The array of modules. internal static ProcessModuleCollection GetModules(int processId) { - var modules = new ProcessModuleCollection(0); - - // Process from the parsed maps file each entry representing a module - foreach (Interop.procfs.ParsedMapsModule entry in Interop.procfs.ParseMapsModules(processId)) - { - int sizeOfImage = (int)(entry.AddressRange.Value - entry.AddressRange.Key); - - // A single module may be split across multiple map entries; consolidate based on - // the name and address ranges of sequential entries. - if (modules.Count > 0) - { - ProcessModule module = modules[modules.Count - 1]; - if (module.FileName == entry.FileName && - ((long)module.BaseAddress + module.ModuleMemorySize == entry.AddressRange.Key)) - { - // Merge this entry with the previous one - module.ModuleMemorySize += sizeOfImage; - continue; - } - } - - // It's not a continuation of a previous entry but a new one: add it. - unsafe - { - modules.Add(new ProcessModule() - { - FileName = entry.FileName, - ModuleName = Path.GetFileName(entry.FileName), - BaseAddress = new IntPtr(unchecked((void*)entry.AddressRange.Key)), - ModuleMemorySize = sizeOfImage, - EntryPointAddress = IntPtr.Zero // unknown - }); - } - } + ProcessModuleCollection modules = Interop.procfs.ParseMapsModules(processId) ?? new(capacity: 0); // Move the main executable module to be the first in the list if it's not already string? exePath = Process.GetExePath(processId);