# Two's Complement: Why -1 Is All Ones

> Signed integers could have been encoded a dozen ways. Hardware settled on two's complement because it makes the adder ignorant of sign — one circuit for everything. The same choice explains INT_MIN, sign extension bugs, and why abs() can return a negative number.

URL: https://harshith.in/blog/twos-complement/
Author: Harshith Sunku (Staff Engineer) — https://harshith.in/about/
Tags: fundamentals, hardware, systems

Print `(int)0xFFFFFFFF` and you get -1. Every bit set, and the value is negative
one. New systems programmers memorize this as a fact; it's better understood as a
*decision* — arguably the best-aggregated design decision in hardware history —
and once you see why, a family of famous bugs (INT_MIN, sign extension, `abs()`
returning negative) stops being trivia and starts being obvious.

## The Problem: Adders Don't Want to Know About Signs

The naive encoding is sign-magnitude: one sign bit, the rest magnitude. It has two
zeros (+0 and -0) and, worse, addition needs special cases — adding a positive and
a negative means *subtracting magnitudes and comparing*, a different circuit path.

Two's complement instead defines the top bit as having **negative weight**. For
8 bits, the place values are:

```text
   bit:      7     6    5    4    3   2   1   0
   weight: -128   64   32   16    8   4   2   1

   11111111 = -128 + 64+32+16+8+4+2+1 = -1
   10000000 = -128                     = INT8_MIN
   01111111 =  127                     = INT8_MAX
```

That's the whole system. Everything else is a consequence.

## Why Hardware Loves It

The payoff: **addition is the same circuit for signed and unsigned**. Add the bit
patterns, discard the carry out, done. `0xFF + 0x01 = 0x00` reads as
255 + 1 = 0 (unsigned, wrapped) or -1 + 1 = 0 (signed) — both correct, same gates.
Subtraction is addition too: negate means invert-all-bits-plus-one
(`-x = ~x + 1`), so `a - b` is `a + ~b + 1`, which is just the adder with an
inverted input and carry-in set. One ALU path handles signed add, unsigned add,
and subtract. Only *comparisons* and *right shifts* need to know signedness —
which is exactly where the instruction set splits (`jl` vs `jb`, `sar` vs `shr`).

There's also exactly one zero, and comparisons against it are a single bit test
away from "is negative."

## The Sharp Edges

The asymmetry is baked in: the range is -128..127, not ±127, because zero takes a
slot on the positive side. Consequences with real CVE counts attached:

- **`-INT_MIN == INT_MIN`.** Negating -2147483648 should give +2147483648, which
  doesn't fit; invert-plus-one wraps back to itself. So `abs(INT_MIN)` returns a
  negative number, and `INT_MIN / -1` traps (SIGFPE on x86 — the one division
  that overflows).
- **Sign extension.** Widening a signed value must replicate the top bit
  (`0xFF` int8 → `0xFFFFFFFF` int32, both -1). Mixing signed and unsigned types
  makes C do this when you didn't expect it — the classic being `char` (signed on
  x86) holding byte 0x80+, compared against an int, suddenly negative. Parsers
  reading "lengths" from `char` buffers have shipped this bug for decades.
- **Signed overflow is UB in C/C++.** The *hardware* wraps predictably; the
  *language* declares overflow undefined so compilers can assume it never happens
  (e.g., folding `x + 1 > x` to true, which deletes overflow checks written in
  exactly that style). Write checks as `x > INT_MAX - y` — test *before* the
  operation — or use `__builtin_add_overflow`.
- **Unsigned "negative" indexes.** `size_t i; ... i - 1` when i is 0 gives
  18446744073709551615, not -1, and `for (size_t i = n - 1; i >= 0; --i)` never
  terminates. The pattern-matched fix: `for (size_t i = n; i-- > 0;)`.

Since C23 (and effectively forever in practice), two's complement is the *only*
representation the C standard permits — the ones'-complement machines are museum
pieces. The UB rules, however, remain.

## Takeaways

- Two's complement = top bit has negative weight. `-x = ~x + 1` falls out.
- It won because signed and unsigned share the adder; only compares and right
  shifts are sign-aware.
- The range is asymmetric: `abs(INT_MIN)` is negative, `INT_MIN / -1` traps.
- Sign extension during implicit conversions is where the quiet bugs live —
  never parse bytes through plain `char`.
- Hardware wraps; C calls it UB anyway. Check for overflow before the operation,
  not after.
