Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 301de75

Browse files
committed
Implement Process.Modules support on Linux
I'd previously left a comment in the implementation that we could potentially implement Process.Modules support by enumerating the contents of the procfs /procs/[pid]/maps file. This commit does that. It uses the heuristic that if the pathname isn't empty and the mapped region has both read and execute permissions, then the entry should be considered a module. We can tweak this logic over time as needed.
1 parent 4e33962 commit 301de75

File tree

6 files changed

+163
-22
lines changed

6 files changed

+163
-22
lines changed

src/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.cs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// Copyright (c) Microsoft. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
using System;
5+
using System.Collections.Generic;
6+
using System.ComponentModel;
7+
using System.Diagnostics;
48
using System.Globalization;
59
using System.IO;
610

@@ -12,6 +16,7 @@ internal static partial class procfs
1216
internal const string SelfExeFilePath = RootPath + "self/exe";
1317
internal const string ProcUptimeFilePath = RootPath + "uptime";
1418
private const string StatFileName = "/stat";
19+
private const string MapsFileName = "/maps";
1520
private const string TaskDirectoryName = "/task/";
1621

1722
internal struct ParsedStat
@@ -67,16 +72,104 @@ internal struct ParsedStat
6772
//internal long cguest_time;
6873
}
6974

75+
internal struct ParsedMapsModule
76+
{
77+
internal string FileName;
78+
internal KeyValuePair<long, long> AddressRange;
79+
}
80+
7081
internal static string GetStatFilePathForProcess(int pid)
7182
{
7283
return RootPath + pid.ToString(CultureInfo.InvariantCulture) + StatFileName;
7384
}
7485

86+
internal static string GetMapsFilePathForProcess(int pid)
87+
{
88+
return RootPath + pid.ToString(CultureInfo.InvariantCulture) + MapsFileName;
89+
}
90+
7591
internal static string GetTaskDirectoryPathForProcess(int pid)
7692
{
7793
return RootPath + pid.ToString(CultureInfo.InvariantCulture) + TaskDirectoryName;
7894
}
7995

96+
internal static IEnumerable<ParsedMapsModule> ParseMapsModules(int pid)
97+
{
98+
try
99+
{
100+
return ParseMapsModulesCore(File.ReadLines(GetMapsFilePathForProcess(pid)));
101+
}
102+
catch (FileNotFoundException) { }
103+
catch (DirectoryNotFoundException) { }
104+
catch (UnauthorizedAccessException) { }
105+
106+
return Array.Empty<ParsedMapsModule>();
107+
}
108+
109+
private static IEnumerable<ParsedMapsModule> ParseMapsModulesCore(IEnumerable<string> lines)
110+
{
111+
Debug.Assert(lines != null);
112+
113+
// Parse each line from the maps file into a ParsedMapsModule result
114+
foreach (string line in lines)
115+
{
116+
// Use a StringParser to avoid string.Split costs
117+
var parser = new StringParser(line, separator: ' ', skipEmpty: true);
118+
119+
// Parse the address range
120+
KeyValuePair<long, long> addressRange =
121+
parser.ParseRaw(delegate (string s, ref int start, ref int end)
122+
{
123+
long startingAddress = 0, endingAddress = 0;
124+
int pos = s.IndexOf('-', start, end - start);
125+
if (pos > 0)
126+
{
127+
string startingString = s.Substring(start, pos);
128+
if (long.TryParse(startingString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out startingAddress))
129+
{
130+
string endingString = s.Substring(pos + 1, end - (pos + 1));
131+
long.TryParse(endingString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out endingAddress);
132+
}
133+
}
134+
return new KeyValuePair<long, long>(startingAddress, endingAddress);
135+
});
136+
137+
// Parse the permissions (we only care about entries with 'r' and 'x' set)
138+
if (!parser.ParseRaw(delegate (string s, ref int start, ref int end)
139+
{
140+
bool sawRead = false, sawExec = false;
141+
for (int i = start; i < end; i++)
142+
{
143+
if (s[i] == 'r')
144+
sawRead = true;
145+
else if (s[i] == 'x')
146+
sawExec = true;
147+
}
148+
return sawRead & sawExec;
149+
}))
150+
{
151+
continue;
152+
}
153+
154+
// Skip past the offset, dev, and inode fields
155+
parser.MoveNext();
156+
parser.MoveNext();
157+
parser.MoveNext();
158+
159+
// Parse the pathname
160+
if (!parser.MoveNext())
161+
{
162+
continue;
163+
}
164+
string pathname = parser.ExtractCurrent();
165+
166+
// We only get here if a we have a non-empty pathname and
167+
// the permissions included both readability and executability.
168+
// Yield the result.
169+
yield return new ParsedMapsModule { FileName = pathname, AddressRange = addressRange };
170+
}
171+
}
172+
80173
private static string GetStatFilePathForThread(int pid, int tid)
81174
{
82175
// Perf note: Calling GetTaskDirectoryPathForProcess will allocate a string,

src/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,11 @@ public bool WaitForExitCore(int milliseconds)
8585
/// <summary>Gets the main module for the associated process.</summary>
8686
public ProcessModule MainModule
8787
{
88-
get { throw new PlatformNotSupportedException(); }
88+
get
89+
{
90+
ProcessModuleCollection pmc = Modules;
91+
return pmc.Count > 0 ? pmc[0] : null;
92+
}
8993
}
9094

9195
/// <summary>Checks whether the process has exited and updates state accordingly.</summary>

src/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Linux.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

44
using System.Collections.Generic;
5+
using System.ComponentModel;
56
using System.Globalization;
67
using System.IO;
78

@@ -28,6 +29,52 @@ public static int[] GetProcessIds()
2829
return pids.ToArray();
2930
}
3031

