Skip to content

Commit

Permalink
feat: add a function for retrieving the module list from a minidump (#33
Browse files Browse the repository at this point in the history
)
  • Loading branch information
nornagon authored Jul 2, 2019
1 parent cc15fce commit e95fa6b
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 7 deletions.
4 changes: 2 additions & 2 deletions build.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
const fs = require('fs')
const path = require('path')
const {spawnSync} = require('child_process')
const { spawnSync } = require('child_process')

const buildDir = path.join(__dirname, 'build')
if (!fs.existsSync(buildDir)) {
fs.mkdirSync(buildDir, {recursive: true})
fs.mkdirSync(buildDir, { recursive: true })
}
spawnSync(path.join(__dirname, 'deps', 'breakpad', 'configure'), [], {
cwd: buildDir,
Expand Down
177 changes: 177 additions & 0 deletions lib/format.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Just enough of the minidump format to extract module names + debug
// identifiers so we can download pdbs

const headerMagic = Buffer.from('MDMP').readUInt32LE(0)

if (!Buffer.prototype.readBigUInt64LE) {
Buffer.prototype.readBigUInt64LE = function(offset) {
// ESLint doesn't support BigInt yet
// eslint-disable-next-line
return BigInt(this.readUInt32LE(offset)) + (BigInt(this.readUInt32LE(offset + 4)) << BigInt(32))
}
}

// MDRawHeader
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#252
function readHeader (buf) {
return {
signature: buf.readUInt32LE(0),
version: buf.readUInt32LE(4),
stream_count: buf.readUInt32LE(8),
stream_directory_rva: buf.readUInt32LE(12),
checksum: buf.readUInt32LE(16),
time_date_stamp: buf.readUInt32LE(20),
flags: buf.readBigUInt64LE(24)
}
}

// MDRawDirectory
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#305
function readDirectory (buf, rva) {
return {
type: buf.readUInt32LE(rva),
location: readLocationDescriptor(buf, rva + 4)
}
}

// MDRawModule
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#386
function readRawModule (buf, rva) {
const module = {
base_of_image: buf.readBigUInt64LE(rva),
size_of_image: buf.readUInt32LE(rva + 8),
checksum: buf.readUInt32LE(rva + 12),
time_date_stamp: buf.readUInt32LE(rva + 16),
module_name_rva: buf.readUInt32LE(rva + 20),
version_info: readVersionInfo(buf, rva + 24),
cv_record: readCVRecord(buf, readLocationDescriptor(buf, rva + 24 + 13 * 4)),
misc_record: readLocationDescriptor(buf, rva + 24 + 13 * 4 + 8)
}
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/processor/minidump.cc#2255
module.version = [
module.version_info.file_version_hi >> 16,
module.version_info.file_version_hi & 0xffff,
module.version_info.file_version_lo >> 16,
module.version_info.file_version_lo & 0xffff
].join('.')
module.name = readString(buf, module.module_name_rva)
return module
}

// MDVSFixedFileInfo
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#129
function readVersionInfo (buf, base) {
return {
signature: buf.readUInt32LE(base),
struct_version: buf.readUInt32LE(base + 4),
file_version_hi: buf.readUInt32LE(base + 8),
file_version_lo: buf.readUInt32LE(base + 12),
product_version_hi: buf.readUInt32LE(base + 16),
product_version_lo: buf.readUInt32LE(base + 20),
file_flags_mask: buf.readUInt32LE(base + 24),
file_flags: buf.readUInt32LE(base + 28),
file_os: buf.readUInt32LE(base + 32),
file_type: buf.readUInt32LE(base + 24),
file_subtype: buf.readUInt32LE(base + 28),
file_date_hi: buf.readUInt32LE(base + 32),
file_date_lo: buf.readUInt32LE(base + 36)
}
}

// MDLocationDescriptor
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#237
function readLocationDescriptor (buf, base) {
return {
data_size: buf.readUInt32LE(base),
rva: buf.readUInt32LE(base + 4)
}
}

// MDGUID
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#81
function readGUID (buf) {
return {
data1: buf.readUInt32LE(0),
data2: buf.readUInt16LE(4),
data3: buf.readUInt16LE(6),
data4: [...buf.subarray(8)]
}
}

// guid_and_age_to_debug_id
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/processor/minidump.cc#2153
function debugIdFromGuidAndAge (guid, age) {
return [
guid.data1.toString(16).padStart(8, '0'),
guid.data2.toString(16).padStart(4, '0'),
guid.data3.toString(16).padStart(4, '0'),
...guid.data4.map(x => x.toString(16).padStart(2, '0')),
age.toString(16)
].join('').toUpperCase()
}

// MDCVInfo{PDB70,ELF}
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#426
function readCVRecord (buf, { rva, data_size: dataSize }) {
if (rva === 0) return
const cv_signature = buf.readUInt32LE(rva)
if (cv_signature !== 0x53445352 /* SDSR */) {
const age = buf.readUInt32LE(rva + 4 + 16)
const guid = readGUID(buf.subarray(rva + 4, rva + 4 + 16))
return {
cv_signature,
guid,
age,
pdb_file_name: buf.subarray(rva + 4 + 16 + 4, rva + dataSize - 1).toString('utf8'),
debug_file_id: debugIdFromGuidAndAge(guid, age)
}
} else {
return {cv_signature}
}
}

// MDString
// https://chromium.googlesource.com/breakpad/breakpad/+/c46151db0ffd1a8dae914e45f1212ef427f61ed3/src/google_breakpad/common/minidump_format.h#357
function readString (buf, rva) {
if (rva === 0) return null
const bytes = buf.readUInt32LE(rva)
return buf.subarray(rva + 4, rva + 4 + bytes).toString('utf16le')
}

// MDStreamType
// https://chromium.googlesource.com/breakpad/breakpad/+/refs/heads/master/src/google_breakpad/common/minidump_format.h#310
const streamTypes = {
MD_MODULE_LIST_STREAM: 4,
}

const streamTypeProcessors = {
[streamTypes.MD_MODULE_LIST_STREAM]: (stream, buf) => {
const numModules = buf.readUInt32LE(stream.location.rva)
const modules = []
const size = 8 + 4 + 4 + 4 + 4 + 13 * 4 + 8 + 8 + 8 + 8
const base = stream.location.rva + 4
for (let i = 0; i < numModules; i++) {
modules.push(readRawModule(buf, base + i * size))
}
stream.modules = modules
return stream
}
}

module.exports.readMinidump = function readMinidump (buf) {
const header = readHeader(buf)
if (header.signature !== headerMagic) {
throw new Error('not a minidump file')
}

const streams = []
for (let i = 0; i < header.stream_count; i++) {
const stream = readDirectory(buf, header.stream_directory_rva + i * 12)
if (stream.type !== 0) {
streams.push((streamTypeProcessors[stream.type] || (s => s))(stream, buf))
}
}
return { header, streams }
}

module.exports.streamTypes = streamTypes
28 changes: 25 additions & 3 deletions lib/minidump.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
var fs = require('fs')
var path = require('path')
var spawn = require('child_process').spawn
const fs = require('fs')
const path = require('path')
const spawn = require('child_process').spawn
const format = require('./format')

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

module.exports.moduleList = function (minidump, callback) {
fs.readFile(minidump, (err, data) => {
if (err) return callback(err)
const { streams } = format.readMinidump(data)
const moduleList = streams.find(s => s.type === format.streamTypes.MD_MODULE_LIST_STREAM)
if (!moduleList) return callback(new Error('minidump does not contain module list'))
const modules = moduleList.modules.map(m => {
const mod = {
version: m.version,
name: m.name
}
if (m.cv_record) {
mod.pdb_file_name = m.cv_record.pdb_file_name
mod.debug_identifier = m.cv_record.debug_file_id
}
return mod
})
callback(null, modules)
})
}

module.exports.walkStack = function (minidump, symbolPaths, callback, commandArgs) {
if (!callback) {
callback = symbolPaths
Expand Down
42 changes: 40 additions & 2 deletions test/minidump-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,44 @@ describe('minidump', function () {
})
})
})

