Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Assembly (x86-64, AArch64, RISC-V)  Module 2 – Bits, bytes and numbers

Two's complement and sign extension

Negate by inverting and adding 1, tell carry from signed overflow, and widen bytes by zero or sign extension on x86-64, AArch64 and RISC-V, with recorded runs.

  • Beginner
  • 25 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Represent a negative integer in two's complement and state the range of an n-bit register
  • Predict whether an addition overflows as an unsigned sum, as a signed sum, or as both
  • Distinguish zero extension from sign extension and choose the load that does each on x86-64, AArch64 and RISC-V
  • Explain why the same byte prints as 200 on one platform and as −56 on another

Before you start

What you’ll learn · Before you start

What you will learn

  • Represent a negative integer in two's complement and state the range of an n-bit register
  • Predict whether an addition overflows as an unsigned sum, as a signed sum, or as both
  • Distinguish zero extension from sign extension and choose the load that does each on x86-64, AArch64 and RISC-V
  • Explain why the same byte prints as 200 on one platform and as −56 on another

Before you start

On this page

Store 200 in a C char on a Mac with Apple silicon and print it as a number: the program prints −56, while the compiler for Linux on AArch64 loads the same byte, 0xc8, as 200. Only the rule that reads it differs. two's complement: The way x86-64, AArch64 and RISC-V store signed integers: the top bit of an n-bit value is worth minus 2 to the power n − 1 and every other bit keeps its usual weight, so the byte 0xc8 is −56. A value is negated by inverting every bit and adding 1. is the rule that gives a pattern of bits a negative reading: the top bit counts as minus its usual weight, so 0xc8 is −128 + 64 + 8 = −56, and a number is negated by inverting every bit and adding 1. sign extension: Widening a value by copying its top bit into every new bit above it, so a two's complement number keeps its signed value: the byte 0xc8 (−56) becomes 0xffffffc8 in 32 bits. movsx, ldrsb and lb sign-extend. keeps that reading when a value moves into a wider register, by copying the top bit into every new bit above it.

Below, you negate numbers by hand and with programs you can change, tell an unsigned overflow from a signed one, widen a byte both ways, read the extending loads of x86-64, AArch64 and RISC-V beside a recorded run, and see why the sign of plain char is a choice each ABI makes. Sizes and register names come from bits, bytes, words and hexadecimal.

Negating a number means inverting every bit and adding 1

Take 56, which is 0011 1000 in eight bits. Its negative has to be the 8-bit pattern that, added to 56, leaves 0 in the register. Inverting every bit gives 1100 0111, which is 199, and 56 + 199 = 255: every bit set, one short of 256. Adding 1 gives 1100 1000, which is 200 as an unsigned integer: An integer type without negative values: every bit has its positive binary weight, so n bits hold 0 to 2 to the power n, minus 1: 0 to 255 for a byte., and 56 + 200 = 256, a 1 followed by eight zeros. An 8-bit register has no ninth bit to keep that 1 in, so it keeps 0000 0000. In eight bits, then, 200 behaves exactly like −56, and two’s complement makes that the official reading of a signed integer: An integer type whose values can be negative. On x86-64, AArch64 and RISC-V it is stored in two's complement, so n bits hold −2 to the power n − 1 up to 2 to the power n − 1, minus 1: −128 to 127 for a byte.: bit 7 is worth −128 instead of +128, and every other bit keeps its weight. Step through the two moves and the two checks:

Interactive · step by step Negating 56 in an 8-bit register Step through the two moves that turn 56 into its negative, then check the result two ways.
Negating 56 in an 8-bit register, step 5 of 5. Showing x = 56, Every bit flipped, Plus 1, Added to x, Read with weights (so 0xc8 is −56). In focus: Read with weights.invertadd 1x = 560011 1000Every bit flipped1100 0111 = 199Plus 11100 1000 = 0xc8Added to x0000 0000Read with weightsso 0xc8 is −56

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Start from 56. 56 is 32 + 16 + 8, so its 8 bits are 0011 1000. Its negative has to be the 8-bit pattern that adds to 56 and leaves zero in the register.
  2. Flip every bit. Inverting gives 1100 0111, which is 199. Added to 56 it makes 1111 1111: every bit set, one short of 256.
  3. Add 1. Adding 1 gives 1100 1000, 0xc8, which reads as 200 unsigned. 56 + 200 = 256 = 2 to the power 8, the first number an 8-bit register cannot hold.
  4. Check by adding. 0011 1000 + 1100 1000 = 1 0000 0000. The carry out of bit 7 has no bit to go into, so the register keeps 0000 0000: x plus the new pattern is zero.
  5. Read it with the weights. In two's complement bit 7 is worth −128 instead of +128. 0xc8 has bits 7, 6 and 3 set: −128 + 64 + 8 = −56, the negative of 56.

The same rule fixes the range of every width. With the top bit worth −2n−1-2^{n-1}, an n-bit register holds signed values from −2n−1-2^{n-1} to 2n−1−12^{n-1}-1, and unsigned ones from 0 to 2n−12^{n}-1. In words: half of the patterns, those with the top bit set, are negative, and the other half are zero and the positive values, so the negative side always has one value more. For eight bits that is −128 to 127 against 0 to 255. The program negates 56 the way the scene did, prints the result in both readings and lists the four register widths. Press Edit, set x to another value from 0 to 255 and run it again:

Negating 56 in eight bits, and the range of each width Python · negate/negate.py
# Two's complement in an 8-bit register: negate by inverting every bit and adding 1.
BITS = 8
MASK = (1 << BITS) - 1  # 0xff: keeps the low 8 bits, as an 8-bit register does


def pattern(n):
    """The 8 bits of n, with a space between the two nibbles."""
    b = format(n & MASK, "08b")
    return b[:4] + " " + b[4:]


x = 56  # change this to any value from 0 to 255
inverted = ~x & MASK
negated = (inverted + 1) & MASK
signed = int.from_bytes(bytes([negated]), "big", signed=True)

print(f"x          {pattern(x)}  {x}")
print(f"inverted   {pattern(inverted)}  {inverted}")
print(f"plus 1     {pattern(negated)}  {negated} = 0x{negated:02x}")
print(f"x + that   {pattern(x + negated)}  the carry out of bit 7 is lost")
print(f"0x{negated:02x} read as a signed byte: {signed}")

