Dữ liệu & CPU
6/23
Bài 6 / 23~18 phútBiểu diễn dữ liệuMiễn phí lượt xem

Byte order và thao tác bit — endianness, mask, shift

Big-endian và little-endian là gì và vì sao quan trọng khi đọc file nhị phân hay packet mạng. Cùng bộ thao tác bit thực dụng: mask, shift, cờ bitmask.

TL;DR: Khi lưu số nguyên nhiều byte xuống bộ nhớ, máy phải chọn thứ tự: byte thấp trước (little-endian, x86/ARM) hay byte cao trước (big-endian, mạng/file chuẩn). Nhầm thứ tự → đọc sai giá trị hoàn toàn. Bên cạnh đó, thao tác bit (AND, OR, XOR, NOT, shift) cho phép gói nhiều cờ boolean vào một số nguyên duy nhất, kiểm tra chẵn/lẻ tức thì, và nhân/chia luỹ thừa 2 không cần phép nhân thật. Hai kỹ năng này là nền tảng khi đọc file binary, parse packet mạng, hay viết permission system.

Bạn ghi số 305419896 (tức 0x12345678) ra file trên máy x86, rồi mở file đó trên một hệ thống khác dùng quy ước ngược — bỗng nhiên đọc về 0x78563412, một con số hoàn toàn khác. Không có bit nào thay đổi, nhưng giá trị đọc được sai hoàn toàn chỉ vì hai bên dùng thứ tự byte khác nhau.

Bài này giải thích vì sao thứ tự byte tồn tại, khi nào nó gây rắc rối, và bộ thao tác bit giúp bạn giải quyết những bài toán thường gặp gọn hơn — cốt lõi để bạn đọc file binary, parse packet mạng, và implement permission flags đúng cách.

1. Analogy — cách viết ngày tháng

Người Mỹ viết ngày 06/16 (tháng trước, ngày sau); người Việt và châu Âu viết 16/06 (ngày trước, tháng sau). Cùng một sự kiện — 16 tháng 6 — nhưng hai quy ước thứ tự khác nhau. Ai không biết quy ước của bên kia sẽ đọc nhầm: "tháng 16" không tồn tại.

Máy tính gặp vấn đề y hệt khi lưu số nguyên 4 byte: phải chọn byte nào đứng trước trong bộ nhớ.

Đời thườngKhái niệm máy tính
Tháng/Ngày (Mỹ)Little-endian (byte thấp trước)
Ngày/Tháng (Việt/EU)Big-endian (byte cao trước)
Ai không biết quy ước → đọc saiChương trình không xử lý endianness → parse sai giá trị
Quy ước phổ biến toàn cầu = ISO 8601Network byte order = big-endian (RFC 791)
💡 Cách nhớ

Little-endian = byte nhỏ (thấp) đứng trước — "little first". Big-endian = byte to (cao) đứng trước — "big first". Tên đặt theo byte đầu tiên được ghi vào địa chỉ thấp nhất.

2. Big-endian và little-endian khác nhau thế nào?

2.1 Ví dụ cụ thể với 0x12345678

Số 0x12345678 (305.419.896 thập phân) chiếm 4 byte:

  • Byte cao nhất (most significant byte): 0x12
  • Byte tiếp theo: 0x34
  • Byte tiếp theo: 0x56
  • Byte thấp nhất (least significant byte): 0x78

Giả sử địa chỉ bắt đầu là 0x1000:

Địa chỉLittle-endian (x86/ARM)Big-endian (mạng)
0x10000x78 (byte thấp trước)0x12 (byte cao trước)
0x10010x560x34
0x10020x340x56
0x10030x12 (byte cao sau)0x78 (byte thấp sau)

Bốn ô địa chỉ từ 0x1000 tới 0x1003 với hai hàng byte: hàng little-endian của x86 và ARM ghi 0x78 0x56 0x34 0x12, hàng big-endian của mạng và PNG ghi 0x12 0x34 0x56 0x78; chương trình little-endian đọc bốn byte big-endian này ra 0x78563412 — sai giá trị mà không sai byte nào

C
// Doc 4 byte tu dia chi 0x1000:
// Little-endian: doc 78 56 34 12 -> ghep nguoc lai -> 0x12345678 (dung)
// Big-endian:    doc 12 34 56 78 -> ghep thuan   -> 0x12345678 (dung)
// Nhung neu chuong trinh little-endian doc file big-endian:
// Doc 12 34 56 78 -> ghep nguoc lai -> 0x78563412 (SAI!)
Python
import struct

# Python minh hoa: ghi va doc voi endianness khac nhau
value = 0x12345678

