Everything a processor stores and computes is a fixed-width binary number. A RISC-V register holds exactly 32 bits, no more and no fewer, and every addition, comparison and shift works on exactly 32 bits. How those bits are read, as a positive number, a negative number, an address or a letter, is not stored anywhere; it is decided by the instruction that uses them. This article builds up the number formats a processor works with, from binary and hexadecimal through two's complement for negative numbers to overflow and sign extension, and shows each of them in real instructions of programs compiled for RISC-V.
13 min read | Beginner
What You'll Learn
- Convert between binary, hexadecimal and decimal for 32-bit values
- Explain two's complement and why processors use it for negative numbers
- Recognize overflow and predict how fixed-width arithmetic wraps around
- Sign-extend and zero-extend a small field to 32 bits, and say when each is right
Binary: Place Values in Base 2
A binary number is written with only the digits 0 and 1, and each position is worth twice the one to its right. In the 8-bit number 0100 1000, reading from the right, the positions are worth 1, 2, 4, 8, 16, 32, 64 and 128. The 1 bits sit at the 8 and 64 positions, so the value is 64 + 8 = 72. That is also the character code of the capital letter H, which is why a greeting that starts with "Hello" starts with the byte 0100 1000 in memory.
The positions are numbered from 0 at the right. Bit 0 is the least significant bit, worth 1; in a 32-bit value, bit 31 is the most significant bit, worth 231 = 2,147,483,648. With n bits there are 2n different patterns: 256 for 8 bits, 65,536 for 16 bits and 4,294,967,296 for 32 bits. The Digital Fundamentals tutorial covers binary and decimal conversion and binary arithmetic at the circuit level.
Hexadecimal: A Shorthand for Binary
Writing 32 binary digits is slow and error-prone, so engineers use hexadecimal (base 16). Hex uses the digits 0-9 and then A-F for the values 10 to 15. Its convenience is that one hex digit stands for exactly four bits, so conversion is done four bits at a time, without arithmetic:
| Binary | Hex | Decimal | Binary | Hex | Decimal | |
|---|---|---|---|---|---|---|
| 0000 | 0 | 0 | 1000 | 8 | 8 | |
| 0001 | 1 | 1 | 1001 | 9 | 9 | |
| 0010 | 2 | 2 | 1010 | A | 10 | |
| 0011 | 3 | 3 | 1011 | B | 11 | |
| 0100 | 4 | 4 | 1100 | C | 12 | |
| 0101 | 5 | 5 | 1101 | D | 13 | |
| 0110 | 6 | 6 | 1110 | E | 14 | |
| 0111 | 7 | 7 | 1111 | F | 15 |
A 32-bit value is therefore 8 hex digits. A typical first instruction of a RISC-V program is stored as 0x00010137; the prefix 0x marks hexadecimal. Split into nibbles (groups of four bits), it is 0000 0000 0000 0001 0000 0001 0011 0111. Addresses, register values and instruction encodings are almost always written in hex, often with an underscore in the middle, such as 0xF000_0004, to make the eight digits easier to read.
Quick Check
Q: What is 0x2C in binary and in decimal?
Show Answer
2 is 0010 and C is 1100, so 0x2C = 0010 1100. In decimal it is 2 x 16 + 12 = 44.
Unsigned Numbers
The simplest way to read 32 bits is as an unsigned number: just add up the place values. The range is 0 (all zeros) to 4,294,967,295 (all ones, 0xFFFFFFFF). Addresses are always unsigned: address 0xF000_0004 is simply a large positive number. Unsigned arithmetic is the arithmetic of a car's odometer: it counts up, and after the largest value it rolls over to zero.
Negative Numbers: Two's Complement
Programs also need negative numbers. Almost every processor represents them in two's complement, and it helps to see it in 4 bits, where there are only 16 patterns:
| Bits | Unsigned | Two's complement | Bits | Unsigned | Two's complement | |
|---|---|---|---|---|---|---|
| 0000 | 0 | 0 | 1000 | 8 | -8 | |
| 0001 | 1 | 1 | 1001 | 9 | -7 | |
| 0010 | 2 | 2 | 1010 | 10 | -6 | |
| 0011 | 3 | 3 | 1011 | 11 | -5 | |
| 0100 | 4 | 4 | 1100 | 12 | -4 | |
| 0101 | 5 | 5 | 1101 | 13 | -3 | |
| 0110 | 6 | 6 | 1110 | 14 | -2 | |
| 0111 | 7 | 7 | 1111 | 15 | -1 |
The rule is that the most significant bit is worth a negative amount: in 4 bits, bit 3 is worth -8 instead of +8. So 1111 = -8 + 4 + 2 + 1 = -1, and 1000 = -8. Numbers with the top bit 0 are zero or positive and look the same as unsigned; numbers with the top bit 1 are negative. For that reason the top bit is called the sign bit. In 32 bits the same rule gives a range of -2,147,483,648 to 2,147,483,647, and -1 is all ones, 0xFFFFFFFF.
To negate a number, invert every bit and add 1. Starting from 3 = 0011: inverting gives 1100, adding 1 gives 1101, which the table shows is -3. The one's and two's complement article in Digital Fundamentals works through more examples.
The reason every processor uses this representation is that addition works without knowing the sign. Add -3 (1101) and 5 (0101) with an ordinary binary adder: the sum is 1 0010, and dropping the fifth bit leaves 0010 = 2, which is correct. The same adder gives the right unsigned answer too, so a processor needs only one adder for both kinds of number, and subtraction is just addition of a negated value.
The Instruction Decides the Meaning
Because the bits are the same, a register does not know whether it holds a signed or an unsigned number. Where the two interpretations give different answers, RISC-V has separate instructions:
| Operation | Signed version | Unsigned version | Example where they differ |
|---|---|---|---|
| compare and branch | blt, bge | bltu, bgeu | 0xFFFFFFFF < 1? Signed: yes (-1 < 1). Unsigned: no. |
| set if less than | slt, slti | sltu, sltiu | same as above |
| shift right | sra (arithmetic) | srl (logical) | 0xFFFFFFF8 >> 1: sra gives 0xFFFFFFFC (-8 / 2 = -4); srl gives 0x7FFFFFFC |
| load a byte | lb | lbu | byte 0x9C: lb gives -100, lbu gives 156 |
Addition, subtraction and the bitwise operations have only one version, because two's complement makes them identical for both. Compilers pick the right instruction from the C types: an int comparison uses blt, an unsigned one uses bltu.
Quick Check
Q: A register holds 0xFFFFFFFE. Is it larger or smaller than 1?
Show Answer
It depends on the instruction. bltu reads it as 4,294,967,294, larger than 1. blt reads it as -2, smaller than 1. The bits are the same; the instruction decides.
Overflow: Results That Do Not Fit
Because every result is exactly 32 bits, a result that needs more bits is cut down to 32 by dropping the bits above bit 31. This is wrap-around, and it happens silently:
| Operation | 32-bit result | Read as unsigned | Read as signed |
|---|---|---|---|
| 0xFFFFFFFF + 1 | 0x00000000 | 4,294,967,295 + 1 wraps to 0 (overflow) | -1 + 1 = 0 (correct) |
| 0x7FFFFFFF + 1 | 0x80000000 | 2,147,483,647 + 1 = 2,147,483,648 (correct) | largest positive + 1 = most negative (overflow) |
The same bit pattern is an overflow under one reading and correct under the other. For unsigned numbers, overflow means the true result was larger than 4,294,967,295. For signed numbers, it means two numbers of the same sign were added and the result came out with the opposite sign. RISC-V does not detect either automatically and does not stop the program; code that must check, such as code that computes the size of a memory buffer, compares the result with its inputs.
Programs often rely on wrap-around deliberately. Checksums and hash functions combine values with additions and shifts that overflow all the time, and give the same answer on every 32-bit machine because all of them wrap the same way. A checksum computed this way comes out identical on a tiny embedded processor and on a PC, as long as both use 32-bit values.
Sign Extension: Making Small Fields Wide
Instructions contain small numbers called immediates: constants encoded in the instruction itself, such as the 1 in "add 1 to this register". In RISC-V, most instructions have room for a 12-bit immediate. Before the processor can add it to a 32-bit register, it must widen it to 32 bits, and it must do so without changing its value.
For a signed 12-bit value, that means copying its sign bit, bit 11, into all 20 new upper bits. This is sign extension. A positive value gets zeros on top, a negative value gets ones, and the value stays the same. A compiler might produce the instruction addi a5, a5, -97, stored as 0xF9F78793. Its top 12 bits are the immediate:
The immediate is 0xF9F = 1111 1001 1111, whose top bit is 1, so it is negative: -2048 + 1951 = -97. Sign-extended it becomes 0xFFFFFF9F, which is -97 in 32 bits, as intended. If the hardware instead filled the upper bits with zeros, which is zero extension, the result would be 0x00000F9F = 3999, and the instruction would add 3999 instead of subtracting 97.
Zero extension is right for values that are unsigned, such as a byte loaded with lbu, a character code, which is always between 0 and 255. Sign extension is right for signed values: immediates and bytes loaded with lb. Getting the choice wrong produces correct results for small positive numbers and wrong results for everything else, which makes it an easy bug to miss.
Building Large Constants
A 12-bit immediate covers only -2048 to 2047. Larger constants are built in two steps. A checksum program needs the number 0xEDB88320, and the compiler produces:
lui a3,0xedb88 # a3 = 0xEDB88 << 12 = 0xEDB88000 (load upper immediate) addi a3,a3,800 # a3 = 0xEDB88000 + 800 = 0xEDB88320 (800 = 0x320)
lui (load upper immediate) carries a 20-bit constant and places it in bits 31 to 12, with zeros below. addi then adds the low 12 bits. Because addi sign-extends its immediate, the assembler has to adjust the upper part when the low part's top bit is 1, as the exercise below shows. Small constants need only one instruction: li a0, -1 in the same program is stored as 0xFFF00513, an addi of the 12-bit immediate 0xFFF, which sign-extends to -1.
Where Does This Matter?
Sign and overflow bugs are a classic source of failures in real systems: a length computed as unsigned that wraps to a huge value, a byte read as signed that turns 200 into -56. Processor verification teams test every instruction with the boundary values 0, 1, -1, the largest positive and the most negative number, because that is where these differences show up.
Characters Are Numbers Too
Text is stored as numbers as well. The common code for English letters, digits and punctuation is ASCII, which gives each character a 7-bit number, stored in one byte. 'A' to 'Z' are 0x41 to 0x5A, 'a' to 'z' are 0x61 to 0x7A, the digits '0' to '9' are 0x30 to 0x39, a space is 0x20 and the newline that ends a line is 0x0A. C marks the end of a string with a byte of value 0.
A message such as "Hello from a CPU!" is therefore just a row of bytes in memory, beginning 0x48 0x65 0x6C 0x6C 0x6F for "Hello". Because the codes of the letters are consecutive, programs can compute with them: subtracting 'a' (97) from a lowercase letter gives its position in the alphabet, 0 to 25, which is exactly what a compiled test for vowels does. And because characters are unsigned bytes, they must be loaded with lbu, not lb, or every byte from 0x80 up would turn into a negative number. Modern software usually stores text as UTF-8, which keeps ASCII unchanged and uses several bytes for other characters.
Common Mistakes
Mistake #1: Assuming a value is signed or unsigned by itself
Registers hold bits. Whether 0xFFFFFFFF means -1 or 4,294,967,295 is decided by the instruction that uses it: blt or bltu, lb or lbu, sra or srl.
Mistake #2: Zero-extending a signed immediate
A negative immediate that is zero-extended becomes a large positive number, and the program silently computes nonsense. Every signed field must be extended from its own sign bit.
Mistake #3: Forgetting that results wrap at 32 bits
Hardware does not grow a 33rd bit. Code and tests must expect wrap-around and check explicitly where overflow would be an error.
Practice Exercise
Challenge: Build a Constant With a Negative Low Part
The constant 0xEDB88320 is built with lui a3,0xedb88 and addi a3,a3,0x320. Now suppose a program needs 0xEDB88820 instead. (a) Why does lui a3,0xedb88 followed by addi a3,a3,0x820 give the wrong value, and what value does it give? (b) What upper value must the lui use to get 0xEDB88820?
Hint
As a 12-bit signed immediate, 0x820 has its top bit (bit 11) set. Work out its signed value first.
Solution
(a) 0x820 = 1000 0010 0000 has bit 11 set, so as a signed 12-bit value it is -2048 + 32 = -2016. The addi therefore subtracts 2016: 0xEDB88000 - 0x7E0 = 0xEDB87820, which is wrong. (b) To compensate, the upper part must be one larger: lui a3,0xedb89 gives 0xEDB89000, and adding -2016 gives 0xEDB89000 - 0x7E0 = 0xEDB88820. Assemblers do this adjustment automatically when they expand li into lui and addi.
Summary
- Binary place values double from the right; one hex digit is exactly four bits, so 32 bits are 8 hex digits.
- Two's complement gives the top bit a negative weight; -1 is all ones, and one adder serves signed and unsigned numbers.
- Where signedness matters, RISC-V has separate instructions: blt/bltu, slt/sltu, sra/srl, lb/lbu.
- Results wrap at 32 bits without warning; overflow depends on whether the values are read as signed or unsigned.
- Immediates are sign-extended: 0xF9F becomes 0xFFFFFF9F (-97), never 3999; large constants are built with lui and addi.