edge = 0x80
print(f"negating 0x80 gives 0x{(~edge + 1) & MASK:02x} again: 8 bits hold -128 but not +128")
print()
print("bits  unsigned range                      signed range")
for n in (8, 16, 32, 64):
    top = 2**n - 1
    low, high = -(2 ** (n - 1)), 2 ** (n - 1) - 1
    print(f"{n:>4}  0 to {top:<30,} {low:,} to {high:,}")

Output

x          0011 1000  56
inverted   1100 0111  199
plus 1     1100 1000  200 = 0xc8
x + that   0000 0000  the carry out of bit 7 is lost
0xc8 read as a signed byte: -56
negating 0x80 gives 0x80 again: 8 bits hold -128 but not +128

bits  unsigned range                      signed range
   8  0 to 255                            -128 to 127
  16  0 to 65,535                         -32,768 to 32,767
  32  0 to 4,294,967,295                  -2,147,483,648 to 2,147,483,647
  64  0 to 18,446,744,073,709,551,615     -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 negate.py

The run confirms each step: 199 after inverting, 200, or 0xc8, after adding 1, and a sum of 0 once the ninth bit is gone. It also shows the one value without a positive partner: negating 0x80 gives 0x80 again, because +128 does not fit in eight bits.

Inverting without adding 1

Flipping the bits of 56 gives 1100 0111, which two’s complement reads as −57, not −56: the inverted pattern is always one below the negative. Add the 1, then check the result by adding it to the original, because only the true negative leaves 0 in the register.

Every ISA in this track stores signed integers this way. Intel’s manual defines the signed integers of x86-64 as two’s complement values with the sign in bit 7, 15, 31 or 63, and since the edition known as C23 the C standard defines every signed integer type the same way, where earlier editions also allowed other representations.

Key idea

In two’s complement the top bit is worth minus its usual weight, so −x is the pattern that adds to x and leaves zero: invert every bit, then add 1. Eight bits hold −128 to 127, and every width has one more negative value than positive ones.

Checkpoint

Check: Negating in two's complement

  1. Question 1 of 2 Which 8-bit pattern is −20 in two's complement?

    Choose one answer.

    A hint

    Write 20 in eight bits first, then make the two moves of the scene.

    Show the answer: Question 1 of 2

    Answer: 1110 1100, which is 0xEC

    Invert every bit of 0001 0100 to get 1110 1011, then add 1: 1110 1100, or 0xEC. Adding it to 20 gives 1 0000 0000, and an 8-bit register keeps only the zero, which is what a negative must do.

  2. Question 2 of 2 What is the most negative value a 16-bit two's complement register can hold?

    Type a number.

    A hint

    Only the top bit carries a negative weight. Set it, and leave every other bit clear.

    Show the answer: Question 2 of 2

    Answer: -32768

    In n bits the top bit is worth −2 to the power n − 1, so with 16 bits it is −32,768, and no other bit can make the value lower. The range table of the negating program prints −32,768 to 32,767.

One adder serves signed and unsigned values; only the overflow test differs

Add 0x64 to 0x64 in an 8-bit register and the result is 0xc8. Read as unsigned numbers, 100 + 100 = 200, which is right. Read as signed numbers, 100 + 100 = −56, which is wrong, because 200 does not fit below 128. The adder did one thing, a binary addition that drops a ninth bit, and both readings come from the same 8 bits. That is why x86-64, AArch64 and RISC-V each have one add instruction for both kinds of integer: Intel’s manual lists ADD and SUB among the instructions that work on unsigned and signed operands alike. What differs between the readings is the test for an integer overflow: An arithmetic result that does not fit in the bits that hold it, so the register keeps only its low bits. For unsigned numbers it shows as a carry out of the top bit; for signed ones, as two inputs of one sign giving a result of the other sign., a result that did not fit:

  • An unsigned sum overflows when it needs a ninth bit, which leaves the adder as a carry out of bit 7: 200 + 100 is 300, and the register keeps 44.
  • A signed sum overflows when it falls outside −128 to 127, which shows as two inputs of the same sign giving a result of the other sign: 100 + 100 gives −56.

The program adds five pairs with one 8-bit adder, prints both readings and both tests, then checks every one of the 65,536 pairs of bytes. Add your own pairs to PAIRS and run it again:

One 8-bit adder, read as unsigned and as signed Python · adder/one_adder.py
# One 8-bit adder, two readings of the same result: the carry speaks for the unsigned
# reading, the overflow for the signed one.
MASK = 0xFF


def signed(b):
    """The signed (two's complement) reading of an 8-bit pattern."""
    return b - 256 if b & 0x80 else b


PAIRS = [(20, 30), (100, 100), (200, 100), (0xC8, 0x38), (0x80, 0xFF)]

print("a     b     sum   unsigned       signed           carry  overflow")
for a, b in PAIRS:
    total = a + b
    s = total & MASK  # the 8 bits the register keeps
    carry = total > MASK  # the unsigned sum needed a ninth bit
    overflow = signed(a) + signed(b) != signed(s)  # the signed sum does not fit
    u_text = f"{a}+{b}={s}"
    s_text = f"{signed(a)}+{signed(b)}={signed(s)}"
    print(f"0x{a:02x}  0x{b:02x}  0x{s:02x}  {u_text:<14} {s_text:<16} {'yes' if carry else 'no':<6} {'yes' if overflow else 'no'}")

# Over every pair of bytes: how often each reading goes wrong, and the top-bit rule
carries = overflows = rule_agrees = 0
for a in range(256):
    for b in range(256):
        s = (a + b) & MASK
        over = signed(a) + signed(b) != signed(s)
        carries += a + b > MASK
        overflows += over
        # rule: both inputs have the same top bit, and the result's top bit differs from it
        rule = ((a ^ s) & (b ^ s) & 0x80) != 0
        rule_agrees += rule == over
print()
print(f"pairs with a carry out (unsigned overflow): {carries:,} of 65,536")
print(f"pairs with signed overflow:                 {overflows:,} of 65,536")
print(f"top-bit rule agrees with the signed sum:    {rule_agrees:,} of 65,536")

Output

a     b     sum   unsigned       signed           carry  overflow
0x14  0x1e  0x32  20+30=50       20+30=50         no     no
0x64  0x64  0xc8  100+100=200    100+100=-56      no     yes
0xc8  0x64  0x2c  200+100=44     -56+100=44       yes    no
0xc8  0x38  0x00  200+56=0       -56+56=0         yes    no
0x80  0xff  0x7f  128+255=127    -128+-1=127      yes    yes

pairs with a carry out (unsigned overflow): 32,640 of 65,536
pairs with signed overflow:                 16,384 of 65,536
top-bit rule agrees with the signed sum:    65,536 of 65,536

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 one_adder.py

