feat(native): read PE and ELF export tables
Add a host-independent export-table reader on the shared native container layer. PE images report each used export-address-table slot with its ordinal, one row per name, its address or forwarder text, and the export directory identity; the PE image reader now exposes its data directories. ELF images report defined default/protected global, weak and unique .dynsym symbols with type, binding and .gnu.version_d version (hidden versions kept distinct, unreadable version tables "unknown"). Unreadable entries are counted in issues and make coverage partial; an ELF without section headers is unsupported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c8ed328538
commit
2dee400f93
6 files changed
+1165
No files matched your search
@@ -0,0 +1,128 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { absoluteLocalPathStringSchema } from "../localPath.js";
|
||||
|
||||
const uint = z.number().int().min(0).max(0xffffffff);
|
||||
const address = z.string().regex(/^0x[0-9a-f]+$/u);
|
||||
|
||||
/** Hard ceiling on the caller-selected input read budget. */
|
||||
export const MAX_INSPECT_NATIVE_EXPORTS_FILE_BYTES = 536870912;
|
||||
/** Fixed bound on reported export rows, so one library cannot flood a reply. */
|
||||
export const MAX_NATIVE_EXPORT_ROWS = 262144;
|
||||
|
||||
/** One explicitly selected PE or ELF library or executable. */
|
||||
export const inspectNativeExportsInputSchema = z.strictObject({
|
||||
path: absoluteLocalPathStringSchema.describe(
|
||||
"Absolute filesystem path to a PE or ELF image",
|
||||
),
|
||||
max_file_bytes: z
|
||||
.number()
|
||||
.int()
|
||||
.min(64)
|
||||
.max(MAX_INSPECT_NATIVE_EXPORTS_FILE_BYTES)
|
||||
.default(268435456)
|
||||
.describe("Maximum stable artifact bytes to read (default 256 MiB)"),
|
||||
});
|
||||
export type InspectNativeExportsInput = z.infer<
|
||||
typeof inspectNativeExportsInputSchema
|
||||
>;
|
||||
|
||||
/**
|
||||
* One PE export-address-table slot as the export directory states it. A
|
||||
* forwarder has no address in this image; an ordinal-only export has no name.
|
||||
*/
|
||||
const peExportSchema = z.strictObject({
|
||||
name: z.string().min(1).nullable(),
|
||||
ordinal: z.number().int().min(0).max(0xffff),
|
||||
address: address.nullable(),
|
||||
forwarder: z.string().min(1).nullable(),
|
||||
});
|
||||
|
||||
/**
|
||||
* One defined, default- or protected-visibility global, weak or unique
|
||||
* dynamic symbol. `version` is null when the symbol carries no version and
|
||||
* "unknown" when the version table could not be read; a hidden version is not
|
||||
* the default one a by-name lookup selects.
|
||||
*/
|
||||
const elfExportSchema = z.strictObject({
|
||||
name: z.string().min(1),
|
||||
/** `st_value` as stored; on 32-bit ARM, bit 0 of a function marks Thumb code. */
|
||||
value: address,
|
||||
size: z.string().regex(/^[0-9]+$/u),
|
||||
type: z.enum(["function", "ifunc", "object", "tls", "other"]),
|
||||
binding: z.enum(["global", "weak", "unique"]),
|
||||
version: z.union([
|
||||
z.null(),
|
||||
z.literal("unknown"),
|
||||
z.strictObject({
|
||||
index: z.number().int().min(2).max(0x7fff),
|
||||
name: z.string().min(1).nullable(),
|
||||
hidden: z.boolean(),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
/** Parsed table issue; rows it affects are counted, not silently dropped. */
|
||||
const nativeExportIssueSchema = z.strictObject({
|
||||
code: z.enum([
|
||||
"malformed_entry",
|
||||
"name_unreadable",
|
||||
"version_unresolved",
|
||||
"table_unavailable",
|
||||
]),
|
||||
location: z.string().min(1).nullable(),
|
||||
message: z.string().min(1),
|
||||
});
|
||||
|
||||
const artifactSchema = z.strictObject({
|
||||
path: z.string().min(1),
|
||||
sha256: z.string().regex(/^[a-f0-9]{64}$/u),
|
||||
bytes: uint,
|
||||
});
|
||||
|
||||
const coverageSchema = z.strictObject({
|
||||
/** complete: every table entry was read; partial: issues name the gaps. */
|
||||
status: z.enum(["complete", "partial", "unsupported"]),
|
||||
reason: z.string().min(1).nullable(),
|
||||
examined_entries: uint,
|
||||
exports: uint,
|
||||
});
|
||||
|
||||
/** Static export inventory of one PE or ELF image, read without execution. */
|
||||
export const nativeExportsSchema = z.discriminatedUnion("format", [
|
||||
z.strictObject({
|
||||
artifact: artifactSchema,
|
||||
format: z.literal("pe"),
|
||||
architecture: z.string().min(1),
|
||||
bits: z.union([z.literal(32), z.literal(64)]),
|
||||
/** Null when the image declares no export directory. */
|
||||
export_directory: z
|
||||
.strictObject({
|
||||
rva: uint,
|
||||
size: uint,
|
||||
dll_name: z.string().nullable(),
|
||||
ordinal_base: uint,
|
||||
timestamp: uint,
|
||||
})
|
||||
.nullable(),
|
||||
exports: z.array(peExportSchema).max(MAX_NATIVE_EXPORT_ROWS),
|
||||
coverage: coverageSchema,
|
||||
issues: z.array(nativeExportIssueSchema),
|
||||
limitations: z.array(z.string().min(1)),
|
||||
}),
|
||||
z.strictObject({
|
||||
artifact: artifactSchema,
|
||||
format: z.literal("elf"),
|
||||
architecture: z.string().min(1),
|
||||
bits: z.union([z.literal(32), z.literal(64)]),
|
||||
byte_order: z.enum(["little", "big"]),
|
||||
exports: z.array(elfExportSchema).max(MAX_NATIVE_EXPORT_ROWS),
|
||||
coverage: coverageSchema,
|
||||
issues: z.array(nativeExportIssueSchema),
|
||||
limitations: z.array(z.string().min(1)),
|
||||
}),
|
||||
]);
|
||||
export type NativeExports = z.infer<typeof nativeExportsSchema>;
|
||||
export type PeExport = z.infer<typeof peExportSchema>;
|
||||
export type ElfExport = z.infer<typeof elfExportSchema>;
|
||||
export type NativeExportIssue = z.infer<typeof nativeExportIssueSchema>;
|
||||
@@ -0,0 +1,291 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
elfExportImage,
|
||||
peExportImage,
|
||||
} from "../../../tests/fixtures/binaryDiagnostics/nativeExports.js";
|
||||
import { nativeExportsSchema } from "../../domain/native/nativeExports.js";
|
||||
import {
|
||||
BinaryFormatFailure,
|
||||
BinaryResourceFailure,
|
||||
} from "../metadata/BinaryContainer.js";
|
||||
import { parseNativeExports } from "./NativeExportParser.js";
|
||||
|
||||
const artifact = { path: "/fixture/lib", sha256: "a".repeat(64), bytes: 1 };
|
||||
const parse = (bytes: Buffer) =>
|
||||
nativeExportsSchema.parse(parseNativeExports(bytes, artifact));
|
||||
|
||||
describe("PE export directories", () => {
|
||||
it.each([32, 64] as const)(
|
||||
"reports named, aliased, ordinal-only and forwarded PE%d exports",
|
||||
(bits) => {
|
||||
const result = parse(
|
||||
peExportImage({
|
||||
bits,
|
||||
ordinalBase: 5,
|
||||
functions: [
|
||||
{ rva: 0x2000 },
|
||||
{ rva: 0x2010 },
|
||||
null,
|
||||
{ forwarder: "OTHER.Target" },
|
||||
],
|
||||
names: [
|
||||
{ name: "Alias", index: 0 },
|
||||
{ name: "Forwarded", index: 3 },
|
||||
{ name: "Primary", index: 0 },
|
||||
],
|
||||
}),
|
||||
);
|
||||
const base = bits === 64 ? 0x180000000 : 0x10000000;
|
||||
|
||||
expect(result).toMatchObject({
|
||||
format: "pe",
|
||||
bits,
|
||||
export_directory: {
|
||||
rva: 0x1000,
|
||||
size: 0x200,
|
||||
dll_name: "fixture.dll",
|
||||
ordinal_base: 5,
|
||||
},
|
||||
coverage: {
|
||||
status: "complete",
|
||||
examined_entries: 4,
|
||||
exports: 4,
|
||||
},
|
||||
issues: [],
|
||||
});
|
||||
expect(result.exports).toEqual([
|
||||
{
|
||||
name: "Alias",
|
||||
ordinal: 5,
|
||||
address: `0x${(base + 0x2000).toString(16)}`,
|
||||
forwarder: null,
|
||||
},
|
||||
{
|
||||
name: "Primary",
|
||||
ordinal: 5,
|
||||
address: `0x${(base + 0x2000).toString(16)}`,
|
||||
forwarder: null,
|
||||
},
|
||||
{
|
||||
name: null,
|
||||
ordinal: 6,
|
||||
address: `0x${(base + 0x2010).toString(16)}`,
|
||||
forwarder: null,
|
||||
},
|
||||
{
|
||||
name: "Forwarded",
|
||||
ordinal: 8,
|
||||
address: null,
|
||||
forwarder: "OTHER.Target",
|
||||
},
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it("counts unreadable and out-of-range names without inventing exports", () => {
|
||||
const result = parse(
|
||||
peExportImage({
|
||||
functions: [{ rva: 0x2000 }, { rva: 0x2010 }],
|
||||
names: [
|
||||
{ name: "Unmapped", index: 0 },
|
||||
{ name: "Stray", index: 9 },
|
||||
{ name: "Kept", index: 1 },
|
||||
],
|
||||
nameRvaOverride: { position: 0, rva: 0x9000 },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.exports.map(({ name }) => name)).toEqual(["Kept"]);
|
||||
expect(result.coverage).toMatchObject({ status: "partial", exports: 1 });
|
||||
expect(result.issues.map(({ code }) => code)).toEqual([
|
||||
"name_unreadable",
|
||||
"malformed_entry",
|
||||
]);
|
||||
});
|
||||
|
||||
it("reports an image without an export directory as exporting nothing", () => {
|
||||
const result = parse(peExportImage({ exportDirectory: false }));
|
||||
|
||||
expect(result).toMatchObject({
|
||||
export_directory: null,
|
||||
exports: [],
|
||||
coverage: { status: "complete", examined_entries: 0 },
|
||||
});
|
||||
expect(result.limitations[0]).toContain("declares no export directory");
|
||||
});
|
||||
|
||||
it("refuses ordinals beyond the 16-bit ordinal space", () => {
|
||||
expect(() =>
|
||||
parseNativeExports(
|
||||
peExportImage({
|
||||
functions: [{ rva: 0x2000 }],
|
||||
ordinalBase: 0xffff + 1,
|
||||
}),
|
||||
artifact,
|
||||
),
|
||||
).toThrow("16-bit ordinals");
|
||||
});
|
||||
|
||||
it("refuses an export table above the inventory allowance", () => {
|
||||
expect(() =>
|
||||
parseNativeExports(
|
||||
peExportImage({ functions: [], declaredFunctions: 0x40001 }),
|
||||
artifact,
|
||||
),
|
||||
).toThrow(BinaryResourceFailure);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ELF dynamic symbol exports", () => {
|
||||
const symbols = [
|
||||
{ name: "plain_fn" },
|
||||
{ name: "versioned_fn", version: 2 },
|
||||
{ name: "old_fn", version: 0x8003 },
|
||||
{ name: "weak_obj", binding: 2, type: 1 },
|
||||
{ name: "ifunc_sym", type: 10 },
|
||||
{ name: "protected_fn", visibility: 3 },
|
||||
{ name: "hidden_fn", visibility: 2 },
|
||||
{ name: "undefined_fn", section: 0 },
|
||||
{ name: "local_fn", binding: 0 },
|
||||
{ name: "local_version_fn", version: 0 },
|
||||
];
|
||||
|
||||
it("lists defined default and protected globals with their versions", () => {
|
||||
const result = parse(
|
||||
elfExportImage({
|
||||
symbols,
|
||||
versionDefinitions: ["libfixture.so", "VER_1", "VER_0"],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
format: "elf",
|
||||
bits: 64,
|
||||
byte_order: "little",
|
||||
architecture: "amd64",
|
||||
coverage: { status: "complete", examined_entries: 10, exports: 6 },
|
||||
issues: [],
|
||||
});
|
||||
expect(
|
||||
result.format === "elf" &&
|
||||
result.exports.map(({ name, type, binding, version }) => ({
|
||||
name,
|
||||
type,
|
||||
binding,
|
||||
version,
|
||||
})),
|
||||
).toEqual([
|
||||
{ name: "plain_fn", type: "function", binding: "global", version: null },
|
||||
{
|
||||
name: "versioned_fn",
|
||||
type: "function",
|
||||
binding: "global",
|
||||
version: { index: 2, name: "VER_1", hidden: false },
|
||||
},
|
||||
{
|
||||
name: "old_fn",
|
||||
type: "function",
|
||||
binding: "global",
|
||||
version: { index: 3, name: "VER_0", hidden: true },
|
||||
},
|
||||
{ name: "weak_obj", type: "object", binding: "weak", version: null },
|
||||
{ name: "ifunc_sym", type: "ifunc", binding: "global", version: null },
|
||||
{
|
||||
name: "protected_fn",
|
||||
type: "function",
|
||||
binding: "global",
|
||||
version: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("reads a big-endian ELF32 table", () => {
|
||||
const result = parse(
|
||||
elfExportImage({
|
||||
bits: 32,
|
||||
little: false,
|
||||
machine: 20,
|
||||
symbols: [{ name: "be_fn", value: 0x10000n, size: 8n }],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
bits: 32,
|
||||
byte_order: "big",
|
||||
architecture: "ppc",
|
||||
exports: [{ name: "be_fn", value: "0x10000", size: "8" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("marks versions unknown when the version table does not fit", () => {
|
||||
const result = parse(
|
||||
elfExportImage({
|
||||
symbols: [{ name: "a" }, { name: "b", version: 2 }],
|
||||
versionDefinitions: ["lib.so", "V1"],
|
||||
truncatedVersions: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
result.format === "elf" && result.exports.map(({ version }) => version),
|
||||
).toEqual(["unknown", "unknown"]);
|
||||
expect(result.coverage.status).toBe("partial");
|
||||
expect(result.issues).toMatchObject([{ code: "version_unresolved" }]);
|
||||
});
|
||||
|
||||
it("reports one issue per undefined version index", () => {
|
||||
const result = parse(
|
||||
elfExportImage({
|
||||
symbols: [
|
||||
{ name: "a", version: 7 },
|
||||
{ name: "b", version: 7 },
|
||||
],
|
||||
versionDefinitions: ["lib.so"],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
result.format === "elf" && result.exports.map(({ version }) => version),
|
||||
).toEqual([
|
||||
{ index: 7, name: null, hidden: false },
|
||||
{ index: 7, name: null, hidden: false },
|
||||
]);
|
||||
expect(result.issues).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("counts a symbol whose name is outside the string table", () => {
|
||||
const result = parse(
|
||||
elfExportImage({
|
||||
symbols: [{ name: "kept" }, { name: "lost", nameOffset: 0x7000 }],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.exports.map(({ name }) => name)).toEqual(["kept"]);
|
||||
expect(result.issues).toMatchObject([{ code: "name_unreadable" }]);
|
||||
expect(result.coverage.status).toBe("partial");
|
||||
});
|
||||
|
||||
it("separates no dynamic symbols from unread section headers", () => {
|
||||
expect(
|
||||
parse(elfExportImage({ symbols: [], dynamicSymbols: false })),
|
||||
).toMatchObject({ exports: [], coverage: { status: "complete" } });
|
||||
expect(
|
||||
parse(elfExportImage({ symbols: [], sectionHeaders: false })),
|
||||
).toMatchObject({
|
||||
exports: [],
|
||||
coverage: {
|
||||
status: "unsupported",
|
||||
reason: expect.stringContaining("no section headers"),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses formats other than PE and ELF", () => {
|
||||
const macho = Buffer.alloc(64);
|
||||
macho.writeUInt32LE(0xfeedfacf, 0);
|
||||
expect(() => parseNativeExports(macho, artifact)).toThrow(
|
||||
BinaryFormatFailure,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,468 @@
|
||||
import {
|
||||
BinaryFormatFailure,
|
||||
BinaryReader,
|
||||
BinaryResourceFailure,
|
||||
mappedFileOffset,
|
||||
} from "../metadata/BinaryContainer.js";
|
||||
import { readElfImage } from "../metadata/ElfImage.js";
|
||||
import { readElfString, readElfSymbol } from "../metadata/ElfSymbol.js";
|
||||
import {
|
||||
readNativeDirectoryFromBuffer,
|
||||
type NativeSection,
|
||||
} from "../metadata/NativeSectionDirectory.js";
|
||||
import { readPeImage } from "../metadata/PeImage.js";
|
||||
import {
|
||||
MAX_NATIVE_EXPORT_ROWS,
|
||||
type ElfExport,
|
||||
type NativeExportIssue,
|
||||
type NativeExports,
|
||||
type PeExport,
|
||||
} from "../../domain/native/nativeExports.js";
|
||||
|
||||
type Artifact = NativeExports["artifact"];
|
||||
|
||||
const SHT_STRTAB = 3;
|
||||
const SHT_DYNSYM = 11;
|
||||
const SHT_GNU_VERDEF = 0x6ffffffd;
|
||||
const SHT_GNU_VERSYM = 0x6fffffff;
|
||||
const PE_EXPORT_DIRECTORY = 0;
|
||||
const PE_EXPORT_DIRECTORY_BYTES = 40;
|
||||
const MAXIMUM_NAME_BYTES = 4096;
|
||||
|
||||
const hex = (value: bigint | number) => `0x${value.toString(16)}`;
|
||||
|
||||
const rowBudget = (count: number, label: string): void => {
|
||||
if (count > MAX_NATIVE_EXPORT_ROWS)
|
||||
throw new BinaryResourceFailure(
|
||||
"structure",
|
||||
MAX_NATIVE_EXPORT_ROWS,
|
||||
`${label} declares ${String(count)} entries, above the ${String(MAX_NATIVE_EXPORT_ROWS)}-entry export inventory allowance`,
|
||||
);
|
||||
};
|
||||
|
||||
const coverage = (
|
||||
issues: readonly NativeExportIssue[],
|
||||
examined: number,
|
||||
exports: number,
|
||||
): NativeExports["coverage"] =>
|
||||
issues.length === 0
|
||||
? { status: "complete", reason: null, examined_entries: examined, exports }
|
||||
: {
|
||||
status: "partial",
|
||||
reason: `${String(issues.length)} export table issue(s) left entries unreported; see issues`,
|
||||
examined_entries: examined,
|
||||
exports,
|
||||
};
|
||||
|
||||
const failureMessage = (cause: unknown): string => {
|
||||
if (cause instanceof BinaryFormatFailure) return cause.message;
|
||||
throw cause;
|
||||
};
|
||||
|
||||
const PE_LIMITATIONS = [
|
||||
"Rows reproduce the PE export directory; they do not establish that an export is reachable, safe to call or loaded at runtime.",
|
||||
"Forwarded exports resolve in another DLL, which is not opened; ordinal-only exports have no name in this image.",
|
||||
];
|
||||
|
||||
/** Read the PE export directory: every used address-table slot and its names. */
|
||||
const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
||||
const image = readPeImage(bytes);
|
||||
const base = image.image_base ?? 0n;
|
||||
const directory = image.pe_data_directories?.[PE_EXPORT_DIRECTORY];
|
||||
const result = {
|
||||
artifact,
|
||||
format: "pe" as const,
|
||||
architecture: image.architecture,
|
||||
bits: image.bits,
|
||||
};
|
||||
if (directory === undefined || directory.virtual_address === 0)
|
||||
return {
|
||||
...result,
|
||||
export_directory: null,
|
||||
exports: [],
|
||||
coverage: coverage([], 0, 0),
|
||||
issues: [],
|
||||
limitations: [
|
||||
"The image declares no export directory, so it exports nothing by name or ordinal.",
|
||||
...PE_LIMITATIONS,
|
||||
],
|
||||
};
|
||||
const reader = new BinaryReader(bytes, true);
|
||||
const map = (rva: number, size: number): number =>
|
||||
mappedFileOffset(
|
||||
image.mappings,
|
||||
base + BigInt(rva),
|
||||
size,
|
||||
image.zero_fills,
|
||||
);
|
||||
const cString = (rva: number): string => {
|
||||
const start = map(rva, 1);
|
||||
const end = bytes
|
||||
.subarray(start, start + MAXIMUM_NAME_BYTES + 1)
|
||||
.indexOf(0);
|
||||
if (end <= 0)
|
||||
throw new BinaryFormatFailure(
|
||||
"malformed",
|
||||
`Export name at RVA ${hex(rva)} is empty, unterminated or longer than ${String(MAXIMUM_NAME_BYTES)} bytes`,
|
||||
);
|
||||
// The whole string must stay inside one file-backed mapping.
|
||||
map(rva, end + 1);
|
||||
return bytes.toString("latin1", start, start + end);
|
||||
};
|
||||
|
||||
const header = map(directory.virtual_address, PE_EXPORT_DIRECTORY_BYTES);
|
||||
const ordinalBase = reader.u32(header + 16);
|
||||
const functionCount = reader.u32(header + 20);
|
||||
const nameCount = reader.u32(header + 24);
|
||||
rowBudget(functionCount, "PE export address table");
|
||||
if (ordinalBase + functionCount > 0x10000)
|
||||
throw new BinaryFormatFailure(
|
||||
"malformed",
|
||||
"PE export ordinal base and address-table size exceed 16-bit ordinals",
|
||||
);
|
||||
rowBudget(nameCount, "PE export name table");
|
||||
const functions =
|
||||
functionCount === 0 ? 0 : map(reader.u32(header + 28), functionCount * 4);
|
||||
const names =
|
||||
nameCount === 0 ? 0 : map(reader.u32(header + 32), nameCount * 4);
|
||||
const ordinals =
|
||||
nameCount === 0 ? 0 : map(reader.u32(header + 36), nameCount * 2);
|
||||
const issues: NativeExportIssue[] = [];
|
||||
let dllName: string | null = null;
|
||||
try {
|
||||
dllName = cString(reader.u32(header + 12));
|
||||
} catch (cause) {
|
||||
issues.push({
|
||||
code: "name_unreadable",
|
||||
location: hex(reader.u32(header + 12)),
|
||||
message: `Export directory DLL name: ${failureMessage(cause)}`,
|
||||
});
|
||||
}
|
||||
|
||||
const namesByFunction = new Map<number, string[]>();
|
||||
const namedFunctions = new Set<number>();
|
||||
for (let index = 0; index < nameCount; index++) {
|
||||
const functionIndex = reader.u16(ordinals + index * 2);
|
||||
const nameRva = reader.u32(names + index * 4);
|
||||
if (functionIndex >= functionCount) {
|
||||
issues.push({
|
||||
code: "malformed_entry",
|
||||
location: hex(nameRva),
|
||||
message: `Export name ${String(index)} selects address-table slot ${String(functionIndex)}, outside its ${String(functionCount)} slots`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
namedFunctions.add(functionIndex);
|
||||
try {
|
||||
const list = namesByFunction.get(functionIndex) ?? [];
|
||||
list.push(cString(nameRva));
|
||||
namesByFunction.set(functionIndex, list);
|
||||
} catch (cause) {
|
||||
issues.push({
|
||||
code: "name_unreadable",
|
||||
location: hex(nameRva),
|
||||
message: failureMessage(cause),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const forwarderEnd = directory.virtual_address + directory.size;
|
||||
const exports: PeExport[] = [];
|
||||
for (let index = 0; index < functionCount; index++) {
|
||||
const rva = reader.u32(functions + index * 4);
|
||||
// A zero slot is an unused ordinal, not an export.
|
||||
if (rva === 0) continue;
|
||||
const ordinal = ordinalBase + index;
|
||||
let target: Pick<PeExport, "address" | "forwarder">;
|
||||
if (rva >= directory.virtual_address && rva < forwarderEnd) {
|
||||
try {
|
||||
target = { address: null, forwarder: cString(rva) };
|
||||
} catch (cause) {
|
||||
issues.push({
|
||||
code: "malformed_entry",
|
||||
location: hex(rva),
|
||||
message: `Forwarder of ordinal ${String(ordinal)}: ${failureMessage(cause)}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
} else target = { address: hex(base + BigInt(rva)), forwarder: null };
|
||||
const exportNames = namesByFunction.get(index);
|
||||
if (exportNames !== undefined)
|
||||
for (const name of exportNames)
|
||||
exports.push({ name, ordinal, ...target });
|
||||
// A slot whose only name was unreadable is reported through its issue.
|
||||
else if (!namedFunctions.has(index))
|
||||
exports.push({ name: null, ordinal, ...target });
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
export_directory: {
|
||||
rva: directory.virtual_address,
|
||||
size: directory.size,
|
||||
dll_name: dllName,
|
||||
ordinal_base: ordinalBase,
|
||||
timestamp: reader.u32(header + 4),
|
||||
},
|
||||
exports,
|
||||
coverage: coverage(issues, functionCount, exports.length),
|
||||
issues,
|
||||
limitations: PE_LIMITATIONS,
|
||||
};
|
||||
};
|
||||
|
||||
const ELF_LIMITATIONS = [
|
||||
"Rows list defined default- or protected-visibility global, weak and unique dynamic symbols; local, hidden, internal and undefined symbols are not exports.",
|
||||
"A hidden symbol version is not the default version that a by-name dlsym lookup selects.",
|
||||
"Rows reproduce the dynamic symbol table; they do not establish that a symbol is reachable, safe to call or loaded at runtime.",
|
||||
];
|
||||
|
||||
const ELF_TYPES: Readonly<Record<number, ElfExport["type"]>> = {
|
||||
1: "object",
|
||||
2: "function",
|
||||
6: "tls",
|
||||
10: "ifunc",
|
||||
};
|
||||
const ELF_BINDINGS: Readonly<Record<number, ElfExport["binding"]>> = {
|
||||
1: "global",
|
||||
2: "weak",
|
||||
10: "unique",
|
||||
};
|
||||
|
||||
/** Version definitions by index, read from `.gnu.version_d`. */
|
||||
const readVersionDefinitions = (
|
||||
reader: BinaryReader,
|
||||
sections: readonly NativeSection[],
|
||||
issues: NativeExportIssue[],
|
||||
): Map<number, string> => {
|
||||
const definitions = new Map<number, string>();
|
||||
const verdef = sections.find(({ type }) => type === SHT_GNU_VERDEF);
|
||||
if (verdef === undefined) return definitions;
|
||||
try {
|
||||
const strings = sections[verdef.link];
|
||||
if (strings?.type !== SHT_STRTAB)
|
||||
throw new BinaryFormatFailure(
|
||||
"malformed",
|
||||
"Version definitions do not link to a string table",
|
||||
);
|
||||
const names = reader.fileRange(
|
||||
strings.offset,
|
||||
strings.size,
|
||||
"Version strings",
|
||||
);
|
||||
const nameBytes = reader.bytes.subarray(
|
||||
names.offset,
|
||||
names.offset + names.size,
|
||||
);
|
||||
const table = reader.fileRange(
|
||||
verdef.offset,
|
||||
verdef.size,
|
||||
"Version definitions",
|
||||
);
|
||||
let offset = 0;
|
||||
for (let index = 0; index < verdef.info; index++) {
|
||||
if (offset + 20 > table.size)
|
||||
throw new BinaryFormatFailure(
|
||||
"malformed",
|
||||
"Version definition chain leaves its section",
|
||||
);
|
||||
const at = table.offset + offset;
|
||||
const versionIndex = reader.u16(at + 4) & 0x7fff;
|
||||
const aux = reader.u32(at + 12);
|
||||
const next = reader.u32(at + 16);
|
||||
if (aux < 20 || offset + aux + 8 > table.size)
|
||||
throw new BinaryFormatFailure(
|
||||
"malformed",
|
||||
"Version definition name entry leaves its section",
|
||||
);
|
||||
definitions.set(
|
||||
versionIndex,
|
||||
readElfString(nameBytes, reader.u32(at + aux)),
|
||||
);
|
||||
if (next === 0) break;
|
||||
offset += next;
|
||||
}
|
||||
} catch (cause) {
|
||||
issues.push({
|
||||
code: "version_unresolved",
|
||||
location: hex(verdef.offset),
|
||||
message: `Version definitions: ${failureMessage(cause)}`,
|
||||
});
|
||||
}
|
||||
return definitions;
|
||||
};
|
||||
|
||||
/** Read defined exported entries of `.dynsym` with their symbol versions. */
|
||||
const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
||||
const image = readElfImage(bytes);
|
||||
const directory = readNativeDirectoryFromBuffer(bytes, "elf");
|
||||
const reader = new BinaryReader(bytes, directory.little);
|
||||
const result = {
|
||||
artifact,
|
||||
format: "elf" as const,
|
||||
architecture: image.architecture,
|
||||
bits: image.bits,
|
||||
byte_order: image.byte_order,
|
||||
};
|
||||
const { sections } = directory;
|
||||
if (sections.length === 0)
|
||||
return {
|
||||
...result,
|
||||
exports: [],
|
||||
coverage: {
|
||||
status: "unsupported",
|
||||
reason:
|
||||
"The ELF has no section headers; enumerating dynamic symbols from the dynamic segment is not implemented",
|
||||
examined_entries: 0,
|
||||
exports: 0,
|
||||
},
|
||||
issues: [],
|
||||
limitations: ELF_LIMITATIONS,
|
||||
};
|
||||
const dynsym = sections.find(({ type }) => type === SHT_DYNSYM);
|
||||
if (dynsym === undefined)
|
||||
return {
|
||||
...result,
|
||||
exports: [],
|
||||
coverage: coverage([], 0, 0),
|
||||
issues: [],
|
||||
limitations: [
|
||||
"The ELF has no dynamic symbol table, so it exports no dynamic symbols.",
|
||||
...ELF_LIMITATIONS,
|
||||
],
|
||||
};
|
||||
const width = directory.bits === 64 ? 24 : 16;
|
||||
const strings = sections[dynsym.link];
|
||||
if (
|
||||
dynsym.entry_size !== BigInt(width) ||
|
||||
dynsym.size % BigInt(width) !== 0n ||
|
||||
strings?.type !== SHT_STRTAB
|
||||
)
|
||||
throw new BinaryFormatFailure(
|
||||
"malformed",
|
||||
"ELF dynamic symbol table entry size, length or string-table link is invalid",
|
||||
);
|
||||
const count = Number(dynsym.size / BigInt(width));
|
||||
rowBudget(count, "ELF dynamic symbol table");
|
||||
const table = reader.table(
|
||||
dynsym.offset,
|
||||
count,
|
||||
width,
|
||||
"ELF dynamic symbols",
|
||||
);
|
||||
const nameRange = reader.fileRange(
|
||||
strings.offset,
|
||||
strings.size,
|
||||
"ELF dynamic strings",
|
||||
);
|
||||
const names = bytes.subarray(
|
||||
nameRange.offset,
|
||||
nameRange.offset + nameRange.size,
|
||||
);
|
||||
const issues: NativeExportIssue[] = [];
|
||||
const versym = sections.find(
|
||||
({ type, link }) => type === SHT_GNU_VERSYM && link === dynsym.index,
|
||||
);
|
||||
let versions: number | null = null;
|
||||
if (versym !== undefined) {
|
||||
if (versym.size === BigInt(count * 2))
|
||||
versions = reader.table(versym.offset, count, 2, "ELF symbol versions");
|
||||
else
|
||||
issues.push({
|
||||
code: "version_unresolved",
|
||||
location: hex(versym.offset),
|
||||
message: `Symbol version table has ${String(versym.size)} bytes for ${String(count)} symbols`,
|
||||
});
|
||||
}
|
||||
const definitions = readVersionDefinitions(reader, sections, issues);
|
||||
const unresolvedVersions = new Set<number>();
|
||||
const exports: ElfExport[] = [];
|
||||
for (let index = 1; index < count; index++) {
|
||||
const at = table + index * width;
|
||||
const symbol = readElfSymbol(
|
||||
bytes.subarray(at, at + width),
|
||||
directory.bits,
|
||||
directory.little,
|
||||
);
|
||||
const binding = ELF_BINDINGS[symbol.binding];
|
||||
const visibility = symbol.other & 3;
|
||||
// Undefined, local and hidden/internal symbols are not exports.
|
||||
if (
|
||||
binding === undefined ||
|
||||
symbol.section_index === 0 ||
|
||||
(visibility !== 0 && visibility !== 3)
|
||||
)
|
||||
continue;
|
||||
let version: ElfExport["version"] = null;
|
||||
if (versym !== undefined && versions === null) version = "unknown";
|
||||
else if (versions !== null) {
|
||||
const raw = reader.u16(versions + index * 2);
|
||||
const versionIndex = raw & 0x7fff;
|
||||
if (versionIndex === 0) continue;
|
||||
if (versionIndex >= 2) {
|
||||
const name = definitions.get(versionIndex) ?? null;
|
||||
if (name === null && !unresolvedVersions.has(versionIndex)) {
|
||||
unresolvedVersions.add(versionIndex);
|
||||
issues.push({
|
||||
code: "version_unresolved",
|
||||
location: hex(symbol.value),
|
||||
message: `Version index ${String(versionIndex)}, first used by symbol ${String(index)}, is named by no version definition`,
|
||||
});
|
||||
}
|
||||
version = {
|
||||
index: versionIndex,
|
||||
name: name === "" ? null : name,
|
||||
hidden: (raw & 0x8000) !== 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
let name: string;
|
||||
try {
|
||||
name = readElfString(names, symbol.name_offset);
|
||||
} catch (cause) {
|
||||
issues.push({
|
||||
code: "name_unreadable",
|
||||
location: hex(symbol.value),
|
||||
message: `Dynamic symbol ${String(index)}: ${failureMessage(cause)}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (name === "") {
|
||||
issues.push({
|
||||
code: "name_unreadable",
|
||||
location: hex(symbol.value),
|
||||
message: `Defined global dynamic symbol ${String(index)} has an empty name`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
exports.push({
|
||||
name,
|
||||
value: hex(symbol.value),
|
||||
size: symbol.size.toString(),
|
||||
type: ELF_TYPES[symbol.type] ?? "other",
|
||||
binding,
|
||||
version,
|
||||
});
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
exports,
|
||||
coverage: coverage(issues, count - 1, exports.length),
|
||||
issues,
|
||||
limitations: ELF_LIMITATIONS,
|
||||
};
|
||||
};
|
||||
|
||||
/** Static export inventory of one PE or ELF image; other formats are unsupported. */
|
||||
export const parseNativeExports = (
|
||||
bytes: Buffer,
|
||||
artifact: Artifact,
|
||||
): NativeExports => {
|
||||
if (bytes.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])))
|
||||
return parseElfExports(bytes, artifact);
|
||||
if (bytes.subarray(0, 2).equals(Buffer.from("MZ")))
|
||||
return parsePeExports(bytes, artifact);
|
||||
throw new BinaryFormatFailure(
|
||||
"unsupported",
|
||||
"Only PE and ELF images are supported; inspect Mach-O exports with inspect_macho",
|
||||
);
|
||||
};
|
||||
@@ -95,6 +95,11 @@ export interface BinaryContainer {
|
||||
readonly byte_order: "little" | "big";
|
||||
/** Declared PE preferred base, when available. */
|
||||
readonly image_base?: bigint;
|
||||
/** PE optional-header data directories, indexed as declared. */
|
||||
readonly pe_data_directories?: readonly {
|
||||
readonly virtual_address: number;
|
||||
readonly size: number;
|
||||
}[];
|
||||
/** Mach-O load-command observation, when available. */
|
||||
readonly macho_chained_fixups?: boolean;
|
||||
readonly mappings: readonly FileMapping[];
|
||||
|
||||
@@ -82,6 +82,10 @@ export const readPeImage = (bytes: Buffer): BinaryContainer => {
|
||||
"malformed",
|
||||
"PE data directories extend beyond the optional header",
|
||||
);
|
||||
const dataDirectories = Array.from({ length: directories }, (_, index) => {
|
||||
const entry = optional + directoriesOffset + index * 8;
|
||||
return { virtual_address: reader.u32(entry), size: reader.u32(entry + 4) };
|
||||
});
|
||||
const imageBase = reader.word(optional + (bits === 64 ? 24 : 28), bits);
|
||||
const count = reader.u16(pe + 6);
|
||||
const start = reader.table(
|
||||
@@ -152,6 +156,7 @@ export const readPeImage = (bytes: Buffer): BinaryContainer => {
|
||||
return {
|
||||
format: "pe",
|
||||
image_base: imageBase,
|
||||
pe_data_directories: dataDirectories,
|
||||
architecture: names.get(machine) ?? `pe-machine-${machine}`,
|
||||
bits,
|
||||
byte_order: "little",
|
||||
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
/** One PE export-address-table slot: code RVA, forwarder text, or unused. */
|
||||
export type PeFixtureFunction =
|
||||
| { readonly rva: number }
|
||||
| { readonly forwarder: string }
|
||||
| null;
|
||||
|
||||
const SECTION_RVA = 0x1000;
|
||||
const SECTION_OFFSET = 0x400;
|
||||
const SECTION_BYTES = 0x400;
|
||||
|
||||
/**
|
||||
* Minimal PE32/PE32+ image whose only section holds an export directory at
|
||||
* RVA 0x1000. Name entries point at `functions` by index.
|
||||
*/
|
||||
export const peExportImage = (options: {
|
||||
readonly bits?: 32 | 64;
|
||||
readonly ordinalBase?: number;
|
||||
readonly functions?: readonly PeFixtureFunction[];
|
||||
readonly names?: readonly { readonly name: string; readonly index: number }[];
|
||||
/** Replace one name's RVA, e.g. with an unmapped one. */
|
||||
readonly nameRvaOverride?: {
|
||||
readonly position: number;
|
||||
readonly rva: number;
|
||||
};
|
||||
readonly exportDirectory?: boolean;
|
||||
readonly declaredFunctions?: number;
|
||||
}): Buffer => {
|
||||
const bits = options.bits ?? 64;
|
||||
const bytes = Buffer.alloc(SECTION_OFFSET + SECTION_BYTES);
|
||||
const wide = bits === 64;
|
||||
bytes.write("MZ", 0, "ascii");
|
||||
bytes.writeUInt32LE(128, 60);
|
||||
bytes.write("PE\0\0", 128, "ascii");
|
||||
bytes.writeUInt16LE(wide ? 0x8664 : 0x14c, 132);
|
||||
bytes.writeUInt16LE(1, 134);
|
||||
const directoriesAt = wide ? 112 : 96;
|
||||
const optionalSize = directoriesAt + 16 * 8;
|
||||
bytes.writeUInt16LE(optionalSize, 148);
|
||||
const optional = 152;
|
||||
bytes.writeUInt16LE(wide ? 0x20b : 0x10b, optional);
|
||||
if (wide) bytes.writeBigUInt64LE(0x180000000n, optional + 24);
|
||||
else bytes.writeUInt32LE(0x10000000, optional + 28);
|
||||
bytes.writeUInt32LE(16, optional + directoriesAt - 4);
|
||||
const section = optional + optionalSize;
|
||||
bytes.write(".edata", section, "ascii");
|
||||
bytes.writeUInt32LE(SECTION_BYTES, section + 8);
|
||||
bytes.writeUInt32LE(SECTION_RVA, section + 12);
|
||||
bytes.writeUInt32LE(SECTION_BYTES, section + 16);
|
||||
bytes.writeUInt32LE(SECTION_OFFSET, section + 20);
|
||||
bytes.writeUInt32LE(0x40000040, section + 36);
|
||||
if (options.exportDirectory === false) return bytes;
|
||||
|
||||
const at = (rva: number) => rva - SECTION_RVA + SECTION_OFFSET;
|
||||
const functions = options.functions ?? [];
|
||||
const names = options.names ?? [];
|
||||
// Directory, address table, name table, ordinals, then strings; forwarders
|
||||
// stay inside the 0x200-byte declared directory range.
|
||||
const eat = SECTION_RVA + 0x40;
|
||||
const nameTable = SECTION_RVA + 0x80;
|
||||
const ordinalTable = SECTION_RVA + 0xc0;
|
||||
let forwarderText = SECTION_RVA + 0x100;
|
||||
let text = SECTION_RVA + 0x200;
|
||||
const string = (value: string, inside: boolean): number => {
|
||||
const rva = inside ? forwarderText : text;
|
||||
bytes.write(`${value}\0`, at(rva), "latin1");
|
||||
if (inside) forwarderText += value.length + 1;
|
||||
else text += value.length + 1;
|
||||
return rva;
|
||||
};
|
||||
bytes.writeUInt32LE(SECTION_RVA, optional + directoriesAt);
|
||||
bytes.writeUInt32LE(0x200, optional + directoriesAt + 4);
|
||||
const directory = at(SECTION_RVA);
|
||||
bytes.writeUInt32LE(0x5f5e0ff, directory + 4);
|
||||
bytes.writeUInt32LE(string("fixture.dll", false), directory + 12);
|
||||
bytes.writeUInt32LE(options.ordinalBase ?? 1, directory + 16);
|
||||
bytes.writeUInt32LE(
|
||||
options.declaredFunctions ?? functions.length,
|
||||
directory + 20,
|
||||
);
|
||||
bytes.writeUInt32LE(names.length, directory + 24);
|
||||
bytes.writeUInt32LE(eat, directory + 28);
|
||||
bytes.writeUInt32LE(nameTable, directory + 32);
|
||||
bytes.writeUInt32LE(ordinalTable, directory + 36);
|
||||
functions.forEach((entry, index) => {
|
||||
const value =
|
||||
entry === null
|
||||
? 0
|
||||
: "rva" in entry
|
||||
? entry.rva
|
||||
: string(entry.forwarder, true);
|
||||
bytes.writeUInt32LE(value, at(eat) + index * 4);
|
||||
});
|
||||
names.forEach(({ name, index }, position) => {
|
||||
const rva =
|
||||
options.nameRvaOverride?.position === position
|
||||
? options.nameRvaOverride.rva
|
||||
: string(name, false);
|
||||
bytes.writeUInt32LE(rva, at(nameTable) + position * 4);
|
||||
bytes.writeUInt16LE(index, at(ordinalTable) + position * 2);
|
||||
});
|
||||
return bytes;
|
||||
};
|
||||
|
||||
/** One `.dynsym` entry; `version` is its raw `.gnu.version` half-word. */
|
||||
export interface ElfFixtureSymbol {
|
||||
readonly name: string;
|
||||
readonly binding?: number;
|
||||
readonly type?: number;
|
||||
readonly visibility?: number;
|
||||
readonly section?: number;
|
||||
readonly value?: bigint;
|
||||
readonly size?: bigint;
|
||||
readonly version?: number;
|
||||
/** Replace the symbol's string-table offset. */
|
||||
readonly nameOffset?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal ELF shared object with `.dynsym`, `.dynstr`, `.gnu.version` and
|
||||
* `.gnu.version_d`. Version definitions are indexed from 1 in order.
|
||||
*/
|
||||
export const elfExportImage = (options: {
|
||||
readonly bits?: 32 | 64;
|
||||
readonly little?: boolean;
|
||||
readonly machine?: number;
|
||||
readonly symbols: readonly ElfFixtureSymbol[];
|
||||
readonly versionDefinitions?: readonly string[];
|
||||
/** Declare a `.gnu.version` table that is one entry short. */
|
||||
readonly truncatedVersions?: boolean;
|
||||
readonly sectionHeaders?: boolean;
|
||||
readonly dynamicSymbols?: boolean;
|
||||
}): Buffer => {
|
||||
const bits = options.bits ?? 64;
|
||||
const little = options.little ?? true;
|
||||
const wide = bits === 64;
|
||||
const bytes = Buffer.alloc(0x1000);
|
||||
const u16 = (offset: number, value: number) =>
|
||||
little
|
||||
? bytes.writeUInt16LE(value, offset)
|
||||
: bytes.writeUInt16BE(value, offset);
|
||||
const u32 = (offset: number, value: number) =>
|
||||
little
|
||||
? bytes.writeUInt32LE(value, offset)
|
||||
: bytes.writeUInt32BE(value, offset);
|
||||
const word = (offset: number, value: bigint) => {
|
||||
if (!wide) u32(offset, Number(value));
|
||||
else if (little) bytes.writeBigUInt64LE(value, offset);
|
||||
else bytes.writeBigUInt64BE(value, offset);
|
||||
};
|
||||
bytes.set([0x7f, 0x45, 0x4c, 0x46, wide ? 2 : 1, little ? 1 : 2, 1]);
|
||||
u16(16, 3);
|
||||
u16(18, options.machine ?? (wide ? 62 : 40));
|
||||
u32(20, 1);
|
||||
const headerSize = wide ? 64 : 52;
|
||||
u16(wide ? 52 : 40, headerSize);
|
||||
u16(wide ? 54 : 42, wide ? 56 : 32);
|
||||
u16(wide ? 58 : 46, wide ? 64 : 40);
|
||||
if (options.sectionHeaders === false) return bytes;
|
||||
|
||||
const dynstr = ["\0"];
|
||||
let dynstrBytes = 1;
|
||||
const intern = (value: string): number => {
|
||||
const offset = dynstrBytes;
|
||||
dynstr.push(`${value}\0`);
|
||||
dynstrBytes += value.length + 1;
|
||||
return offset;
|
||||
};
|
||||
const definitions = options.versionDefinitions ?? [];
|
||||
const definitionNames = definitions.map(intern);
|
||||
const symbolWidth = wide ? 24 : 16;
|
||||
const symbols = [null, ...options.symbols];
|
||||
const dynsymAt = 0x200;
|
||||
symbols.forEach((symbol, index) => {
|
||||
if (symbol === null) return;
|
||||
const at = dynsymAt + index * symbolWidth;
|
||||
const info = ((symbol.binding ?? 1) << 4) | (symbol.type ?? 2);
|
||||
const other = symbol.visibility ?? 0;
|
||||
const name = symbol.nameOffset ?? intern(symbol.name);
|
||||
u32(at, name);
|
||||
if (wide) {
|
||||
bytes[at + 4] = info;
|
||||
bytes[at + 5] = other;
|
||||
u16(at + 6, symbol.section ?? 1);
|
||||
word(at + 8, symbol.value ?? BigInt(0x1000 + index * 16));
|
||||
word(at + 16, symbol.size ?? 16n);
|
||||
} else {
|
||||
word(at + 4, symbol.value ?? BigInt(0x1000 + index * 16));
|
||||
word(at + 8, symbol.size ?? 16n);
|
||||
bytes[at + 12] = info;
|
||||
bytes[at + 13] = other;
|
||||
u16(at + 14, symbol.section ?? 1);
|
||||
}
|
||||
});
|
||||
const dynstrAt = 0x600;
|
||||
Buffer.from(dynstr.join(""), "latin1").copy(bytes, dynstrAt);
|
||||
const versymAt = 0x900;
|
||||
symbols.forEach((symbol, index) =>
|
||||
u16(versymAt + index * 2, symbol?.version ?? (index === 0 ? 0 : 1)),
|
||||
);
|
||||
const verdefAt = 0xa00;
|
||||
definitions.forEach((_, index) => {
|
||||
const at = verdefAt + index * 28;
|
||||
u16(at, 1);
|
||||
u16(at + 2, index === 0 ? 1 : 0);
|
||||
u16(at + 4, index + 1);
|
||||
u16(at + 6, 1);
|
||||
u32(at + 12, 20);
|
||||
u32(at + 16, index === definitions.length - 1 ? 0 : 28);
|
||||
u32(at + 20, definitionNames[index]!);
|
||||
});
|
||||
const shstrtab =
|
||||
"\0.dynsym\0.dynstr\0.gnu.version\0.gnu.version_d\0.shstrtab\0";
|
||||
const shstrtabAt = 0xc00;
|
||||
bytes.write(shstrtab, shstrtabAt, "latin1");
|
||||
const sections: (readonly [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
])[] = [
|
||||
// name, type, offset, size, link, info, entsize
|
||||
[0, 0, 0, 0, 0, 0, 0],
|
||||
...(options.dynamicSymbols === false
|
||||
? []
|
||||
: ([
|
||||
[1, 11, dynsymAt, symbols.length * symbolWidth, 2, 1, symbolWidth],
|
||||
[9, 3, dynstrAt, dynstrBytes, 0, 0, 0],
|
||||
[
|
||||
17,
|
||||
0x6fffffff,
|
||||
versymAt,
|
||||
(symbols.length - (options.truncatedVersions === true ? 1 : 0)) * 2,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
],
|
||||
[
|
||||
30,
|
||||
0x6ffffffd,
|
||||
verdefAt,
|
||||
definitions.length * 28,
|
||||
2,
|
||||
definitions.length,
|
||||
0,
|
||||
],
|
||||
] as const)),
|
||||
[45, 3, shstrtabAt, shstrtab.length, 0, 0, 0],
|
||||
];
|
||||
const sectionAt = 0xd00;
|
||||
const sectionWidth = wide ? 64 : 40;
|
||||
sections.forEach(([name, type, offset, size, link, info, entsize], index) => {
|
||||
const at = sectionAt + index * sectionWidth;
|
||||
u32(at, name);
|
||||
u32(at + 4, type);
|
||||
word(at + (wide ? 24 : 16), BigInt(offset));
|
||||
word(at + (wide ? 32 : 20), BigInt(size));
|
||||
u32(at + (wide ? 40 : 24), link);
|
||||
u32(at + (wide ? 44 : 28), info);
|
||||
word(at + (wide ? 56 : 36), BigInt(entsize));
|
||||
});
|
||||
word(wide ? 40 : 32, BigInt(sectionAt));
|
||||
u16(wide ? 60 : 48, sections.length);
|
||||
u16(wide ? 62 : 50, sections.length - 1);
|
||||
return bytes;
|
||||
};
|
||||
Reference in new issue
Block a user