-
Notifications
You must be signed in to change notification settings - Fork 0
/
dotnet-mapgen-v2.py
154 lines (142 loc) · 5.81 KB
/
dotnet-mapgen-v2.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#!/usr/bin/env python
#
# USAGE: dotnet-mapgen [-h] {generate,merge} PID
#
# In generate mode, this tool reads the /tmp/perfinfo-PID.map file generated
# by the CLR when running with COMPlus_PerfMapEnabled=1, and finds all load
# events for managed assemblies. For each managed assembly found in this way,
# the tool runs crossgen to generate a symbol mapping file (akin to debuginfo).
#
# In merge mode, this tool finds the load address of each managed assembly in
# the target process, and merges the addresses from the crossgen-generated
# map files into the main /tmp/perf-PID.map file for the process. The crossgen-
# generated map files contain relative addresses, so the tool has to translate
# these into absolute addresses using the load address of each managed assembly
# in the target process.
#
# Copyright (C) 2017, Sasha Goldshtein
# Licensed under the MIT License
import argparse
import glob
import os
import shutil
import subprocess
import tempfile
def bail(error):
print("ERROR: " + error)
exit(1)
def get_assembly_list(pid):
assemblies = []
try:
with open("/tmp/perfinfo-%d.map" % pid) as f:
for line in f:
parts = line.split(';')
if len(parts) < 2 or parts[0] != "ImageLoad":
continue
assemblies.append(parts[1])
except IOError:
bail("error opening /tmp/perfinfo-%d.map file" % pid)
return assemblies
def find_libcoreclr(pid):
libcoreclr = subprocess.check_output(
"cat /proc/%d/maps | grep libcoreclr.so | head -1 | awk '{ print $6 }'"
% pid, shell=True)
return libcoreclr
def download_crossgen(libcoreclr):
# Updated for CoreCLR 2.0. project.json doesn't work anymore, need to generate
# a .csproj and restore from that. In the meantime, portable RIDs showed up,
# so we no longer need anything more specific than "linux-x64" (assuming, of
# course, that we're on Linux x64).
coreclr_ver = "2.0.0"
rid = "linux-x64"
tmp_folder = tempfile.mkdtemp(prefix="crossgen")
project = os.path.join(tmp_folder, "project.csproj")
with open(project, "w") as f:
f.write("""
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETCore.Runtime.CoreCLR" Version="%s" />
</ItemGroup>
</Project>\n""" % coreclr_ver)
subprocess.check_call("dotnet restore %s --packages %s -r %s >/dev/null 2>&1"
% (project, tmp_folder, rid), shell=True)
shutil.copy(
"%s/runtime.%s.microsoft.netcore.runtime.coreclr/%s/tools/crossgen"
% (tmp_folder, rid, coreclr_ver),
os.path.dirname(libcoreclr))
print("crossgen succesfully downloaded and placed in libcoreclr's dir")
shutil.rmtree(tmp_folder)
def find_crossgen(pid):
libcoreclr = find_libcoreclr(pid)
path = os.path.dirname(libcoreclr)
crossgen = os.path.join(path, "crossgen")
if not os.path.isfile(crossgen):
print("couldn't find crossgen, trying to fetch it automatically...")
download_crossgen(libcoreclr)
return crossgen
def generate(pid):
assemblies = get_assembly_list(pid)
crossgen = find_crossgen(pid)
asm_list = str.join(":", assemblies)
succeeded, failed = (0, 0)
for assembly in assemblies:
rc = subprocess.call(("%s /Trusted_Platform_Assemblies '%s' " +
"/CreatePerfMap /tmp %s >/dev/null 2>&1") %
(crossgen, asm_list, assembly), shell=True)
if rc == 0:
succeeded += 1
else:
failed += 1
#print ("crossgen failed: %s" % (assembly))
print("crossgen map generation: %d succeeded, %d failed" %
(succeeded, failed))
def get_base_address(pid, assembly):
hexaddr = subprocess.check_output(
"cat /proc/%d/maps | grep %s | head -1 | cut -d '-' -f 1" %
(pid, assembly), shell=True)
if hexaddr == '':
return -1
return int(hexaddr, 16)
def append_perf_map(assembly, asm_map, pid):
base_address = get_base_address(pid, assembly)
lines_to_add = ""
with open(asm_map) as f:
for line in f:
parts = line.split()
offset, size, symbol = parts[0], parts[1], str.join(" ", parts[2:])
offset = int(offset, 16) + base_address
lines_to_add += "%016x %s %s\n" % (offset, size, symbol)
with open("/tmp/perf-%d.map" % pid, "a") as perfmap:
perfmap.write(lines_to_add)
def merge(pid):
assemblies = get_assembly_list(pid)
succeeded, failed = (0, 0)
for assembly in assemblies:
# TODO The generated map files have a GUID embedded in them, which
# allows multiple versions to coexist (probably). How do we get
# this GUID? E.g.:
# System.Runtime.ni.{819d412e-d773-4dbb-8d01-20d412b6cf09}.map
matches = glob.glob("/tmp/%s.ni.{*}.map" %
os.path.splitext(os.path.basename(assembly))[0])
if len(matches) == 0:
#print ("perf map merge failed: %s" % (assembly))
failed += 1
else:
append_perf_map(assembly, matches[0], pid)
succeeded += 1
print("perfmap merging: %d succeeded, %d failed" % (succeeded, failed))
parser = argparse.ArgumentParser(description=
"Generates map files for crossgen-compiled assemblies, and merges them " +
"into the main perf map file. Built for use with .NET Core on Linux.")
parser.add_argument("action", choices=["generate", "merge"],
help="the action to perform")
parser.add_argument("pid", type=int, help="the dotnet process id")
args = parser.parse_args()
if args.action == "generate":
generate(args.pid)
elif args.action == "merge":
merge(args.pid)