Each reading fails in its own cases. 0xc8 + 0x64 carries out and is still right as a signed sum, −56 + 100 = 44; 0x64 + 0x64 carries nothing and is wrong as a signed sum; 0x80 + 0xff is wrong both ways. Across all 65,536 pairs, 32,640 carry out and 16,384 overflow as signed sums, and the sign rule agrees with the exact signed sum in all 65,536 of them. The three ISAs report the two tests in different ways:

ISA Unsigned overflow Signed overflow Which instructions record them
x86-64 carry flag, CF overflow flag, OF add, sub and the other arithmetic instructions
AArch64 carry flag, C overflow flag, V only the flag-setting forms, such as adds, subs and cmp
RISC-V no flag: bltu compares the sum with an input no flag: a few compares and a branch none

Intel’s manual describes CF as the overflow condition of unsigned arithmetic and OF as that of signed arithmetic. Arm’s A64 guide explains that an s at the end of a data-processing instruction makes it update the N, Z, C and V flags. The RISC-V manual leaves overflow checks out of the instruction set and shows them as branches: add t0, t1, t2 followed by bltu t0, t1, overflow for an unsigned sum. MySmartCoPilot’s programmer calculator works at a width and a signedness you choose: set it to signed 8-bit and type 100 + 100, and it shows −56 with a note that 200 does not fit.

Key idea

One adder computes both readings. A carry out of the top bit says the unsigned sum did not fit; two inputs of one sign giving a result of the other sign say the signed sum did not fit.

Checkpoint

Check: Carry and signed overflow from one adder

  1. Question 1 of 2 An 8-bit add computes 0x50 + 0x40 and leaves 0x90. Which statement about the result is true?

    Choose one answer.

    A hint

    Work out both sums in decimal and compare each with the range of its reading.

    Show the answer: Question 1 of 2

    Answer: A signed overflow but no carry: 80 + 64 is 144, which is above 127, and 0x90 reads as −112

    0x50 is 80 and 0x40 is 64. As unsigned bytes 80 + 64 = 144 fits, so there is no carry. As signed bytes 144 is above 127, so the 8 bits kept, 0x90, read as −112: two positive inputs gave a negative result.

  2. Question 2 of 2 Which of these 8-bit additions carry out of bit 7 without a signed overflow? Choose every one that does.

    Choose every answer that is right.

    A hint

    For each pair, check the unsigned sum against 255 and the signed sum against −128 to 127.

    Show the answer: Question 2 of 2

    Answer:

    • 0xFF + 0x01
    • 0xC8 + 0x64

    A carry means the unsigned sum needed a ninth bit; a signed overflow means two inputs of one sign gave a result of the other sign. 0xC8 + 0x64 and 0xFF + 0x01 carry with a signed result that is still right, the case the program's table shows for 200 + 100.

Zero extension keeps the unsigned value, sign extension keeps the signed one

A byte loaded into a 64-bit register needs 56 more bits above it, and what goes into them decides which reading of the byte survives. Take 0xc8 again. zero extension: Widening a value by filling every new bit above it with 0, so an unsigned number keeps its value: the byte 0xc8 (200) becomes 0x000000c8 in 32 bits. movzx, ldrb and lbu zero-extend. fills the new bits with 0 and gives 0x00000000000000c8, which is 200 under both readings: the unsigned value is kept and the −56 is lost. Sign extension fills them with copies of bit 7 and gives 0xffffffffffffffc8, which is −56 as a signed 64-bit number: the signed value is kept, and read as unsigned the same bits are 18,446,744,073,709,551,560. A byte whose bit 7 is 0, such as 0x38 or 0x7f, comes out the same both ways. Step through 0xc8 widened to 32 bits:

Interactive · step by step Widening the byte 0xc8 to 32 bits, two ways Step through both ways of filling the 24 new bits, and see which reading of the byte each one keeps.
Widening the byte 0xc8 to 32 bits, two ways, step 4 of 4. Showing The byte 0xc8, Zero extension (0x000000c8 = 200), Sign extension (0xffffffc8 = −56), Low byte of each. In focus: Low byte of each.zeroscopies of bit 7The byte 0xc81100 1000Zero extension0x000000c8 = 200Sign extension0xffffffc8 = −56Low byte of each0xc8 in both

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. One byte, two readings. The byte 0xc8 is 1100 1000. Read unsigned it is 200; read in two's complement it is −56. A 32-bit register needs 24 more bits above it.
  2. Fill with zeros. Zero extension puts 0 in bits 31 to 8: 0x000000c8. The unsigned reading, 200, survives, and the signed reading is now 200 as well.
  3. Fill with copies of bit 7. Sign extension copies bit 7, a 1, into bits 31 to 8: 0xffffffc8. The signed reading, −56, survives; read unsigned, the same 32 bits are 4,294,967,240.
  4. Both keep the byte. Cut either result back to 8 bits and 0xc8 comes back. Extension never touches the byte itself: it chooses which reading the wider value keeps.

Neither way changes the byte itself; each picks the reading that the wider value keeps. The program widens four bytes both ways, prints the two 64-bit forms of 0xc8 and checks a way to sign-extend with arithmetic alone. Put your own bytes in BYTES and run it again:

Widening four bytes by zero and by sign extension Python · extend/extend.py
# Widening one byte to 32 bits: zero extension keeps the unsigned value,
# sign extension keeps the signed value, and both keep the byte itself.
BYTES = [0x38, 0x7F, 0x80, 0xC8]  # try your own bytes, from 0x00 to 0xff

print("byte  zero-extended      sign-extended")
for byte in BYTES:
    raw = bytes([byte])
    u = int.from_bytes(raw, "little", signed=False)  # as a zero-extending load reads it
    s = int.from_bytes(raw, "little", signed=True)  # as a sign-extending load reads it
    print(f"0x{byte:02x}  0x{u & 0xFFFFFFFF:08x} = {u:<4}  0x{s & 0xFFFFFFFF:08x} = {s}")

b = 0xC8
s = int.from_bytes(bytes([b]), "little", signed=True)
print()
print(f"0x{b:02x} widened to 64 bits: 0x{b:016x} or 0x{s & (2**64 - 1):016x}")
print(f"low 8 bits of both: 0x{b & 0xFF:02x} and 0x{s & 0xFF:02x}")

# Sign extension without a library: flip bit 7, then subtract its weight
same = all((x ^ 0x80) - 0x80 == int.from_bytes(bytes([x]), "little", signed=True) for x in range(256))
print(f"(x ^ 0x80) - 0x80 equals the signed reading for all 256 bytes: {same}")

