diff --git a/.github/scripts/check-debug-producer.py b/.github/scripts/check-debug-producer.py new file mode 100644 index 0000000..763d09a --- /dev/null +++ b/.github/scripts/check-debug-producer.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Exercise the selected platform's real seal and named debuggee runner.""" +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path +import re +import struct +import subprocess +import tomllib + + +def elf_sections(image: Path) -> dict[str, tuple[int, int]]: + data = image.read_bytes() + assert data[:6] == b"\x7fELF\x02\x01", "the produced image is little-endian ELF64" + offset = struct.unpack_from("= start + name = names[start:end].decode("ascii") + if header[1] != 8: # NOBITS has no file bytes. + assert header[4] + header[5] <= len(data) + result[name] = (header[1], header[5]) + return result + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--arch", required=True, choices=("x86_64", "aarch64")) + parser.add_argument("--mcpp", default=os.environ.get("MCPP", "mcpp")) + args = parser.parse_args() + root = Path(__file__).resolve().parents[2] + fixture = root / "checks/debug-producer" + logs = fixture / "target/producer-contract" + logs.mkdir(parents=True, exist_ok=True) + triple = args.arch + "-openkal" + manifest = tomllib.loads((root / "mcpp.toml").read_text(encoding="utf-8")) + assert manifest["platform"]["link"]["seal"] == [ + ["okld", "note", "{}"], ["okld", "place", "{}"], + ["okld", "check", "{}", "--with", "{needed}"], + ], "the build-program bootstrap retains its declarative seal" + + def command(label: str, words: list[str]) -> dict: + result = subprocess.run([args.mcpp, *words], cwd=fixture, text=True, encoding="utf-8", + stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=180) + (logs / (label + ".stdout")).write_text(result.stdout, encoding="utf-8") + (logs / (label + ".stderr")).write_text(result.stderr, encoding="utf-8") + assert result.returncode == 0, f"{label} failed with exit {result.returncode}" + return json.loads(result.stdout)["data"] + + environment = command("environment", ["self", "env", "--target", triple, "--format", "json"]) + selected_bin = Path(environment["toolchain"]["compiler"]).parent + rows = [] + for profile, switches in [("dev", []), ("inspect", ["--profile", "inspect"]), + ("release", ["--release"])]: + launch = command(profile, ["run", "--target", triple, *switches, + "--runner", "debuggee", "--print-launch"]) + image = Path(launch["program"]) + assert image.is_file() + assert launch["via"] == "runner" and Path(launch["argv"][0]).stem == "okrun" + assert len(launch["argv"]) == 3 and launch["argv"][1] == "--debugger" + assert Path(launch["argv"][2]).samefile(image) + ninja = (image.parent.parent / "build.ninja").read_text(encoding="utf-8") + matches = re.findall(r"platform-step seal --steps-hex ([0-9a-f]+)", ninja) + assert len(matches) == 1 + steps = [line.split("\t") for line in bytes.fromhex(matches[0]).decode().splitlines()] + assert len(steps) == 4, "the dynamic seal replaces rather than appends to the bootstrap" + assert Path(steps[0][0]).stem == "llvm-objcopy" + assert Path(steps[0][0]).parent.samefile(selected_bin), "objcopy belongs to the selected toolchain" + assert steps[0][1:] == ["--remove-section=.rela.*", "--remove-section=!.rela.debug_*", + "--remove-section=!.rela.dyn", "--remove-section=!.rela.plt", "{}"] + for step, expected in zip(steps[1:], [["note", "{}"], ["place", "{}"], + ["check", "{}", "--with", "{needed}"]]): + assert Path(step[0]).stem == "okld" and step[1:] == expected + sections = elf_sections(image) + assert all(not name.startswith(".rela.") or name.startswith(".rela.debug_") + or name in {".rela.dyn", ".rela.plt"} for name in sections), "ordinary static relocations are pruned" + if profile == "inspect": + assert sections.get(".debug_info", (0, 0))[1] > 0 + assert sections.get(".rela.debug_info", (0, 0))[0] == 4 + assert sections[".rela.debug_info"][1] > 0 + if profile == "release": + assert ".debug_info" not in sections and ".rela.debug_info" not in sections + env = os.environ.copy() + env.update(launch["env"]) + result = subprocess.run(launch["argv"], cwd=launch["cwd"], env=env, + capture_output=True, timeout=30) + assert result.returncode == 0, f"{profile} actual debuggee admission/execution failed" + rows.append({"profile": profile, "seal_steps": len(steps), "actual_debuggee_exit": result.returncode, + "retained_debug_relocations": any(name.startswith(".rela.debug_") for name in sections)}) + ordinary = command("ordinary", ["run", "--target", triple, "--profile", "inspect", + "--print-launch"]) + assert len(ordinary["argv"]) == 2, "the ordinary runner has no debuggee argument" + assert Path(ordinary["argv"][1]).samefile(ordinary["program"]) + env = os.environ.copy() + env.update(ordinary["env"]) + result = subprocess.run(ordinary["argv"], cwd=ordinary["cwd"], env=env, + capture_output=True, timeout=30) + assert result.returncode == 0 + receipt = {"profiles": rows, "ordinary_runner_exit": result.returncode, + "scope": "Real producer/ELF/seal/launch/native runner checks; native debugger frontends are qualified separately."} + (logs / "verified.json").write_text(json.dumps(receipt, indent=2) + "\n", encoding="utf-8") + print(json.dumps(receipt)) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3a46a3..a372a00 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ on: env: MCPP_SOURCE_REF: ${{ github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF || '' }} - MCPP_VERSION: 2026.11.3.1 + MCPP_VERSION: 2026.11.5.1 XLINGS_VERSION: v2026.10.26.1 XLINGS_NON_INTERACTIVE: '1' @@ -155,6 +155,11 @@ jobs: OKRUN_PLACE=0 "$okrun" "$image" one two | tee anywhere.log grep -q "image ok: 3 args" anywhere.log || { echo "::error::the image placed anywhere"; exit 1; } + - name: Debug relocations and the selected debuggee runner + run: | + py=python3; command -v python3 > /dev/null 2>&1 && python3 -c '' 2>/dev/null || py=python + "$py" .github/scripts/check-debug-producer.py --arch '${{ matrix.arch }}' + - name: ThinLTO keeps thread-local storage emulated working-directory: checks/lto run: | diff --git a/README.md b/README.md index 54532c5..5950139 100644 --- a/README.md +++ b/README.md @@ -32,12 +32,13 @@ mcpp pack --target x86_64-openkal --pkg-format okb # one file okrun runs | layers | `c-abi` openkal-musl (`posix`), `c++-abi` and `compiler-runtime` openkal-llvm-runtime | | tools | `okld`, `okrun`, `okprof` (openkal-tools) | | build | `-fPIC -femulated-tls -fvisibility=hidden -fasynchronous-unwind-tables`, stack-clash protection, `-ffunction-sections -fdata-sections`; frame pointers, `-fstack-protector-strong -mstack-protector-guard=global`, `-ftrivial-auto-var-init=zero`, libc++'s fast hardening (0.23.0); on x86_64 `-fcf-protection=full`; on aarch64 `-ffixed-x18 -mno-outline-atomics -mbranch-protection=pac-ret+bti` (not `standard`, which clang 23 counts the guarded control stack in) | -| link | `ld.lld`, entry `ok_main`, 64 KiB pages, relative relocations packed, `--gc-sections --icf=safe`, `--build-id=sha1`, the image notes produced by `okld note-source` | -| seal | `okld note`; `okld place`, the writable segments computed for the base the image's bytes choose (openkal 0.23); then `okld check --with` the shared images the program needs | +| link | `ld.lld`, entry `ok_main`, 64 KiB pages, relative relocations packed, `--gc-sections --icf=safe`, `--build-id=sha1`, retained debug relocations (0.24.0), the image notes produced by `okld note-source` | +| seal | the selected LLVM toolchain's `llvm-objcopy` prunes ordinary static relocations (0.24.0); `okld note`; `okld place`, the writable segments computed for the base the image's bytes choose (openkal 0.23); then `okld check --with` the shared images the program needs | | naming | `{name}.oke`, `lib{name}.okso` | | runner | `okrun {}`, on a machine of the image's architecture; `hosts` the systems and architectures okrun is built for, where a package that needs it can run | +| runner `debuggee` | `okrun --debugger {}` for a host debugger (0.24.0) | | carrier | openkal-run, section `.oke`, entry `carrier/main.cpp`: with nothing carried the runner of a package, with an image or a zip of images the program that runs them from memory | -| plugin | `build.mcpp` provides the `okb` package format through `okld bundle --package` | +| plugin | `build.mcpp` provides the `okb` package format through `okld bundle --package` and selects the toolchain program used by the image seal | | hosted | the toolchain and the layers for the host triples, over the system's openkal implementation | | versions | the release group of the openkal family | @@ -46,7 +47,8 @@ Compiling bitcode with `-femulated-tls` alone does not retain that choice at the final link. Both ordinary object files and ThinLTO builds use the platform's emulated thread-local storage. -Platform 0.23.7 selects openkal-musl 0.27.6 and openkal-llvm-runtime 0.18.7. +Platform 0.24.0 selects openkal-musl 0.28.0, openkal-llvm-runtime 0.19.0 +and image bindings 0.5.0 over the OpenKal 0.24 implementations. The C library calls the optional resolved-name operation directly, preserving its fallback, error and output-bound checks. @@ -54,6 +56,40 @@ A project overrides any group under `[target.]`, for example `[target.'cfg(platform = "openkal")'.layers] "c++-abi" = false`, and selects another platform, or none, with `platform = "..."` or `--platform`. +## Source debugging + +Platform 0.24.0 retains the relocation information used by image 0.5.0's +debugger object. Debug information follows the selected build profile, including +custom profiles; the seal preserves ELF zlib compression. A profile with +`debug = false` produces no source debug information. + +The `debuggee` runner supplies the command for a host debugger: + +```bash +mcpp run --target x86_64-openkal --runner debuggee --print-launch +mcpp run --target x86_64-openkal --profile inspect --runner debuggee --print-launch +``` + +The second command uses a custom profile declared as follows: + +```toml +[profile.inspect] +opt = 0 +debug = true +split_debuginfo = "off" +``` + +The launch descriptor includes the runner, `--debugger`, the image, working +directory and environment. A debugger starts that command. GDB reads the JIT +interface; an LLDB session enables it with +`settings set plugin.jit-loader.gdb.enable on` before starting the program. +This named runner is a debugged program, not a board debug-server command. + +The verified source-debugger combination is Linux x86-64 with the GDB and LLDB +bundled in CLion 2026.1.5.2. A release without DWARF retains symbol-level +breakpoints where its symbols remain. External debug sidecars and native +debugger frontends on other hosts are not qualified by this check. + ## What an image is observed and held by (0.23.0) Every frame of an image is a link of the frame-pointer chain, which the diff --git a/build.mcpp b/build.mcpp index 826cbb8..9f1fa93 100644 --- a/build.mcpp +++ b/build.mcpp @@ -19,6 +19,27 @@ import std; namespace pf = mcpp::plugins::platform; int main() { + if (std::string_view(mcpp::target()).ends_with("-openkal")) { + // Keep source relocations without making debug support depend on a + // profile's name. Prune ordinary static link relocations before the + // image's notes/placement are written; those appended bytes must not + // pass through objcopy afterwards. + // The API path is UTF-8, including on Windows. Preserve those bytes + // for the command and convert explicitly only for filesystem queries. + auto objcopy = std::format("{}/bin/llvm-objcopy", mcpp::toolchain_dir()); + if (!std::filesystem::is_regular_file(std::filesystem::u8path(objcopy))) objcopy += ".exe"; + if (!std::filesystem::is_regular_file(std::filesystem::u8path(objcopy))) { + std::println(std::cerr, "openkal-platform: the selected LLVM toolchain has no llvm-objcopy"); + return 1; + } + mcpp::seal(0, objcopy.c_str()); + for (const char* word : {"--remove-section=.rela.*", "--remove-section=!.rela.debug_*", + "--remove-section=!.rela.dyn", "--remove-section=!.rela.plt", "{}"}) + mcpp::seal(0, word); + for (const char* word : {"okld", "note", "{}"}) mcpp::seal(1, word); + for (const char* word : {"okld", "place", "{}"}) mcpp::seal(2, word); + for (const char* word : {"okld", "check", "{}", "--with", "{needed}"}) mcpp::seal(3, word); + } const char* stated_compress = std::getenv("MCPP_PACK_COMPRESS"); const std::string compress = stated_compress ? stated_compress : ""; if (!compress.empty() && !compress.starts_with("gzip") && !compress.starts_with("none")) { diff --git a/checks/debug-producer/mcpp.toml b/checks/debug-producer/mcpp.toml new file mode 100644 index 0000000..6b0bd27 --- /dev/null +++ b/checks/debug-producer/mcpp.toml @@ -0,0 +1,22 @@ +[package] +name = "debug-producer" +version = "0.1.0" + +[target.'cfg(os = "openkal")'] +platform = { path = "../.." } + +# This fixture measures the image producer, without recompiling the hosted +# C/C++ libraries for every profile. Its entry needs no runtime services. +[target.'cfg(os = "openkal")'.layers] +c-abi = false +"c++-abi" = false +compiler-runtime = false + +[targets.debug-producer] +kind = "bin" +main = "src/main.c" + +[profile.inspect] +opt = 0 +debug = true +split_debuginfo = "off" diff --git a/checks/debug-producer/src/main.c b/checks/debug-producer/src/main.c new file mode 100644 index 0000000..7ef72d6 --- /dev/null +++ b/checks/debug-producer/src/main.c @@ -0,0 +1,6 @@ +__attribute__((visibility("default"))) +int ok_main(void) { + volatile int answer = 40; + answer += 2; + return answer == 42 ? 0 : 1; +} diff --git a/mcpp.toml b/mcpp.toml index 2d42602..5273a55 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -12,7 +12,7 @@ [package] namespace = "mcpplibs" name = "openkal-platform" -version = "0.23.7" +version = "0.24.0" description = "The platform of ok images (-openkal): their toolchain, C library and C++ run-time, tools, compile and link conventions, seal, runner and carrier, as one versioned statement mcpp reads." license = "Apache-2.0" authors = ["mcpplibs"] @@ -28,26 +28,30 @@ toolchain = "llvm@23.1.3" runner = { cmd = ["okrun", "{}"], host = "same-arch", hosts = ["linux-x86_64", "linux-aarch64", "macos-aarch64", "windows-x86_64"] } # `mcpp pack --mode self-contained`: a program of the packing machine that # carries the image in a section and runs it through libokrun (M3). -carrier = { package = "openkal-run@0.4.4", section = ".oke", main = "carrier/main.cpp" } +carrier = { package = "openkal-run@0.5.0", section = ".oke", main = "carrier/main.cpp" } # The `okb` package format (`mcpp pack --pkg-format okb`): the images of every # architecture as one file okrun runs (openkal-image docs/okb.md). plugin = "build.mcpp" +# A program launched under a host debugger, distinct from a board debug server. +[platform.runners] +debuggee = ["okrun", "--debugger", "{}"] + # The C library and the C++ run-time inside an image. A package of the graph # that provides a layer takes its place (`provides`), and `false` leaves it out. [platform.layers] -compiler-runtime = "openkal-llvm-runtime@0.18.7" -"c++-abi" = "openkal-llvm-runtime@0.18.7" -c-abi = { name = "openkal-musl", version = "0.27.6", default-features = false, features = ["posix", "malloc-mimalloc"] } +compiler-runtime = "openkal-llvm-runtime@0.19.0" +"c++-abi" = "openkal-llvm-runtime@0.19.0" +c-abi = { name = "openkal-musl", version = "0.28.0", default-features = false, features = ["posix", "malloc-mimalloc"] } # Built for the building machine: `okld` writes and checks the notes, `okrun` # runs an image, `okprof` reads what a measured run recorded (0.23.0: the # order of a start's first executions, which a producer lays its code out by # and writes as the touch note). [platform.tools] -okld = "openkal-tools@0.4.4" -okrun = "openkal-tools@0.4.4" -okprof = "openkal-tools@0.4.4" +okld = "openkal-tools@0.5.0" +okrun = "openkal-tools@0.5.0" +okprof = "openkal-tools@0.5.0" # Every unit of an image (SPEC 12.2): # -fPIC the image is ET_DYN and has no text relocation; @@ -137,11 +141,14 @@ asmflags = ["-ffixed-x18", "-mno-outline-atomics", "-mbranch-protection=pac-ret+ driver = "lld" start = "ok_main" flags = ["-shared", "-z", "max-page-size=65536", "-z", "pack-relative-relocs", "-z", "now", "-z", "noexecstack", - "--hash-style=both", "--gc-sections", "--icf=safe", "--eh-frame-hdr", "--build-id=sha1", + "--hash-style=both", "--gc-sections", "--icf=safe", "--eh-frame-hdr", "--build-id=sha1", "--emit-relocs", "-mllvm", "-emulated-tls"] inputs = [{ from = ["okld", "note-source"], as = "ok-notes.S" }] exe = "{name}.oke" shared = "lib{name}.okso" +# The declarative seal is also available while preparing the platform's +# build program. Its program statement replaces this sequence with the selected +# toolchain's objcopy followed by the same note/place/check operations. seal = [["okld", "note", "{}"], ["okld", "place", "{}"], ["okld", "check", "{}", "--with", "{needed}"]] # A host triple selecting this platform (`x86_64-linux-gnu`, `aarch64-macos`, @@ -155,9 +162,9 @@ targets = ["x86_64-linux-gnu", "aarch64-linux-gnu", "x86_64-linux-musl", "aarc toolchain = "llvm@23.1.3" [platform.hosted.layers] -compiler-runtime = "openkal-llvm-runtime@0.18.7" -"c++-abi" = "openkal-llvm-runtime@0.18.7" -c-abi = { name = "openkal-musl", version = "0.27.6", default-features = false, features = ["posix", "malloc-mimalloc"] } +compiler-runtime = "openkal-llvm-runtime@0.19.0" +"c++-abi" = "openkal-llvm-runtime@0.19.0" +c-abi = { name = "openkal-musl", version = "0.28.0", default-features = false, features = ["posix", "malloc-mimalloc"] } # The family released together for openkal 0.17 (the 0.17.2 round: okb, the # runner's bundle and library interfaces, musl's renameat2; the 0.17.3 round: @@ -186,20 +193,20 @@ c-abi = { name = "openkal-musl", version = "0.27.6", default-features # relative relocations of an image packed): # one version of each in a build (their pins are exact). [platform.versions] -openkal = "0.23.0" -openkal-linux = "0.24.1" -openkal-macos = "0.21.3" -openkal-windows = "0.19.0" -openkal-musl = "0.27.6" -openkal-llvm-runtime = "0.18.7" -openkal-ld = "0.4.4" -openkal-run = "0.4.4" -openkal-tools = "0.4.4" +openkal = "0.24.0" +openkal-linux = "0.25.0" +openkal-macos = "0.22.0" +openkal-windows = "0.20.0" +openkal-musl = "0.28.0" +openkal-llvm-runtime = "0.19.0" +openkal-ld = "0.5.0" +openkal-run = "0.5.0" +openkal-tools = "0.5.0" # The plugin's tool, `okld bundle`, which writes the okb, and its library, the # provider of a package format (mcpp.plugins.platform). [build-dependencies] -openkal-tools = { version = "0.4.4", tools = ["okld"] } +openkal-tools = { version = "0.5.0", tools = ["okld"] } [build-dependencies.mcpp] plugins = { version = "0.20.0", features = ["plugins-platform"], host-module = true }