refactor(pe): share CodeView range interpretation

This commit is contained in:
morluto committed 2026-10-26 16:04:18 +08:00
1 parent a7d71f47ee
commit 92bdb4443f
3 files changed
+104 -45

No files matched your search

+10 -22
View File
@@ -82,6 +82,7 @@ import {
parseExecutableHeader,
parsePeRecordFacts,
peDebugDirectoryEntry,
peCodeViewDataRanges,
peRvaToFileOffset,
type ExecutableMetadata,
} from "../domain/binaryTarget.js";
@@ -1347,28 +1348,15 @@ const readPeCodeView = async (
debugFileOffset,
);
throwIfTargetResolutionCancelled(signal);
const numEntries = Math.floor(debugDirRead.bytesRead / 28);
for (let j = 0; j < numEntries; j++) {
const entryOffset = j * 28;
const type = debugDirectoryBuffer.readUInt32LE(entryOffset + 12);
if (type !== 2) continue; // IMAGE_DEBUG_TYPE_CODEVIEW
const sizeOfData = debugDirectoryBuffer.readUInt32LE(entryOffset + 16);
let pointerToRawData = debugDirectoryBuffer.readUInt32LE(entryOffset + 24);
if (pointerToRawData === 0) {
const addressOfRawData = debugDirectoryBuffer.readUInt32LE(
entryOffset + 20,
);
const mapped = peRvaToFileOffset(
addressOfRawData,
sectionTable,
numberOfSections,
debugDir.sizeOfHeaders,
Math.min(sizeOfData, 4096),
);
if (mapped === undefined) continue;
pointerToRawData = mapped;
}
if (sizeOfData < 16) continue;
for (const {
fileOffset: pointerToRawData,
declaredSize: sizeOfData,
} of peCodeViewDataRanges(
debugDirectoryBuffer.subarray(0, debugDirRead.bytesRead),
sectionTable,
numberOfSections,
debugDir.sizeOfHeaders,
)) {
const maxCodeViewRead = Math.min(sizeOfData, 4096);
const codeViewBuffer = Buffer.alloc(maxCodeViewRead);
const codeViewRead = await handle.read(
+39 -23
View File
@@ -630,6 +630,35 @@ export const peRvaToFileOffset = (
return undefined;
};
/** CodeView RVA admission uses the same bounded prefix as target file reads. */
const PE_CODEVIEW_MAPPING_BYTES = 4096;
/** Interpret complete debug entries once; callers own directory and payload read bounds. */
export function* peCodeViewDataRanges(
directory: Buffer,
sectionTable: Buffer,
numberOfSections: number,
sizeOfHeaders: number,
): Generator<{ readonly fileOffset: number; readonly declaredSize: number }> {
for (let offset = 0; offset + 28 <= directory.length; offset += 28) {
if (directory.readUInt32LE(offset + 12) !== 2) continue;
const declaredSize = directory.readUInt32LE(offset + 16);
if (declaredSize < 16) continue;
const rawPointer = directory.readUInt32LE(offset + 24);
const fileOffset =
rawPointer !== 0
? rawPointer
: peRvaToFileOffset(
directory.readUInt32LE(offset + 20),
sectionTable,
numberOfSections,
sizeOfHeaders,
Math.min(declaredSize, PE_CODEVIEW_MAPPING_BYTES),
);
if (fileOffset !== undefined) yield { fileOffset, declaredSize };
}
}
/** Parse CodeView metadata from a bounded in-memory PE image buffer. */
export const parsePeCodeViewFromBuffer = (
fileBytes: Buffer,
@@ -673,29 +702,16 @@ export const parsePeCodeViewFromBuffer = (
debugDir.debugSize,
fileBytes.length - debugFileOffset,
);
const numEntries = Math.floor(maxDebugBytes / 28);
for (let j = 0; j < numEntries; j++) {
const entryOffset = debugFileOffset + j * 28;
const type = fileBytes.readUInt32LE(entryOffset + 12);
if (type !== 2) continue; // IMAGE_DEBUG_TYPE_CODEVIEW
const sizeOfData = fileBytes.readUInt32LE(entryOffset + 16);
let pointerToRawData = fileBytes.readUInt32LE(entryOffset + 24);
if (pointerToRawData === 0) {
const addressOfRawData = fileBytes.readUInt32LE(entryOffset + 20);
const mapped = peRvaToFileOffset(
addressOfRawData,
sectionTable,
numberOfSections,
debugDir.sizeOfHeaders,
Math.min(sizeOfData, 4096),
);
if (mapped === undefined) continue;
pointerToRawData = mapped;
}
if (
sizeOfData < 16 ||
fileBytes.length < pointerToRawData + Math.min(sizeOfData, 24)
)
for (const {
fileOffset: pointerToRawData,
declaredSize: sizeOfData,
} of peCodeViewDataRanges(
fileBytes.subarray(debugFileOffset, debugFileOffset + maxDebugBytes),
sectionTable,
numberOfSections,
debugDir.sizeOfHeaders,
)) {
if (fileBytes.length < pointerToRawData + Math.min(sizeOfData, 24))
continue;
const codeViewData = fileBytes.subarray(
pointerToRawData,
+55
View File
@@ -0,0 +1,55 @@
import { expect, it } from "vitest";
import { peCodeViewDataRanges } from "./binaryTarget.js";
const entry = (type: number, size: number, rva: number, raw: number) => {
const bytes = Buffer.alloc(28);
bytes.writeUInt32LE(type, 12);
bytes.writeUInt32LE(size, 16);
bytes.writeUInt32LE(rva, 20);
bytes.writeUInt32LE(raw, 24);
return bytes;
};
const section = () => {
const bytes = Buffer.alloc(40);
bytes.writeUInt32LE(0x2000, 12);
bytes.writeUInt32LE(4096, 16);
bytes.writeUInt32LE(0x400, 20);
return bytes;
};
it("prefers physical pointers and maps only missing pointers through section or header RVAs", () => {
const directory = Buffer.concat([
entry(2, 32, 0x2000, 0x900),
entry(2, 32, 0x2020, 0),
entry(2, 32, 0x100, 0),
entry(2, 32, 0x8000, 0),
]);
expect([...peCodeViewDataRanges(directory, section(), 1, 0x400)]).toEqual([
{ fileOffset: 0x900, declaredSize: 32 },
{ fileOffset: 0x420, declaredSize: 32 },
{ fileOffset: 0x100, declaredSize: 32 },
]);
});
it("ignores other debug types, undersized records and incomplete trailing entries", () => {
const directory = Buffer.concat([
entry(1, 32, 0, 0x500),
entry(2, 15, 0, 0x600),
entry(2, 16, 0, 0x700),
entry(2, 32, 0, 0x800).subarray(0, 27),
]);
expect([...peCodeViewDataRanges(directory, Buffer.alloc(0), 0, 0)]).toEqual([
{ fileOffset: 0x700, declaredSize: 16 },
]);
});
it("maps the bounded payload prefix while retaining its full declared size", () => {
const directory = Buffer.concat([
entry(2, 8192, 0x2000, 0),
entry(2, 8192, 0x2001, 0),
]);
expect([...peCodeViewDataRanges(directory, section(), 1, 0x400)]).toEqual([
{ fileOffset: 0x400, declaredSize: 8192 },
]);
});