32+
/// <summary>Gets an array of module infos for the specified process.</summary>
33+
/// <param name="processId">The ID of the process whose modules should be enumerated.</param>
34+
/// <returns>The array of modules.</returns>
35+
internal static ModuleInfo[] GetModuleInfos(int processId)
36+
{
37+
var modules = new List<ModuleInfo>();
38+
39+
// Process from the parsed maps file each entry representing a module
40+
foreach (Interop.procfs.ParsedMapsModule entry in Interop.procfs.ParseMapsModules(processId))
41+
{
42+
int sizeOfImage = (int)(entry.AddressRange.Value - entry.AddressRange.Key);
43+
44+
// A single module may be split across multiple map entries; consolidate based on
45+
// the name and address ranges of sequential entries.
46+
if (modules.Count > 0)
47+
{
48+
ModuleInfo mi = modules[modules.Count - 1];
49+
if (mi._fileName == entry.FileName &&
50+
((long)mi._baseOfDll + mi._sizeOfImage == entry.AddressRange.Key))
51+
{
52+
// Merge this entry with the previous one
53+
modules[modules.Count - 1]._sizeOfImage += sizeOfImage;
54+
continue;
55+
}
56+
}
57+
58+
// It's not a continuation of a previous entry but a new one: add it.
59+
modules.Add(new ModuleInfo()
60+
{
61+
_fileName = entry.FileName,
62+
_baseName = Path.GetFileName(entry.FileName),
63+
_baseOfDll = new IntPtr(entry.AddressRange.Key),
64+
_sizeOfImage = sizeOfImage,
65+
_entryPoint = IntPtr.Zero // unknown
66+
});
67+
}
68+
69+
// Return the set of modules found
70+
if (modules.Count == 0)
71+
{
72+
// Match Windows behavior when failing to enumerate modules
73+
throw new Win32Exception(SR.EnumProcessModuleFailed);
74+
}
75+
return modules.ToArray();
76+
}
77+
3178
// -----------------------------
3279
// ---- PAL layer ends here ----
3380
// -----------------------------

src/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.OSX.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ private unsafe static ProcessInfo CreateProcessInfo(int pid)
7070
return procInfo;
7171
}
7272

73+
/// <summary>Gets an array of module infos for the specified process.</summary>
74+
/// <param name="processId">The ID of the process whose modules should be enumerated.</param>
75+
/// <returns>The array of modules.</returns>
76+
internal static ModuleInfo[] GetModuleInfos(int processId)
77+
{
78+
// We currently don't provide support for modules on OS X.
79+
return Array.Empty<ModuleInfo>();
80+
}
81+
7382
// ----------------------------------
7483
// ---- Unix PAL layer ends here ----
7584
// ----------------------------------

src/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Unix.cs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,6 @@ public static int GetProcessIdFromHandle(SafeProcessHandle processHandle)
7777
return (int)processHandle.DangerousGetHandle(); // not actually dangerous; just wraps a process ID
7878
}
7979

80-
/// <summary>Gets an array of module infos for the specified process.</summary>
81-
/// <param name="processId">The ID of the process whose modules should be enumerated.</param>
82-
/// <returns>The array of modules.</returns>
83-
public static ModuleInfo[] GetModuleInfos(int processId)
84-
{
85-
// Not currently supported, but we can simply return an empty array rather than throwing.
86-
// Could potentially be done via /proc/pid/maps and some heuristics to determine
87-
// which entries correspond to modules.
88-
return Array.Empty<ModuleInfo>();
89-
}
90-
9180
/// <summary>Gets whether the named machine is remote or local.</summary>
9281
/// <param name="machineName">The machine name.</param>
9382
/// <returns>true if the machine is remote; false if it's local.</returns>

src/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests/ProcessTest.cs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -229,14 +229,11 @@ public void Process_MachineName()
229229
public void Process_MainModule()
230230
{
231231
// Get MainModule property from a Process object
232-
ProcessModule mainModule = null;
233-
if (global::Interop.IsWindows)
234-
{
235-
mainModule = _process.MainModule;
236-
}
237-
else
232+
ProcessModule mainModule = _process.MainModule;
233+
234+
if (!global::Interop.IsOSX) // OS X doesn't currently implement modules support
238235
{
239-
Assert.Throws<PlatformNotSupportedException>(() => _process.MainModule);
236+
Assert.NotNull(mainModule);
240237
}
241238

242239
if (mainModule != null)
@@ -320,11 +317,13 @@ public void Process_Modules()
320317
{
321318
// Validated that we can get a value for each of the following.
322319
Assert.NotNull(pModule);
323-
Assert.NotNull(pModule.BaseAddress);
324-
Assert.NotNull(pModule.EntryPointAddress);
320+
Assert.NotEqual(IntPtr.Zero, pModule.BaseAddress);
325321
Assert.NotNull(pModule.FileName);
326-
int memSize = pModule.ModuleMemorySize;
327322
Assert.NotNull(pModule.ModuleName);
323+
324+
// Just make sure these don't throw
325+
IntPtr addr = pModule.EntryPointAddress;
326+
int memSize = pModule.ModuleMemorySize;
328327
}
329328
}
330329

0 commit comments

Comments
 (0)