Output

byte  zero-extended      sign-extended
0x38  0x00000038 = 56    0x00000038 = 56
0x7f  0x0000007f = 127   0x0000007f = 127
0x80  0x00000080 = 128   0xffffff80 = -128
0xc8  0x000000c8 = 200   0xffffffc8 = -56

0xc8 widened to 64 bits: 0x00000000000000c8 or 0xffffffffffffffc8
low 8 bits of both: 0xc8 and 0xc8
(x ^ 0x80) - 0x80 equals the signed reading for all 256 bytes: True

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 extend.py

0x80 becomes 128 or −128 and 0xc8 becomes 200 or −56, while 0x38 and 0x7f stay 56 and 127 either way. The last line is a check worth keeping: flipping bit 7 with ^ 0x80 and then subtracting 128 gives the signed value of each of the 256 bytes, which is sign extension written as arithmetic. Python’s int.from_bytes does the same with signed=True, and its documentation says that this argument chooses two’s complement.

Zero-extending a signed value

Widening −56 from a byte with zero extension puts 200 in the wider register: a positive number with the same low byte. A test such as x < 0 then fails, and any sum with it is 256 too large. Widen a signed value with sign extension and an unsigned one with zero extension, and choose by the type of the value, not by the value you expect it to hold.

Checkpoint

Check: Zero extension and sign extension

  1. One question A sign-extending load widens the byte 0x9C to 32 bits. What does the register hold?

    Choose one answer.

    A hint

    Look at bit 7 of the byte first.

    Show the answer: One question

    Answer: 0xFFFFFF9C

    Sign extension copies bit 7 into every bit above it. Bit 7 of 0x9C is 1, so the result is 0xFFFFFF9C, which is −100 as a signed 32-bit number, the same value as the signed byte.

Each ISA has a zero-extending and a sign-extending load

The hardware widens as part of a load, or in one instruction between registers, and each ISA names both choices. The table gives the forms for a byte and for a 32-bit value; the loads read the byte at the address in rdi, x0 or a0:

Widening x86-64 AArch64 RV64
byte, zero extension movzx eax, byte ptr [rdi] ldrb w0, [x0] lbu a0, 0(a0)
byte, sign extension movsx rax, byte ptr [rdi] ldrsb x0, [x0] lb a0, 0(a0)
32 to 64 bits, zero extension mov eax, edi mov w0, w0 lwu from memory, or two shifts
32 to 64 bits, sign extension movsxd rax, edi sxtw x0, w0 lw from memory, or sext.w a0, a0

The AArch64 column runs on the Mac that recorded this page. Each function in widen.s widens the byte 0xc8 one way, and the C program prints all 64 bits of x0 after each:

widen.s and the C program that calls it

S · asm/widen.s

// Four ways to widen the byte at [x0], and one 32-bit write, on Apple arm64.
// Every function leaves its result in x0, and the C program prints all 64 bits of it.
        .text
        .globl  _load_ldrb_w, _load_ldrsb_w, _load_ldrsb_x, _sxtw_x, _write_w
        .p2align 2

_load_ldrb_w:
        ldrb    w0, [x0]        // zero-extend the byte into w0
        ret

_load_ldrsb_w:
        ldrsb   w0, [x0]        // sign-extend the byte to 32 bits, into w0
        ret

_load_ldrsb_x:
        ldrsb   x0, [x0]        // sign-extend the byte to all 64 bits
        ret

_sxtw_x:
        sxtw    x0, w0          // copy bit 31 of w0 into bits 63 to 32 of x0
        ret

_write_w:
        mov     w0, w0          // a 32-bit write of the low half to itself
        ret

C · asm/widen_main.c

/* Calls each function of widen.s on the byte 0xc8 and prints the whole 64-bit x0 it left. */
#include <stdint.h>
#include <stdio.h>

uint64_t load_ldrb_w(const uint8_t *p);
uint64_t load_ldrsb_w(const uint8_t *p);
uint64_t load_ldrsb_x(const uint8_t *p);
uint64_t sxtw_x(uint64_t x);
uint64_t write_w(uint64_t x);

static void show(const char *what, uint64_t x0) {
    printf("%-16s x0 = 0x%016llx = %lld\n", what, (unsigned long long)x0, (long long)x0);
}

int main(void) {
    const uint8_t byte = 0xc8; /* the byte that (char)200 stores */
    uint64_t wide = load_ldrsb_x(&byte);

    show("ldrb  w0, [x0]", load_ldrb_w(&byte));
    show("ldrsb w0, [x0]", load_ldrsb_w(&byte));
    show("ldrsb x0, [x0]", wide);
    show("sxtw  x0, w0", sxtw_x(load_ldrsb_w(&byte)));
    show("mov   w0, w0", write_w(wide));
    return 0;
}
Four widenings of 0xc8 and one 32-bit write, on Apple silicon Python · widen_session.py
"""Four ways to widen the byte 0xc8 on AArch64, and one 32-bit write, run natively; each line prints all of x0."""

import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session  # noqa: E402

session.run("""
# Assemble widen.s with the C program that calls each function on the byte 0xc8, then run it
clang -o widen asm/widen_main.c asm/widen.s
./widen; echo "exit=$?"
""")

Output

# Assemble widen.s with the C program that calls each function on the byte 0xc8, then run it
$ clang -o widen asm/widen_main.c asm/widen.s
$ ./widen; echo "exit=$?"
ldrb  w0, [x0]   x0 = 0x00000000000000c8 = 200
ldrsb w0, [x0]   x0 = 0x00000000ffffffc8 = 4294967240
ldrsb x0, [x0]   x0 = 0xffffffffffffffc8 = -56
sxtw  x0, w0     x0 = 0xffffffffffffffc8 = -56
mov   w0, w0     x0 = 0x00000000ffffffc8 = 4294967240
exit=0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 widen_session.py

ldrb gave 200 and ldrsb x0 gave −56, as the table promises, and sxtw turned the 32-bit −56 into the 64-bit one. ldrsb w0 is the line to look at twice: it sign-extended the byte only up to bit 31, 0xffffffc8, and the write to w0 then cleared bits 63 to 32, so x0 holds 0x00000000ffffffc8, which is 4,294,967,240 as a 64-bit number. Arm’s A64 guide states both rules: an s in the load sign-extends as far as the size of the destination register, and every write to a w register clears the top half of the x register. mov w0, w0 uses the second rule alone to zero-extend a 32-bit value.

