From 543e64d66d987e9b3689ea0efc1cdbeb2afc9531 Mon Sep 17 00:00:00 2001 From: swananan Date: Sun, 6 Sep 2026 11:49:20 +0800 Subject: [PATCH] fix: preserve bitfield width during integer promotion --- .../scalar_types_program.c | 32 +++++++++++ e2e-tests/tests/scalar_types_execution.rs | 54 +++++++++++++++++++ ghostscope-dwarf/src/semantics/c_integer.rs | 48 ++++++++++++++++- 3 files changed, 133 insertions(+), 1 deletion(-) diff --git a/e2e-tests/tests/fixtures/scalar_types_program/scalar_types_program.c b/e2e-tests/tests/fixtures/scalar_types_program/scalar_types_program.c index f534ed33..6477202e 100644 --- a/e2e-tests/tests/fixtures/scalar_types_program/scalar_types_program.c +++ b/e2e-tests/tests/fixtures/scalar_types_program/scalar_types_program.c @@ -255,9 +255,41 @@ __attribute__((noinline)) static void scalar_probe(int iter) &format_len); } +volatile int promotion_negative = -1; +volatile struct { + unsigned int u24:24; + unsigned int u25:25; + unsigned int u31:31; + unsigned int u32:32; +} promotion_fields = {1, 1, 1, 1}; +volatile _Bool promotion_expected24, promotion_expected25; +volatile _Bool promotion_expected31, promotion_expected32; +volatile int64_t promotion_expected_bitnot[4]; +volatile int64_t promotion_expected_division[4]; +volatile int64_t promotion_expected_remainder[4]; + int main(void) { int iter = 0; + /* The native C compiler supplies an independent promotion oracle. */ + promotion_expected24 = promotion_negative < promotion_fields.u24; + promotion_expected25 = promotion_negative < promotion_fields.u25; + promotion_expected31 = promotion_negative < promotion_fields.u31; + promotion_expected32 = promotion_negative < promotion_fields.u32; + + /* Widen only the results so the operands retain native integer promotions. */ + promotion_expected_bitnot[0] = ~promotion_fields.u24; + promotion_expected_bitnot[1] = ~promotion_fields.u25; + promotion_expected_bitnot[2] = ~promotion_fields.u31; + promotion_expected_bitnot[3] = ~promotion_fields.u32; + promotion_expected_division[0] = promotion_fields.u24 / promotion_negative; + promotion_expected_division[1] = promotion_fields.u25 / promotion_negative; + promotion_expected_division[2] = promotion_fields.u31 / promotion_negative; + promotion_expected_division[3] = promotion_fields.u32 / promotion_negative; + promotion_expected_remainder[0] = promotion_fields.u24 % promotion_negative; + promotion_expected_remainder[1] = promotion_fields.u25 % promotion_negative; + promotion_expected_remainder[2] = promotion_fields.u31 % promotion_negative; + promotion_expected_remainder[3] = promotion_fields.u32 % promotion_negative; while (iter < 20000) { scalar_probe(iter++); diff --git a/e2e-tests/tests/scalar_types_execution.rs b/e2e-tests/tests/scalar_types_execution.rs index 2e0a5bdf..fbaac425 100644 --- a/e2e-tests/tests/scalar_types_execution.rs +++ b/e2e-tests/tests/scalar_types_execution.rs @@ -7,6 +7,60 @@ use common::{init, OptimizationLevel, FIXTURES}; use std::path::Path; use std::time::Duration; +#[tokio::test] +async fn test_c_bitfield_integer_promotions_match_the_native_compiler() -> anyhow::Result<()> { + init(); + let binary = FIXTURES.get_test_binary("scalar_types_program")?; + let target = spawn_scalar_types_binary(&binary).await?; + let script = r#" +trace scalar_anchor { + print "PROMOTED:{}:{}:{}:{}", promotion_negative < promotion_fields.u24, promotion_negative < promotion_fields.u25, promotion_negative < promotion_fields.u31, promotion_negative < promotion_fields.u32; + print "NATIVE:{}:{}:{}:{}", promotion_expected24, promotion_expected25, promotion_expected31, promotion_expected32; +} +"#; + let (code, stdout, stderr) = run_ghostscope_with_script_for_target(script, 3, &target).await?; + target.terminate().await?; + assert_eq!(code, 0, "stderr={stderr} stdout={stdout}"); + assert!(stdout.contains("NATIVE:true:true:true:false"), "{stdout}"); + assert!(stdout.contains("PROMOTED:true:true:true:false"), "{stdout}"); + Ok(()) +} + +#[tokio::test] +async fn test_c_bitfield_integer_operators_match_the_native_compiler() -> anyhow::Result<()> { + init(); + let binary = FIXTURES.get_test_binary("scalar_types_program")?; + let target = spawn_scalar_types_binary(&binary).await?; + let script = r#" +trace scalar_anchor { + print "PROMOTED_BITNOT:{}:{}:{}:{}", ~promotion_fields.u24, ~promotion_fields.u25, ~promotion_fields.u31, ~promotion_fields.u32; + print "NATIVE_BITNOT:{}:{}:{}:{}", promotion_expected_bitnot[0], promotion_expected_bitnot[1], promotion_expected_bitnot[2], promotion_expected_bitnot[3]; + print "PROMOTED_DIVISION:{}:{}:{}:{}", promotion_fields.u24 / promotion_negative, promotion_fields.u25 / promotion_negative, promotion_fields.u31 / promotion_negative, promotion_fields.u32 / promotion_negative; + print "NATIVE_DIVISION:{}:{}:{}:{}", promotion_expected_division[0], promotion_expected_division[1], promotion_expected_division[2], promotion_expected_division[3]; + print "PROMOTED_REMAINDER:{}:{}:{}:{}", promotion_fields.u24 % promotion_negative, promotion_fields.u25 % promotion_negative, promotion_fields.u31 % promotion_negative, promotion_fields.u32 % promotion_negative; + print "NATIVE_REMAINDER:{}:{}:{}:{}", promotion_expected_remainder[0], promotion_expected_remainder[1], promotion_expected_remainder[2], promotion_expected_remainder[3]; +} +"#; + let (code, stdout, stderr) = run_ghostscope_with_script_for_target(script, 3, &target).await?; + target.terminate().await?; + assert_eq!(code, 0, "stderr={stderr} stdout={stdout}"); + for expected in [ + "BITNOT:-2:-2:-2:4294967294", + "DIVISION:-1:-1:-1:0", + "REMAINDER:0:0:0:1", + ] { + assert!( + stdout.contains(&format!("NATIVE_{expected}")), + "Expected native result {expected}. STDOUT: {stdout}" + ); + assert!( + stdout.contains(&format!("PROMOTED_{expected}")), + "Expected traced result {expected}. STDOUT: {stdout}" + ); + } + Ok(()) +} + async fn run_ghostscope_with_script_for_target( script_content: &str, timeout_secs: u64, diff --git a/ghostscope-dwarf/src/semantics/c_integer.rs b/ghostscope-dwarf/src/semantics/c_integer.rs index 100af39d..6222a690 100644 --- a/ghostscope-dwarf/src/semantics/c_integer.rs +++ b/ghostscope-dwarf/src/semantics/c_integer.rs @@ -9,6 +9,8 @@ use crate::TypeInfo; pub struct CIntegerComparisonType { pub size: u64, pub is_unsigned: bool, + /// Effective value width for bitfields; storage bytes alone lose promotion rules. + pub bitfield_width: Option, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -19,10 +21,14 @@ pub struct CIntegerComparisonPlan { impl CIntegerComparisonType { pub fn promoted(self) -> Self { - if self.size < 4 { + let fits_int = self.bitfield_width.map_or(self.size < 4, |width| { + width < 32 || (width == 32 && !self.is_unsigned) + }); + if fits_int { Self { size: 4, is_unsigned: false, + bitfield_width: None, } } else { self @@ -33,6 +39,7 @@ impl CIntegerComparisonType { Self { size: 8, is_unsigned: false, + bitfield_width: None, } } } @@ -48,6 +55,7 @@ pub fn c_integer_comparison_type(ty: &TypeInfo) -> Option Option c_integer_comparison_type(underlying_type).map(|mut ty| { ty.size = (*bit_size as u64).max(1).div_ceil(8); + ty.bitfield_width = Some(*bit_size); ty }), TypeInfo::TypedefType { @@ -145,6 +154,36 @@ mod tests { int_type("int", 4, crate::constants::DW_ATE_signed.0 as u16) } + #[test] + fn bitfield_promotions_use_value_width_at_the_int_boundary() { + for unsigned in [false, true] { + for width in [1, 24, 25, 31, 32] { + let bitfield = TypeInfo::BitfieldType { + underlying_type: Box::new(int_type( + if unsigned { "unsigned int" } else { "int" }, + 4, + if unsigned { + crate::constants::DW_ATE_unsigned.0 as u16 + } else { + crate::constants::DW_ATE_signed.0 as u16 + }, + )), + bit_offset: 0, + bit_size: width, + }; + let ty = c_integer_comparison_type(&bitfield).unwrap(); + assert_eq!(ty.is_unsigned, unsigned, "storage signedness is unchanged"); + let plan = usual_c_arithmetic_comparison_plan( + c_integer_comparison_type(&signed_int()).unwrap(), + ty, + ); + assert_eq!(plan.size, 4); + assert_eq!(plan.is_unsigned, unsigned && width == 32); + assert_eq!(ty.promoted().promoted(), ty.promoted()); + } + } + } + #[test] fn integer_comparison_type_handles_enums_and_bitfields() { let enum_type = TypeInfo::EnumType { @@ -162,6 +201,7 @@ mod tests { Some(CIntegerComparisonType { size: 4, is_unsigned: true, + bitfield_width: None, }) ); @@ -175,6 +215,7 @@ mod tests { Some(CIntegerComparisonType { size: 2, is_unsigned: false, + bitfield_width: Some(9), }) ); } @@ -187,6 +228,7 @@ mod tests { Some(CIntegerComparisonType { size: 1, is_unsigned: false, + bitfield_width: None, }) ); assert!(!is_c_signed_integer_type(&bool_type)); @@ -198,10 +240,12 @@ mod tests { let u8_type = CIntegerComparisonType { size: 1, is_unsigned: true, + bitfield_width: None, }; let i8_type = CIntegerComparisonType { size: 1, is_unsigned: false, + bitfield_width: None, }; assert_eq!( @@ -218,10 +262,12 @@ mod tests { let u64_type = CIntegerComparisonType { size: 8, is_unsigned: true, + bitfield_width: None, }; let i32_type = CIntegerComparisonType { size: 4, is_unsigned: false, + bitfield_width: None, }; assert_eq!(