describe('moduleList()', function () {
describe('on a Linux dump', () => {
it('calls back with a module list', function (done) {
const dumpPath = path.join(__dirname, 'fixtures', 'linux.dmp')
minidump.moduleList(dumpPath, (err, modules) => {
if (err) return done(err)
assert.notEqual(modules.length, 0)
assert(modules.some(m => m.name.endsWith('/electron')))
done()
})
})
})

describe('on a Windows dump', () => {
it('calls back with a module list', function (done) {
const dumpPath = path.join(__dirname, 'fixtures', 'windows.dmp')
minidump.moduleList(dumpPath, (err, modules) => {
if (err) return done(err)
assert.notEqual(modules.length, 0)
assert(modules.some(m => m.name.endsWith('\\electron.exe')))
done()
})
})
})

describe('on a macOS dump', () => {
it('calls back with a module list', function (done) {
const dumpPath = path.join(__dirname, 'fixtures', 'mac.dmp')
minidump.moduleList(dumpPath, (err, modules) => {
if (err) return done(err)
assert.notEqual(modules.length, 0)
assert(modules.some(m => m.name.endsWith('/Electron Helper')))
done()
})
})
})
})
})

var downloadElectron = function (callback) {
Expand All @@ -100,7 +138,7 @@ var downloadElectron = function (callback) {
if (error) return callback(error)

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

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

var symbolsPath = temp.mkdirSync('node-minidump-')
extractZip(zipPath, {dir: symbolsPath}, function (error) {
extractZip(zipPath, { dir: symbolsPath }, function (error) {
if (error) return callback(error)
callback(null, path.join(symbolsPath, 'electron.breakpad.syms'))
})
Expand Down

0 comments on commit e95fa6b

Please sign in to comment.