# Ghi theo little-endian (< = little-endian trong struct format)
le_bytes = struct.pack('<I', value)
print("Little-endian bytes:", [hex(b) for b in le_bytes])
# Output: ['0x78', '0x56', '0x34', '0x12']

# Ghi theo big-endian (> = big-endian)
be_bytes = struct.pack('>I', value)
print("Big-endian bytes:   ", [hex(b) for b in be_bytes])
# Output: ['0x12', '0x34', '0x56', '0x78']

# Doc nham: doc le_bytes nhung interpret theo big-endian
wrong = struct.unpack('>I', le_bytes)[0]
print("Doc nham endianness:", hex(wrong))
# Output: 0x78563412 -- SAI!

2.2 Khi nào endianness quan trọng

Quan trọng — cần xử lý tường minh:

  • Đọc/ghi file binary (PNG, BMP, WAV, ZIP…): mỗi format quy định rõ endianness trong spec. PNG dùng big-endian cho mọi trường số nguyên.
  • Giao tiếp mạng: TCP/IP (RFC 791) quy định network byte order = big-endian. Mọi header packet (port, địa chỉ IP dạng số) phải convert sang big-endian trước khi gửi.
  • Ép kiểu con trỏ (reinterpret_cast trong C++, unsafe trong Rust): đọc bytes thô trực tiếp theo layout bộ nhớ.

Không quan trọng — CPU tự xử lý:

  • Tính toán bình thường trong RAM (a + b, a * b): CPU đọc và ghi theo endianness của chính nó — nhất quán tuyệt đối, kết quả luôn đúng.
  • Truy cập biến trong ngôn ngữ bậc cao: compiler và runtime lo hết, bạn chỉ thấy giá trị logic.
💡 Quy tắc đơn giản

Endianness chỉ quan trọng khi dữ liệu vượt ranh giới — ra file, ra mạng, hoặc đọc bytes thô. Trong phạm vi một chương trình đang chạy, endianness trong suốt.

Thử ngẫmbạn nhận báo cáo bug "giá trị đọc từ file đúng trên macOS nhưng sai trên server Linux ARM cũ". Cả hai đều little-endian. Endianness có phải nguyên nhân ở đây, hay bạn nên nghi ngờ điều khác?

3. Thao tác bit — bộ công cụ thực dụng

Bên cạnh endianness, thao tác trực tiếp trên từng bit của một số nguyên là kỹ năng thường gặp trong systems programming, embedded, và permission system.

3.1 Các toán tử bit cơ bản

Toán tửTênÝ nghĩa
&ANDBit kết quả = 1 chỉ khi CẢ HAI bit đều 1
|ORBit kết quả = 1 khi ÍT NHẤT MỘT bit = 1
^XORBit kết quả = 1 khi HAI bit KHÁC NHAU
~NOTĐảo ngược mọi bit (bitwise complement)
<<Shift tráiDịch tất cả bit sang trái N vị trí, điền 0 bên phải
>>Shift phảiDịch tất cả bit sang phải N vị trí
Python
a = 0b1010  # 10 decimal
b = 0b1100  # 12 decimal

print(bin(a & b))   # 0b1000  (8)  -- AND: chi bit ca hai deu 1
print(bin(a | b))   # 0b1110  (14) -- OR:  bit nao co it nhat 1 con 1
print(bin(a ^ b))   # 0b0110  (6)  -- XOR: bit khac nhau thi = 1
print(bin(~a))      # -0b1011 (-11)-- NOT: dao nguoc (two's complement)
print(bin(a << 1))  # 0b10100 (20) -- shift trai 1: nhan 2
print(bin(a >> 1))  # 0b101   (5)  -- shift phai 1: chia 2 (bo phan le)

3.2 Pattern thực dụng: bitmask và cờ boolean

Bitmask là dùng từng bit của một số nguyên để đại diện cho một boolean flag độc lập. Thay vì dùng 8 biến bool riêng lẻ, dùng 1 số int với 8 bit.

Bốn phép bitmask trên cùng giá trị perms bằng 0b100: bật dùng OR cho 100 thành 110; xoá dùng AND với NOT của mask cho 110 về 100; hỏi dùng AND rồi so khác 0; đảo dùng XOR cho 100 thành 101 — ba bit thay ba biến bool và mỗi phép gọn trong một lệnh CPU

Ví dụ: permission Unix-style (3 bit)

Python
# Permission flags: 3 bit cuoi cua mot so nguyen
READ    = 0b100  # bit 2 = quyen doc   (4)
WRITE   = 0b010  # bit 1 = quyen ghi   (2)
EXECUTE = 0b001  # bit 0 = quyen chay  (1)