Sign-extending to 32 bits when the value is used as 64

ldrsb w1, [x0] followed by add x2, x2, x1 adds 4,294,967,240 for the byte 0xc8, not −56, because x1 was only sign-extended to 32 bits. Load with ldrsb x1, [x0], or follow the 32-bit load with sxtw x1, w1, whenever the value takes part in 64-bit arithmetic such as an address.

x86-64 and RV64 follow the same pattern. On x86-64 a write to a 32-bit register clears the upper half, as Intel’s manual says, so movsx eax, byte ptr [rdi] stops at bit 31 just as ldrsb w0 does, and movsxd sign-extends 32 bits to 64. RV64 keeps every 32-bit value sign-extended in its 64-bit registers, even an unsigned one: its manual makes lw and the W instructions, such as addw, copy bit 31 upwards, and gives lwu for a 32-bit value that must stay zero-extended. sext.w is another name for addiw a0, a0, 0. The same functions for those two ISAs are Linux sources, shown here and not run:

The same widenings for x86-64 (NASM) and RV64 (GNU as)

ASM · asm/widen_x86.asm

; The same widenings on x86-64 Linux, for NASM (shown on the page, not run on it).
; Each function leaves its 64-bit result in rax.
        section .text
        global  load_movzx, load_movsx_32, load_movsx_64, sext_32_to_64, load_mov_al

load_movzx:
        movzx   eax, byte [rdi]     ; zero-extend the byte; writing eax also zeroes bits 63 to 32
        ret

load_movsx_32:
        movsx   eax, byte [rdi]     ; sign-extend to 32 bits; writing eax zeroes bits 63 to 32
        ret

load_movsx_64:
        movsx   rax, byte [rdi]     ; sign-extend the byte to all 64 bits
        ret

sext_32_to_64:
        movsxd  rax, edi            ; copy bit 31 of edi into bits 63 to 32 of rax
        ret

load_mov_al:
        mov     al, byte [rdi]      ; an 8-bit write: bits 63 to 8 of rax keep whatever they held
        ret

S · asm/widen_rv64.s

# The same widenings on RV64 Linux, for GNU as (shown on the page, not run on it).
# Each function leaves its 64-bit result in a0.
        .text
        .globl  load_lbu, load_lb, load_lwu, load_lw, sext_32_to_64

load_lbu:
        lbu     a0, 0(a0)       # zero-extend the byte to 64 bits
        ret

load_lb:
        lb      a0, 0(a0)       # sign-extend the byte to 64 bits
        ret

load_lwu:
        lwu     a0, 0(a0)       # zero-extend a 32-bit word to 64 bits
        ret

load_lw:
        lw      a0, 0(a0)       # sign-extend a 32-bit word to 64 bits
        ret

sext_32_to_64:
        sext.w  a0, a0          # addiw a0, a0, 0: copy bit 31 into bits 63 to 32
        ret

Key idea

Choose the extending load by the type of the value, zero-extending for unsigned and sign-extending for signed, and check how far it extends: on AArch64 and x86-64 a 32-bit destination stops the extension at bit 31 and clears the rest.

Checkpoint

Check: The extending loads of each ISA

  1. Question 1 of 2 On AArch64, ldrsb w0, [x1] loads the byte 0xF0. What is all of x0 afterwards?

    Choose one answer.

    A hint

    Two rules apply in turn, the one of the load and the one of every write to a w register.

    Show the answer: Question 1 of 2

    Answer: 0x00000000FFFFFFF0

    ldrsb w0 sign-extends to 32 bits only, and every write to a w register clears the upper 32 bits of the x register. The recorded run showed the same for 0xc8: 0x00000000ffffffc8, which is 4,294,967,240.

  2. Question 2 of 2 Which RV64 load leaves a 32-bit word zero-extended in a 64-bit register?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: lwu

    RV64 has the pairs lb and lbu, lh and lhu, lw and lwu: the plain form sign-extends and the u form zero-extends. ld needs no extension because it fills the whole register.

Plain char is signed on some ABIs and unsigned on others

C has three character types, and the standard leaves the sign of plain char to each implementation: section 6.2.5 of C23 says it behaves like either signed char or unsigned char. Each ABI then picks one. The x86-64 psABI lists char as a signed byte, and Apple’s ARM64 document says that on its platforms char is a signed type. AAPCS64, the procedure call standard that Linux on AArch64 follows, maps char to an unsigned byte, and the RISC-V psABI makes it unsigned too. The compiler picks the load that matches. The session asks clang which instruction widens one plain char for four targets, compiling only, then builds and runs (char)200 twice, the second time with -funsigned-char:

char_load.c and char200.c

C (char_load.c) · asm/char_load.c

/* Widen one plain char to int: the compiler must pick a zero- or a sign-extending load. */
int widen(const char *p) { return *p; }

C (char200.c) · asm/char200.c

/* Store 200 in a plain char and print it as an int. */
#include <stdio.h>

int main(void) {
    char c = (char)200; /* the byte 0xc8 */
    printf("(char)200 = %d\n", c);
    return 0;
}
The load each ABI picks for a plain char, and (char)200 run twice Python · loads_session.py
"""Which load each compiler picks for a plain char, for four ABIs, and what (char)200 prints on the host."""

import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session  # noqa: E402

session.run(r"""
# Compile widen() for four targets and keep only its load instruction (nothing runs yet)
clang --target=arm64-apple-macos -O2 -S -o - asm/char_load.c | grep -E '^[[:space:]]+(ldr|mov|lb)' | tr -s '\t' ' '
clang --target=aarch64-linux-gnu -O2 -S -o - asm/char_load.c | grep -E '^[[:space:]]+(ldr|mov|lb)' | tr -s '\t' ' '
clang --target=x86_64-linux-gnu -masm=intel -O2 -S -o - asm/char_load.c | grep -E '^[[:space:]]+(ldr|mov|lb)' | tr -s '\t' ' '
clang-23 --target=riscv64-linux-gnu -O2 -S -o - asm/char_load.c | grep -E '^[[:space:]]+(ldr|mov|lb)' | tr -s '\t' ' '
# Build and run (char)200 for the host, then again with char made unsigned
clang -o char200 asm/char200.c
./char200
clang -funsigned-char -o char200u asm/char200.c
./char200u
""")

Output

# Compile widen() for four targets and keep only its load instruction (nothing runs yet)
$ clang --target=arm64-apple-macos -O2 -S -o - asm/char_load.c | grep -E '^[[:space:]]+(ldr|mov|lb)' | tr -s '\t' ' '
 ldrsb w0, [x0]
