06. Integers
Integers#
A computer does not store the abstract idea of the number 42. It stores a finite pattern of bits, and the type of the value tells us how that pattern should be interpreted. The same 32 bits could represent a signed integer, an unsigned integer, a floating-point number, a Unicode code point or even a MIPS instruction. Without context, a bit pattern is only a bit pattern.
This note develops the representation of integers from the ground up. The ideas here are what allow us to make sense of the values shown by mipsy, understand why overflow occurs and predict what the byte-loading instructions in Using MIPS will do. It follows the assessable conventions in the official COMP1521 integer notes, with additional explanation around the places where the representation tends to become confusing.
Positional Number Systems#
The decimal number 4705 is shorthand for:
Decimal is a base 10, or radix 10, number system. There are ten possible digits, 0 through 9, and the position of a digit determines the power of ten by which it is multiplied. There is nothing fundamentally special about ten; it is simply the base humans commonly use.
In a base system, a number with digits has the value:
Every digit must be between and . For example, 1216 is valid in base 7 because all of its digits are less than 7:
Warning
A sequence of digits does not fully specify a number unless its base is known. 10 means ten in decimal, two in binary, eight in octal and sixteen in hexadecimal.
Binary#
Computers commonly represent values using binary, or base 2. Binary has only two digits, 0 and 1; each binary digit is therefore called a bit. The place values are powers of two:
| Position | ||||
|---|---|---|---|---|
| Decimal value | 8 | 4 | 2 | 1 |
Consequently:
The bit on the far right has the smallest place value and is called the least significant bit (LSB). The bit on the far left has the largest place value and is called the most significant bit (MSB).
Converting binary to decimal#
To convert from binary to decimal, add the place values corresponding to the 1 bits. For example:
Converting decimal to binary#
One method is to repeatedly divide by two and record each remainder:
| Division | Quotient | Remainder |
|---|---|---|
| 12 | 1, the LSB | |
| 6 | 0 | |
| 3 | 0 | |
| 1 | 1 | |
| 0 | 1, the MSB |
Reading the remainders from bottom to top gives .
Another method is to subtract powers of two. The largest power of two no greater than 25 is 16, leaving 9. We can then use 8, leaving 1, before finally using 1. The selected place values are therefore 11001.
Hexadecimal#
Binary values become long very quickly. Hexadecimal, or base 16, gives us a compact notation which still exposes the underlying bits. Its digits are:
| Hex digit | Decimal | Four bits |
|---|---|---|
0 |
0 | 0000 |
1 |
1 | 0001 |
2 |
2 | 0010 |
3 |
3 | 0011 |
4 |
4 | 0100 |
5 |
5 | 0101 |
6 |
6 | 0110 |
7 |
7 | 0111 |
8 |
8 | 1000 |
9 |
9 | 1001 |
A |
10 | 1010 |
B |
11 | 1011 |
C |
12 | 1100 |
D |
13 | 1101 |
E |
14 | 1110 |
F |
15 | 1111 |
Since , exactly four bits correspond to one hexadecimal digit. To convert between binary and hexadecimal, group the bits into groups of four starting from the radix point:
1011 1100 0110 0001 0100 1110
B C 6 1 4 E
Thus .
Hexadecimal place values are powers of 16. For example:
Octal#
Octal, or base 8, uses the digits 0 through 7. Since , one octal digit represents exactly three bits. Octal is less common for general bit patterns than hexadecimal, but we will see it again when writing Unix file permissions.
111 010 = 72 in octal = 58 in decimal
The important grouping rule is therefore:
| Base | Prefix in C | Bits per digit |
|---|---|---|
| Binary | 0b |
1 |
| Octal | 0 |
3 |
| Decimal | none | not a power of two |
| Hexadecimal | 0x |
4 |
Integer Constants in C and MIPS#
C and mipsy accept several notations for an integer constant:
int a = 42; // decimal
int b = 0x2A; // hexadecimal
int c = 052; // octal
int d = 0b101010; // binary in the COMP1521 toolchainAll four variables contain the same value. The prefix changes how the source text is parsed; it does not permanently attach a base to the value in memory. Binary integer constants were only standardised recently and are not accepted by every older C compiler, although the COMP1521 toolchain accepts the form shown here.
The printf format specifier determines the notation used when displaying the value:
printf("%d\n", a); // 42
printf("%x\n", a); // 2a
printf("%o\n", a); // 52Warning
A leading zero makes an integer constant octal. 010 is eight, not ten. This is particularly easy to miss when formatting values such as months or days with leading zeroes.
Bits, Bytes and Integer Types#
A byte is the smallest independently addressable unit of memory. On the systems used in COMP1521, one byte contains eight bits. Common C type sizes on CSE are:
| Type | Bytes | Bits | Typical signed range |
|---|---|---|---|
char |
1 | 8 | implementation-dependent signedness |
short |
2 | 16 | to |
int |
4 | 32 | to |
long |
8 | 64 | to on CSE |
long long |
8 | 64 | to |
These sizes are common, but C does not require every machine to use them. sizeof(type) reports the size on the current system, whilst <limits.h> provides constants such as INT_MIN, INT_MAX and UINT_MAX.
When the precise width matters, <stdint.h> provides fixed-width types:
#include <stdint.h>
int8_t signed_byte;
uint8_t unsigned_byte;
int16_t signed_halfword;
uint32_t unsigned_word;
int64_t signed_doubleword;We use these types frequently in COMP1521 because uint32_t tells the reader considerably more than “some unsigned integer”. The GNU C Library's integer type documentation gives a broader overview of the exact-width types.
Why getchar returns an int#
getchar must represent every possible byte value and the special negative value EOF. A char may not have enough distinct values for both:
int c;
while ((c = getchar()) != EOF) {
putchar(c);
}If c were a signed 8-bit char, the byte 0xFF could become -1 and be mistaken for EOF. If plain char were unsigned, the real EOF value could instead be converted into a non-negative byte and never compare equal. The wider int return type avoids both problems.
Unsigned Integers#
An -bit unsigned integer assigns every bit a non-negative place value. Its range is:
An unsigned 8-bit integer therefore ranges from 0 to 255:
00000000 = 0
00000001 = 1
11111111 = 255
For a 32-bit unsigned int, the range on CSE is 0 to , or 4,294,967,295.
Unsigned wrap-around#
There are only possible patterns in an -bit unsigned type. Arithmetic performed in that unsigned type is defined modulo , so a result outside the range wraps around.
uint8_t x = 255;
x = x + 1; // 0
uint8_t y = 0;
y = y - 1; // 255There is one extra C detail in these uint8_t examples. Types narrower than int undergo integer promotion, so x + 1 and y - 1 are calculated as int on the CSE machines. The value then wraps when it is converted back to uint8_t by the assignment. With a 32-bit unsigned int, the arithmetic itself is already unsigned and wraps modulo .
This is not the computer “forgetting” to check for overflow. The modulo conversion and unsigned-arithmetic rules are specified by C and are useful when wrap-around is intentional. They are dangerous when an attacker can make a size calculation wrap into a much smaller value.
Signed Integers and Two's Complement#
Signed integers need to represent both positive and negative values. Modern systems use two's complement representation.
For a signed -bit value, the most significant bit has place value , whilst the remaining bits retain their ordinary positive place values:
This gives the range:
For eight bits:
01111111 = 127
00000001 = 1
00000000 = 0
11111111 = -1
11111110 = -2
10000000 = -128
There is one more negative value than positive value. In particular, -128 has no positive int8_t counterpart.
Interpreting a negative bit pattern#
If the leading bit of an -bit pattern is 1, interpret it as an unsigned number and subtract :
Alternatively, invert the bits and add one to obtain the magnitude:
11111011 representation of -5
00000100 invert every bit
00000101 add one -> magnitude 5
The same procedure transforms +5 into -5, because negation in two's complement is ~x + 1.
Why two's complement is useful#
Two's complement has only one representation of zero, and the same binary adder can handle both signed and unsigned addition. Consider 5 + (-2) using eight bits:
00000101
+ 11111110
----------
1 00000011
The ninth carry bit does not fit and is discarded, leaving 00000011, or 3. The hardware performed ordinary binary addition; the interpretation of the operands made it signed arithmetic.
Signed overflow#
The mathematical result of signed arithmetic may fall outside the representable range. In C, signed integer overflow is undefined behaviour. A compiler is permitted to assume it never occurs and optimise accordingly. This is importantly different from the defined modulo behaviour of unsigned arithmetic.
int x = INT_MAX;
int y = x + 1; // undefined behaviourOn a typical two's-complement processor the bits may appear to wrap to INT_MIN, but portable C must not rely upon that outcome. Before performing a potentially overflowing signed calculation, either prove that the result is in range, check the operands, or use an appropriate wider/unsigned representation.
Note
Checkpoint
- An unsigned -bit value ranges from to and wraps modulo .
- A two's-complement signed value uses the same bits but gives the top bit weight .
- The bits alone do not say whether they are signed; the type and operation supply that meaning.
The Same Bits, Different Meanings#
The pattern 11111111 illustrates why type information matters:
| Interpretation | Value |
|---|---|
uint8_t |
255 |
int8_t |
-1 |
| part of a UTF-8 stream | not a complete valid character by itself |
| an instruction field | depends on the instruction format |
A cast may change only the interpretation of existing bits, or it may also change the width and therefore require extension or truncation. You should always ask two questions:
- How many bits are present?
- Are those bits being interpreted as signed or unsigned?
Extending and Truncating Values#
MIPS general-purpose registers are 32 bits wide, but memory can hold byte and halfword values. Loading a smaller value therefore requires deciding what fills the unused upper bits.
Zero extension#
An unsigned value is zero-extended: zeroes are placed in all new upper positions.
8-bit value: 10000000 = 128 unsigned
32-bit result: 00000000 00000000 00000000 10000000
The MIPS instructions lbu and lhu load an unsigned byte or halfword and zero-extend it to 32 bits.
Sign extension#
A signed value is sign-extended: the original sign bit is copied into every new upper position.
8-bit value: 10000000 = -128 signed
32-bit result: 11111111 11111111 11111111 10000000
The MIPS instructions lb and lh treat the loaded value as signed and sign-extend it. Copying the sign bit preserves the value because each new leading 1 contributes the negative place value needed to balance the new positive place values.
| Instruction | Bytes loaded | Extension |
|---|---|---|
lb |
1 | sign extension |
lbu |
1 | zero extension |
lh |
2 | sign extension |
lhu |
2 | zero extension |
lw |
4 | none; the register is already 4 bytes |
Note
If the byte in memory is 0xCD, then lb produces 0xFFFFFFCD, whereas lbu produces 0x000000CD. The low eight bits are identical; only their extension and resulting interpretation differ.
Truncation#
Converting to a narrower type discards the high-order bits that do not fit:
32-bit value: 0x1234ABCD
low 16 bits: 0xABCD
low 8 bits: 0xCD
Truncation can change a value dramatically. It is safe only when the programmer knows that the discarded bits are unnecessary or deliberately wants a modulo- result.
Endianness#
Multi-byte values occupy several consecutive byte addresses. Endianness determines which byte is stored at the lowest address.
Suppose the 32-bit word 0x12345678 begins at address 0x1000:
| Address | Big-endian byte | Little-endian byte |
|---|---|---|
0x1000 |
0x12 |
0x78 |
0x1001 |
0x34 |
0x56 |
0x1002 |
0x56 |
0x34 |
0x1003 |
0x78 |
0x12 |
- Big-endian stores the most significant byte at the lowest address.
- Little-endian stores the least significant byte at the lowest address.
Mipsy-web uses little-endian memory. Consequently, this code loads 0x78 into $t1:
.text
main:
li $t0, 0x12345678
sw $t0, my_word
lbu $t1, my_word
.data
my_word:
.space 4Endianness changes the order of bytes in memory, not the order of bits within a byte and not the written order of hexadecimal digits in a register. If mipsy shows $t0 = 0x12345678, the register still contains that numeric value regardless of how a later sw arranges its bytes.
flowchart TB
W["Word: 0x12345678"] --> BE["Big-endian<br/>low to high: 12 34 56 78"]
W --> LE["Little-endian<br/>low to high: 78 56 34 12"]
Note
Checkpoint
- Widening preserves a value through zero extension or sign extension; narrowing discards high bits.
- Endianness changes byte order in memory, not the written order of bits inside a register.
- Before predicting a conversion, write down the source width, destination width, and signedness.
Integers as MIPS Instructions#
Every classic MIPS instruction is itself a 32-bit value stored in the text segment. The processor does not see the textual instruction add $s1, $t1, $t0; the assembler encodes its operation and operands into fields.
0x01288820
= 000000 01001 01000 10001 00000 100000
opcode rs rt rd shamt funct
For this R-type instruction:
opcode = 000000selects the R-type group;rs = 01001is register 9,$t1;rt = 01000is register 8,$t0;rd = 10001is register 17,$s1;shamt = 00000is the unused shift amount here;funct = 100000selects signedadd.
The resulting instruction is:
add $s1, $t1, $t0This is another example of the same principle: the bits acquire meaning only when we know the format through which they are being interpreted.
Common Mistakes#
- Treating a bit pattern as inherently signed or unsigned. Signedness belongs to the interpretation.
- Forgetting the width.
0xFFis -1 as anint8_t, but 255 as auint32_t. - Assuming signed overflow safely wraps. In C, it is undefined behaviour.
- Believing endianness reverses all the bits. It reorders bytes, not bits within each byte.
- Using
lbwhen the source byte is unsigned. Values from0x80to0xFFwill be sign-extended into negative 32-bit values. - Reading
010as decimal ten in C. It is an octal constant with the value eight. - Negating
INT_MIN. Its positive magnitude cannot be represented in the same signed type.
Practice#
- Convert to hexadecimal and decimal.
- Write
-42as an 8-bit two's-complement pattern. - Interpret
0xA7as bothuint8_tandint8_t. - A little-endian machine stores
0xCAFEBABEbeginning at address0x2000. Which byte appears at addresses0x2000through0x2003? - What values do
lbandlbuproduce when loading the byte0x80?
Note
- Answers
1011 0110is0xB6, or 182 decimal.42is00101010; invert and add one to obtain11010110.- It is 167 as
uint8_t, or asint8_t. BE,BA,FE,CArespectively.lbproduces0xFFFFFF80(-128);lbuproduces0x00000080(128).
The next step is to stop treating these bits merely as numbers and begin deliberately manipulating individual positions using Bitwise Operations.