Skip to content

Commit

Permalink
Aggregate adjacent memory sizes regardless of r+x (#45401)
Browse files Browse the repository at this point in the history
* Aggregate adjacent memory sizes regardless of r+x
When adjacent memory ranges of same module differ by permission, the
line should not be skipped due to the lack of readability/executability
flags.

* Account for rows preceding the first one with r+w

* Account for last line

* Update src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.ParseMapModules.cs

Co-authored-by: Tom Deseyn <[email protected]>

* Update src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.ParseMapModules.cs

* Move module flag assignment after the final commit

* Improve readibility of moduleHasReadAndExecFlags

* Update src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.ParseMapModules.cs

* Update src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.ParseMapModules.cs

* Decouple line parsing from ProcessModule parsing

Co-authored-by: Tom Deseyn <[email protected]>
Co-authored-by: Eirik Tsarpalis <[email protected]>
  • Loading branch information
3 people authored Dec 10, 2020
1 parent 8dec5ad commit ed0f1c1
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 121 deletions.
Original file line number Diff line number Diff line change
@@ -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<string> 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;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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/";

Expand Down Expand Up @@ -78,12 +76,6 @@ internal struct ParsedStat
//internal long cguest_time;
}

internal struct ParsedMapsModule
{
internal string FileName;
internal KeyValuePair<long, long> AddressRange;
}

internal static string GetExeFilePathForProcess(int pid)
{
return RootPath + pid.ToString(CultureInfo.InvariantCulture) + ExeFileName;
Expand All @@ -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;
Expand All @@ -114,80 +101,6 @@ internal static string GetFileDescriptorDirectoryPathForProcess(int pid)
return RootPath + pid.ToString(CultureInfo.InvariantCulture) + FileDescriptorDirectoryName;
}

internal static IEnumerable<ParsedMapsModule> ParseMapsModules(int pid)
{
try
{
return ParseMapsModulesCore(File.ReadLines(GetMapsFilePathForProcess(pid)));
}
catch (IOException) { }
catch (UnauthorizedAccessException) { }

return Array.Empty<ParsedMapsModule>();
}

private static IEnumerable<ParsedMapsModule> ParseMapsModulesCore(IEnumerable<string> 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<long, long> 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<long, long>(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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@
Link="Common\Interop\Linux\Interop.cgroups.cs" />
<Compile Include="$(CommonPath)Interop\Linux\procfs\Interop.ProcFsStat.cs"
Link="Common\Interop\Linux\Interop.ProcFsStat.cs" />
<Compile Include="$(CommonPath)Interop\Linux\procfs\Interop.ProcFsStat.ParseMapModules.cs"
Link="Common\Interop\Linux\Interop.ProcFsStat.ParseMapModules.cs" />
<Compile Include="$(CommonPath)Interop\Linux\procfs\Interop.ProcFsStat.TryReadStatusFile.cs"
Link="Common\Interop\Linux\Interop.ProcFsStat.TryReadStatusFile.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.SchedGetSetAffinity.cs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,40 +38,7 @@ public static ProcessInfo[] GetProcessInfos(string machineName)
/// <returns>The array of modules.</returns>
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);
Expand Down

0 comments on commit ed0f1c1

Please sign in to comment.