signed values implicitly cast to unsiged → even for comparison operators

  • -1 > 0U because -1 cast to unsigned ist umax.

Common mistake is passing a signed int into a function that expects unsigned → memory leak or vuln.

Sign extension:

  • to extend from w to w+k bits, make k copies of the sign bit (MSB).
  • two’s complement works that way → we add -127 (or whatever the minus is) + … those bits.
    • by induction → extending by 1 does since the top 1 is now a large +
    • this gives me .
      unsigned is always zero extension!

Unsigned: Adding numbers is always with carry truncated → in C at least (in assembly, you can recover carry flag)

  • addition in C is an abelian group (since it’s mod addition)
    Signed: same carry truncation happens
  • (int) ((uint) u + (uint) v) == u + v same thing!
  • you can overflow and underflow
    • this happens when there is not carry into the MSB and it’s suddenly positive

Multipliation:

  • Unsigned:
    • we need twice the number of bits after multiplication
    • discard the top bits
    • for w-long ints
    • it’s a commutative ring with unsigned addition
  • Signed also discards the top half
    • the lower half is exactly the same as the unsigned mult!
    • it works with the - * + case as well, it automatically gives you the MSB bit set

Multiplication is much cheaper than division → we can shift and multiply with special numbers to divide, which is much faster! (Note, this mostly only works on unsigned numbers!!!)

  • shifting is multiplying or dividing by 2
  • (u << 5) - (u << 3) == u * 24
    • because 32 - 8 = 2
    • TODO: why does this not overflow sometimes????
  • right shift is floored division (euclidean)

With signed integers, division gets more complicated → the MSB is also shifted somewhere → we need arithmetic shift commands!

  • arithmetic shifting fills the left with 1s, as seen before this compensates
  • but! it rounds the wrong direction with u < 0!
  • we want to round towards 0!!!
    • so we want but do
    • compiler does this automatically by checking for negatives

2.1. Bit-Level Representation (CS:APP 2.1)

2.1.1. Byte Ordering

Endianness

A multi-byte object is stored at consecutive addresses, and its address is that of the lowest byte. Little-endian (x86) stores the least significant byte first; big-endian stores the most significant byte first.

Example: int x = 0x01234567 at address 0x100:

  • little-endian: 0x100: 67 45 23 01
  • big-endian: 0x100: 01 23 45 67

To detect the byte order we cast an int to (char *). Then we can access individual bytes.

2.1.2. Bitwise and Logical Operators

Bitwise vs Logical Operators

& | ^ ~ act on each bit position independently. && || ! treat any nonzero value as true, return 0 or 1, and short-circuit.

Example (8-bit):

  • 0x69 & 0x55 → 0x41
  • 0x69 && 0x55 → 1
  • ~0x41 → 0xBE
  • !0x41 → 0

Bit of x:

  • set: x | (1u << k)
  • clear: x & ~(1u << k)
  • toggle: x ^ (1u << k)
  • test: (x >> k) & 1

2.1.3. Shifts

Shift Operations

We can shift either right or left:

  • left shift:
    • x << k drops the top bits and fills with 0 from the right.
  • right shift:
    • A logical right shift fills with 0
    • An arithmetic right shift fills with copies of the MSB

Note: an arithmetic shift occurs in C for signed types, not unsigned. Otherwise a logical shift is used.

Example: For x = 10100010:

  • x << 3 → 00010000
  • logical x >> 2 → 00101000
  • arithmetic x >> 2 → 11101000

2.2. Integer Encodings (CS:APP 2.2)

2.2.1. Unsigned and Two’s Complement

Encodings

For a bit vector :

The MSB of a two’s-complement number has negative weight (e.g. for ).

Ranges and Identities

Note: Identities

    • → asymmetric range (-128 to 127 for 8 bits)
  • .
  • The pattern of all ones is unsigned and signed.

Example

2.2.2. Signed–Unsigned Conversion

T2U and U2T

Casting between signed and unsigned values of the same width keeps the bits, and

Proof: For the same bit vector, , and exactly when the signed value is negative.

  • the shift by +TMAX only happens when the sign bit is 1!

Example:

