Assembly (x86-64, AArch64, RISC-V) Module 2 – Bits, bytes and numbers
Endianness, strings and text in memory
How integers and strings sit as bytes in memory: the little-endian rule, the byte-swap instructions and the gap between UTF-8 byte count and character count.
What you will learn
- Predict the byte order of multi-byte integers in little-endian memory
- Identify the byte-swap instructions bswap, movbe, rev and rev8 on three ISAs
- Decode a UTF-8 encoding and distinguish byte count from code-point count
- Describe three string directives (NASM db, GNU as .ascii and .asciz)
- Use ASCII, UTF-8 and a null terminator to lay a string out in memory
Before you start
What you’ll learn · Before you start
What you will learn
- Predict the byte order of multi-byte integers in little-endian memory
- Identify the byte-swap instructions bswap, movbe, rev and rev8 on three ISAs
- Decode a UTF-8 encoding and distinguish byte count from code-point count
- Describe three string directives (NASM db, GNU as .ascii and .asciz)
- Use ASCII, UTF-8 and a null terminator to lay a string out in memory
Before you start
On this page
A 64-bit integer takes eight bytes of memory, and a string takes one byte per ASCII character, three per Devanagari character and four per most emoji. The order in which those bytes land, and how a byte count differs from a character count, decides whether a program reads back what it wrote. On every mainstream x86-64, AArch64 and RISC-V system the value 0x1122334455667788 is stored with 0x88 at the lowest address and 0x11 at the highest; the Devanagari word for namaste takes 18 bytes to hold six code points.
Below, you write those bytes with Python’s struct module, swap them with two short idioms,
name the byte-swap instructions each ISA offers, and see why len(s.encode('utf-8')) is not
always len(s). It builds on hexadecimal: Base 16, written with the digits 0 to 9 and A to F. Each digit stands for four bits, so 0x2C is the byte 0010 1100, which is 44; the prefix 0x marks a hexadecimal number in C, Python and most assemblers. More in “Bits, bytes, words and hexadecimal” and byte sizes from the
earlier bits-bytes-hex lesson.
Little-endian stores the lowest byte first
A multi-byte integer in a register is one number; in memory it is a run of bytes at consecutive addresses, and some rule has to decide which byte goes where. The rule every mainstream x86-64, AArch64 and RISC-V system uses is called the little-endian: The byte-order rule in which the lowest-value byte of a multi-byte value sits at the lowest address. Every mainstream x86-64, AArch64 and RISC-V system is little-endian in practice. rule: the lowest-value byte of the number sits at the lowest address. The reversed rule, with the highest-value byte at the lowest address, is called big-endian: The byte-order rule in which the highest-value byte of a multi-byte value sits at the lowest address. Internet protocols call this 'network byte order' and use it on the wire. and is mostly seen in network protocols and in a few older systems. The choice is called the value’s byte order: The order the bytes of a multi-byte value take in memory, from the base address upwards. The two orders in use are little-endian (low byte first) and big-endian (high byte first)., or its endianness: The rule a computer follows to place the bytes of a multi-byte value in memory. Little-endian puts the lowest-value byte at the lowest address; big-endian puts the highest-value byte there..
A Python one-liner shows both orders for the same number. The struct module’s pack turns an
integer into a bytes object under a format letter: '<' means little-endian and '>' means
big-endian, and 'Q' is an unsigned 64-bit integer:
"""Show the byte order of 0x1122334455667788 under little-endian and big-endian packing.
Every mainstream computer that runs this track's three ISAs stores integers little-endian: the
least significant byte sits at the lowest address. The struct module packs a value under a chosen
byte order ('<' little, '>' big), and printing the bytes side by side shows the reversal.
"""
import struct
value = 0x1122334455667788
little = struct.pack("<Q", value)
big = struct.pack(">Q", value)
def hexpairs(data: bytes) -> str:
return " ".join(f"{b:02x}" for b in data)
print(f"value = 0x{value:016x}")
print(f"little-endian ('<Q') = [{hexpairs(little)}]")
print(f"big-endian ('>Q') = [{hexpairs(big)}]")
print()
print("Memory layout, little-endian (byte at address + 0 on the left):")
print(" offset: 0 1 2 3 4 5 6 7")
print(" byte: " + " ".join(f"{b:02x}" for b in little)) Output
value = 0x1122334455667788
little-endian ('<Q') = [88 77 66 55 44 33 22 11]
big-endian ('>Q') = [11 22 33 44 55 66 77 88]
Memory layout, little-endian (byte at address + 0 on the left):
offset: 0 1 2 3 4 5 6 7
byte: 88 77 66 55 44 33 22 11
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 endianness_bytes.py
The two lines of hex are mirror images of each other. The little-endian output starts with the low byte 0x88 at offset 0 and ends with the high byte 0x11 at offset 7; the big-endian output starts with 0x11 and ends with 0x88. Walk the eight bytes of the little-endian store one at a time to see where each one lands:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- One value, eight bytes. The processor holds 0x1122334455667788 in a 64-bit register. To store it, memory has to carry eight bytes, one at each address from the base to the base plus seven. The byte order is set by the computer, not by the number.
- Low byte goes first. On every mainstream x86-64, AArch64 and RISC-V system the lowest-value byte sits at the lowest address, which is called the little-endian rule. 'Little end first' names what you see when you read memory from the base upwards.
- Offset 0 gets 0x88. The low 8 bits of 0x1122334455667788 are 0x88, so the byte at the base address of the store is 0x88. If you dumped the memory with a hex viewer right now, the first byte would read 88.
- Offsets 1 to 6 fill in order. Each later offset takes the next byte of the value, from the low end towards the high end. Offset 1 gets 0x77, offset 2 gets 0x66, offset 3 gets 0x55, offset 4 gets 0x44, offset 5 gets 0x33 and offset 6 gets 0x22. Each step moves up by one byte.
- Offset 7 gets 0x11. The highest-address byte of this store takes the highest-value byte, 0x11. The 64-bit number is now spread across eight memory addresses from the base to the base plus seven, with the halves at opposite ends.
- Read it back as bytes. Reading the eight bytes from the base upwards gives 88 77 66 55 44 33 22 11, which is the exact output of struct.pack('<Q', 0x1122334455667788) in Python. A big-endian write would have placed 11 first and 88 last.
The flow starts with the whole 64-bit value, states the little-endian rule, and then places the first byte, the middle six and the last byte in turn. The ending matches the Python output byte for byte: 88 77 66 55 44 33 22 11.
Key idea
On the three ISAs this track teaches, a multi-byte number spells itself out from its low byte at the base address up to its high byte at the top of the store, which is the opposite of the usual pen-and-paper reading of the number.
MySmartCoPilot’s hex viewer shows any file byte by byte, and its data inspector reads the bytes at the cursor as a 64-bit integer in either byte order, so the same eight bytes can be read both ways.
Checkpoint
Check: Little-endian byte order
Byte swap turns one order into the other
A program that reads a value stored on a big-endian machine, or over a network that uses The order of bytes used by Internet protocols for multi-byte integers, which is big-endian., and runs on a little-endian processor has to reverse the bytes before it uses the value. The reversal is called a byte swap: The operation that reverses the bytes of a multi-byte value, turning its little-endian form into the big-endian form or back. x86-64 does it with bswap, AArch64 with rev and RISC-V with rev8 under the Zbb extension., and every ISA this track teaches has a one-instruction form of it:
- x86-64 has
bswapfor a 32-bit or 64-bit register andmovbeto load or store a 2-, 4- or 8-byte value with its bytes reversed in one step. Intel documents both in Volume 2 of the SDM. - AArch64 has
rev, which reverses every byte of its register in either width (rev x0, x0for eight bytes,rev w0, w0for four),rev32to reverse the bytes inside each 32-bit half of a 64-bit register, andrev16to reverse the bytes inside each 16-bit half. The Arm A64 ISA guide names each one. - RISC-V has
rev8in the Zbb bit-manipulation extension, which reverses every byte of an XLEN-wide register. Zbb is not part of the RV64GC base, so a general RV64 build needs-march=rv64gc_zbband a matching run-time to userev8; without Zbb the swap is a sequence of shifts and masks.rev8works on the full register width only, so the specification’s own hint for a 4-byte swap on RV64 isrev8followed by a shift right by 32.
Each ISA gets a listing of its own below, and all three load the same eight bytes from memory before reversing them. The files are listings rather than programs, so this lesson does not assemble them; the Python example after them reproduces the same swap end to end.
S (swap_x64.s) · swap_x64.s
# x86-64 byte reversal, in GNU as Intel syntax. bswap reverses a whole register;
# movbe reverses the bytes of a memory operand while it moves it.
.intel_syntax noprefix
mov rax, qword ptr [rdi] # rax = the eight bytes at rdi, in memory order
bswap rax # reverse all eight: 0x1122334455667788 -> 0x8877665544332211
bswap eax # the 32-bit form reverses the low four bytes only
movbe rax, qword ptr [rdi] # load eight big-endian bytes, reversed on the way in
movbe qword ptr [rdi], rax # store rax with its bytes reversed S (swap_a64.s) · swap_a64.s
// AArch64 byte reversal. rev takes every byte of its register; rev32 and rev16
// reverse the bytes inside each 32-bit or 16-bit part of it.
ldr x0, [x1] // x0 = the eight bytes at x1, in memory order
rev x0, x0 // reverse all eight: 0x1122334455667788 -> 0x8877665544332211
rev w2, w0 // the 32-bit form reverses four bytes
rev32 x0, x0 // reverse the bytes inside each 32-bit half of x0
rev16 w0, w0 // reverse the bytes inside each 16-bit half of w0 S (swap_rv64.s) · swap_rv64.s
# RV64 byte reversal. rev8 belongs to the Zbb extension, so this file assembles
# only with -march=rv64gc_zbb; an RV64GC build has no byte-reverse instruction.
ld a0, 0(a1) # a0 = the eight bytes at a1, in memory order
rev8 a0, a0 # reverse all eight: 0x1122334455667788 -> 0x8877665544332211
srai a0, a0, 32 # rev8 is full-width only: shift back by 32 for a 4-byte swap In Python, two short idioms do the same swap at the byte level. The first writes the integer out
in big-endian order with int.to_bytes and reads the same bytes back as little-endian with
int.from_bytes; the second does the equivalent round trip through struct:
"""Reverse the byte order of a 64-bit value two ways: int.to_bytes/from_bytes and struct.
A byte swap turns a value stored one way into the same bits read the other way: it is the
software form of x86's bswap, Arm's rev and RISC-V's rev8 (from the Zbb extension). Both
versions here produce the same number from the same input, which makes the trade-off between
the two Python idioms visible: to_bytes/from_bytes names the two orders in words; struct uses
format letters.
"""
import struct
value = 0x1122334455667788
def swap_with_int(x: int) -> int:
"""Write x out in big-endian order, then read the same bytes as little-endian."""
big_bytes = x.to_bytes(8, byteorder="big")
return int.from_bytes(big_bytes, byteorder="little")
def swap_with_struct(x: int) -> int:
"""Pack x as big-endian, unpack the same bytes as little-endian."""
big_bytes = struct.pack(">Q", x)
(reversed_value,) = struct.unpack("<Q", big_bytes)
return reversed_value
a = swap_with_int(value)
b = swap_with_struct(value)
print(f"input = 0x{value:016x}")
print(f"swap via to_bytes = 0x{a:016x}")
print(f"swap via struct = 0x{b:016x}")
print(f"two idioms agree = {a == b}")
round_trip = swap_with_int(a)
print(f"swap of the swap = 0x{round_trip:016x} (back to the input)") Output
input = 0x1122334455667788 swap via to_bytes = 0x8877665544332211 swap via struct = 0x8877665544332211 two idioms agree = True swap of the swap = 0x1122334455667788 (back to the input)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 byte_swap.py
Both idioms print 0x8877665544332211, the exact mirror of the input, and the two agree. The
second swap takes the output back to 0x1122334455667788, because reversing the order twice
restores it. On the processor side, the same idea runs in one instruction on x86-64 (bswap)
and AArch64 (rev), and in one instruction on RISC-V where the CPU has Zbb.
Checkpoint
Check: Byte-swap instructions
Strings are bytes in memory, with ASCII at the base
A string in memory is a run of bytes, one byte per character for English text and more bytes for most other scripts. The ASCII: A 7-bit character code defined in RFC 20 that assigns 128 codes to the English letters, digits and punctuation; each character fits in one byte. UTF-8 encodes the same code points as ASCII in one byte each. standard, documented in RFC 20, assigns a 7-bit code to each of 128 characters: the uppercase letters A through Z take values 0x41 to 0x5A, the lowercase letters 0x61 to 0x7A, the digits 0x30 to 0x39 and the space 0x20. Those codes fit in one byte each, so the 5-character string “hello” takes five bytes: 0x68 0x65 0x6C 0x6C 0x6F.
Two ways to mark the end of a string live together on real systems. A C-style string ends with a
null terminator: A single byte of value 0 appended to a string to mark its end. The C library's strlen, puts and printf all walk forward until they meet one; a length-prefixed call like write(2) uses a separate length instead., a single byte of value 0 that is not part of
the text; strlen, puts and printf all walk forward until they meet one. A length-prefixed
string, like what write(2) takes, carries the length as a separate number, so the content may
hold a 0 byte.
Each assembler has its own string directives that lay these bytes out at a known label. The two listings below are not assembled by this lesson; they name what each line produces:
ASM · strings_nasm.asm
; NASM lays out exactly the bytes the line lists, so the terminator is yours to add.
section .rodata
msg: db "hello", 0 ; 6 bytes: 68 65 6c 6c 6f 00
text: db "hello" ; 5 bytes, with nothing marking the end
banner: db "ping", 10 ; 5 bytes: 70 69 6e 67 0a, a newline and no NUL S · strings_gnu.s
# GNU as string directives. .ascii writes the characters and nothing else;
# .asciz and .string append one zero byte.
.section .rodata
plain: .ascii "hello" # 5 bytes: 68 65 6c 6c 6f
zeroed: .asciz "hello" # 6 bytes: the same five, then 00
same: .string "hello" # the same six bytes as .asciz
line: .ascii "hello\n" # 6 bytes: the newline is the single byte 0a The directives differ only in what they put after the characters. For the same five letters, this is what each line leaves at its label:
| Directive | Bytes at the label | Ends with a 0 byte |
|---|---|---|
NASM db "hello" |
68 65 6c 6c 6f (5) |
no |
NASM db "hello", 0 |
68 65 6c 6c 6f 00 (6) |
yes, written by hand |
GNU as .ascii "hello" |
68 65 6c 6c 6f (5) |
no |
GNU as .asciz "hello" |
68 65 6c 6c 6f 00 (6) |
yes, from the directive |
GNU as .string "hello" |
68 65 6c 6c 6f 00 (6) |
yes, from the directive |
GNU as .ascii "hello\n" |
68 65 6c 6c 6f 0a (6) |
no |
The last row is the one that catches people out: an escape inside the string counts once, so
\n is the single byte 0x0A rather than two characters, and .ascii still appends nothing after
it. That is why strlen on a label written with .ascii walks past its end, while write(2)
with a length of 5 is content with either form. MySmartCoPilot’s
text to binary converter lists the bytes of each character of
a string, so a label’s bytes can be predicted before assembling.
Checkpoint
Check: Strings and ASCII in memory
UTF-8 encodes code points in one to four bytes
ASCII covers 128 characters, which is enough for an English keyboard. For every other script, and for the emoji, the modern answer is Unicode: each character has a number called its code point: The number Unicode assigns to a character, written as U+XXXX. UTF-8 and other encodings turn code points into bytes; U+0928 is the Devanagari letter NA, for example., written as U+XXXX, and a program chooses an encoding to turn code points into bytes. The encoding this section follows is UTF-8: The byte encoding of Unicode defined in the Unicode Standard section 3.9 and RFC 3629. It stores each code point in one to four bytes under a leading-bits rule, with ASCII code points taking one byte each., which the Unicode Standard defines in section 3.9 and RFC 3629 specifies for Internet use. UTF-8 encodes each code point in one to four bytes under a simple range rule: U+0000 to U+007F take one byte each and match ASCII, U+0080 to U+07FF take two bytes, U+0800 to U+FFFF take three, and U+10000 to U+10FFFF take four. The first byte of a multi-byte sequence names the length by its leading bits (110…, 1110…, 11110…) and the bytes after it start with 10, which marks a continuation.
A Python one-liner compares len(s), which counts code points, with len(s.encode('utf-8')),
which counts storage bytes. Three samples make the gap visible: a 5-letter ASCII word, the
Devanagari word for namaste, and the rocket emoji.
"""Count UTF-8 bytes beside code points on three samples: ASCII, Devanagari, an emoji.
Byte length and character count are the same thing only for ASCII text; UTF-8 encodes any
Unicode code point in 1 to 4 bytes, so a buffer sized by len(s) is not big enough for a non-
ASCII string, and a byte index into the encoded form is not a character index.
"""
samples = [
("hello", "hello"),
("namaste (devanagari)", "\u0928\u092e\u0938\u094d\u0924\u0947"),
("rocket emoji", "\U0001f680"),
]
def hexpairs(data: bytes) -> str:
return " ".join(f"{b:02x}" for b in data)
print(f"{'sample':24} {'code points':>12} {'bytes':>5} utf-8 bytes")
print("-" * 70)
for label, text in samples:
encoded = text.encode("utf-8")
print(f"{label:24} {len(text):>12} {len(encoded):>5} [{hexpairs(encoded)}]")
print()
print("Each code point of 'namaste' in U+XXXX form:")
for ch in "\u0928\u092e\u0938\u094d\u0924\u0947":
print(f" U+{ord(ch):04X} {len(ch.encode('utf-8'))} bytes")
print()
print("Rocket emoji code point and its UTF-8 bytes:")
rocket = "\U0001f680"
print(f" U+{ord(rocket):04X} {len(rocket.encode('utf-8'))} bytes") Output
sample code points bytes utf-8 bytes ---------------------------------------------------------------------- hello 5 5 [68 65 6c 6c 6f] namaste (devanagari) 6 18 [e0 a4 a8 e0 a4 ae e0 a4 b8 e0 a5 8d e0 a4 a4 e0 a5 87] rocket emoji 1 4 [f0 9f 9a 80] Each code point of 'namaste' in U+XXXX form: U+0928 3 bytes U+092E 3 bytes U+0938 3 bytes U+094D 3 bytes U+0924 3 bytes U+0947 3 bytes Rocket emoji code point and its UTF-8 bytes: U+1F680 4 bytes
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 utf8_sizes.py
The ASCII word reports 5 code points and 5 bytes; namaste reports 6 code points and 18 bytes; the rocket emoji reports 1 code point and 4 bytes. Walk through the Devanagari word to see how its six code points become eighteen bytes:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- One word, how many bytes?. Python's len() counts characters, which Unicode calls code points, not storage bytes. For the Devanagari word namaste the character count and the byte count disagree, so a buffer sized by len() is too small for the encoded text.
- Each character is a code point. namaste in Devanagari uses six code points, from U+0928 (DEVANAGARI LETTER NA) to U+0947 (DEVANAGARI VOWEL SIGN E). The U+XXXX form is the Unicode name, independent of how the characters are stored on disk or in memory.
- Three bytes per character here. The Unicode Standard splits code points by range. Values from U+0800 to U+FFFF, which covers Devanagari (U+0900 to U+097F), take three UTF-8 bytes each. Values up to U+007F take one byte, so plain English text in UTF-8 is identical to ASCII.
- Encode the first code point. U+0928 encodes to the three bytes 0xE0 0xA4 0xA8. The first byte starts with the bits 1110, which flags a three-byte sequence; each byte that follows starts with 10, which marks a continuation. Replacing one of those continuation bytes breaks the character.
- Six code points, 18 bytes. Each of the remaining five code points also lies in U+0800 to U+FFFF, so each adds another three bytes. 6 × 3 = 18, which matches the recorded len(s.encode('utf-8')) = 18 against len(s) = 6 of the Python example.
- Read the bytes back. Reading the encoded form byte by byte gives e0 a4 a8 e0 a4 ae e0 a4 b8 e0 a5 8d e0 a4 a4 e0 a5 87. Dividing those 18 bytes into six groups of three recovers the six code points, which is how a UTF-8 reader turns bytes back into characters.
The flow starts with the word, lists its six code points from U+0928 to U+0947, states the three-bytes-per-code-point rule for the U+0800 to U+FFFF range, encodes U+0928 into 0xE0 0xA4 0xA8, multiplies six by three to reach 18 bytes, and reads the full encoded form back as e0 a4 a8 e0 a4 ae e0 a4 b8 e0 a5 8d e0 a4 a4 e0 a5 87. The character count and the byte count agree only when the text is pure ASCII; above U+007F they always disagree. MySmartCoPilot’s Unicode character inspector shows the code point and the UTF-8 bytes of every character pasted into it.
Byte index is not character index
A program that walks a UTF-8 string one byte at a time and reports the position of each character counts the wrong thing: for namaste it would report 18 positions, not 6. Count code points by skipping the continuation bytes (the ones that start with 10), or decode the bytes with a UTF-8 reader first.
Checkpoint
Check: UTF-8 bytes and code points
Practise
The self-check covers the four ideas above, from the byte at offset 0 to the bytes of an emoji; it asks first about the one you have answered least well:
Practice that adapts to you
Practise this lesson
8 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Why is a Devanagari string wider in bytes than in characters, and where does that gap show up in code?
Unicode gives every character a code point, a number such as U+0928, and UTF-8 is the rule that
turns code points into bytes. The rule packs code points U+0000 to U+007F into one byte each
(which is why English ASCII text takes the same room in UTF-8 as it does in ASCII) and spreads
code points above that over two, three or four bytes under a leading-bits scheme that the Unicode
Standard sets out in section 3.9. Devanagari characters sit in U+0900 to U+097F, which falls in
the three-byte range, so a Devanagari word of six code points takes 18 bytes; an emoji above U+FFFF,
such as the rocket at U+1F680, takes four bytes. The gap shows up as soon as a program sizes a buffer by len(s) and
then copies in s.encode('utf-8'): the destination is too small and the write stops in the
middle of a character, which the next reader cannot decode. It also shows up when a program steps
through a string one byte at a time and treats each step as a character, which overcounts and
splits characters across its own boundaries. The fix on both sides is the same: size the buffer
by len(s.encode('utf-8')), which is the exact byte count, and walk the bytes with a UTF-8
reader (or skip the continuation bytes that start with 10 when counting code points by hand).
The module bank holds two harder cases for this lesson: a length prefix read from a socket in the wrong byte order, and a database column sized for characters that UTF-8 stores in up to four bytes. Their rows give the first step and the follow-ups:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A teammate's server on x86-64 Linux reads a 32-bit length prefix from a TCP socket and allocates a buffer of that size. A client that sends a length of 1 makes the server try to allocate 16,777,216 bytes. What happened, and what is the one-line fix?
-
Mid to senior
You design a database column for a user handle up to 20 characters, which must accept Devanagari and emoji. A teammate writes CHAR(20) and expects it to hold 20 of any character. Where will that break on real input, and what sizing rule do you give them instead?
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
On x86-64, AArch64 and RISC-V, in what order do the bytes of 0x1122334455667788 sit in memory?
88 77 66 55 44 33 22 11: the lowest-value byte at the lowest address. All three are little-endian in practice, the reverse of how the number is written on paper.
-
What is big-endian order, and where is it still common?
The highest-value byte sits at the lowest address. Internet protocols use it on the wire and call it network byte order.
-
Which instruction reverses the byte order of a register on each ISA?
bswap on x86-64 (movbe does it as part of a load or store), rev on AArch64, with rev16 and rev32 for smaller groups, and rev8 on RISC-V, which needs the Zbb extension that RV64GC does not include.
-
How do C strings and calls like write(2) know where text ends?
A C string ends at a null terminator, one byte of value 0 that strlen and printf walk forward to. write(2) and other length-based interfaces take the number of bytes as a separate argument instead.
-
What are the code point count and the UTF-8 byte count of the Devanagari word namaste, and why?
6 code points but 18 bytes. Devanagari sits in U+0900 to U+097F, and UTF-8 needs three bytes for each code point in that range; ASCII takes one byte, and the rocket emoji four.
-
Why is a buffer sized by character count a bug for most non-English text?
UTF-8 stores many characters in two to four bytes, so the encoded text is longer than the count of characters. Size buffers by the encoded length, and decode or skip continuation bytes when you count characters.
Key takeaways
- Every mainstream x86-64, AArch64 and RISC-V system stores a multi-byte integer with its lowest
byte at the lowest address:
struct.pack('<Q', 0x1122334455667788)produces 88 77 66 55 44 33 22 11, which is the memory the processor sees. - A byte swap reverses that order in one instruction: x86-64 has
bswapfor a register andmovbefor load or store, AArch64 hasrev,rev16andrev32, and RISC-V hasrev8under the Zbb extension, which the RV64GC base does not include. - ASCII assigns 7-bit codes to 128 characters, so
"hello"is five bytes; a null terminator is a single byte of value 0 that marks the end for C-style strings, whilewrite(2)and other length-prefixed interfaces carry the length as a separate number. - UTF-8 encodes each Unicode code point in one to four bytes, so
len(s)(code points) andlen(s.encode('utf-8'))(bytes) are the same only for pure ASCII; namaste has 6 code points and 18 bytes, the rocket emoji has 1 code point and 4 bytes. - A buffer sized by character count is too small for most non-English text, and a byte-by-byte scan is not the same as a code-point scan: size by the encoded length, and decode or skip continuation bytes when counting characters.
Terms in this lesson
- ASCII
- A 7-bit character code defined in RFC 20 that assigns 128 codes to the English letters, digits and punctuation; each character fits in one byte. UTF-8 encodes the same code points as ASCII in one byte each.
- big-endian
- The byte-order rule in which the highest-value byte of a multi-byte value sits at the lowest address. Internet protocols call this 'network byte order' and use it on the wire.
- byte order
- The order the bytes of a multi-byte value take in memory, from the base address upwards. The two orders in use are little-endian (low byte first) and big-endian (high byte first).
- byte swap
- The operation that reverses the bytes of a multi-byte value, turning its little-endian form into the big-endian form or back. x86-64 does it with bswap, AArch64 with rev and RISC-V with rev8 under the Zbb extension.
- code point
- The number Unicode assigns to a character, written as U+XXXX. UTF-8 and other encodings turn code points into bytes; U+0928 is the Devanagari letter NA, for example.
- endianness
- The rule a computer follows to place the bytes of a multi-byte value in memory. Little-endian puts the lowest-value byte at the lowest address; big-endian puts the highest-value byte there.
- little-endian
- The byte-order rule in which the lowest-value byte of a multi-byte value sits at the lowest address. Every mainstream x86-64, AArch64 and RISC-V system is little-endian in practice.
- null terminator
- A single byte of value 0 appended to a string to mark its end. The C library's strlen, puts and printf all walk forward until they meet one; a length-prefixed call like write(2) uses a separate length instead.
- UTF-8
- The byte encoding of Unicode defined in the Unicode Standard section 3.9 and RFC 3629. It stores each code point in one to four bytes under a leading-bits rule, with ASCII code points taking one byte each.
References
- The Unicode Standard, section 3.9 'Unicode Encoding Forms' (UTF-8) (Unicode Consortium)
- RFC 3629: UTF-8, a transformation format of ISO 10646 (Internet Engineering Task Force)
- RFC 20: ASCII format for network interchange (Internet Engineering Task Force)
- Intel 64 and IA-32 Architectures Software Developer's Manual, Volume 2 (BSWAP, MOVBE) (Intel Corporation)
- Learn the architecture: A64 Instruction Set Architecture Guide (REV, REV16, REV32) (Arm Limited)
- The RISC-V Instruction Set Manual, Volume I: Unprivileged ISA (Zbb, rev8) (RISC-V International)
- RISC-V Profiles: RVA22U64 and RVA23U64 mandatory extensions (RISC-V International)
- Python documentation: struct, Interpret bytes as packed binary data (Python Software Foundation)
- NASM, the Netwide Assembler documentation (db, dw, dd) (The NASM Development Team)
- GNU assembler documentation: pseudo-ops (.ascii, .asciz, .string) (GNU Project (sourceware.org))
Report a problem with this lesson
Kept only in this browser. Your Learn progress