# --- Set bit: dung OR de bat co ---
perms = 0
perms = perms | READ    # bat quyen doc
perms = perms | WRITE   # bat quyen ghi
print(bin(perms))       # 0b110 (6) -- doc + ghi

# --- Clear bit: dung AND voi NOT cua mask ---
perms = perms & ~WRITE  # tat quyen ghi
print(bin(perms))       # 0b100 (4) -- chi doc

# --- Test bit: dung AND, ket qua != 0 thi bit dang bat ---
can_read    = (perms & READ)    != 0  # True
can_write   = (perms & WRITE)   != 0  # False
can_execute = (perms & EXECUTE) != 0  # False

# --- Toggle bit: dung XOR de dao trang thai ---
perms = perms ^ EXECUTE  # bat quyen chay
print(bin(perms))        # 0b101 (5) -- doc + chay

Tương tự trong Java:

Java
static final int READ    = 0b100;
static final int WRITE   = 0b010;
static final int EXECUTE = 0b001;

int perms = 0;
perms |= READ;              // set bit READ
perms &= ~WRITE;            // clear bit WRITE
boolean canRead = (perms & READ) != 0;  // test bit READ
perms ^= EXECUTE;           // toggle bit EXECUTE

3.3 Pattern khác hay dùng

Kiểm tra số chẵn/lẻ — dùng & 1 (AND với bit thấp nhất):

Python
def is_odd(n):
    return (n & 1) == 1

print(is_odd(7))   # True  -- 0b0111 & 0b0001 = 1
print(is_odd(8))   # False -- 0b1000 & 0b0001 = 0

Nhanh hơn phép chia % 2 vì CPU không cần thực hiện phép chia. Compiler hiện đại thường tự tối ưu % 2 thành & 1; viết & 1 khi muốn làm rõ ý định bit-level.

Nhân/chia luỹ thừa 2 bằng shift:

Python
x = 5
print(x << 1)  # 10  -- x * 2
print(x << 2)  # 20  -- x * 4
print(x << 3)  # 40  -- x * 8

print(x >> 1)  # 2   -- x // 2 (bo phan le)
print(x >> 2)  # 1   -- x // 4

x << n tương đương x * (2^n); x >> n tương đương x // (2^n). Compiler hiện đại thường tự tối ưu phép nhân luỹ thừa 2 thành shift — bạn viết x * 4 cũng được; shift explicit hữu ích khi cần rõ ý định bit-level.

📚 Đào sâu (tuỳ chọn) — Network byte order và hàm htonl/ntohl

C cung cấp 4 hàm chuẩn để convert endianness khi viết network code:

  • htons(x) — host to network short (16-bit): đổi từ endianness máy chủ sang big-endian
  • htonl(x) — host to network long (32-bit): tương tự cho 32-bit
  • ntohs(x) — network to host short (16-bit): ngược lại
  • ntohl(x) — network to host long (32-bit): ngược lại

Trên máy big-endian, các hàm này là no-op. Trên x86 (little-endian), chúng swap byte order. Code dùng đúng htonl/ntohl chạy đúng trên cả hai kiến trúc.

Vì sao x86 chọn little-endian? Lý do thực dụng: khi cộng hai số nhiều byte, CPU bắt đầu từ byte thấp nhất (carry lan dần lên byte cao). Với little-endian, byte thấp nằm ở địa chỉ thấp nhất — CPU không cần biết trước độ dài số để bắt đầu tính carry.

Arithmetic vs Logical shift phải cho số âm: >> trên số có dấu thường là arithmetic shift (giữ nguyên bit dấu, điền 1 bên trái nếu số âm) — (-8) >> 1 ra -4. Trên số không dấu là logical shift (điền 0). Chi tiết phụ thuộc ngôn ngữ: Java có >>> cho logical shift phải.

Thử ngẫmXOR có tính chất a ^ a = 0. Chạy a = a ^ b, b = a ^ b, a = a ^ b trên hai biến nguyên — kết quả cuối của ab là gì?

4. Áp dụng vào code của bạn

4.1 Dùng bitmask thay nhiều biến boolean

Giả sử bạn cần biểu diễn trạng thái của một task trong pipeline (running, paused, failed, cancelled, retrying).

Sai — 5 biến bool rời rạc:

Python
# Code sai: kho kiem tra to hop trang thai, de inconsistent
is_running   = True
is_paused    = False
is_failed    = False
is_cancelled = False
is_retrying  = False

# Lam sao biet trang thai chinh xac? Va sao dam bao chi co 1 trang thai active?

Đúng — 1 int bitmask:

