Lesson 03 / 16
Signed Integers and Two's Complement
How negative numbers are encoded within a bit pattern, two's complement representation, and overflow behavior.
Contents
The previous two lessons treated numbers as non-negative. Hardware, however, has no extra place to hold a minus sign: a 32-bit register has only 32 bits. The sign must be encoded within the same pattern as the value itself.
This lesson asks how that encoding is done. More than one answer is possible, but hardware has converged on essentially a single answer, and the reason is that this method leaves the addition circuit completely unchanged.
Two Intuitive Approaches and Their Problems
In sign-magnitude representation, the leftmost bit carries the sign and the
remaining bits carry the magnitude. In eight bits, 0000 0101 is written for and
1000 0101 for . This representation is close to human intuition and has two
problems.
First, zero has two representations: 0000 0000 and 1000 0000. When two patterns
carry the same value, equality comparison stops being a matter of comparing bits.
Second, and more serious: addition is no longer plain addition. Computing
at the bit level gives 0000 0101 + 1000 0101 = 1000 1010; under the sign-magnitude
interpretation this pattern means , whereas the result should have been zero. The
hardware would need to strip out the signs, compare the magnitudes, and perform
subtraction.
In one’s complement representation, a negative number is obtained by flipping
every bit of the positive value: becomes 1111 1010. Addition comes closer to
plain addition, but the carry bit must be added back at the start; zero is again
represented twice (0000 0000 and 1111 1111).
Two’s Complement
Two’s complement representation eliminates both problems. Its definition fits in one sentence: the leftmost bit has a negative positional value. An -bit pattern has the value:
For eight bits, the leftmost bit has weight , and the others carry the usual . Examples:
| Pattern | Computation | Value |
|---|---|---|
0000 0101 |
||
1111 1011 |
||
1111 1111 |
||
1000 0000 |
||
0111 1111 |
Zero has a single representation: 0000 0000. The leftmost bit still gives away the
sign — if it is , the number is negative — but it is not a separate sign field; it
is part of the value.
The range of -bit two’s complement is:
The range is not symmetric: in eight bits, is representable but is not. There is one more value on the negative side, because zero consumes a pattern on the positive side.
Producing the Negative
Finding a number’s negative takes two steps: flip every bit, then add one.
For : 0000 0101 flip 1111 1010 add one 1111 1011.
The result is the pattern from the table.
Applying the same operation to returns to : 1111 1011
0000 0100 0000 0101. The operation is its own inverse.
Why the rule works can be seen as follows: the sum of a pattern and its bitwise inverse is the pattern with every bit set to , that is, . Adding one gives ; in an -bit register, the value corresponds to zero. Hence adding to “flip and add one” yields zero — which is exactly the definition of a negative.
There is a single exception: the smallest value. Applying the operation to
again yields 1000 0000. Since lies outside the range, this value has no
negative in eight bits.
Why Addition Is a Single Circuit
The decisive hardware advantage of two’s complement is this: signed addition is the same operation as unsigned addition. The bits are added, and the overflowing carry bit is discarded.
Computing in eight bits:
0000 0101 (5) + 1111 1101 (-3) ------------- 1 0000 0010 (carry bit discarded) 0000 0010 (2)
The result is correct, and the signs were never inspected. Subtraction, too, needs no separate circuit: computing is done by producing the negative of and adding it. Comparison operations are also built on the same adder.
This is a concrete example of a representation choice directly reducing hardware complexity. The representation closest to intuition (sign-magnitude) gives the most expensive circuit; the representation furthest from intuition (two’s complement) gives the cheapest one.
Overflow
Fixed width means results that fall outside the range cannot be represented. The result does not vanish; it wraps around from the other end of the range.
Under an unsigned interpretation, in eight bits, gives
1111 1111 + 0000 0001 = 1 0000 0000; discarding the carry leaves 0000 0000, that
is, . The number has not grown — it has wrapped around.
Under a signed interpretation, gives
0111 1111 + 0000 0001 = 1000 0000, and this pattern means . The sum of two
positive numbers has come out negative.
The difference between the two cases is nothing but a different interpretation of the same bit operation. Hardware reports both kinds of overflow with separate flags; which one is meaningful is decided by the program, which knows whether the value is being read as signed or unsigned.
The practical consequence of overflow is that numeric code can silently produce a wrong result. The sum of two large positive numbers coming out negative, loop counters wrapping around, and array indices pointing to unexpected locations all belong to this class. The security dimension of this behavior is treated separately in the cybersecurity curriculum.
Extension
When a value is moved into a wider type, the newly empty bits on the left must be filled. The rule for filling them depends on the interpretation, so that the value is preserved:
- For unsigned values, the left bits are filled with zero (zero extension).
- For signed values, the left bits are filled with a copy of the sign bit (sign
extension). The eight-bit pattern
1111 1011() becomes1111 1111 1111 1011in sixteen bits, and its value is preserved.
Applying the wrong rule silently changes the value: the eight-bit pattern, under
zero extension, becomes 0000 0000 1111 1011 in sixteen bits, that is, . In the
opposite direction, a sixteen-bit pattern read as unsigned gives . This
is one of the typical sources of error in code that converts between types of
different width.
The Shared Example under a Signed Interpretation
The leftmost bit of the course’s shared example, the 0x41424344 pattern, is —
the first hexadecimal digit is 4, that is, 0100. The pattern therefore carries the
same value under a 32-bit signed interpretation as it does under the unsigned one:
.
Had the first digit been 8 instead — that is, 0x81424344 — the leftmost bit would
be , and the same operations would give a very different value:
No example shows the distance between a bit sequence and its meaning better than this one: changing a single bit shifts the value by more than two billion.
Seeing It in Practice
Python’s integers are not fixed-width; they do not overflow, they grow. To observe fixed-width hardware behavior, masking is used:
def fixed_width(value: int, bits: int = 8) -> int: """Reduces a value to its bits-wide two's complement interpretation.""" mask = (1 << bits) - 1 # 0xFF for 8 bits value &= mask # discard excess bits sign_bit = 1 << (bits - 1) # 0x80 for 8 bits if value & sign_bit: value -= 1 << bits # the leftmost bit has negative weight return value print(fixed_width(0b11111011)) # -5 print(fixed_width(127 + 1)) # -128 (signed overflow) print(fixed_width(-1)) # -1 print((255 + 1) & 0xFF) # 0 (unsigned wraparound) print(fixed_width(0x41424344, 32)) # 1094861636 print(fixed_width(0x81424344, 32)) # -2126363836
The body of fixed_width is a direct translation of this lesson’s definition: the
bits that do not fit the width are discarded first, then negative weight is applied to
the leftmost bit.
Summary
- In a fixed-width representation, the sign is encoded within the pattern itself rather than in a separate field.
- In two’s complement representation the leftmost bit has a negative positional value; the range of bits is , and zero has a single representation.
- A number’s negative is produced by flipping every bit and adding one; the smallest value is the exception to this operation.
- Signed addition is performed by the same circuit as unsigned addition; this is the reason two’s complement is preferred.
- When the range is exceeded, the result does not vanish — it wraps around from the other end: under unsigned interpretation , under signed interpretation .
- The fill rule under extension depends on interpretation; for signed values the sign bit is copied.
Next Step
Integers are exact within the range they can represent: every value either exists or it does not. Fractional numbers are a different matter — an infinite number of real values must be packed into a finite number of patterns. The next lesson takes up how this is done, and why the sum does not come out to exactly .
To keep your progress and take notes, Log in
My notes
Log in to take notes.