Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions .github/scripts/check-debug-producer.py
Original file line number Diff line number Diff line change
@@ -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("<Q", data, 40)[0]
width, count, strings = struct.unpack_from("<HHH", data, 58)
assert width == 64 and 0 < strings < count and offset + width * count <= len(data)
headers = [struct.unpack_from("<IIQQQQIIQQ", data, offset + i * width) for i in range(count)]
names_header = headers[strings]
names = data[names_header[4]:names_header[4] + names_header[5]]
assert len(names) == names_header[5]
result = {}
for header in headers:
start = header[0]
assert start < len(names)
end = names.find(b"\0", start)
assert end >= 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()
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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: |
Expand Down
44 changes: 40 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand All @@ -46,14 +47,49 @@ 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.

A project overrides any group under `[target.<selector>]`, 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
Expand Down
21 changes: 21 additions & 0 deletions build.mcpp
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
Expand Down
22 changes: 22 additions & 0 deletions checks/debug-producer/mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"
6 changes: 6 additions & 0 deletions checks/debug-producer/src/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
__attribute__((visibility("default")))
int ok_main(void) {
volatile int answer = 40;
answer += 2;
return answer == 42 ? 0 : 1;
}
51 changes: 29 additions & 22 deletions mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
[package]
namespace = "mcpplibs"
name = "openkal-platform"
version = "0.23.7"
version = "0.24.0"
description = "The platform of ok images (<arch>-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"]
Expand All @@ -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;
Expand Down Expand Up @@ -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`,
Expand All @@ -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:
Expand Down Expand Up @@ -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 }
Loading