From d10071d46ec075a394da49680dfee9c448c19741 Mon Sep 17 00:00:00 2001 From: tamnd <1218621+tamnd@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:24:27 +0700 Subject: [PATCH 1/2] Take the phase of a coverage record's version word from libgcov too rucc reads the release of the libgcov.a a coverage build links from the version word in the archive, but only took a word that ends in *, the mark of a release. The snapshot of GCC 16 that Ubuntu packages as gcc-16 ends its word in a space, so rucc fell back to B60* and libgcov refused the counts with "Version mismatch - expected 16.0 (experimental) (B60 ) got 16.0 (release) (B60*)". The scan now takes the phase GCC can write there as well, and the record copies it. --- crates/rucc-driver/src/compile.rs | 4 +++- crates/rucc-driver/src/gcov.rs | 35 ++++++++++++++++++++++--------- crates/rucc-opt/src/coverage.rs | 12 +++++++++-- crates/rucc-session/src/lib.rs | 6 +++--- 4 files changed, 41 insertions(+), 16 deletions(-) diff --git a/crates/rucc-driver/src/compile.rs b/crates/rucc-driver/src/compile.rs index 9fbec9508..2e31f65b2 100644 --- a/crates/rucc-driver/src/compile.rs +++ b/crates/rucc-driver/src/compile.rs @@ -1036,10 +1036,12 @@ fn coverage(opts: &Options, target: &TargetInfo, counts: &str) -> rucc_opt::cove rucc_target::ObjectFormat::MachO => (Some("__DATA,__mod_init_func,mod_init_funcs"), None), rucc_target::ObjectFormat::Wasm => (None, None), }; + let (major, minor, phase) = opts.gcov.unwrap_or((opts.gnuc.major, opts.gnuc.minor, b'*')); rucc_opt::coverage::Coverage { counts: counts.to_owned(), count: opts.profile_data.arcs, - gnuc: opts.gcov.unwrap_or((opts.gnuc.major, opts.gnuc.minor)), + gnuc: (major, minor), + phase, ctor: ctor.map(str::to_owned), dtor: dtor.map(str::to_owned), } diff --git a/crates/rucc-driver/src/gcov.rs b/crates/rucc-driver/src/gcov.rs index 464aa3543..0237bfcbf 100644 --- a/crates/rucc-driver/src/gcov.rs +++ b/crates/rucc-driver/src/gcov.rs @@ -10,21 +10,29 @@ //! instructions that compare a record against it and write it to the head of a `.gcda` file, so the //! word is in the bytes of the archive. The word is four characters, such as `B33*` for 13.3, and //! it is stored low byte first, which is how every target this links for stores a word. +//! +//! The last character is the phase. `gcc/genversion.cc` writes `*` for a release and a prerelease +//! and the first character of `DEV-PHASE` for anything else, which is `e` for a build of the trunk. +//! The snapshot of GCC 16 that Ubuntu packages as `gcc-16` writes a space there. libgcov compares +//! the whole word, so the phase is taken from the library too. -/// The major and minor GCC release of the `libgcov.a` with these bytes, or `None` when no version -/// word is in it. +/// The major and minor GCC release of the `libgcov.a` with these bytes and the phase that ends its +/// version word, or `None` when no version word is in it. /// /// The word that comes up most often is the answer. The archive has it several times, and a run of /// four bytes that only looks like one is not likely to come up as often. #[must_use] -pub(crate) fn release(archive: &[u8]) -> Option<(u32, u32)> { - let mut seen: Vec<((u32, u32), usize)> = Vec::new(); +pub(crate) fn release(archive: &[u8]) -> Option<(u32, u32, u8)> { + let mut seen: Vec<((u32, u32, u8), usize)> = Vec::new(); for quad in archive.windows(4) { - let &[b'*', minor @ b'0'..=b'9', units @ b'0'..=b'9', tens @ b'A'..=b'Z'] = quad else { + let &[phase, minor @ b'0'..=b'9', units @ b'0'..=b'9', tens @ b'A'..=b'Z'] = quad else { continue; }; + if !matches!(phase, b'*' | b' ' | b'a'..=b'z') { + continue; + } let major = u32::from(tens - b'A') * 10 + u32::from(units - b'0'); - let release = (major, u32::from(minor - b'0')); + let release = (major, u32::from(minor - b'0'), phase); match seen.iter_mut().find(|(known, _)| *known == release) { Some((_, count)) => *count += 1, None => seen.push((release, 1)), @@ -40,10 +48,17 @@ mod tests { #[test] fn the_release_is_the_version_word_the_library_holds_most() { // `B33*` low byte first is GCC 13.3, and `A95*` is 9.5. - assert_eq!(release(b"\x0f\x1f*33B\x90\x90*33B..*25B"), Some((13, 3))); - assert_eq!(release(b"cmp *59A"), Some((9, 5))); - // Lower case and a missing star are not a version word. - assert_eq!(release(b"*33b 33B* plain text"), None); + assert_eq!(release(b"\x0f\x1f*33B\x90\x90*33B..*25B"), Some((13, 3, b'*'))); + assert_eq!(release(b"cmp *59A"), Some((9, 5, b'*'))); + // A lower case letter and a missing phase are not a version word. + assert_eq!(release(b"*33b x3B: plain text"), None); + } + + #[test] + fn the_phase_of_a_build_that_is_not_a_release_is_kept() { + // The trunk writes `e`, and Ubuntu's snapshot of GCC 16 writes a space. + assert_eq!(release(b"\x0fe07B\x90e07B"), Some((17, 0, b'e'))); + assert_eq!(release(b"\x0f 06B\x90 06B\x90 06B"), Some((16, 0, b' '))); assert_eq!(release(b""), None); } } diff --git a/crates/rucc-opt/src/coverage.rs b/crates/rucc-opt/src/coverage.rs index 1ea9f06b7..6b8081deb 100644 --- a/crates/rucc-opt/src/coverage.rs +++ b/crates/rucc-opt/src/coverage.rs @@ -49,6 +49,9 @@ pub struct Coverage { /// built for the same version the headers were told about. The kernel's `gcc_4_7.c` picks its /// own copy of the layout by `__GNUC__`. pub gnuc: (u32, u32), + /// The last character of the version word: `*` for a release, and whatever the libgcov the + /// link takes was built with otherwise, such as `e` for a build of GCC's trunk. + pub phase: u8, /// The section the pointer to the constructor goes in, or `None` where the format has none. pub ctor: Option, /// The section the pointer to the destructor goes in. Where this is `None` and the constructor @@ -60,12 +63,13 @@ impl Coverage { /// The version word in the record and at the top of the `.gcda` file. /// /// Four characters, from `gcc/gcov-iov.cc`: the major version in tens as a letter from `A` - /// and in units as a digit, the minor version as a digit, and `*` for a release. + /// and in units as a digit, the minor version as a digit, and the phase. #[must_use] pub fn version(&self) -> u32 { let (major, minor) = self.gnuc; let letters = [b'A' as u32 + major / 10, b'0' as u32 + major % 10, b'0' as u32 + minor]; - letters.iter().fold(0, |word, &letter| (word << 8) | (letter & 0xff)) << 8 | u32::from(b'*') + letters.iter().fold(0, |word, &letter| (word << 8) | (letter & 0xff)) << 8 + | u32::from(self.phase) } /// How many merge functions the record holds, one per kind of counter the version knows. @@ -814,6 +818,7 @@ mod tests { counts: "a.gcda".to_owned(), count: true, gnuc: (major, minor), + phase: b'*', ctor: None, dtor: None, } @@ -825,6 +830,9 @@ mod tests { assert_eq!(claiming(13, 2).version().to_be_bytes(), *b"B32*"); assert_eq!(claiming(16, 0).version().to_be_bytes(), *b"B60*"); assert_eq!(claiming(9, 4).version().to_be_bytes(), *b"A94*"); + // A snapshot keeps the phase its libgcov was built with. + let snapshot = Coverage { phase: b' ', ..claiming(16, 0) }; + assert_eq!(snapshot.version().to_be_bytes(), *b"B60 "); } #[test] diff --git a/crates/rucc-session/src/lib.rs b/crates/rucc-session/src/lib.rs index 0260f2b7d..ef7aee315 100644 --- a/crates/rucc-session/src/lib.rs +++ b/crates/rucc-session/src/lib.rs @@ -2670,13 +2670,13 @@ pub struct Options { /// `-target x86_64-pc-windows-msvc`: the SDK headers take a path for `__GNUC__` that they /// were never tested on with the MSVC runtime. The flag is the way to ask, as it is in clang. pub gnuc_given: bool, - /// The GCC release of the `libgcov.a` the link takes, as the major and the minor version, when - /// a coverage build found one. + /// The GCC release of the `libgcov.a` the link takes, as the major and the minor version and + /// the phase that ends its version word, `*` for a release, when a coverage build found one. /// /// libgcov writes the counts of a record only when the record has its own release, and /// [`Options::gnuc`] is the newest release while the GCC on the machine is often older. So the /// record follows this when it is set, and follows [`Options::gnuc`] when it is not. - pub gcov: Option<(u32, u32)>, + pub gcov: Option<(u32, u32, u8)>, /// The GNU assembler release claimed, from `-fgnu-as-version=`, which is what /// `-Wa,--version` prints. pub gnu_as: GasVersion, From 18d2dfb6cc4e2e9b2b3b9be09c9b5f8520e1d6f7 Mon Sep 17 00:00:00 2001 From: tamnd <1218621+tamnd@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:24:42 +0700 Subject: [PATCH 2/2] changelog: #3713 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93cc8faa8..1a039b94e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ All notable changes are recorded here. The format follows [Keep a Changelog](htt - A structure passed by value on i386 is copied into the outgoing area a word at a time, so a large one no longer has memcpy's own arguments written over the ones already stored. 00204 of c-testsuite printed garbage for the first of two large structures (#3710). - `tests/tools/perf.sh` skips a call graph that perf cannot record on the machine, with a line that gives perf's reason, rather than stopping with exit 255. The GitHub arm runner refuses `--call-graph=dwarf` this way (#3711). - Three tests that failed only on a Windows host hold there: two compared paths as strings and missed one joined with a `\`, and six lane checks in `wasm_simd_lanes` linked for a host that has no sysroot in the test job (#3712). +- A program built with `--coverage` against a snapshot GCC's `libgcov.a`, such as Ubuntu's `gcc-16` package, writes its counts. rucc took the release from the library's version word but always ended the word in `*`, the mark of a release, and libgcov refused a record whose phase did not match its own (#3713). ## 0.30.8