Raise through cached unwind plans on x86-64 Linux (#2455)

This commit is contained in:
Chris Tate
2026-10-28 02:22:18 -05:00
committed by GitHub
18 changed files with 1449 additions and 31 deletions
+1
View File
@@ -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}`);
+11 -1
View File
@@ -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;
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/unwind-landing-pad-chains.ts"
],
"diags": []
}
+2
View File
@@ -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 */
+707 -8
View File
@@ -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
+165
View File
@@ -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;
}
+101
View File
@@ -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
+131
View File
@@ -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
}
+55
View File
@@ -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,
);
+4 -1
View File
@@ -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
+104
View File
@@ -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 }