$ clang --target=aarch64-linux-gnu -O2 -S -o - asm/char_load.c | grep -E '^[[:space:]]+(ldr|mov|lb)' | tr -s '\t' ' '
 ldrb w0, [x0]
$ clang --target=x86_64-linux-gnu -masm=intel -O2 -S -o - asm/char_load.c | grep -E '^[[:space:]]+(ldr|mov|lb)' | tr -s '\t' ' '
 movsx eax, byte ptr [rdi]
$ clang-23 --target=riscv64-linux-gnu -O2 -S -o - asm/char_load.c | grep -E '^[[:space:]]+(ldr|mov|lb)' | tr -s '\t' ' '
 lbu a0, 0(a0)
# Build and run (char)200 for the host, then again with char made unsigned
$ clang -o char200 asm/char200.c
$ ./char200
(char)200 = -56
$ clang -funsigned-char -o char200u asm/char200.c
$ ./char200u
(char)200 = 200

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 loads_session.py

Platform Plain char is Load the compiler picked
macOS on Apple silicon signed ldrsb w0, [x0]
Linux on x86-64 signed movsx eax, byte ptr [rdi]
Linux on AArch64 unsigned ldrb w0, [x0]
Linux on RISC-V unsigned lbu a0, 0(a0)

The first two loads sign-extend, so 0xc8 becomes −56; the last two zero-extend, so the same byte becomes 200. On the Mac the program printed −56, and with -funsigned-char, an option that clang documents for exactly this choice, it printed 200. The byte and the processor stayed the same; the type of char changed.

Where the sign matters, say which one you mean. Code that treats bytes as numbers, such as a checksum, a hash or a table indexed by a byte, should use unsigned char or uint8_t. The program adds up the bytes of the UTF-8 word café both ways; Python’s struct module reads a byte as a signed char with the format b and as an unsigned char with B:

One byte, and one checksum, read both ways Python · char/char_byte.py
# One byte, two ABIs: plain char is a signed byte on some platforms and an unsigned byte
# on others. struct reads a byte as signed char ("b") or as unsigned char ("B").
import struct

byte = b"\xc8"  # the byte that (char)200 stores
print("read as signed char:  ", struct.unpack("b", byte)[0])
print("read as unsigned char:", struct.unpack("B", byte)[0])

# The same choice inside a checksum that adds up the bytes of a text
text = "café".encode("utf-8")
signed_sum = sum(struct.unpack(f"{len(text)}b", text))
unsigned_sum = sum(struct.unpack(f"{len(text)}B", text))
high = sum(1 for x in text if x >= 0x80)
print()
print("bytes of café:", text.hex(" "))
print("sum as signed chars:  ", signed_sum)
print("sum as unsigned chars:", unsigned_sum)
print(f"difference: {unsigned_sum - signed_sum} = 256 x {high} bytes from 0x80 up")

Output

read as signed char:   -56
read as unsigned char: 200

bytes of café: 63 61 66 c3 a9
sum as signed chars:   150
sum as unsigned chars: 662
difference: 512 = 256 x 2 bytes from 0x80 up

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 char_byte.py

The two bytes of é, c3 and a9, are above 0x7f, so the signed sum is 150 and the unsigned sum is 662: 512 apart, 256 for each of them. A checksum written with plain char gives one of those answers on macOS and the other on Linux on AArch64, for the same file.

Checkpoint

Check: The sign of plain char

  1. Question 1 of 2 The program that prints (char)200 with %d is built for Linux on AArch64 instead of macOS. What does it print?

    Choose one answer.

    A hint

    Look at the load the compiler picked for that target in the session.

    Show the answer: Question 1 of 2

    Answer: 200, because the Linux AArch64 ABI makes plain char unsigned

    For aarch64-linux-gnu the compiler loaded the char with ldrb, a zero-extending load, so the byte 0xc8 becomes 200. For arm64-apple-macos it used ldrsb, and the run on Apple silicon printed −56.

  2. Question 2 of 2 A checksum adds the bytes of a file and must match a reference that counts each byte as 0 to 255, on macOS and on Linux on AArch64 alike. How should the byte variable be declared?

    Choose one answer.

    A hint

    Only one of the declarations reads a byte from 0x80 up the same way on both platforms and as a positive value.

    Show the answer: Question 2 of 2

    Answer: unsigned char, or uint8_t

    When a byte is a number, say which kind it is. unsigned char and uint8_t read 0xc3 as 195 everywhere; plain char reads it as −61 where char is signed.

Practise

The exercise asks for both widenings as Python functions on whole 64-bit patterns, the way a register holds them. Its sample tests try the bytes 0x00, 0x7f, 0x80 and 0xff, inputs wider than a byte, and both ends of the 64-bit signed range:

Exercise · Easy · Python

Widen a byte the way the load instructions do