Python
# Code dung: bitmask ro rang, kiem tra to hop de
RUNNING   = 1 << 0  # 0b00001
PAUSED    = 1 << 1  # 0b00010
FAILED    = 1 << 2  # 0b00100
CANCELLED = 1 << 3  # 0b01000
RETRYING  = 1 << 4  # 0b10000

status = RUNNING
# Bat dau retry: dang chay va dang retry (to hop)
status |= RETRYING        # status = RUNNING | RETRYING

# Kiem tra
is_active   = bool(status & RUNNING)   # True
is_retrying = bool(status & RETRYING)  # True
is_done     = bool(status & (FAILED | CANCELLED))  # False

# Reset ve trang thai sach
status = FAILED   # chi mot trang thai

Bitmask đặc biệt hữu ích khi cần biểu diễn tập con (một task có thể vừa RUNNING vừa RETRYING), truyền nhiều flag qua một tham số hàm, hoặc lưu nhiều boolean vào một cột int trong database.

4.2 Đọc dữ liệu binary — luôn xét endianness

Sai — đọc file binary bỏ qua endianness:

Python
# Doc sai: khong xet endianness, ket qua phu thuoc machine
with open("data.bin", "rb") as f:
    raw = f.read(4)
    value = int.from_bytes(raw, byteorder='little')  # gia su little?
    # Neu file theo big-endian -> gia tri sai hoan toan

Đúng — doc spec, convert tường minh:

Python
import struct

# Tra spec: PNG dung big-endian cho moi truong so nguyen
# https://www.w3.org/TR/PNG/#7Integers-and-byte-order

def read_png_chunk_length(f):
    raw = f.read(4)
    # Dung '>' (big-endian) theo PNG spec
    length = struct.unpack('>I', raw)[0]
    return length

# Doi voi Java (doc tu InputStream):
# DataInputStream tu dong xu ly big-endian: dis.readInt() la big-endian
# ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getInt() cho little-endian

Nguyên tắc: trước khi đọc bất kỳ file binary hay packet nào, tra spec để biết endianness của format đó, rồi dùng API đúng (struct.pack/unpack Python, ByteBuffer.order() Java, BinaryReader C#...).

5. Liên hệ các bài khác

6. Tóm tắt

  • Little-endian (x86, ARM phổ biến): byte thấp nhất lưu ở địa chỉ thấp nhất. Big-endian (network, một số format file): byte cao nhất lưu trước.
  • Endianness chỉ quan trọng khi dữ liệu vượt ranh giới — ghi file binary, gửi packet mạng, hoặc ép kiểu con trỏ. Trong tính toán nội bộ, CPU xử lý tự động.
  • Network byte order = big-endian (RFC 791). Hàm htonl/ntohl convert giữa endianness máy chủ và network.
  • & để test/clear bit, | để set bit, ^ để toggle bit, ~ để đảo tất cả bit.
  • Bitmask: gói nhiều cờ boolean vào 1 số nguyên — tiết kiệm bộ nhớ, truyền qua 1 tham số, dễ kiểm tra tổ hợp.
  • & 1 kiểm tra chẵn/lẻ; << n nhân 2^n; >> n chia 2^n (bỏ phần lẻ).
  • Trước khi đọc file binary hay packet: tra spec để biết endianness, rồi dùng API convert tường minh.

7. Tự kiểm tra

Tự kiểm tra
0/7 câu đã trả lời
  1. Q1
    Số 0x0A0B0C0D được lưu theo little-endian vào 4 byte. Thứ tự 4 byte đó là gì (từ địa chỉ thấp đến cao)?
  2. Q2
    Vì sao khi viết code tính toán bình thường (cộng, nhân hai số int) bạn không cần lo về endianness, nhưng khi ghi số đó ra file thì phải lo?
  3. Q3
    Cho perms = 0b101 (READ=4, WRITE=2, EXECUTE=1). Viết biểu thức để: (a) thêm quyền WRITE; (b) kiểm tra có quyền READ không; (c) xoá quyền EXECUTE.
  4. Q4
    Vì sao dùng bitmask để lưu nhiều boolean flag lại tiện hơn dùng nhiều biến bool riêng lẻ?
  5. Q5
    Biểu thức n & (n - 1) cho kết quả gì khi n là luỹ thừa của 2? Vì sao pattern này hay được dùng?
  6. Q6
    Network byte order là gì và vì sao giao thức mạng TCP/IP chọn quy ước đó?
  7. Q7
    Vì sao x << 3 tương đương nhân 8, và khi nào shift phải >> có thể cho kết quả bất ngờ?

Bài tiếp theo: Tổng kết & cheat sheet

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên

Bài tiếp theo

Module 1 — Tổng kết & cheat sheet biểu diễn dữ liệu