Skip to content

Commit e95fa6b

Browse files
authored
feat: add a function for retrieving the module list from a minidump (#33)
1 parent cc15fce commit e95fa6b

File tree

4 files changed

+244
-7
lines changed

4 files changed

+244
-7
lines changed

build.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
const fs = require('fs')
22
const path = require('path')
3-
const {spawnSync} = require('child_process')
3+
const { spawnSync } = require('child_process')
44

55
const buildDir = path.join(__dirname, 'build')
66
if (!fs.existsSync(buildDir)) {
7-
fs.mkdirSync(buildDir, {recursive: true})
7+
fs.mkdirSync(buildDir, { recursive: true })
88
}
99
spawnSync(path.join(__dirname, 'deps', 'breakpad', 'configure'), [], {
1010
cwd: buildDir,

lib/format.js

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Just enough of the minidump format to extract module names + debug
2+
// identifiers so we can download pdbs
3+
4+
const headerMagic = Buffer.from('MDMP').readUInt32LE(0)
5+
6+
if (!Buffer.prototype.readBigUInt64LE) {
7+
Buffer.prototype.readBigUInt64LE = function(offset) {
8+
// ESLint doesn't support BigInt yet
9+
// eslint-disable-next-line
10+
return BigInt(this.readUInt32LE(offset)) + (BigInt(this.readUInt32LE(offset + 4)) << BigInt(32))
11+
}
12+
}
13+
14+
// MDRawHeader
15+
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#252
16+
function readHeader (buf) {
17+
return {
18+
signature: buf.readUInt32LE(0),
19+
version: buf.readUInt32LE(4),
20+
stream_count: buf.readUInt32LE(8),
21+
stream_directory_rva: buf.readUInt32LE(12),
22+
checksum: buf.readUInt32LE(16),
23+
time_date_stamp: buf.readUInt32LE(20),
24+
flags: buf.readBigUInt64LE(24)
25+
}
26+
}
27+
28+
// MDRawDirectory
29+
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#305
30+
function readDirectory (buf, rva) {
31+
return {
32+
type: buf.readUInt32LE(rva),
33+
location: readLocationDescriptor(buf, rva + 4)
34+
}
35+
}
36+
37+
// MDRawModule
38+
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#386
39+
function readRawModule (buf, rva) {
40+
const module = {
41+
base_of_image: buf.readBigUInt64LE(rva),
42+
size_of_image: buf.readUInt32LE(rva + 8),
43+
checksum: buf.readUInt32LE(rva + 12),
44+
time_date_stamp: buf.readUInt32LE(rva + 16),
45+
module_name_rva: buf.readUInt32LE(rva + 20),
46+
version_info: readVersionInfo(buf, rva + 24),
47+
cv_record: readCVRecord(buf, readLocationDescriptor(buf, rva + 24 + 13 * 4)),
48+
misc_record: readLocationDescriptor(buf, rva + 24 + 13 * 4 + 8)
49+
}
50+
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/processor/minidump.cc#2255
51+
module.version = [
52+
module.version_info.file_version_hi >> 16,
53+
module.version_info.file_version_hi & 0xffff,
54+
module.version_info.file_version_lo >> 16,
55+
module.version_info.file_version_lo & 0xffff
56+
].join('.')
57+
module.name = readString(buf, module.module_name_rva)
58+
return module
59+
}
60+
61+
// MDVSFixedFileInfo
62+
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#129
63+
function readVersionInfo (buf, base) {
64+
return {
65+
signature: buf.readUInt32LE(base),
66+
struct_version: buf.readUInt32LE(base + 4),
67+
file_version_hi: buf.readUInt32LE(base + 8),
68+
file_version_lo: buf.readUInt32LE(base + 12),
69+
product_version_hi: buf.readUInt32LE(base + 16),
70+
product_version_lo: buf.readUInt32LE(base + 20),
71+
file_flags_mask: buf.readUInt32LE(base + 24),
72+
file_flags: buf.readUInt32LE(base + 28),
73+
file_os: buf.readUInt32LE(base + 32),
74+
file_type: buf.readUInt32LE(base + 24),
75+
file_subtype: buf.readUInt32LE(base + 28),
76+
file_date_hi: buf.readUInt32LE(base + 32),
77+
file_date_lo: buf.readUInt32LE(base + 36)
78+
}
79+
}
80+
81+
// MDLocationDescriptor
82+
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#237
83+
function readLocationDescriptor (buf, base) {
84+
return {
85+
data_size: buf.readUInt32LE(base),
86+
rva: buf.readUInt32LE(base + 4)
87+
}
88+
}
89+
90+
// MDGUID
91+
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#81
92+
function readGUID (buf) {
93+
return {
94+
data1: buf.readUInt32LE(0),
95+
data2: buf.readUInt16LE(4),
96+
data3: buf.readUInt16LE(6),
97+
data4: [...buf.subarray(8)]
98+
}
99+
}
100+
101+
// guid_and_age_to_debug_id
102+
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/processor/minidump.cc#2153
103+
function debugIdFromGuidAndAge (guid, age) {
104+
return [
105+
guid.data1.toString(16).padStart(8, '0'),
106+
guid.data2.toString(16).padStart(4, '0'),
107+
guid.data3.toString(16).padStart(4, '0'),
108+
...guid.data4.map(x => x.toString(16).padStart(2, '0')),
109+
age.toString(16)
110+
].join('').toUpperCase()
111+
}
112+
113+
// MDCVInfo{PDB70,ELF}
114+
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#426
115+
function readCVRecord (buf, { rva, data_size: dataSize }) {
116+
if (rva === 0) return
117+
const cv_signature = buf.readUInt32LE(rva)
118+
if (cv_signature !== 0x53445352 /* SDSR */) {
119+
const age = buf.readUInt32LE(rva + 4 + 16)
120+
const guid = readGUID(buf.subarray(rva + 4, rva + 4 + 16))
121+
return {
122+
cv_signature,
123+
guid,
124+
age,
125+
pdb_file_name: buf.subarray(rva + 4 + 16 + 4, rva + dataSize - 1).toString('utf8'),
126+
debug_file_id: debugIdFromGuidAndAge(guid, age)
127+
}
128+
} else {
129+
return {cv_signature}
130+
}
131+
}
132+
133+
// MDString
134+
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#357
135+
function readString (buf, rva) {
136+
if (rva === 0) return null
137+
const bytes = buf.readUInt32LE(rva)
138+
return buf.subarray(rva + 4, rva + 4 + bytes).toString('utf16le')
139+
}
140+
141+
// MDStreamType
142+
// https://chromium.googlesource.com/breakpad/breakpad/+/refs/heads/master/src/google_breakpad/common/minidump_format.h#310
143+
const streamTypes = {
144+
MD_MODULE_LIST_STREAM: 4,
145+
}
146+
147+
const streamTypeProcessors = {
148+
[streamTypes.MD_MODULE_LIST_STREAM]: (stream, buf) => {
149+
const numModules = buf.readUInt32LE(stream.location.rva)
150+
const modules = []
151+
const size = 8 + 4 + 4 + 4 + 4 + 13 * 4 + 8 + 8 + 8 + 8
152+
const base = stream.location.rva + 4
153+
for (let i = 0; i < numModules; i++) {
154+
modules.push(readRawModule(buf, base + i * size))
155+
}
156+
stream.modules = modules
157+
return stream
158+
}
159+
}
160+
161+
module.exports.readMinidump = function readMinidump (buf) {
162+
const header = readHeader(buf)
163+
if (header.signature !== headerMagic) {
164+
throw new Error('not a minidump file')
165+
}
166+
167+
const streams = []
168+
for (let i = 0; i < header.stream_count; i++) {
169+
const stream = readDirectory(buf, header.stream_directory_rva + i * 12)
170+
if (stream.type !== 0) {
171+
streams.push((streamTypeProcessors[stream.type] || (s => s))(stream, buf))
172+
}
173+
}
174+
return { header, streams }
175+
}
176+
177+
module.exports.streamTypes = streamTypes

lib/minidump.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
var fs = require('fs')
2-
var path = require('path')
3-
var spawn = require('child_process').spawn
1+
const fs = require('fs')
2+
const path = require('path')
3+
const spawn = require('child_process').spawn
4+
const format = require('./format')
45

56
const exe = process.platform === 'win32' ? '.exe' : ''
67
const commands = {
@@ -36,6 +37,27 @@ function execute (command, args, callback) {
3637
var globalSymbolPaths = []
3738
module.exports.addSymbolPath = Array.prototype.push.bind(globalSymbolPaths)
3839

40+
module.exports.moduleList = function (minidump, callback) {
41+
fs.readFile(minidump, (err, data) => {
42+
if (err) return callback(err)
43+
const { streams } = format.readMinidump(data)
44+
const moduleList = streams.find(s => s.type === format.streamTypes.MD_MODULE_LIST_STREAM)
45+
if (!moduleList) return callback(new Error('minidump does not contain module list'))
46+
const modules = moduleList.modules.map(m => {
47+
const mod = {
48+
version: m.version,
49+
name: m.name
50+
}
51+
if (m.cv_record) {
52+
mod.pdb_file_name = m.cv_record.pdb_file_name
53+
mod.debug_identifier = m.cv_record.debug_file_id
54+
}
55+
return mod
56+
})
57+
callback(null, modules)
58+
})
59+
}
60+
3961
module.exports.walkStack = function (minidump, symbolPaths, callback, commandArgs) {
4062
if (!callback) {
4163
callback = symbolPaths

test/minidump-test.js

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,44 @@ describe('minidump', function () {
8888
})
8989
})
9090
})
91+
92+
describe('moduleList()', function () {
93+
describe('on a Linux dump', () => {
94+
it('calls back with a module list', function (done) {
95+
const dumpPath = path.join(__dirname, 'fixtures', 'linux.dmp')
96+
minidump.moduleList(dumpPath, (err, modules) => {
97+
if (err) return done(err)
98+
assert.notEqual(modules.length, 0)
99+
assert(modules.some(m => m.name.endsWith('/electron')))
100+
done()
101+
})
102+
})
103+
})
104+
105+
describe('on a Windows dump', () => {
106+
it('calls back with a module list', function (done) {
107+
const dumpPath = path.join(__dirname, 'fixtures', 'windows.dmp')
108+
minidump.moduleList(dumpPath, (err, modules) => {
109+
if (err) return done(err)
110+
assert.notEqual(modules.length, 0)
111+
assert(modules.some(m => m.name.endsWith('\\electron.exe')))
112+
done()
113+
})
114+
})
115+
})
116+
117+
describe('on a macOS dump', () => {
118+
it('calls back with a module list', function (done) {
119+
const dumpPath = path.join(__dirname, 'fixtures', 'mac.dmp')
120+
minidump.moduleList(dumpPath, (err, modules) => {
121+
if (err) return done(err)
122+
assert.notEqual(modules.length, 0)
123+
assert(modules.some(m => m.name.endsWith('/Electron Helper')))
124+
done()
125+
})
126+
})
127+
})
128+
})
91129
})
92130

93131
var downloadElectron = function (callback) {
@@ -100,7 +138,7 @@ var downloadElectron = function (callback) {
100138
if (error) return callback(error)
101139

102140
var electronPath = temp.mkdirSync('node-minidump-')
103-
extractZip(zipPath, {dir: electronPath}, function (error) {
141+
extractZip(zipPath, { dir: electronPath }, function (error) {
104142
if (error) return callback(error)
105143

106144
if (process.platform === 'darwin') {
@@ -123,7 +161,7 @@ var downloadElectronSymbols = function (platform, callback) {
123161
if (error) return callback(error)
124162

125163
var symbolsPath = temp.mkdirSync('node-minidump-')
126-
extractZip(zipPath, {dir: symbolsPath}, function (error) {
164+
extractZip(zipPath, { dir: symbolsPath }, function (error) {
127165
if (error) return callback(error)
128166
callback(null, path.join(symbolsPath, 'electron.breakpad.syms'))
129167
})

0 commit comments

Comments
 (0)