A byte load takes the low byte of a value in memory and fills a 64-bit register with it. Write three Python functions that give the register's 64 bits the way the hardware does, as a whole number from 0 to 2⁶⁴ − 1:

  1. zext8(x): what a zero-extending load leaves (ldrb on AArch64, lbu on RISC-V, movzx on x86-64). Only the low 8 bits of x count; every bit above them is 0.
  2. sext8(x): what a sign-extending load to 64 bits leaves (ldrsb x0, lb, movsx rax). Only the low 8 bits of x count, and bit 7 of that byte is copied into bits 63 to 8.
  3. as_signed64(r): the value of a 64-bit register pattern r read as a signed (two's complement) integer, from −2⁶³ to 2⁶³ − 1.

So sext8(0xc8) is 0xffffffffffffffc8, as_signed64(sext8(0xc8)) is −56 and as_signed64(zext8(0xc8)) is 200.

Use integer arithmetic and comparisons only: no int.from_bytes or struct, so that each step is one you could write as an instruction. The sample tests try the bytes 0x00, 0x7f, 0x80 and 0xff, values wider than a byte, and the edges of the 64-bit signed range.

Starter code · widen.py

def zext8(x):
    """The 64-bit register after a zero-extending byte load of the low byte of x."""
    return 0  # replace with your code


def sext8(x):
    """The 64-bit register after a sign-extending byte load of the low byte of x."""
    return 0  # replace with your code


def as_signed64(r):
    """The 64-bit pattern r read as a two's complement integer."""
    return 0  # replace with your code
The sample tests · test_widen.py
"""Sample tests: the four bytes of the track's harness, wider inputs and the edges of the signed range."""

from widen import as_signed64, sext8, zext8

ONES_ABOVE_THE_BYTE = 0xFFFF_FFFF_FFFF_FF00


def test_zext8_keeps_the_unsigned_value():
    assert zext8(0x00) == 0x00
    assert zext8(0x7F) == 0x7F
    assert zext8(0x80) == 0x80
    assert zext8(0xFF) == 0xFF


def test_zext8_reads_only_the_low_byte():
    assert zext8(0x1C8) == 0xC8
    assert zext8(0xFFFF_FFFF_FFFF_FFC8) == 0xC8


def test_sext8_copies_bit_7_upwards():
    assert sext8(0x00) == 0x00
    assert sext8(0x7F) == 0x7F
    assert sext8(0x80) == ONES_ABOVE_THE_BYTE | 0x80
    assert sext8(0xFF) == 0xFFFF_FFFF_FFFF_FFFF


def test_sext8_reads_only_the_low_byte():
    assert sext8(0x1C8) == ONES_ABOVE_THE_BYTE | 0xC8
    assert sext8(0x37F) == 0x7F


def test_as_signed64_reads_bit_63_as_minus_2_to_the_63():
    assert as_signed64(0) == 0
    assert as_signed64(0x7FFF_FFFF_FFFF_FFFF) == 2**63 - 1
    assert as_signed64(0x8000_0000_0000_0000) == -(2**63)
    assert as_signed64(0xFFFF_FFFF_FFFF_FFC8) == -56
    assert as_signed64(0x0000_0000_FFFF_FFC8) == 4_294_967_240


def test_the_byte_0xc8_both_ways():
    assert as_signed64(sext8(0xC8)) == -56
    assert as_signed64(zext8(0xC8)) == 200
A hint

Start every function by keeping only the bits the load reads: x & 0xff is the byte. For sext8, look at bit 7 of that byte: when it is 0 the result is the byte itself, and when it is 1 the 56 bits above it are all 1s, which is the same as adding 0xffffffffffffff00. For as_signed64, a pattern with bit 63 set stands for the pattern minus 2⁶⁴.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Fifteen self-check questions follow, from the weight of bit 7 to a pointer moved by a byte offset. They start with the idea you have answered least well, and each one is explained the moment you check it:

Practice that adapts to you

Practise this lesson

15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 15 In 8 bits, what do you get when you negate 0x80 by inverting every bit and adding 1?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: 0x80 again, which is −128

    The program printed it: negating 0x80 gives 0x80 again. The signed range of 8 bits, −128 to 127, has one more negative value than positive ones, and −128 is the one left over.

  2. Question 2 of 15 In an 8-bit two's complement value, what is bit 7 worth?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: −128

    With bit 7 worth −128, the byte 0xc8 is −128 + 64 + 8 = −56, the reading the scene checks at its last step.

  3. Question 3 of 15 Eight bits give 256 patterns, yet the signed range is −128 to 127. Where does the asymmetry come from?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: Zero takes one of the 128 patterns with bit 7 clear, so only 127 of them are positive

    Splitting the patterns by bit 7 gives 128 negative values and 128 others, and zero is one of those others. The same holds at every width: 32 bits hold −2,147,483,648 to 2,147,483,647.

  4. Question 4 of 15 Which x86-64 flag reports that a signed addition overflowed?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: OF, the overflow flag

    One add sets both: CF for the unsigned reading and OF for the signed one. AArch64's adds sets C and V for the same two readings.

  5. Question 5 of 15 Two 8-bit inputs both have bit 7 set, and their sum has bit 7 clear. What has happened?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: A signed overflow, since two negative numbers gave a positive result

    0x80 + 0xFF is the example of the program: −128 + −1 = −129 is below −128, so the 8 bits kept, 0x7F, read as 127. The sign rule agreed with the exact signed sum on all 65,536 pairs.

  6. Question 6 of 15 RISC-V has no flags. After add t0, t1, t2, how does code tell that the unsigned sum wrapped?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: By comparing the sum with an input: bltu t0, t1 branches when it wrapped

    The RISC-V manual gives this exact pair, add then bltu, for unsigned addition, and a short sequence of compares for the signed case. x86-64 and AArch64 record the same facts in flags instead.

  7. Question 7 of 15 What does zero extension put in the bits above the original value?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: Zeros, whatever the value's top bit is

    The byte 0xc8 becomes 0x00000000000000c8, 200, under zero extension and 0xffffffffffffffc8, −56, under sign extension. Both keep the byte in the low 8 bits.

  8. Question 8 of 15 A byte is widened to 64 bits once by zero extension and once by sign extension. What do the two results always share?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: Their low 8 bits, the original byte

    The program printed it: the low 8 bits of both widened forms of 0xc8 are 0xc8. Extension chooses which reading survives; it never changes the bits it starts from.

  9. Question 9 of 15 Without a sign-extending instruction, which expression turns a byte value b from 0 to 255 into its signed value?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: (b ^ 0x80) − 0x80

    For b below 128, flipping bit 7 adds 128 and the subtraction takes it back. For b from 128 up it removes 128, and the subtraction takes away another 128: 200 becomes 72 − 128 = −56.

  10. Question 10 of 15 Which x86-64 instruction sign-extends a 32-bit register into a 64-bit one?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: movsxd

    movsxd rax, edi copies bit 31 of edi into bits 63 to 32 of rax; AArch64 does the same with sxtw x0, w0 and RV64 with sext.w, which is addiw a0, a0, 0.

  11. Question 11 of 15 On RV64, addw a0, a0, a1 produces the 32-bit sum 0x80000000. What is all of a0 afterwards?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: 0xFFFFFFFF80000000

    RV64's manual states the rule: the W instructions ignore the upper 32 bits of their inputs and sign-extend their 32-bit result, and the compilers keep every 32-bit value in that form, unsigned or not.

  12. Question 12 of 15 AArch64 code loads a signed byte offset with ldrsb w1, [x0] and then adds x1 to a pointer in x2. The byte is 0xFF. What goes wrong?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: x1 is 0x00000000FFFFFFFF, so the pointer moves about 4 GiB forward instead of 1 byte back

    Load with ldrsb x1, which sign-extends to 64 bits, or follow ldrsb w1 with sxtw x1, w1. Then x1 is 0xFFFFFFFFFFFFFFFF, −1, and the pointer moves back by one byte.

  13. Question 13 of 15 What does the C standard say about the sign of plain char?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: The implementation chooses; char behaves like either signed char or unsigned char

    The standard makes char a third type with the range of one of the other two. The four ABIs of this lesson split two and two: signed on x86-64 and Apple silicon, unsigned on Linux AArch64 and RISC-V.

  14. Question 14 of 15 On Apple silicon, the second build of the session printed 200. Which compiler option produced that?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: -funsigned-char

    clang documents -fsigned-char and -funsigned-char for this choice. The flag changes char only in the code it builds: a library built without it still reads the same byte with the platform's own sign.

  15. Question 15 of 15 The UTF-8 bytes of café are 63 61 66 c3 a9, and their sum read as unsigned chars is 662. What is their sum read as signed chars?

    Type a number.

    A hint

    Only bytes from 0x80 up change when they are read as signed.

    Show the answer: Question 15 of 15

    Answer: 150

    The two bytes c3 and a9 are 195 and 169 unsigned but −61 and −87 signed, 256 less each. 662 − 2 × 256 = 150, the sum the program printed.

Interview questions

A warm-up: the byte 0x80 goes into a 64-bit register once by zero extension and once by sign extension. What does each leave, and which reading of the byte does each keep?

Zero extension leaves 0x0000000000000080, which is 128 whether the register is read as signed or as unsigned, so it keeps the unsigned reading of the byte. Sign extension copies bit 7, a 1, into all 56 new bits and leaves 0xffffffffffffff80, which is −128 as a signed 64-bit number, so it keeps the signed reading; read as unsigned, the same bits are 18,446,744,073,709,551,488. The low byte is 0x80 in both, because extension never touches the bits it starts from. A strong answer also names the loads, movzx or movsx on x86-64, ldrb or ldrsb on AArch64 and lbu or lb on RISC-V, and says that the choice follows the type of the value, unsigned char or signed char, not the number the programmer hopes to see.

The module’s bank adds two questions that put these rules into working code: a byte-copy loop whose end test depends on the sign of char, and an overflow check that the compiler removes. Open a row for the first step and the follow-up questions:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A small C tool copies its input with char c; while ((c = getchar()) != EOF) putchar(c);. Built for Linux on AArch64 it never stops; built for macOS it stops early on some files. Explain both failures from the code each compiler generates, and give the fix.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A teammate guards a signed int addition in C with if (a + b < a) return ERR_OVERFLOW;. At -O2 the guard never fires for two large positive inputs, and the disassembly shows no comparison of the sum, only a test of the sign bit of b. What did the compiler do, and how should the check be written?

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. How do you negate a number in two's complement?

    Invert every bit, then add 1. The result is the pattern that adds to the original and leaves zero in the register.

  2. What is the byte 0xc8 as a signed number, and why?

    -56. In two's complement the top bit of an n-bit value is worth minus 2 to the power n-1, so 0xc8 is -128 + 72 = -56. Read as unsigned, the same byte is 200.

  3. Why does negating -128 in an 8-bit register give -128 again?

    Eight bits hold -128 to 127: the negatives outnumber the positives by one, so +128 has no pattern. Inverting 0x80 and adding 1 lands on 0x80 again.

  4. One adder gives the sum for signed and unsigned numbers. How do you tell that each reading did not fit?

    For unsigned numbers, a carry out of the top bit. For signed numbers, two inputs of the same sign giving a result of the other sign. x86-64 records them in CF and OF, AArch64's flag-setting instructions in C and V, and RISC-V code checks them with branches.

  5. The byte 0x80 is widened to 64 bits by zero extension and by sign extension. What does each give?

    Zero extension gives 0x0000000000000080, which is 128 and keeps the unsigned reading. Sign extension copies bit 7 into every new bit and gives 0xffffffffffffff80, which is -128 and keeps the signed reading.

  6. Which loads zero-extend and which sign-extend a byte on each ISA?

    movzx and movsx on x86-64, ldrb and ldrsb on AArch64, lbu and lb on RISC-V. Pick by the type of the value, and remember that a 32-bit destination on AArch64 and x86-64 stops the extension at bit 31 and clears the rest.

  7. Is plain char signed or unsigned?

    The ABI decides. It is signed on x86-64 and Apple silicon but unsigned on Linux for AArch64 and RISC-V, so code that treats bytes as numbers should say unsigned char or uint8_t.

Key takeaways

  • Two’s complement gives the top bit of an n-bit value the weight minus 2 to the power n − 1, so 0xc8 is −56 in eight bits, and a value is negated by inverting every bit and adding 1.
  • An n-bit register holds one more negative value than positive ones, −128 to 127 for a byte, so negating the most negative value gives it back unchanged.
  • One adder serves both readings: a carry out of the top bit means the unsigned sum did not fit, and two inputs of one sign giving a result of the other sign mean the signed sum did not fit, which x86-64 records in CF and OF, AArch64’s flag-setting instructions in C and V, and RISC-V code checks with branches.
  • Zero extension fills the new bits with 0 and keeps the unsigned value, sign extension copies the top bit and keeps the signed value, and each ISA has a load for each, such as ldrb and ldrsb, with a 32-bit destination stopping the extension at bit 31.
  • The sign of plain char is the ABI’s choice, signed on x86-64 and Apple silicon and unsigned on Linux AArch64 and RISC-V, so code that treats bytes as numbers should declare them unsigned char or uint8_t.

Terms in this lesson

integer overflow
An arithmetic result that does not fit in the bits that hold it, so the register keeps only its low bits. For unsigned numbers it shows as a carry out of the top bit; for signed ones, as two inputs of one sign giving a result of the other sign.
sign extension
Widening a value by copying its top bit into every new bit above it, so a two's complement number keeps its signed value: the byte 0xc8 (−56) becomes 0xffffffc8 in 32 bits. movsx, ldrsb and lb sign-extend.
signed integer
An integer type whose values can be negative. On x86-64, AArch64 and RISC-V it is stored in two's complement, so n bits hold −2 to the power n − 1 up to 2 to the power n − 1, minus 1: −128 to 127 for a byte.
two's complement
The way x86-64, AArch64 and RISC-V store signed integers: the top bit of an n-bit value is worth minus 2 to the power n − 1 and every other bit keeps its usual weight, so the byte 0xc8 is −56. A value is negated by inverting every bit and adding 1.
unsigned integer
An integer type without negative values: every bit has its positive binary weight, so n bits hold 0 to 2 to the power n, minus 1: 0 to 255 for a byte.
zero extension
Widening a value by filling every new bit above it with 0, so an unsigned number keeps its value: the byte 0xc8 (200) becomes 0x000000c8 in 32 bits. movzx, ldrb and lbu zero-extend.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.