When multiword arithmetic uses ADC/SBC, Pseudo C drops the carry or borrow computed from the low halves. The high-half result consequently ignores an essential input.
repro(1, 2, UINT64_MAX, 1) should return 4, but the displayed addition returns 3. repro_borrow(5, 2, 0, 1) should return 2, but the displayed subtraction returns 3.
Original C
#include <stdint.h>
uint64_t repro(uint64_t a, uint64_t b, uint64_t low_a, uint64_t low_b)
{
// Add two 128-bit integers and return the high half.
// Adding UINT64_MAX + 1 in the low half must carry into a + b.
__uint128_t lhs = ((__uint128_t)a << 64) | low_a;
__uint128_t rhs = ((__uint128_t)b << 64) | low_b;
return (lhs + rhs) >> 64;
}
uint64_t repro_borrow(uint64_t a, uint64_t b, uint64_t low_a, uint64_t low_b)
{
// Subtract two 128-bit integers and return the high half.
// Subtracting 1 from 0 in the low half must borrow from a - b.
__uint128_t lhs = ((__uint128_t)a << 64) | low_a;
__uint128_t rhs = ((__uint128_t)b << 64) | low_b;
return (lhs - rhs) >> 64;
}
Observed Pseudo C
Comments below annotate the captured output; the expressions themselves are unchanged.
uint64_t repro(uint64_t a, uint64_t b, uint64_t low_a, uint64_t low_b) __pure
{
// BUG: the low-half addition carries 1, but that input is absent.
return a + b;
}
uint64_t repro_borrow(uint64_t a, uint64_t b, uint64_t low_a, uint64_t low_b) __pure
{
// BUG: the low-half subtraction borrows 1, but that input is absent.
return a - b - 0;
}
The corresponding AArch64 code is 28 bytes across the two functions:
repro:
cmn x2, x3 // Compute the carry from low_a + low_b.
adc x0, x0, x1 // Include that carry in the high-half addition.
ret
repro_borrow:
cmp x2, x3 // Determine whether the low-half subtraction borrows.
sub x8, x0, x1
sbc x0, x8, xzr // Subtract the borrow from the high-half result.
ret
HLIL retains both inputs: return adc.q(a, b, low_a + low_b u< low_a) and return sbb.q(a - b, 0, low_a u< low_b). Those carry/borrow inputs disappear in Pseudo C.
15-carry-borrow.zip
When multiword arithmetic uses ADC/SBC, Pseudo C drops the carry or borrow computed from the low halves. The high-half result consequently ignores an essential input.
repro(1, 2, UINT64_MAX, 1)should return 4, but the displayed addition returns 3.repro_borrow(5, 2, 0, 1)should return 2, but the displayed subtraction returns 3.Original C
Observed Pseudo C
Comments below annotate the captured output; the expressions themselves are unchanged.
The corresponding AArch64 code is 28 bytes across the two functions:
HLIL retains both inputs:
return adc.q(a, b, low_a + low_b u< low_a)andreturn sbb.q(a - b, 0, low_a u< low_b). Those carry/borrow inputs disappear in Pseudo C.15-carry-borrow.zip