Implicit Conversion:

Example: for (unsigned i = n-1; i >= 0; i--) never terminates because unsigned >= 0 is always true!


TODO

2.2.3. Sign Extension and Truncation

Sign Extension

Extending a -bit two’s-complement number to bits by prepending copies of the MSB preserves its value. Unsigned numbers are always zero-extended.

Proof: By induction on , so it suffices to show . If , nothing changes. If , the old MSB changes weight from to and the new MSB contributes . The net change in those two positions is , which equals the old contribution.

Example: 1011 becomes 11111011 , while unsigned 1011 becomes 00001011. For short sx = -12345;, the cast (unsigned) sx first sign-extends to int and then reinterprets: .

Truncation

Truncating to its low bits gives

Example: (short) 53191 .

2.3. Integer Arithmetic (CS:APP 2.3)

2.3.1. Addition

Unsigned Addition

. The carry out of the MSB is discarded in C; in assembly it survives in the carry flag CF.

Group Structure

is an abelian group with identity 0 and inverse for (the inverse of 0 is 0).

Proof: It is with canonical representatives, so closure, associativity and commutativity are inherited.

Two's-Complement Addition

, i.e. the same adder circuit. Hence (int)((unsigned)u + (unsigned)v) == u + v. For :

Overflow Detection

  • Signed: overflow occurs iff and have the same sign and the result has the other sign. Equivalently, the carry into the MSB differs from the carry out of it.
  • Unsigned: overflow occurs iff .

Example ():

SumBitsResultCase
0101 + 0101 = 1010positive overflow
1000 + 1011 = (1)0011negative overflow
unsigned

Signed overflow is UB in ISO C. The course models it as wraparound (as with gcc -fwrapv), but optimizers may assume it never happens, e.g. by folding x + 1 > x to true.

2.3.2. Negation

Two's-Complement Negation

for all , including .

Proof: is the all-ones pattern, i.e. ; add 1 to both sides. For : 100..0 011..1 100..0.

Example: 0101 1010 1011 . This explains 1 + (a << 3) + ~a .

2.3.3. Multiplication

Truncated Multiplication

The exact product of two -bit numbers needs up to bits; C keeps only the low :

and form the commutative ring .

Bit-Level Equivalence

and produce identical bit patterns: . One imul instruction therefore serves both.

Proof: , so . Both results are the same residue, reinterpreted.

Example (, 101 011):

InterpretationExact productLow 3 bits
unsigned 001111111
signed 110111111

Mixed signs come out right (MSB set) as long as the true product fits in bits.

2.3.4. Multiplication by Constants

Shift as Multiplication

For signed and unsigned : x << k . Hence, if has a run of ones from bit down to bit :

A shift and an add take about 1 cycle each; imul takes about 3.

Intermediate Overflow Cancels

(x << 5) - (x << 3) == x * 24 holds for all , even when x << 5 overflows.

Proof: Every operation is in , so . x * 24 is reduced modulo as well, so both sides are the same residue, wraparound included.
→ It gives the correct answer, as long as the answer is representable!

  • note this works since two’s complement works modulo for left shifts!

Example:

ProductShift form
11000(x << 5) - (x << 3)
(x << 3) - x
(x << 4) - (x << 1)
(HS22 option d)(a << 4) + (a << 2) + (a << 1)

2.3.5. Division by Powers of Two

Unsigned Division by Shift

For unsigned : u >> k , using a logical shift.

Proof: . The shift keeps and drops , which is exactly the remainder.

Signed Division by Shift

For signed , the arithmetic shift gives x >> k , which rounds toward . C requires rounding toward 0, i.e. for . For :

Ceiling via Floor

For integers and : .

Proof: Write with . If , both sides equal . Otherwise , so the right side is .

Example: . Then -12340 >> 4 , which is wrong, while (-12340 + 15) >> 4 , which is correct. The compiler emits the sign test itself: ((a < 0) ? a + 3 : a) >> 2 == a / 4.

For other constants, compilers multiply by a reciprocal and shift. For example, unsigned 32-bit x / 3 becomes ((uint64_t)x * 0xAAAAAAAB) >> 33.