mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
Raise through cached unwind plans on x86-64 Linux (#2455)
This commit is contained in:
@@ -21,6 +21,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation |
|
||||
| `json-unions` | cpu | `JSON.parse` into discriminated unions: an event log with eight kinds and nested expression trees with nine, evaluated after the cast |
|
||||
| `keyed-option-writes` | cpu | class methods whose names are also keys of option records written through element accesses: `enabled[name] = true` over a union of literal names and `totals["count"] = n` |
|
||||
| `layered-errors` | cpu | validation errors thrown below a stack of small functions that hold strings and arrays, and caught per entry in a loop: about half of 640,000 entries fail |
|
||||
| `line-reader` | cpu | `readline` over a file stream: 'line' events for a 67 MB access log (every 50th line not ASCII), with a status count, a path tally and byte totals |
|
||||
| `log-lines` | cpu | 200k immediate `console.log`/`process.stdout.write` lines into the runner's pipe |
|
||||
| `log-summary`, `inventory-report` | cpu | the multi-module applications from `benchmarks/builds`, on 300k-line inputs |
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/layered-errors.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// A configuration loader whose validation errors are thrown at the bottom
|
||||
// of a stack of small layers and caught per entry at the top: each layer
|
||||
// holds strings and arrays while the error passes through it.
|
||||
class ConfigError extends Error {
|
||||
readonly key: string;
|
||||
constructor(key: string, message: string) {
|
||||
super(message);
|
||||
this.key = key;
|
||||
}
|
||||
}
|
||||
|
||||
let seed = 4242;
|
||||
function random(): number {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed / 4294967296;
|
||||
}
|
||||
|
||||
function fail(key: string, text: string): never {
|
||||
throw new ConfigError(key, `invalid value "${text}" for ${key}`);
|
||||
}
|
||||
function digits(key: string, text: string): number {
|
||||
let value = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text.charCodeAt(i);
|
||||
if (c < 48 || c > 57) fail(key, text);
|
||||
value = value * 10 + (c - 48);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function parseNumber(key: string, text: string): number {
|
||||
return text.length === 0 ? fail(key, text) : digits(key, text);
|
||||
}
|
||||
function checkRange(key: string, text: string, max: number): number {
|
||||
const value = parseNumber(key, text);
|
||||
return value > max ? fail(key, text) : value;
|
||||
}
|
||||
function coerce(key: string, raw: string, max: number): number {
|
||||
const trimmed = raw.trim();
|
||||
const parts = [key, trimmed];
|
||||
const value = checkRange(key, trimmed, max);
|
||||
return value + parts.length;
|
||||
}
|
||||
function resolveValue(key: string, raw: string, defaults: Map<string, string>): number {
|
||||
const text = raw === "default" ? (defaults.get(key) ?? "") : raw;
|
||||
const label = "value:" + key;
|
||||
return coerce(key, text, 100000) + label.length;
|
||||
}
|
||||
function loadEntry(section: string, entry: string, defaults: Map<string, string>): number {
|
||||
const eq = entry.indexOf("=");
|
||||
const key = section + "." + entry.slice(0, eq);
|
||||
const path = [section, key];
|
||||
return resolveValue(key, entry.slice(eq + 1), defaults) + path.length;
|
||||
}
|
||||
function loadEntryWithContext(section: string, entry: string, defaults: Map<string, string>): number {
|
||||
const context = `${section}/${entry.length}`;
|
||||
return loadEntry(section, entry, defaults) + context.length;
|
||||
}
|
||||
|
||||
function makeEntry(i: number): string {
|
||||
const r = random();
|
||||
const name = "k" + (i % 97);
|
||||
if (r < 0.3) return `${name}=x${i}`;
|
||||
if (r < 0.4) return `${name}=`;
|
||||
if (r < 0.5) return `${name}=${200000 + i}`;
|
||||
if (r < 0.55) return `${name}=default`;
|
||||
return `${name}= ${Math.floor(random() * 90000)} `;
|
||||
}
|
||||
|
||||
const rounds = Number(process.argv[2] ?? "1");
|
||||
const defaults = new Map<string, string>();
|
||||
for (let i = 0; i < 97; i += 2) defaults.set(`net.k${i}`, String(i * 10));
|
||||
const sections = ["net", "db", "cache", "auth"];
|
||||
const entries: string[] = [];
|
||||
for (let i = 0; i < 4000; i++) entries.push(makeEntry(i));
|
||||
|
||||
let loaded = 0;
|
||||
let sum = 0;
|
||||
let failed = 0;
|
||||
const failedKeys = new Map<string, number>();
|
||||
for (let round = 0; round < rounds * 40; round++) {
|
||||
for (const section of sections) {
|
||||
for (const entry of entries) {
|
||||
try {
|
||||
sum += loadEntryWithContext(section, entry, defaults);
|
||||
loaded++;
|
||||
} catch (error) {
|
||||
if (!(error instanceof ConfigError)) throw error;
|
||||
failed++;
|
||||
failedKeys.set(error.key, (failedKeys.get(error.key) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`loaded ${loaded} failed ${failed} sum ${sum} distinct failing keys ${failedKeys.size}`);
|
||||
console.log(`net.k0 failures ${failedKeys.get("net.k0") ?? 0}`);
|
||||
@@ -97,12 +97,22 @@ export class BlockBuilder {
|
||||
if (s === UNWIND_RESUME) {
|
||||
const exception = this.tmp();
|
||||
this.line(`${exception} = load { ptr, i32 }, ptr ${UNWIND_SLOT}`);
|
||||
s = `resume { ptr, i32 } ${exception}`;
|
||||
this.resumeUnwind(exception);
|
||||
return;
|
||||
}
|
||||
this.cur.term = ` ${this.withDebugLocation(s)}`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Ends the block by continuing an unwind with `exception`, the value
|
||||
* of a landing pad (UNWIND_RESUME_DECLARATION). */
|
||||
resumeUnwind(exception: string): void {
|
||||
const token = this.tmp();
|
||||
this.line(`${token} = extractvalue { ptr, i32 } ${exception}, 0`);
|
||||
this.line(`call void @scr_unwind_resume(ptr ${token})`);
|
||||
this.terminate("unreachable");
|
||||
}
|
||||
|
||||
br(label: string, attachment = ""): void {
|
||||
this.terminate(`br label %${label}${attachment}`);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ import { emitIntegerClone, emitIntegerDispatcher } from "./integer-clones.js";
|
||||
import { beginFunctionLoops, LOOPS } from "./loop-state.js";
|
||||
import { runningExtremumUpdates } from "./running-extrema.js";
|
||||
import { findIntegerOrNaNLocals } from "./wrapping-sums.js";
|
||||
import { UNWIND_PERSONALITY, UNWIND_SLOT } from "./unwind-abi.js";
|
||||
import { UNWIND_PERSONALITY, UNWIND_RESUME_DECLARATION, UNWIND_SLOT } from "./unwind-abi.js";
|
||||
|
||||
export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
const B = new BlockBuilder();
|
||||
@@ -681,7 +681,10 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
emitter.privateGlobalSlots.clear();
|
||||
// Landing pads (emitDirectCall) need the personality.
|
||||
const personality = emitter.usesLandingPads ? ` ${UNWIND_PERSONALITY}` : "";
|
||||
if (emitter.usesLandingPads) emitter.declare(`declare i32 @__gcc_personality_v0(...)`);
|
||||
if (emitter.usesLandingPads) {
|
||||
emitter.declare(`declare i32 @__gcc_personality_v0(...)`);
|
||||
emitter.declare(UNWIND_RESUME_DECLARATION);
|
||||
}
|
||||
let body = `define internal ${ret} @${symbol}${int32Abi ? INT32_ABI_SUFFIX : ""}(${params.join(", ")}) ${attrs}${personality}${debug} { ; ${fn.name}\n${B.render()}\n}`;
|
||||
if (int32Abi) body += "\n" + emitter.emitInt32AbiWrapper(fn, symbol, int32Abi);
|
||||
if (emitter.integerClones.members.has(fn.name)) {
|
||||
|
||||
@@ -160,6 +160,7 @@ import {
|
||||
UNWIND_PERSONALITY,
|
||||
UNWIND_RAISE,
|
||||
UNWIND_RAISE_DECLARATION,
|
||||
UNWIND_RESUME_DECLARATION,
|
||||
UNWIND_RESUME,
|
||||
UNWIND_SLOT,
|
||||
UnwindAbiViolation,
|
||||
@@ -6110,7 +6111,8 @@ export class LlEmitter {
|
||||
const exception = B.tmp();
|
||||
B.line(`${exception} = landingpad { ptr, i32 } cleanup`);
|
||||
for (const index of borrowed) this.releaseFrame(`%p${index}`, fn.params[index]!.type);
|
||||
B.terminate(`resume { ptr, i32 } ${exception}`);
|
||||
this.declare(UNWIND_RESUME_DECLARATION);
|
||||
B.resumeUnwind(exception);
|
||||
B.startBlock(ok);
|
||||
} else B.line(ret === "void" ? call : `%result = ${call}`);
|
||||
// A borrowed result is reachable from the parameters: own it first. A
|
||||
|
||||
@@ -93,7 +93,9 @@ test("a method of a class outside any hierarchy is on the unwinding ABI; closure
|
||||
// closure over mid is a pointer target and returns with the flag.
|
||||
const add = body(ir, "Counter_add");
|
||||
expect(add).toMatch(/invoke double @sc_b?f_[^(]*mid/);
|
||||
expect(add).toMatch(/resume \{ ptr, i32 \} %[\w.]+/);
|
||||
expect(add).toMatch(/call void @scr_unwind_resume\(ptr %[\w.]+\)\n\s+unreachable/);
|
||||
expect(add).not.toMatch(/\bresume \{/);
|
||||
expect(ir).toContain("declare void @scr_unwind_resume(ptr) cold");
|
||||
expect(add).not.toContain("@scr_unwind_raise");
|
||||
const closures = ir
|
||||
.split(/\n(?=define )/)
|
||||
@@ -212,7 +214,10 @@ test("an invoked wrapper of a raising function resumes the unwind; its address m
|
||||
expect(out).toContain(
|
||||
"%r = invoke double @sc_bf_body.i32(i32 %i) to label %ub.ok.0 unwind label %ub.lp.0",
|
||||
);
|
||||
expect(out).toContain("resume { ptr, i32 } %ub.v.0");
|
||||
expect(out).toContain(
|
||||
"%ub.e.0 = extractvalue { ptr, i32 } %ub.v.0, 0\n call void @scr_unwind_resume(ptr %ub.e.0)\n unreachable",
|
||||
);
|
||||
expect(out).toContain("declare void @scr_unwind_resume(ptr) cold");
|
||||
expect(() =>
|
||||
bridgePlainUnwindCalls([
|
||||
`${raising}\n@table = internal constant [1 x ptr] [ptr @sc_bf_body.i32]`,
|
||||
@@ -277,11 +282,27 @@ test("calls that cannot unwind are marked nounwind", () => {
|
||||
" unreachable",
|
||||
"}",
|
||||
].join("\n");
|
||||
const [out] = bridgePlainUnwindCalls([raising]);
|
||||
const resuming = [
|
||||
"define internal void @sc_bf_resumes(ptr %b) #0 personality ptr @__gcc_personality_v0 {",
|
||||
"entry:",
|
||||
" invoke void @sc_bf_raises(ptr %b) to label %ok unwind label %lp",
|
||||
"ok:",
|
||||
" ret void",
|
||||
"lp:",
|
||||
" %e = landingpad { ptr, i32 } cleanup",
|
||||
" %t = extractvalue { ptr, i32 } %e, 0",
|
||||
" call void @scr_unwind_resume(ptr %t)",
|
||||
" unreachable",
|
||||
"}",
|
||||
].join("\n");
|
||||
const [out] = bridgePlainUnwindCalls([[raising, resuming].join("\n")]);
|
||||
expect(out).toContain("%n = call i64 @scr_bytes_length(ptr %b) nounwind ; length");
|
||||
expect(out).toContain("call void @scr_bytes_release(ptr %b) nounwind, !dbg !4");
|
||||
expect(out).toContain("%r = call ptr %b(ptr %b) nounwind");
|
||||
expect(out).toContain(" call void @scr_unwind_raise()\n");
|
||||
// The resume unwinds: where LLVM inlines the function at an invoke, the
|
||||
// call becomes an invoke of the caller's landing pad.
|
||||
expect(out).toContain(" call void @scr_unwind_resume(ptr %t)\n");
|
||||
});
|
||||
|
||||
test("functions without a plain call of a raising function change only their call lines", () => {
|
||||
|
||||
@@ -76,9 +76,11 @@ function recursiveFunctions(edges: ReadonlyMap<string, readonly string[]>): Set<
|
||||
* than a `call`'s callee, and not called from an async or generator body
|
||||
* (coroutine bodies keep plain calls).
|
||||
*
|
||||
* A native unwind costs about 2 µs a raise and 0.1 µs a frame, against a
|
||||
* few nanoseconds for a flag test, so the functions an exception is likely to
|
||||
* cross many of keep the flag: those with a `throw` or rethrow of their own
|
||||
* A native unwind costs about 0.05 µs a raise and 10 to 25 ns a frame on
|
||||
* x86-64 (scr_unwind.c's cached plans) and about 2 µs a raise and 0.1 µs a
|
||||
* frame through libgcc's unwinder, against a few nanoseconds for a flag
|
||||
* test, so the functions an exception is likely to cross many of keep the
|
||||
* flag: those with a `throw` or rethrow of their own
|
||||
* and those up to THROW_LEVELS calls above one (a handler near the throw
|
||||
* takes it without a native unwind), and the recursive ones (deep stacks
|
||||
* are recursions). */
|
||||
@@ -254,8 +256,8 @@ export function unwindAbiFunctions(
|
||||
* its own takes the exception. */
|
||||
export const UNWIND_RAISE = "call void @scr_unwind_raise()\n unreachable";
|
||||
/** What the unwind of a landing pad ends with when no handler of the
|
||||
* function takes the exception: the unwind goes on (`resume`) with the
|
||||
* exception the landing pad stored in UNWIND_SLOT. The block builder
|
||||
* function takes the exception: the unwind goes on (scr_unwind_resume) with
|
||||
* the exception the landing pad stored in UNWIND_SLOT. The block builder
|
||||
* expands it (BlockBuilder.terminate). */
|
||||
export const UNWIND_RESUME = "resume { ptr, i32 } <unwind slot>";
|
||||
/** The function's slot for the exception its landing pads take. */
|
||||
@@ -264,13 +266,17 @@ export const UNWIND_SLOT = "%unw.exc";
|
||||
* would call the sanitizer's no-return hook before it, which fails on fiber
|
||||
* stacks; scr_unwind_raise clears the unwound frames' shadow itself. */
|
||||
export const UNWIND_RAISE_DECLARATION = "declare void @scr_unwind_raise() cold";
|
||||
/** scr_unwind_resume's declaration. Landing pads call it with the
|
||||
* exception they took to continue the unwind (BlockBuilder.resumeUnwind);
|
||||
* like scr_unwind_raise, it is not `noreturn`. */
|
||||
export const UNWIND_RESUME_DECLARATION = "declare void @scr_unwind_resume(ptr) cold";
|
||||
|
||||
/** The personality of functions with landing pads: the landing pads are
|
||||
* cleanups that scr_unwind_raise's forced unwind runs. */
|
||||
* cleanups that scr_unwind_raise's unwind runs. */
|
||||
export const UNWIND_PERSONALITY = "personality ptr @__gcc_personality_v0";
|
||||
|
||||
/** An unwind leaving the function: a raise, or a landing pad's `resume`. */
|
||||
const UNWINDS = /call void @scr_unwind_raise\(\)|\n\s+resume /;
|
||||
/** An unwind leaving the function: a raise, or a landing pad's resume. */
|
||||
const UNWINDS = /call void @scr_unwind_raise\(\)|call void @scr_unwind_resume\(/;
|
||||
/** A pending-flag test: the thread's exception word, the active cell, or the runtime query. */
|
||||
const PENDING_TEST = /@scr_exc_flag\b|%exc\.cell\b|@scr_exc_active\b|@scr_exc_pending\(/;
|
||||
|
||||
@@ -307,15 +313,16 @@ function argumentsEnd(line: string, open: number): number {
|
||||
return close;
|
||||
}
|
||||
|
||||
/** A call that cannot unwind, marked so: only scr_unwind_raise and the
|
||||
* raising functions propagate an unwind (runtime code and every function
|
||||
* reached through a pointer return normally), so every other call is
|
||||
* `nounwind`, and the inliner keeps it a call where it inlines a raising
|
||||
* function at an invoke. */
|
||||
/** A call that cannot unwind, marked so: only scr_unwind_raise,
|
||||
* scr_unwind_resume and the raising functions propagate an unwind (runtime
|
||||
* code and every function reached through a pointer return normally), so
|
||||
* every other call is `nounwind`, and the inliner keeps it a call where it
|
||||
* inlines a raising function at an invoke. */
|
||||
function markNounwind(line: string): string {
|
||||
const call = line.indexOf("call ");
|
||||
const callee = CALLEE.exec(line.slice(call));
|
||||
if (callee === null || callee[0] === "@scr_unwind_raise(") return line;
|
||||
if (callee === null || callee[0] === "@scr_unwind_raise(" || callee[0] === "@scr_unwind_resume(")
|
||||
return line;
|
||||
const close = argumentsEnd(line, call + callee.index + callee[0].length - 1);
|
||||
return close >= line.length
|
||||
? line
|
||||
@@ -446,6 +453,7 @@ export function bridgePlainUnwindCalls(parts: string[]): string[] {
|
||||
}
|
||||
if (violations.size > 0) throw new UnwindAbiViolation(violations);
|
||||
let personality = false;
|
||||
let resumes = false;
|
||||
let counter = 0;
|
||||
const out = split.map((chunks) =>
|
||||
chunks
|
||||
@@ -525,8 +533,11 @@ export function bridgePlainUnwindCalls(parts: string[]): string[] {
|
||||
result.push(
|
||||
`${pad}:`,
|
||||
`${indent}%ub.v.${n} = landingpad { ptr, i32 } cleanup`,
|
||||
`${indent}resume { ptr, i32 } %ub.v.${n}`,
|
||||
`${indent}%ub.e.${n} = extractvalue { ptr, i32 } %ub.v.${n}, 0`,
|
||||
`${indent}call void @scr_unwind_resume(ptr %ub.e.${n})`,
|
||||
`${indent}unreachable`,
|
||||
);
|
||||
resumes = true;
|
||||
result.push(`${ok}:`);
|
||||
} else if (m[2] !== undefined) {
|
||||
const type = (m[3] ?? "").replace(RETURN_ATTRIBUTES, "").trim();
|
||||
@@ -585,5 +596,7 @@ export function bridgePlainUnwindCalls(parts: string[]): string[] {
|
||||
);
|
||||
if (personality && !out.some((part) => part.includes("@__gcc_personality_v0(...)")))
|
||||
out[0] = `declare i32 @__gcc_personality_v0(...)\n${out[0]}`;
|
||||
if (resumes && !out.some((part) => part.includes(UNWIND_RESUME_DECLARATION)))
|
||||
out[0] = `${UNWIND_RESUME_DECLARATION}\n${out[0]}`;
|
||||
return out;
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/unwind-landing-pad-chains.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -129,6 +129,8 @@ _Static_assert(offsetof(ScrExcCell, kind) == 0 && sizeof(ScrExcKind) == 4 && SCR
|
||||
* functions, unwind-abi.ts). Linux (glibc and musl) only; it never
|
||||
* returns. */
|
||||
SCR_COLD _Noreturn void scr_unwind_raise(void);
|
||||
/* Continues the unwind from a landing pad with the exception it took. */
|
||||
SCR_COLD void scr_unwind_resume(void *exc);
|
||||
SCR_COLD void scr_throw_f64(double v);
|
||||
SCR_COLD void scr_throw_bool(bool v);
|
||||
SCR_COLD void scr_throw_str(ScrStr *v); /* takes ownership */
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
/* scr_unwind_raise: the native unwind of the unwinding exception ABI of
|
||||
* emitted functions (the compiler's unwind-abi.ts). Linux (glibc and musl)
|
||||
* only; elsewhere this unit is empty. */
|
||||
* emitted functions (the compiler's unwind-abi.ts), and scr_unwind_resume,
|
||||
* which emitted landing pads call to continue it. Linux (glibc and musl)
|
||||
* only; elsewhere this unit is empty.
|
||||
*
|
||||
* Emitted landing pads are cleanups: they release the frame's references
|
||||
* and then either take the exception (a handler of theirs, or a frame on
|
||||
* the pending-flag ABI that returns with it pending) or resume the unwind.
|
||||
* So the unwind is a single pass through every landing pad; the exception
|
||||
* itself stays in the active cell.
|
||||
*
|
||||
* On x86-64 the unwind walks with plans cached per return address. The
|
||||
* first raise through a return address finds the function's FDE through
|
||||
* the .eh_frame_hdr of the object that contains it, runs the CFI up to the
|
||||
* call, and finds the call site's landing pad in the LSDA. The plan records
|
||||
* how to compute the frame's CFA, where each callee-saved register is
|
||||
* saved, and the landing pad; later raises only look plans up. A landing
|
||||
* pad runs with rax holding the raise's token and rdx 0 (a cleanup). When a
|
||||
* frame's FDE uses CFI or encodings the plans do not express, or a return
|
||||
* address has no FDE, the unwind goes on from the frame the walk started at
|
||||
* with libgcc's forced unwind, as on other Linux targets. */
|
||||
#include "scr_runtime.h"
|
||||
|
||||
#if defined(__linux__) && !defined(__ANDROID__) && !defined(__wasi__)
|
||||
@@ -16,6 +34,7 @@
|
||||
* which link it in. */
|
||||
#pragma weak _Unwind_ForcedUnwind
|
||||
#pragma weak _Unwind_GetCFA
|
||||
#pragma weak _Unwind_Resume
|
||||
|
||||
#if defined(__SANITIZE_ADDRESS__)
|
||||
#define SCR_UNWIND_ASAN 1
|
||||
@@ -34,11 +53,6 @@
|
||||
static SCR_TL uintptr_t scr_unwind_low;
|
||||
#endif
|
||||
|
||||
/* Emitted landing pads are cleanups: they release the frame's references
|
||||
* and then either take the exception (a handler of theirs, or a frame on
|
||||
* the pending-flag ABI that returns with it pending) or resume the unwind.
|
||||
* So the unwind is forced, a single pass through every landing pad; the
|
||||
* exception itself stays in the active cell. */
|
||||
static _Unwind_Reason_Code scr_unwind_stop(int version, _Unwind_Action actions,
|
||||
_Unwind_Exception_Class cls,
|
||||
struct _Unwind_Exception *exc,
|
||||
@@ -69,7 +83,10 @@ static _Unwind_Reason_Code scr_unwind_stop(int version, _Unwind_Action actions,
|
||||
|
||||
static SCR_TL struct _Unwind_Exception scr_unwind_exception;
|
||||
|
||||
void scr_unwind_raise(void) {
|
||||
/* libgcc's forced unwind, from the caller of the function that calls this.
|
||||
* It does not return; it is not declared noreturn, so sanitized builds call
|
||||
* no sanitizer hook before it (see scr_unwind_stop). */
|
||||
static __attribute__((noinline)) void scr_unwind_forced(void) {
|
||||
struct _Unwind_Exception *exc = &scr_unwind_exception;
|
||||
memset(exc, 0, sizeof *exc);
|
||||
exc->exception_class = 0x5343524950540000ull; /* "SCRIPT\0\0" */
|
||||
@@ -80,4 +97,686 @@ void scr_unwind_raise(void) {
|
||||
fputs("scriptc: the native unwinder could not raise an exception\n", stderr);
|
||||
abort();
|
||||
}
|
||||
|
||||
#if defined(__x86_64__)
|
||||
#include <link.h>
|
||||
|
||||
/* DWARF register numbers. */
|
||||
enum {
|
||||
SCR_UNW_RBX = 3,
|
||||
SCR_UNW_RBP = 6,
|
||||
SCR_UNW_RSP = 7,
|
||||
SCR_UNW_R12 = 12,
|
||||
SCR_UNW_R13 = 13,
|
||||
SCR_UNW_R14 = 14,
|
||||
SCR_UNW_R15 = 15,
|
||||
SCR_UNW_RA = 16,
|
||||
SCR_UNW_REGS = 17
|
||||
};
|
||||
|
||||
/* The callee-saved registers a plan restores, in the order of its bits. */
|
||||
static const uint8_t scr_unwind_callee[6] = {SCR_UNW_RBX, SCR_UNW_RBP, SCR_UNW_R12,
|
||||
SCR_UNW_R13, SCR_UNW_R14, SCR_UNW_R15};
|
||||
|
||||
typedef struct {
|
||||
uint64_t pc; /* the return address the plan is for (0: an empty slot) */
|
||||
uint64_t pad; /* the call site's landing pad, or 0 */
|
||||
int32_t cfa_off; /* the CFA is cfa_reg's value plus cfa_off */
|
||||
uint8_t cfa_reg; /* SCR_UNW_RSP or SCR_UNW_RBP */
|
||||
uint8_t saved; /* bit i: scr_unwind_callee[i] is saved at CFA + off[i] * 8 */
|
||||
int8_t off[6];
|
||||
uint8_t fallback; /* the frame is left to libgcc's unwinder */
|
||||
} ScrUnwindPlan;
|
||||
|
||||
_Static_assert(sizeof(ScrUnwindPlan) == 32, "a plan is half a cache line");
|
||||
|
||||
#define SCR_UNWIND_PLAN_BITS 14
|
||||
#define SCR_UNWIND_PLAN_SLOTS (1u << SCR_UNWIND_PLAN_BITS)
|
||||
static ScrUnwindPlan scr_unwind_plans[SCR_UNWIND_PLAN_SLOTS];
|
||||
static const ScrUnwindPlan scr_unwind_fallback_plan = {.fallback = 1};
|
||||
static size_t scr_unwind_plan_count;
|
||||
/* Inserts take this lock; lookups take none. A plan is published by the
|
||||
* release store of its pc. */
|
||||
static int scr_unwind_lock;
|
||||
|
||||
/* Reads a value in DWARF pointer encoding enc. datarel is the base of
|
||||
* datarel values (0 where they do not occur). Sets *bad for an encoding the
|
||||
* plans do not read, which includes indirect values. Like libgcc, a zero
|
||||
* value stays zero. */
|
||||
static uint64_t scr_unwind_read(const uint8_t **p, uint8_t enc, uint64_t datarel, int *bad) {
|
||||
const uint8_t *start = *p;
|
||||
uint64_t v = 0;
|
||||
switch (enc & 0x0f) {
|
||||
case 0x00: /* absptr */
|
||||
case 0x04: /* udata8 */
|
||||
case 0x0c: /* sdata8 */
|
||||
memcpy(&v, *p, 8);
|
||||
*p += 8;
|
||||
break;
|
||||
case 0x01: { /* uleb128 */
|
||||
unsigned shift = 0;
|
||||
uint8_t b;
|
||||
do {
|
||||
b = *(*p)++;
|
||||
if (shift < 64) v |= (uint64_t)(b & 0x7f) << shift;
|
||||
shift += 7;
|
||||
} while (b & 0x80);
|
||||
break;
|
||||
}
|
||||
case 0x02: {
|
||||
uint16_t x;
|
||||
memcpy(&x, *p, 2);
|
||||
*p += 2;
|
||||
v = x;
|
||||
break;
|
||||
}
|
||||
case 0x03: {
|
||||
uint32_t x;
|
||||
memcpy(&x, *p, 4);
|
||||
*p += 4;
|
||||
v = x;
|
||||
break;
|
||||
}
|
||||
case 0x0a: {
|
||||
int16_t x;
|
||||
memcpy(&x, *p, 2);
|
||||
*p += 2;
|
||||
v = (uint64_t)(int64_t)x;
|
||||
break;
|
||||
}
|
||||
case 0x0b: {
|
||||
int32_t x;
|
||||
memcpy(&x, *p, 4);
|
||||
*p += 4;
|
||||
v = (uint64_t)(int64_t)x;
|
||||
break;
|
||||
}
|
||||
default: /* sleb128 and unknown formats */
|
||||
*bad = 1;
|
||||
return 0;
|
||||
}
|
||||
if (v == 0) return 0;
|
||||
switch (enc & 0x70) {
|
||||
case 0x00:
|
||||
break;
|
||||
case 0x10: /* pcrel */
|
||||
v += (uint64_t)(uintptr_t)start;
|
||||
break;
|
||||
case 0x30: /* datarel */
|
||||
if (datarel == 0) *bad = 1;
|
||||
v += datarel;
|
||||
break;
|
||||
default:
|
||||
*bad = 1;
|
||||
return 0;
|
||||
}
|
||||
if (enc & 0x80) *bad = 1; /* indirect: read only through scr_unwind_skip */
|
||||
return v;
|
||||
}
|
||||
|
||||
static uint64_t scr_unwind_uleb(const uint8_t **p) {
|
||||
int bad = 0;
|
||||
return scr_unwind_read(p, 0x01, 0, &bad);
|
||||
}
|
||||
|
||||
static int64_t scr_unwind_sleb(const uint8_t **p) {
|
||||
int64_t v = 0;
|
||||
unsigned shift = 0;
|
||||
uint8_t b;
|
||||
do {
|
||||
b = *(*p)++;
|
||||
if (shift < 64) v |= (int64_t)((uint64_t)(b & 0x7f) << shift);
|
||||
shift += 7;
|
||||
} while (b & 0x80);
|
||||
if (shift < 64 && (b & 0x40)) v |= (int64_t)(~(uint64_t)0 << shift);
|
||||
return v;
|
||||
}
|
||||
|
||||
/* A search for the loaded object that contains pc, and its .eh_frame_hdr
|
||||
* search table. */
|
||||
typedef struct {
|
||||
uint64_t pc;
|
||||
const uint8_t *hdr;
|
||||
const uint8_t *table;
|
||||
size_t count;
|
||||
} ScrUnwindObject;
|
||||
|
||||
static int scr_unwind_find_object(struct dl_phdr_info *info, size_t size, void *data) {
|
||||
(void)size;
|
||||
ScrUnwindObject *object = data;
|
||||
int contains = 0;
|
||||
const uint8_t *h = NULL;
|
||||
for (int i = 0; i < info->dlpi_phnum; i++) {
|
||||
const ElfW(Phdr) *phdr = &info->dlpi_phdr[i];
|
||||
uint64_t start = (uint64_t)info->dlpi_addr + phdr->p_vaddr;
|
||||
if (phdr->p_type == PT_LOAD && object->pc >= start && object->pc - start < phdr->p_memsz)
|
||||
contains = 1;
|
||||
else if (phdr->p_type == PT_GNU_EH_FRAME)
|
||||
h = (const uint8_t *)(uintptr_t)start;
|
||||
}
|
||||
if (!contains) return 0;
|
||||
/* Version 1 with the table LLVM's and GNU's linkers write: datarel
|
||||
* sdata4 pairs, sorted by initial location. */
|
||||
if (h != NULL && h[0] == 1 && h[1] != 0xff && h[2] != 0xff && h[3] == 0x3b) {
|
||||
int bad = 0;
|
||||
const uint8_t *p = h + 4;
|
||||
(void)scr_unwind_read(&p, h[1], (uint64_t)(uintptr_t)h, &bad);
|
||||
uint64_t count = scr_unwind_read(&p, h[2], (uint64_t)(uintptr_t)h, &bad);
|
||||
if (!bad) {
|
||||
object->hdr = h;
|
||||
object->table = p;
|
||||
object->count = (size_t)count;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* The FDE whose initial location is the greatest one at or below pc in
|
||||
* the table of the object that contains pc. */
|
||||
static const uint8_t *scr_unwind_find_fde(uint64_t pc) {
|
||||
ScrUnwindObject object = {.pc = pc};
|
||||
dl_iterate_phdr(scr_unwind_find_object, &object);
|
||||
if (object.table == NULL) return NULL;
|
||||
uint64_t base = (uint64_t)(uintptr_t)object.hdr;
|
||||
size_t lo = 0, hi = object.count;
|
||||
while (lo < hi) {
|
||||
size_t mid = lo + (hi - lo) / 2;
|
||||
int32_t start;
|
||||
memcpy(&start, object.table + mid * 8, 4);
|
||||
if (base + (uint64_t)(int64_t)start <= pc) lo = mid + 1;
|
||||
else hi = mid;
|
||||
}
|
||||
if (lo == 0) return NULL;
|
||||
int32_t fde;
|
||||
memcpy(&fde, object.table + (lo - 1) * 8 + 4, 4);
|
||||
return (const uint8_t *)(uintptr_t)(base + (uint64_t)(int64_t)fde);
|
||||
}
|
||||
|
||||
#define SCR_UNWIND_UNSAVED INT32_MIN
|
||||
|
||||
typedef struct {
|
||||
uint64_t cfa_reg;
|
||||
int64_t cfa_off;
|
||||
int32_t saved[SCR_UNW_REGS]; /* CFA-relative offsets, or SCR_UNWIND_UNSAVED */
|
||||
uint64_t args_size;
|
||||
} ScrUnwindRules;
|
||||
|
||||
/* Whether the plans track register reg: the callee-saved registers, the
|
||||
* stack pointer and the return address. */
|
||||
static int scr_unwind_tracked(uint64_t reg) {
|
||||
return reg == SCR_UNW_RBX || reg == SCR_UNW_RBP || reg == SCR_UNW_RSP ||
|
||||
(reg >= SCR_UNW_R12 && reg <= SCR_UNW_RA);
|
||||
}
|
||||
|
||||
static void scr_unwind_save(ScrUnwindRules *r, uint64_t reg, int64_t off, int *bad) {
|
||||
if (reg >= SCR_UNW_REGS) return; /* vector and other registers stay as they are */
|
||||
if (off < INT32_MIN + 1 || off > INT32_MAX) *bad = 1;
|
||||
else r->saved[reg] = (int32_t)off;
|
||||
}
|
||||
|
||||
/* Runs CFI instructions up to the location pc. Sets *bad for an
|
||||
* instruction the plans do not express. */
|
||||
static void scr_unwind_run_cfi(const uint8_t *p, const uint8_t *end, uint64_t code_align,
|
||||
int64_t data_align, uint64_t loc, uint64_t pc, ScrUnwindRules *r,
|
||||
const ScrUnwindRules *initial, int *bad) {
|
||||
ScrUnwindRules stack[8];
|
||||
int depth = 0;
|
||||
while (p < end && loc <= pc && !*bad) {
|
||||
uint8_t op = *p++;
|
||||
uint8_t low = op & 0x3f;
|
||||
switch (op & 0xc0) {
|
||||
case 0x40: /* advance_loc */
|
||||
loc += low * code_align;
|
||||
continue;
|
||||
case 0x80: /* offset */
|
||||
scr_unwind_save(r, low, (int64_t)scr_unwind_uleb(&p) * data_align, bad);
|
||||
continue;
|
||||
case 0xc0: /* restore */
|
||||
if (low < SCR_UNW_REGS) r->saved[low] = initial->saved[low];
|
||||
continue;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
switch (op) {
|
||||
case 0x00: /* nop */
|
||||
break;
|
||||
case 0x02: /* advance_loc1 */
|
||||
loc += *p++ * code_align;
|
||||
break;
|
||||
case 0x03: { /* advance_loc2 */
|
||||
uint16_t d;
|
||||
memcpy(&d, p, 2);
|
||||
p += 2;
|
||||
loc += d * code_align;
|
||||
break;
|
||||
}
|
||||
case 0x04: { /* advance_loc4 */
|
||||
uint32_t d;
|
||||
memcpy(&d, p, 4);
|
||||
p += 4;
|
||||
loc += d * code_align;
|
||||
break;
|
||||
}
|
||||
case 0x05: { /* offset_extended */
|
||||
uint64_t reg = scr_unwind_uleb(&p);
|
||||
scr_unwind_save(r, reg, (int64_t)scr_unwind_uleb(&p) * data_align, bad);
|
||||
break;
|
||||
}
|
||||
case 0x06: { /* restore_extended */
|
||||
uint64_t reg = scr_unwind_uleb(&p);
|
||||
if (reg < SCR_UNW_REGS) r->saved[reg] = initial->saved[reg];
|
||||
break;
|
||||
}
|
||||
case 0x07: /* undefined */
|
||||
case 0x08: { /* same_value */
|
||||
uint64_t reg = scr_unwind_uleb(&p);
|
||||
if (op == 0x07 && scr_unwind_tracked(reg)) *bad = 1;
|
||||
else if (reg < SCR_UNW_REGS) r->saved[reg] = SCR_UNWIND_UNSAVED;
|
||||
break;
|
||||
}
|
||||
case 0x09: { /* register */
|
||||
uint64_t reg = scr_unwind_uleb(&p);
|
||||
(void)scr_unwind_uleb(&p);
|
||||
if (scr_unwind_tracked(reg)) *bad = 1;
|
||||
break;
|
||||
}
|
||||
case 0x0a: /* remember_state */
|
||||
if (depth == 8) *bad = 1;
|
||||
else stack[depth++] = *r;
|
||||
break;
|
||||
case 0x0b: { /* restore_state: the CFA rule and args size stay */
|
||||
if (depth == 0) {
|
||||
*bad = 1;
|
||||
break;
|
||||
}
|
||||
uint64_t args_size = r->args_size;
|
||||
*r = stack[--depth];
|
||||
r->args_size = args_size;
|
||||
break;
|
||||
}
|
||||
case 0x0c: /* def_cfa */
|
||||
r->cfa_reg = scr_unwind_uleb(&p);
|
||||
r->cfa_off = (int64_t)scr_unwind_uleb(&p);
|
||||
break;
|
||||
case 0x0d: /* def_cfa_register */
|
||||
r->cfa_reg = scr_unwind_uleb(&p);
|
||||
break;
|
||||
case 0x0e: /* def_cfa_offset */
|
||||
r->cfa_off = (int64_t)scr_unwind_uleb(&p);
|
||||
break;
|
||||
case 0x11: { /* offset_extended_sf */
|
||||
uint64_t reg = scr_unwind_uleb(&p);
|
||||
scr_unwind_save(r, reg, scr_unwind_sleb(&p) * data_align, bad);
|
||||
break;
|
||||
}
|
||||
case 0x12: /* def_cfa_sf */
|
||||
r->cfa_reg = scr_unwind_uleb(&p);
|
||||
r->cfa_off = scr_unwind_sleb(&p) * data_align;
|
||||
break;
|
||||
case 0x13: /* def_cfa_offset_sf */
|
||||
r->cfa_off = scr_unwind_sleb(&p) * data_align;
|
||||
break;
|
||||
case 0x2e: /* GNU_args_size */
|
||||
r->args_size = scr_unwind_uleb(&p);
|
||||
break;
|
||||
default: /* expressions, val_offset, set_loc and vendor instructions */
|
||||
*bad = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* The landing pad of the call site that contains pc in an LSDA, or 0. */
|
||||
static uint64_t scr_unwind_lsda_pad(const uint8_t *p, uint64_t func_start, uint64_t pc, int *bad) {
|
||||
uint8_t lpstart_enc = *p++;
|
||||
uint64_t lpstart = func_start;
|
||||
if (lpstart_enc != 0xff) lpstart = scr_unwind_read(&p, lpstart_enc, 0, bad);
|
||||
uint8_t ttype_enc = *p++;
|
||||
if (ttype_enc != 0xff) (void)scr_unwind_uleb(&p);
|
||||
uint8_t cs_enc = *p++;
|
||||
uint64_t cs_len = scr_unwind_uleb(&p);
|
||||
const uint8_t *cs_end = p + cs_len;
|
||||
while (p < cs_end && !*bad) {
|
||||
uint64_t start = scr_unwind_read(&p, cs_enc, 0, bad);
|
||||
uint64_t len = scr_unwind_read(&p, cs_enc, 0, bad);
|
||||
uint64_t pad = scr_unwind_read(&p, cs_enc, 0, bad);
|
||||
(void)scr_unwind_uleb(&p); /* action */
|
||||
if (pc < func_start + start) return 0;
|
||||
if (pc < func_start + start + len) return pad ? lpstart + pad : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The personality routine of emitted functions; the walk reads their
|
||||
* LSDAs itself. Frames with any other personality are left to libgcc. */
|
||||
extern _Unwind_Reason_Code __gcc_personality_v0(int, _Unwind_Action, _Unwind_Exception_Class,
|
||||
struct _Unwind_Exception *,
|
||||
struct _Unwind_Context *);
|
||||
#pragma weak __gcc_personality_v0
|
||||
|
||||
/* Builds the plan of return address ra. */
|
||||
static void scr_unwind_build(ScrUnwindPlan *plan, uint64_t ra) {
|
||||
memset(plan, 0, sizeof *plan);
|
||||
plan->fallback = 1;
|
||||
uint64_t pc = ra - 1; /* within the call instruction */
|
||||
const uint8_t *fde = scr_unwind_find_fde(pc);
|
||||
if (fde == NULL) return;
|
||||
uint32_t fde_len;
|
||||
memcpy(&fde_len, fde, 4);
|
||||
if (fde_len == 0 || fde_len == 0xffffffffu) return;
|
||||
const uint8_t *fde_end = fde + 4 + fde_len;
|
||||
int32_t cie_off;
|
||||
memcpy(&cie_off, fde + 4, 4);
|
||||
if (cie_off == 0) return;
|
||||
const uint8_t *cie = fde + 4 - cie_off;
|
||||
uint32_t cie_len;
|
||||
memcpy(&cie_len, cie, 4);
|
||||
if (cie_len == 0 || cie_len == 0xffffffffu) return;
|
||||
const uint8_t *cie_end = cie + 4 + cie_len;
|
||||
const uint8_t *q = cie + 8; /* past the length and the CIE id */
|
||||
uint8_t version = *q++;
|
||||
if (version != 1 && version != 3) return;
|
||||
const char *aug = (const char *)q;
|
||||
q += strlen(aug) + 1;
|
||||
uint64_t code_align = scr_unwind_uleb(&q);
|
||||
int64_t data_align = scr_unwind_sleb(&q);
|
||||
uint64_t ra_reg = version == 1 ? *q++ : scr_unwind_uleb(&q);
|
||||
if (ra_reg != SCR_UNW_RA) return;
|
||||
int bad = 0;
|
||||
uint8_t fde_enc = 0x00, lsda_enc = 0xff;
|
||||
if (aug[0] == 'z') {
|
||||
uint64_t aug_len = scr_unwind_uleb(&q);
|
||||
const uint8_t *aug_end = q + aug_len;
|
||||
for (const char *a = aug + 1; *a != '\0'; a++) {
|
||||
if (*a == 'R') {
|
||||
fde_enc = *q++;
|
||||
} else if (*a == 'L') {
|
||||
lsda_enc = *q++;
|
||||
} else if (*a == 'P') {
|
||||
uint8_t enc = *q++;
|
||||
uint64_t slot = scr_unwind_read(&q, enc & 0x7f, 0, &bad);
|
||||
uint64_t personality = slot;
|
||||
if ((enc & 0x80) && slot != 0) memcpy(&personality, (const void *)(uintptr_t)slot, 8);
|
||||
if (personality != (uint64_t)(uintptr_t)&__gcc_personality_v0) bad = 1;
|
||||
} else {
|
||||
return; /* signal frames and unknown augmentations */
|
||||
}
|
||||
}
|
||||
q = aug_end;
|
||||
} else if (aug[0] != '\0') {
|
||||
return;
|
||||
}
|
||||
if (bad) return;
|
||||
ScrUnwindRules initial = {.cfa_reg = SCR_UNW_RSP, .cfa_off = 8};
|
||||
for (int i = 0; i < SCR_UNW_REGS; i++) initial.saved[i] = SCR_UNWIND_UNSAVED;
|
||||
scr_unwind_run_cfi(q, cie_end, code_align, data_align, 0, UINT64_MAX, &initial, &initial, &bad);
|
||||
const uint8_t *f = fde + 8;
|
||||
uint64_t func_start = scr_unwind_read(&f, fde_enc, 0, &bad);
|
||||
uint64_t func_len = scr_unwind_read(&f, fde_enc & 0x0f, 0, &bad);
|
||||
if (bad || pc < func_start || pc - func_start >= func_len) return;
|
||||
const uint8_t *lsda = NULL;
|
||||
if (aug[0] == 'z') {
|
||||
uint64_t aug_len = scr_unwind_uleb(&f);
|
||||
const uint8_t *aug_end = f + aug_len;
|
||||
if (lsda_enc != 0xff && aug_len != 0)
|
||||
lsda = (const uint8_t *)(uintptr_t)scr_unwind_read(&f, lsda_enc, 0, &bad);
|
||||
f = aug_end;
|
||||
}
|
||||
ScrUnwindRules r = initial;
|
||||
scr_unwind_run_cfi(f, fde_end, code_align, data_align, func_start, pc, &r, &initial, &bad);
|
||||
if (bad || r.args_size != 0) return;
|
||||
if (r.cfa_reg != SCR_UNW_RSP && r.cfa_reg != SCR_UNW_RBP) return;
|
||||
if (r.cfa_off < INT32_MIN || r.cfa_off > INT32_MAX) return;
|
||||
if (r.saved[SCR_UNW_RA] != -8) return;
|
||||
uint8_t saved = 0;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
int32_t o = r.saved[scr_unwind_callee[i]];
|
||||
if (o == SCR_UNWIND_UNSAVED) continue;
|
||||
if (o % 8 != 0 || o / 8 < INT8_MIN || o / 8 > INT8_MAX) return;
|
||||
saved |= (uint8_t)(1u << i);
|
||||
plan->off[i] = (int8_t)(o / 8);
|
||||
}
|
||||
uint64_t pad = lsda ? scr_unwind_lsda_pad(lsda, func_start, pc, &bad) : 0;
|
||||
if (bad) return;
|
||||
plan->cfa_reg = (uint8_t)r.cfa_reg;
|
||||
plan->cfa_off = (int32_t)r.cfa_off;
|
||||
plan->saved = saved;
|
||||
plan->pad = pad;
|
||||
plan->fallback = 0;
|
||||
}
|
||||
|
||||
static uint64_t scr_unwind_hash(uint64_t ra) {
|
||||
return (ra * 0x9E3779B97F4A7C15ull) >> (64 - SCR_UNWIND_PLAN_BITS);
|
||||
}
|
||||
|
||||
static __attribute__((noinline)) const ScrUnwindPlan *scr_unwind_insert(uint64_t ra) {
|
||||
while (__atomic_exchange_n(&scr_unwind_lock, 1, __ATOMIC_ACQUIRE))
|
||||
while (__atomic_load_n(&scr_unwind_lock, __ATOMIC_RELAXED)) __builtin_ia32_pause();
|
||||
const ScrUnwindPlan *result = &scr_unwind_fallback_plan;
|
||||
uint64_t h = scr_unwind_hash(ra);
|
||||
ScrUnwindPlan *slot = &scr_unwind_plans[h];
|
||||
while (slot->pc != 0 && slot->pc != ra) {
|
||||
h = (h + 1) & (SCR_UNWIND_PLAN_SLOTS - 1);
|
||||
slot = &scr_unwind_plans[h];
|
||||
}
|
||||
if (slot->pc == ra) {
|
||||
result = slot;
|
||||
} else if (scr_unwind_plan_count < SCR_UNWIND_PLAN_SLOTS / 4 * 3) {
|
||||
/* A full table leaves further return addresses to libgcc. */
|
||||
scr_unwind_plan_count++;
|
||||
ScrUnwindPlan built;
|
||||
scr_unwind_build(&built, ra);
|
||||
/* Lookups read pc atomically, so the plan's other fields are written
|
||||
* first and pc only by the release store that publishes them. */
|
||||
memcpy((char *)slot + sizeof slot->pc, (const char *)&built + sizeof built.pc,
|
||||
sizeof *slot - sizeof slot->pc);
|
||||
__atomic_store_n(&slot->pc, ra, __ATOMIC_RELEASE);
|
||||
result = slot;
|
||||
}
|
||||
__atomic_store_n(&scr_unwind_lock, 0, __ATOMIC_RELEASE);
|
||||
return result;
|
||||
}
|
||||
|
||||
static const ScrUnwindPlan *scr_unwind_plan(uint64_t ra) {
|
||||
uint64_t h = scr_unwind_hash(ra);
|
||||
for (;;) {
|
||||
const ScrUnwindPlan *plan = &scr_unwind_plans[h];
|
||||
uint64_t pc = __atomic_load_n(&plan->pc, __ATOMIC_ACQUIRE);
|
||||
if (pc == ra) return plan;
|
||||
if (pc == 0) return scr_unwind_insert(ra);
|
||||
h = (h + 1) & (SCR_UNWIND_PLAN_SLOTS - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Restores the callee-saved registers and the stack pointer of regs
|
||||
* (indexed by DWARF number) and jumps to the landing pad with rax holding
|
||||
* the token and rdx 0. */
|
||||
static _Noreturn __attribute__((naked)) void scr_unwind_install(const uint64_t *regs, uint64_t pad,
|
||||
const void *token) {
|
||||
__asm__ volatile("movq %rdx, %rax\n\t"
|
||||
"movq %rsi, %r11\n\t"
|
||||
"movq 24(%rdi), %rbx\n\t"
|
||||
"movq 48(%rdi), %rbp\n\t"
|
||||
"movq 96(%rdi), %r12\n\t"
|
||||
"movq 104(%rdi), %r13\n\t"
|
||||
"movq 112(%rdi), %r14\n\t"
|
||||
"movq 120(%rdi), %r15\n\t"
|
||||
"movq 56(%rdi), %rsp\n\t"
|
||||
"xorl %edx, %edx\n\t"
|
||||
"jmpq *%r11\n\t");
|
||||
}
|
||||
|
||||
/* Identifies a walk's raise to the landing pads it runs: they pass it to
|
||||
* scr_unwind_resume. The thrown value is in the active cell. */
|
||||
static const char scr_unwind_token;
|
||||
|
||||
#ifdef SCR_UNWIND_TESTING
|
||||
/* The walk's raises and resumes that went on with libgcc's unwinder, and
|
||||
* the frames whose stack pointer or callee-saved registers differ from
|
||||
* libgcc's for the same return address. */
|
||||
size_t scr_unwind_test_fallbacks;
|
||||
size_t scr_unwind_test_mismatches;
|
||||
|
||||
typedef struct {
|
||||
uint64_t ip, cfa, regs[6];
|
||||
} ScrUnwindTestFrame;
|
||||
|
||||
typedef struct {
|
||||
ScrUnwindTestFrame frames[512];
|
||||
int count, next;
|
||||
} ScrUnwindTestTrace;
|
||||
|
||||
static _Thread_local ScrUnwindTestTrace scr_unwind_test_trace;
|
||||
|
||||
static _Unwind_Reason_Code scr_unwind_test_record(struct _Unwind_Context *context, void *arg) {
|
||||
ScrUnwindTestTrace *trace = arg;
|
||||
if (trace->count == 512) return _URC_END_OF_STACK;
|
||||
ScrUnwindTestFrame *frame = &trace->frames[trace->count++];
|
||||
frame->ip = _Unwind_GetIP(context);
|
||||
frame->cfa = _Unwind_GetCFA(context);
|
||||
for (int i = 0; i < 6; i++) frame->regs[i] = _Unwind_GetGR(context, scr_unwind_callee[i]);
|
||||
return _URC_NO_REASON;
|
||||
}
|
||||
|
||||
/* Records libgcc's frames from here, before the walk changes anything. */
|
||||
static void scr_unwind_test_start(void) {
|
||||
scr_unwind_test_trace.count = 0;
|
||||
scr_unwind_test_trace.next = 0;
|
||||
_Unwind_Backtrace(scr_unwind_test_record, &scr_unwind_test_trace);
|
||||
}
|
||||
|
||||
/* Compares the walk's frame, regs at its call, with libgcc's: libgcc's
|
||||
* CFA there is the callee's, the frame's stack pointer at the call. */
|
||||
static void scr_unwind_test_check(const uint64_t *regs) {
|
||||
ScrUnwindTestTrace *trace = &scr_unwind_test_trace;
|
||||
while (trace->next < trace->count && trace->frames[trace->next].ip != regs[SCR_UNW_RA])
|
||||
trace->next++;
|
||||
if (trace->next == trace->count) {
|
||||
if (trace->count < 512) __atomic_add_fetch(&scr_unwind_test_mismatches, 1, __ATOMIC_RELAXED);
|
||||
return;
|
||||
}
|
||||
const ScrUnwindTestFrame *frame = &trace->frames[trace->next++];
|
||||
int same = frame->cfa == regs[SCR_UNW_RSP];
|
||||
for (int i = 0; i < 6; i++) same &= frame->regs[i] == regs[scr_unwind_callee[i]];
|
||||
if (!same) __atomic_add_fetch(&scr_unwind_test_mismatches, 1, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
#define SCR_UNWIND_FALLBACK() __atomic_add_fetch(&scr_unwind_test_fallbacks, 1, __ATOMIC_RELAXED)
|
||||
#define SCR_UNWIND_TEST_START() scr_unwind_test_start()
|
||||
#define SCR_UNWIND_TEST_CHECK(regs) scr_unwind_test_check(regs)
|
||||
#else
|
||||
#define SCR_UNWIND_FALLBACK() ((void)0)
|
||||
#define SCR_UNWIND_TEST_START() ((void)0)
|
||||
#define SCR_UNWIND_TEST_CHECK(regs) ((void)0)
|
||||
#endif
|
||||
|
||||
/* Walks from the frame regs describes, a caller's registers at its call,
|
||||
* and installs the first frame with a landing pad. Returns when a frame is
|
||||
* left to libgcc, before anything has been installed. */
|
||||
static __attribute__((no_sanitize("address"))) void scr_unwind_walk(uint64_t *regs) {
|
||||
SCR_UNWIND_TEST_START();
|
||||
for (;;) {
|
||||
const ScrUnwindPlan *plan = scr_unwind_plan(regs[SCR_UNW_RA]);
|
||||
if (plan->fallback) return;
|
||||
SCR_UNWIND_TEST_CHECK(regs);
|
||||
if (plan->pad != 0) {
|
||||
#ifdef SCR_UNWIND_ASAN
|
||||
/* The frames below the landing pad's never return: clear their
|
||||
* redzones' shadow, up to the pad frame's stack pointer. */
|
||||
uintptr_t sp;
|
||||
__asm__ volatile("movq %%rsp, %0" : "=r"(sp));
|
||||
if (regs[SCR_UNW_RSP] > sp)
|
||||
__asan_unpoison_memory_region((void *)sp, regs[SCR_UNW_RSP] - sp);
|
||||
#endif
|
||||
scr_unwind_install(regs, plan->pad, &scr_unwind_token);
|
||||
}
|
||||
uint64_t cfa = regs[plan->cfa_reg] + (uint64_t)(int64_t)plan->cfa_off;
|
||||
const uint64_t *frame = (const uint64_t *)(uintptr_t)cfa;
|
||||
for (unsigned m = plan->saved; m != 0; m &= m - 1) {
|
||||
int i = __builtin_ctz(m);
|
||||
regs[scr_unwind_callee[i]] = frame[plan->off[i]];
|
||||
}
|
||||
regs[SCR_UNW_RA] = frame[-1];
|
||||
regs[SCR_UNW_RSP] = cfa;
|
||||
}
|
||||
}
|
||||
|
||||
/* Called by the entry stubs below with their caller's registers. */
|
||||
__attribute__((visibility("hidden"), used)) void scr_unwind_walk_raise(uint64_t *regs) {
|
||||
scr_unwind_walk(regs);
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"), used)) void scr_unwind_walk_resume(uint64_t *regs,
|
||||
const void *exc) {
|
||||
if (exc == &scr_unwind_token) scr_unwind_walk(regs);
|
||||
}
|
||||
|
||||
/* Where the entry stubs go when the walk returns, with the stack as it was
|
||||
* at their entry: the caller is the emitted frame. */
|
||||
__attribute__((visibility("hidden"), used, no_sanitize("address"))) void
|
||||
scr_unwind_raise_slow(void) {
|
||||
SCR_UNWIND_FALLBACK();
|
||||
scr_unwind_forced();
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"), used, no_sanitize("address"))) void
|
||||
scr_unwind_resume_slow(void *exc) {
|
||||
/* A landing pad that libgcc's unwind ran passes libgcc's exception. */
|
||||
if (exc != &scr_unwind_token && _Unwind_Resume != NULL) _Unwind_Resume(exc);
|
||||
SCR_UNWIND_FALLBACK();
|
||||
scr_unwind_forced();
|
||||
}
|
||||
|
||||
/* The entry stubs store the caller's callee-saved registers, stack pointer
|
||||
* after the return and return address into a register array on the stack,
|
||||
* indexed by DWARF number (17 slots: 136 bytes, which keeps the stack
|
||||
* 16-byte aligned at the call). */
|
||||
#define SCR_UNWIND_CAPTURE \
|
||||
"subq $136, %rsp\n\t" \
|
||||
".cfi_adjust_cfa_offset 136\n\t" \
|
||||
"movq %rbx, 24(%rsp)\n\t" \
|
||||
"movq %rbp, 48(%rsp)\n\t" \
|
||||
"movq %r12, 96(%rsp)\n\t" \
|
||||
"movq %r13, 104(%rsp)\n\t" \
|
||||
"movq %r14, 112(%rsp)\n\t" \
|
||||
"movq %r15, 120(%rsp)\n\t" \
|
||||
"movq 136(%rsp), %rax\n\t" \
|
||||
"movq %rax, 128(%rsp)\n\t" \
|
||||
"leaq 144(%rsp), %rax\n\t" \
|
||||
"movq %rax, 56(%rsp)\n\t"
|
||||
|
||||
__attribute__((naked)) void scr_unwind_raise(void) {
|
||||
__asm__ volatile(SCR_UNWIND_CAPTURE "movq %rsp, %rdi\n\t"
|
||||
"call scr_unwind_walk_raise\n\t"
|
||||
"addq $136, %rsp\n\t"
|
||||
".cfi_adjust_cfa_offset -136\n\t"
|
||||
"jmp scr_unwind_raise_slow\n\t");
|
||||
}
|
||||
|
||||
/* Landing pads continue the unwind here with the exception they took. */
|
||||
__attribute__((naked)) void scr_unwind_resume(void *exc) {
|
||||
__asm__ volatile(SCR_UNWIND_CAPTURE "movq %rdi, 40(%rsp)\n\t"
|
||||
"movq %rdi, %rsi\n\t"
|
||||
"movq %rsp, %rdi\n\t"
|
||||
"call scr_unwind_walk_resume\n\t"
|
||||
"movq 40(%rsp), %rdi\n\t"
|
||||
"addq $136, %rsp\n\t"
|
||||
".cfi_adjust_cfa_offset -136\n\t"
|
||||
"jmp scr_unwind_resume_slow\n\t");
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
void scr_unwind_raise(void) {
|
||||
scr_unwind_forced();
|
||||
__builtin_unreachable();
|
||||
}
|
||||
|
||||
/* Landing pads continue the unwind here with the exception they took. */
|
||||
void scr_unwind_resume(void *exc) {
|
||||
if (_Unwind_Resume != NULL) _Unwind_Resume(exc);
|
||||
scr_unwind_forced();
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
/* Raises through the synthetic frames of test_unwind_frames.ll and
|
||||
* test_unwind_frames.S with scr_unwind.c's walk: landing pads run inner
|
||||
* first, the catching frame sees its callee-saved registers restored, every
|
||||
* frame the walk steps through has the stack pointer and callee-saved
|
||||
* registers libgcc's unwinder computes for it, and only frames with CFI the
|
||||
* plans do not express go to libgcc's unwinder.
|
||||
* Each case runs three times (the first builds plans, the others reuse
|
||||
* them), and again from several threads at once. */
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef void Frame(void **chain);
|
||||
extern Frame ir_raise, ir_plain, ir_plain_fp, ir_alloca, ir_pad, ir_pad_fp;
|
||||
extern Frame asm_remember, asm_rbp, asm_expr;
|
||||
extern int64_t ir_catch(void **chain, int64_t a, int64_t b, int64_t c, int64_t d, int64_t e,
|
||||
int64_t f);
|
||||
extern size_t scr_unwind_test_fallbacks;
|
||||
extern size_t scr_unwind_test_mismatches;
|
||||
|
||||
static _Thread_local void **cleanup_base;
|
||||
static _Thread_local int cleanups[512];
|
||||
static _Thread_local int cleanup_count;
|
||||
|
||||
/* A landing pad of the frame at chain[-1] ran. */
|
||||
void test_cleanup(void **chain) {
|
||||
if (cleanup_count < 512) cleanups[cleanup_count] = (int)(chain - cleanup_base) - 1;
|
||||
cleanup_count++;
|
||||
}
|
||||
|
||||
void test_touch(void *p) { memset(p, 0x5a, 16); }
|
||||
|
||||
void test_return(void **chain) { (void)chain; }
|
||||
|
||||
static int64_t checksum(int64_t a, int64_t b, int64_t c, int64_t d, int64_t e, int64_t f) {
|
||||
uint64_t s = (uint64_t)a * 3 ^ (uint64_t)b * 5;
|
||||
s += (uint64_t)c * 7;
|
||||
s ^= (uint64_t)d * 11;
|
||||
s += (uint64_t)e * 13;
|
||||
s ^= (uint64_t)f * 17;
|
||||
return (int64_t)(s & INT64_MAX);
|
||||
}
|
||||
|
||||
/* Writes over the stack the unwound frames used: in sanitized builds, a
|
||||
* redzone the unwind left poisoned would be reported here. */
|
||||
static __attribute__((noinline)) void scrub(void) {
|
||||
volatile char big[32768];
|
||||
memset((char *)big, 0, sizeof big);
|
||||
}
|
||||
|
||||
static void fail(const char *name, const char *what) {
|
||||
fprintf(stderr, "%s: %s\n", name, what);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/* Runs frames[0..n) below ir_catch. A case that ends in test_return
|
||||
* raises nothing. Returns the case's fallbacks to libgcc. */
|
||||
static size_t run(const char *name, Frame *const *frames, int n, int64_t seed) {
|
||||
void *chain[512];
|
||||
int expected[512];
|
||||
int pads = 0;
|
||||
if (n >= 511) fail(name, "too many frames");
|
||||
for (int i = 0; i < n; i++) chain[i] = (void *)frames[i];
|
||||
chain[n] = NULL;
|
||||
int raises = frames[n - 1] == ir_raise;
|
||||
if (raises)
|
||||
for (int i = n - 1; i >= 0; i--)
|
||||
if (frames[i] == ir_pad || frames[i] == ir_pad_fp) expected[pads++] = i;
|
||||
size_t before = __atomic_load_n(&scr_unwind_test_fallbacks, __ATOMIC_RELAXED);
|
||||
cleanup_base = chain;
|
||||
cleanup_count = 0;
|
||||
int64_t a = seed, b = seed ^ 0x1234, c = seed + 77, d = seed * 31, e = ~seed, f = seed << 3;
|
||||
int64_t got = ir_catch(chain, a, b, c, d, e, f);
|
||||
int64_t want = checksum(a, b, c, d, e, f) | (raises ? INT64_MIN : 0);
|
||||
if (got != want) fail(name, "the catching frame's values or raise flag differ");
|
||||
if (cleanup_count != pads) fail(name, "a different number of landing pads ran");
|
||||
for (int i = 0; i < pads; i++)
|
||||
if (cleanups[i] != expected[i]) fail(name, "landing pads ran out of order");
|
||||
scrub();
|
||||
if (__atomic_load_n(&scr_unwind_test_mismatches, __ATOMIC_RELAXED) != 0)
|
||||
fail(name, "a frame's stack pointer or callee-saved registers differ from libgcc's");
|
||||
return __atomic_load_n(&scr_unwind_test_fallbacks, __ATOMIC_RELAXED) - before;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
Frame *frames[16];
|
||||
int count;
|
||||
size_t fallbacks; /* raises or resumes that go to libgcc */
|
||||
} Case;
|
||||
|
||||
/* Sanitized builds realign a frame with a dynamic alloca through r10 and
|
||||
* describe it with DWARF expressions, which go to libgcc. */
|
||||
#if defined(__SANITIZE_ADDRESS__)
|
||||
#define ALLOCA_FALLBACKS 1
|
||||
#elif defined(__has_feature)
|
||||
#if __has_feature(address_sanitizer)
|
||||
#define ALLOCA_FALLBACKS 1
|
||||
#endif
|
||||
#endif
|
||||
#ifndef ALLOCA_FALLBACKS
|
||||
#define ALLOCA_FALLBACKS 0
|
||||
#endif
|
||||
|
||||
static const Case cases[] = {
|
||||
{"returns", {ir_pad, ir_plain, test_return}, 3, 0},
|
||||
{"raise", {ir_raise}, 1, 0},
|
||||
{"plain frames", {ir_plain, ir_plain_fp, ir_plain, ir_raise}, 4, 0},
|
||||
{"dynamic alloca", {ir_pad, ir_alloca, ir_pad, ir_raise}, 4, ALLOCA_FALLBACKS},
|
||||
{"landing pads", {ir_pad, ir_plain, ir_pad_fp, ir_pad, ir_raise}, 5, 0},
|
||||
{"remember and rbp", {asm_remember, ir_pad, asm_rbp, asm_remember, ir_raise}, 5, 0},
|
||||
{"libgcc above a pad", {asm_expr, ir_pad, ir_raise}, 3, 1},
|
||||
{"libgcc between pads", {ir_pad, asm_expr, ir_pad_fp, ir_raise}, 4, 1},
|
||||
{"libgcc at the raise", {ir_pad, asm_expr, ir_raise}, 3, 1},
|
||||
};
|
||||
#define CASES (sizeof cases / sizeof cases[0])
|
||||
|
||||
static void run_cases(int64_t seed) {
|
||||
for (size_t i = 0; i < CASES; i++) {
|
||||
size_t fallbacks = run(cases[i].name, cases[i].frames, cases[i].count, seed + (int64_t)i);
|
||||
if (fallbacks != cases[i].fallbacks) fail(cases[i].name, "a different number of fallbacks");
|
||||
}
|
||||
}
|
||||
|
||||
/* A deep recursion of every frame kind. */
|
||||
static void run_deep(int64_t seed) {
|
||||
static Frame *const kinds[] = {ir_pad, ir_plain, asm_remember, asm_rbp, ir_pad_fp, ir_plain_fp};
|
||||
Frame *frames[301];
|
||||
for (int i = 0; i < 300; i++) frames[i] = kinds[i % 6];
|
||||
frames[300] = ir_raise;
|
||||
if (run("deep", frames, 301, seed) != 0) fail("deep", "a fallback to libgcc");
|
||||
}
|
||||
|
||||
static void *thread_main(void *arg) {
|
||||
int64_t seed = (int64_t)(intptr_t)arg;
|
||||
for (int round = 0; round < 200; round++) {
|
||||
for (size_t i = 0; i < CASES; i++) {
|
||||
if (cases[i].fallbacks != 0) continue; /* the shared counter is checked alone */
|
||||
if (run(cases[i].name, cases[i].frames, cases[i].count, seed + round) != 0)
|
||||
fail(cases[i].name, "a fallback to libgcc in a thread");
|
||||
}
|
||||
if (round % 20 == 0) run_deep(seed);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* With an argument, the threads run first, so they build the plans
|
||||
* concurrently. */
|
||||
int main(int argc, char **argv) {
|
||||
(void)argv;
|
||||
if (argc == 1)
|
||||
for (int round = 0; round < 3; round++) {
|
||||
run_cases(round * 1000);
|
||||
run_deep(round);
|
||||
}
|
||||
pthread_t threads[8];
|
||||
for (int i = 0; i < 8; i++)
|
||||
if (pthread_create(&threads[i], NULL, thread_main, (void *)(intptr_t)(i * 7919)) != 0)
|
||||
fail("threads", "pthread_create failed");
|
||||
for (int i = 0; i < 8; i++) pthread_join(threads[i], NULL);
|
||||
puts("unwind checks passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/* Frames for test_unwind.c with CFI that compilers emit rarely. Like the
|
||||
* frames in test_unwind_frames.ll, each takes a chain and calls its first
|
||||
* function with the rest. */
|
||||
.text
|
||||
|
||||
/* Saves rbx and r12, and leaves through an early epilogue under
|
||||
* remember_state before its call, so the call's rules come from
|
||||
* restore_state. */
|
||||
.globl asm_remember
|
||||
.type asm_remember,@function
|
||||
asm_remember:
|
||||
.cfi_startproc
|
||||
pushq %rbx
|
||||
.cfi_def_cfa_offset 16
|
||||
.cfi_offset %rbx, -16
|
||||
pushq %r12
|
||||
.cfi_def_cfa_offset 24
|
||||
.cfi_offset %r12, -24
|
||||
subq $8, %rsp
|
||||
.cfi_def_cfa_offset 32
|
||||
movq (%rdi), %rax
|
||||
testq %rax, %rax
|
||||
jne 1f
|
||||
.cfi_remember_state
|
||||
addq $8, %rsp
|
||||
.cfi_def_cfa_offset 24
|
||||
popq %r12
|
||||
.cfi_def_cfa_offset 16
|
||||
.cfi_restore %r12
|
||||
popq %rbx
|
||||
.cfi_def_cfa_offset 8
|
||||
.cfi_restore %rbx
|
||||
retq
|
||||
.cfi_restore_state
|
||||
1:
|
||||
movabsq $0x5151515151515151, %rbx
|
||||
movabsq $0x5252525252525252, %r12
|
||||
addq $8, %rdi
|
||||
callq *%rax
|
||||
addq $8, %rsp
|
||||
.cfi_def_cfa_offset 24
|
||||
popq %r12
|
||||
.cfi_def_cfa_offset 16
|
||||
popq %rbx
|
||||
.cfi_def_cfa_offset 8
|
||||
retq
|
||||
.cfi_endproc
|
||||
.size asm_remember, .-asm_remember
|
||||
|
||||
/* An rbp frame that saves r13 and r14 below rbp and moves the stack
|
||||
* pointer by an amount that depends on the chain's address. */
|
||||
.globl asm_rbp
|
||||
.type asm_rbp,@function
|
||||
asm_rbp:
|
||||
.cfi_startproc
|
||||
pushq %rbp
|
||||
.cfi_def_cfa_offset 16
|
||||
.cfi_offset %rbp, -16
|
||||
movq %rsp, %rbp
|
||||
.cfi_def_cfa_register %rbp
|
||||
pushq %r13
|
||||
pushq %r14
|
||||
.cfi_offset %r13, -24
|
||||
.cfi_offset %r14, -32
|
||||
movq %rdi, %rax
|
||||
andq $0x70, %rax
|
||||
subq %rax, %rsp
|
||||
movabsq $0x5353535353535353, %r13
|
||||
movabsq $0x5454545454545454, %r14
|
||||
movq (%rdi), %rax
|
||||
addq $8, %rdi
|
||||
callq *%rax
|
||||
leaq -16(%rbp), %rsp
|
||||
popq %r14
|
||||
popq %r13
|
||||
popq %rbp
|
||||
.cfi_def_cfa %rsp, 8
|
||||
retq
|
||||
.cfi_endproc
|
||||
.size asm_rbp, .-asm_rbp
|
||||
|
||||
/* Defines its CFA with a DWARF expression (rsp + 16), which the plans do
|
||||
* not express: raises through it go to libgcc's unwinder. */
|
||||
.globl asm_expr
|
||||
.type asm_expr,@function
|
||||
asm_expr:
|
||||
.cfi_startproc
|
||||
pushq %rbx
|
||||
.cfi_escape 0x0f, 0x02, 0x77, 0x10
|
||||
.cfi_offset %rbx, -16
|
||||
movabsq $0x5555555555555555, %rbx
|
||||
movq (%rdi), %rax
|
||||
addq $8, %rdi
|
||||
callq *%rax
|
||||
popq %rbx
|
||||
.cfi_def_cfa %rsp, 8
|
||||
retq
|
||||
.cfi_endproc
|
||||
.size asm_expr, .-asm_expr
|
||||
|
||||
.section .note.GNU-stack,"",@progbits
|
||||
@@ -0,0 +1,131 @@
|
||||
; Frames for test_unwind.c, shaped like emitted code on the unwinding
|
||||
; exception ABI. Every frame takes a chain: a null-terminated array of frame
|
||||
; functions, of which it calls the first with the rest. Each clobbers the
|
||||
; callee-saved registers, so its prologue saves them and its CFI says where.
|
||||
target triple = "x86_64-unknown-linux-gnu"
|
||||
|
||||
declare void @scr_unwind_raise() cold
|
||||
declare void @scr_unwind_resume(ptr) cold
|
||||
declare i32 @__gcc_personality_v0(...)
|
||||
declare void @test_cleanup(ptr)
|
||||
declare void @test_touch(ptr)
|
||||
|
||||
; Clobbers every callee-saved register the frame pointer leaves free.
|
||||
define private void @clobber() alwaysinline {
|
||||
call void asm sideeffect "movq $$0x6161616161616161, %rbx\0Amovq $$0x6262626262626262, %r12\0Amovq $$0x6363636363636363, %r13\0Amovq $$0x6464646464646464, %r14\0Amovq $$0x6565656565656565, %r15", "~{rbx},~{r12},~{r13},~{r14},~{r15}"()
|
||||
ret void
|
||||
}
|
||||
|
||||
; A local that escapes, so sanitized builds give the frame redzones.
|
||||
define private void @local() alwaysinline sanitize_address {
|
||||
%buf = alloca [48 x i8], align 16
|
||||
call void @test_touch(ptr %buf)
|
||||
ret void
|
||||
}
|
||||
|
||||
define void @ir_raise(ptr %chain) sanitize_address {
|
||||
call void @local()
|
||||
call void @clobber()
|
||||
call void @scr_unwind_raise()
|
||||
unreachable
|
||||
}
|
||||
|
||||
define void @ir_plain(ptr %chain) sanitize_address {
|
||||
call void @local()
|
||||
%fn = load ptr, ptr %chain
|
||||
%rest = getelementptr ptr, ptr %chain, i64 1
|
||||
call void @clobber()
|
||||
call void %fn(ptr %rest)
|
||||
ret void
|
||||
}
|
||||
|
||||
define void @ir_plain_fp(ptr %chain) sanitize_address "frame-pointer"="all" {
|
||||
call void @local()
|
||||
%fn = load ptr, ptr %chain
|
||||
%rest = getelementptr ptr, ptr %chain, i64 1
|
||||
call void @clobber()
|
||||
call void %fn(ptr %rest)
|
||||
ret void
|
||||
}
|
||||
|
||||
; A frame whose stack pointer moves by a runtime amount: its CFA is rbp's.
|
||||
define void @ir_alloca(ptr %chain) sanitize_address {
|
||||
%bits = ptrtoint ptr %chain to i64
|
||||
%n = and i64 %bits, 112
|
||||
%size = add i64 %n, 16
|
||||
%dyn = alloca i8, i64 %size, align 16
|
||||
call void @test_touch(ptr %dyn)
|
||||
%fn = load ptr, ptr %chain
|
||||
%rest = getelementptr ptr, ptr %chain, i64 1
|
||||
call void @clobber()
|
||||
call void %fn(ptr %rest)
|
||||
ret void
|
||||
}
|
||||
|
||||
; A frame with a landing pad: it records its chain position and continues
|
||||
; the unwind. The chain position is live across the call.
|
||||
define void @ir_pad(ptr %chain) sanitize_address personality ptr @__gcc_personality_v0 {
|
||||
entry:
|
||||
call void @local()
|
||||
%fn = load ptr, ptr %chain
|
||||
%rest = getelementptr ptr, ptr %chain, i64 1
|
||||
call void @clobber()
|
||||
invoke void %fn(ptr %rest) to label %ok unwind label %lp
|
||||
ok:
|
||||
ret void
|
||||
lp:
|
||||
%e = landingpad { ptr, i32 } cleanup
|
||||
call void @test_cleanup(ptr %chain)
|
||||
%token = extractvalue { ptr, i32 } %e, 0
|
||||
call void @scr_unwind_resume(ptr %token)
|
||||
unreachable
|
||||
}
|
||||
|
||||
define void @ir_pad_fp(ptr %chain) sanitize_address "frame-pointer"="all" personality ptr @__gcc_personality_v0 {
|
||||
entry:
|
||||
call void @local()
|
||||
%fn = load ptr, ptr %chain
|
||||
%rest = getelementptr ptr, ptr %chain, i64 1
|
||||
call void @clobber()
|
||||
invoke void %fn(ptr %rest) to label %ok unwind label %lp
|
||||
ok:
|
||||
ret void
|
||||
lp:
|
||||
%e = landingpad { ptr, i32 } cleanup
|
||||
call void @test_cleanup(ptr %chain)
|
||||
%token = extractvalue { ptr, i32 } %e, 0
|
||||
call void @scr_unwind_resume(ptr %token)
|
||||
unreachable
|
||||
}
|
||||
|
||||
; Takes the exception, as a pending-flag frame does: returns a checksum of
|
||||
; six values live across the call, which hold the callee-saved registers,
|
||||
; and whether the call raised in bit 63.
|
||||
define i64 @ir_catch(ptr %chain, i64 %a, i64 %b, i64 %c, i64 %d, i64 %e, i64 %f) sanitize_address personality ptr @__gcc_personality_v0 {
|
||||
entry:
|
||||
call void @local()
|
||||
%x1 = mul i64 %a, 3
|
||||
%x2 = mul i64 %b, 5
|
||||
%x3 = mul i64 %c, 7
|
||||
%x4 = mul i64 %d, 11
|
||||
%x5 = mul i64 %e, 13
|
||||
%x6 = mul i64 %f, 17
|
||||
%fn = load ptr, ptr %chain
|
||||
%rest = getelementptr ptr, ptr %chain, i64 1
|
||||
invoke void %fn(ptr %rest) to label %ok unwind label %lp
|
||||
ok:
|
||||
br label %done
|
||||
lp:
|
||||
%l = landingpad { ptr, i32 } cleanup
|
||||
br label %done
|
||||
done:
|
||||
%raised = phi i64 [ 0, %ok ], [ -9223372036854775808, %lp ]
|
||||
%s1 = xor i64 %x1, %x2
|
||||
%s2 = add i64 %s1, %x3
|
||||
%s3 = xor i64 %s2, %x4
|
||||
%s4 = add i64 %s3, %x5
|
||||
%s5 = xor i64 %s4, %x6
|
||||
%s6 = and i64 %s5, 9223372036854775807
|
||||
%r = or i64 %s6, %raised
|
||||
ret i64 %r
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const testDir = import.meta.dirname;
|
||||
|
||||
// scr_unwind.c walks with cached plans on x86-64 Linux only.
|
||||
test.runIf(process.platform === "linux" && process.arch === "x64")(
|
||||
"the unwinder runs landing pads in order, restores callee-saved registers and falls back to libgcc",
|
||||
async () => {
|
||||
const buildDir = join(testDir, "build");
|
||||
await mkdir(buildDir, { recursive: true });
|
||||
const frames = [join(testDir, "test_unwind_frames.ll"), join(testDir, "test_unwind_frames.S")];
|
||||
// The frames also run from a shared library, whose FDEs are in its own
|
||||
// .eh_frame_hdr.
|
||||
const library = join(buildDir, "libtest_unwind_frames.so");
|
||||
await execFileAsync("clang", ["-O1", "-shared", "-fPIC", "-o", library, ...frames]);
|
||||
for (const build of ["plain", "asan", "tsan", "shared"] as const) {
|
||||
const bin = join(buildDir, `test_unwind_${build}`);
|
||||
await execFileAsync("clang", [
|
||||
"-std=c11",
|
||||
"-O1",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-D_GNU_SOURCE",
|
||||
"-DSCR_UNWIND_TESTING",
|
||||
...(build === "asan" ? ["-fsanitize=address"] : []),
|
||||
...(build === "tsan" ? ["-fsanitize=thread"] : []),
|
||||
"-pthread",
|
||||
"-o",
|
||||
bin,
|
||||
join(testDir, "test_unwind.c"),
|
||||
...(build === "shared" ? [library, `-Wl,-rpath,${buildDir}`] : frames),
|
||||
join(testDir, "../src/scr_unwind.c"),
|
||||
// The library's landing pads and the program share libgcc_s's unwinder.
|
||||
...(build === "shared" ? ["-shared-libgcc"] : ["-lgcc_eh"]),
|
||||
]);
|
||||
const env = {
|
||||
...process.env,
|
||||
ASAN_OPTIONS: "detect_leaks=0:halt_on_error=1",
|
||||
TSAN_OPTIONS: "halt_on_error=1",
|
||||
};
|
||||
// ThreadSanitizer checks the threads' concurrent plan inserts and
|
||||
// lookups; the libgcc fallback cases run in the other builds.
|
||||
for (const args of build === "tsan" ? [["threads"]] : [[], ["threads"]]) {
|
||||
const run = await execFileAsync(bin, args, { env });
|
||||
expect(run.stdout).toBe("unwind checks passed\n");
|
||||
}
|
||||
}
|
||||
},
|
||||
120_000,
|
||||
);
|
||||
@@ -415,7 +415,9 @@ export function nodeIslandSymbols(names) {
|
||||
* whose parameters the lib types `any`: captures a match can leave unset
|
||||
* and results the checker types `any`. bench/record-untyped-writes, also
|
||||
* added later, writes typed records through `unknown` and `Object.assign`
|
||||
* views. Every other bench workload is fully typed and must link none. The gate and
|
||||
* views. bench/layered-errors, also added later, constructs and throws
|
||||
* Error subclass instances, as bench/validate-errors does. Every other bench
|
||||
* workload is fully typed and must link none. The gate and
|
||||
* tests/harness/bench-dynamic-tier.test.ts enforce that, and the test also
|
||||
* fails when a listed workload stops linking them, so remove a workload here
|
||||
* when a leak fix makes its binary clean. */
|
||||
@@ -440,6 +442,7 @@ export const DYNAMIC_WORKLOADS = new Set([
|
||||
"bench/throw-chains",
|
||||
"bench/regex-replacers",
|
||||
"bench/record-untyped-writes",
|
||||
"bench/layered-errors",
|
||||
]);
|
||||
|
||||
/** Names of the functions (text and weak symbols) in `nm` output. Mach-O
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// Exceptions thrown below functions that hold references and propagate by
|
||||
// unwinding: landing pads release a frame's strings and arrays and continue
|
||||
// the unwind, also where the frame is inlined into a caller whose own handler
|
||||
// takes the exception, and through methods, closures and deep chains.
|
||||
function leaf(n: number): number {
|
||||
if (n % 3 === 0) throw new RangeError("leaf " + n);
|
||||
return n * 2;
|
||||
}
|
||||
function first(n: number): number {
|
||||
return leaf(n) + 1;
|
||||
}
|
||||
function second(n: number): number {
|
||||
return first(n) * 3;
|
||||
}
|
||||
function third(n: number): number {
|
||||
return second(n) - 2;
|
||||
}
|
||||
function holder(n: number): number {
|
||||
const parts: string[] = ["a" + n];
|
||||
const r = third(n);
|
||||
parts.push("b");
|
||||
return r + parts.length;
|
||||
}
|
||||
function labelled(n: number): number {
|
||||
const label = "u" + n;
|
||||
return holder(n) + label.length;
|
||||
}
|
||||
function deeper(n: number, depth: number): number {
|
||||
const name = "d" + depth;
|
||||
if (depth === 0) return labelled(n) + name.length;
|
||||
return deeper(n, depth - 1) + name.length;
|
||||
}
|
||||
class Counter {
|
||||
total = 0;
|
||||
add(n: number): void {
|
||||
this.total += labelled(n);
|
||||
}
|
||||
}
|
||||
|
||||
function caught(n: number): string {
|
||||
try {
|
||||
return "ok " + labelled(n);
|
||||
} catch (e) {
|
||||
return "caught " + (e as Error).message;
|
||||
}
|
||||
}
|
||||
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < 7; i++) out.push(caught(i));
|
||||
console.log(out.join(", "));
|
||||
|
||||
const counter = new Counter();
|
||||
let failures = 0;
|
||||
for (let round = 0; round < 2000; round++) {
|
||||
for (let i = 0; i < 6; i++) {
|
||||
try {
|
||||
counter.add(i);
|
||||
} catch {
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("counter", counter.total, "failures", failures);
|
||||
|
||||
let deep = 0;
|
||||
let deepFailures = 0;
|
||||
for (let i = 0; i < 300; i++) {
|
||||
try {
|
||||
deep += deeper(i, i % 40);
|
||||
} catch (e) {
|
||||
deepFailures++;
|
||||
if (i < 10) console.log((e as Error).name, (e as Error).message);
|
||||
}
|
||||
}
|
||||
console.log("deep", deep, "failures", deepFailures);
|
||||
|
||||
const viaClosure = (n: number): number => labelled(n) + 1;
|
||||
let closureFailures = 0;
|
||||
for (let i = 0; i < 30; i++) {
|
||||
try {
|
||||
viaClosure(i);
|
||||
} catch {
|
||||
closureFailures++;
|
||||
}
|
||||
}
|
||||
console.log("closure failures", closureFailures);
|
||||
|
||||
function finallyOrder(n: number): string {
|
||||
const log: string[] = [];
|
||||
try {
|
||||
try {
|
||||
log.push("try");
|
||||
labelled(n);
|
||||
log.push("after");
|
||||
} finally {
|
||||
log.push("finally");
|
||||
}
|
||||
} catch (e) {
|
||||
log.push("catch " + (e as Error).message);
|
||||
}
|
||||
return log.join(" > ");
|
||||
}
|
||||
console.log(finallyOrder(3));
|
||||
console.log(finallyOrder(4));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4100, "san": 69700 }
|
||||
Reference in New Issue
Block a user