Computer Systems: A Programmer’s Perspective Chương 3 · Machine-Level Representation of Programs

Chương 3 · Biểu diễn chương trình ở mức máy Chapter 3 · Machine-Level Representation of Programs

Dưới chương trình C của bạn là một dãy byte mà bộ xử lý đọc tuần tự Máy tính không thấy mã C của bạn — nó chỉ thấy các con số Beneath your C program lies a sequence of bytes the processor reads one by one

Bryant và O’Hallaron mở chương bằng một thực tế dễ bị quên: máy tính thực thi mã máy (machine code) — những dãy byte mã hoá các thao tác cấp thấp để xử lý dữ liệu, quản lý bộ nhớ, đọc/ghi thiết bị và giao tiếp mạng.1 Trình biên dịch C như gcc biến mã nguồn thành mã hợp ngữ (assembly code), rồi assembler và linker dựng thành mã máy thực thi được.1

Bạn viết chương trình bằng ngôn ngữ dễ đọc như C. Nhưng máy tính không đọc được chữ — nó chỉ chạy được các con số gọi là mã máy. Có một chương trình đặc biệt (trình biên dịch) dịch từ C sang các con số đó. Chương này dạy bạn đọc ngược lại: nhìn các con số và hiểu máy đang làm gì. Nhớ nhé: dưới mỗi dòng C là một dãy byte, và ta học cách đọc dãy byte đó.

Bryant and O’Hallaron open the chapter with a fact that is easy to forget: a computer executes machine code — sequences of bytes encoding the low-level operations that manipulate data, manage memory, read and write devices, and communicate over networks.1 A C compiler such as gcc turns source into assembly code, and then an assembler and linker build the executable machine code.1

Vì sao một lập trình viên C phải học thứ này? Các tác giả trả lời: để đọc được mã do trình biên dịch sinh ra, bạn hiểu được khả năng tối ưu của nó và các điểm kém hiệu quả còn sót lại; nhiều cách tấn công hệ thống khai thác chính cách chương trình lưu trạng thái điều khiển lúc chạy, nên hiểu biểu diễn mức máy là điều kiện để phòng thủ.1 Đọc mã máy là một dạng kỹ thuật dịch ngược (reverse engineering): hiểu một hệ thống bằng cách nghiên cứu nó và lần ngược lại.1

Luận đề của chương: ở mức máy, chương trình chỉ còn là byte lệnh thao tác lên một trạng thái bộ xử lý gọn gàng — bộ đếm chương trình (program counter), 16 thanh ghi (register), các cờ điều kiện (condition code) — và một bộ nhớ phẳng đánh địa chỉ theo byte; nắm trạng thái đó cùng tập lệnh x86-64 là đủ để đọc hiểu mọi cấu trúc C sau khi biên dịch.1

Ý chính: máy tính chỉ cần nhớ vài thứ — đang chạy đến lệnh nào, 16 ô nhớ siêu nhanh (thanh ghi), vài lá cờ nhỏ, và một dải bộ nhớ lớn. Hiểu mấy thứ đó là đọc được mã máy. Nhớ nhé: ít thứ thôi — con trỏ lệnh, 16 thanh ghi, mấy lá cờ, và bộ nhớ.

The chapter’s thesis: at the machine level a program is just instruction bytes acting on a compact processor state — a program counter, 16 registers, a few condition codes — and a flat, byte-addressable memory; grasp that state plus the x86-64 instruction set and you can read almost any compiled C construct.1

Cụm khái niệm 01 Concept cluster 01

Mã máy, ISA, và chuỗi biên dịch Chữ của bạn biến thành số như thế nào Machine code, the ISA, and the compilation chain

Trình bày của sách dựa trên x86-64, ngôn ngữ máy của hầu hết máy tính xách tay, máy để bàn và máy chủ hiện nay.1 Dòng lệnh này tiến hoá rất lâu, bắt đầu từ bộ xử lý 16-bit đầu tiên của Intel năm 1978, mở rộng lên 32-bit rồi 64-bit; kết quả là một thiết kế khá kỳ lạ, chỉ hợp lý khi nhìn dưới góc độ lịch sử.15

Chương này dùng một loại máy phổ biến gọi là x86-64 — loại chip bên trong gần như mọi laptop, PC và máy chủ. Nó ra đời từ năm 1978 và cứ lớn dần, nên có nhiều chỗ hơi lạ do lịch sử để lại. Nhớ nhé: x86-64 là “tiếng nói” của đa số máy tính bạn đang dùng.

The book’s presentation is based on x86-64, the machine language of most of today’s laptops, desktops and servers.1 It evolved over a long history, starting with Intel’s first 16-bit processor in 1978, expanding to 32 bits and then 64 bits; the result is a rather peculiar design that makes sense only in historical perspective.15

Định nghĩa. Kiến trúc tập lệnh (instruction set architecture, ISA) định nghĩa trạng thái bộ xử lý, định dạng lệnh, và tác động của mỗi lệnh lên trạng thái. Hầu hết ISA, kể cả x86-64, mô tả chương trình như thể mỗi lệnh chạy xong rồi mới tới lệnh sau — dù phần cứng thật sự thực thi nhiều lệnh đồng thời.1

Nói đơn giản. ISA là bản hợp đồng: nó liệt kê máy có những lệnh gì và mỗi lệnh làm gì. Nhờ hợp đồng này, trình biên dịch biết phải sinh ra các con số nào. Nhớ nhé: ISA là bảng “thực đơn” các lệnh của máy.

Definition. The instruction set architecture (ISA) defines the processor state, the instruction format, and the effect of each instruction on the state. Most ISAs, x86-64 included, describe a program as if each instruction finishes before the next begins — even though the hardware really executes many at once.1

p1.c ─(gcc)→ p1.s
  p1.s ─(as)→ p1.o
    p1.o ─(ld)→ p
C → asm → object → executable

mã C → mã hợp ngữ
  → tệp đối tượng
    → chương trình chạy được

p1.c ─(gcc)→ p1.s
  p1.s ─(as)→ p1.o
    p1.o ─(ld)→ p
C → asm → object → executable

  1. machine code · bytes

    Mã máy — chương trình chỉ là một dãy byte Chương trình thật ra là một dãy số Machine code — a program is just bytes

    Bài học cốt lõi: chương trình mà máy chạy chỉ đơn giản là một dãy byte mã hoá chuỗi lệnh, và máy biết rất ít về mã nguồn đã sinh ra chúng.1 Với hàm multstore, các tác giả cho thấy đoạn mã đối tượng dài đúng 14 byte, với biểu diễn thập lục phân 53 48 89 d3 e8 … 5b c3.1

    Chương trình chạy được, nếu nhìn vào bên trong, chỉ là một chuỗi số. Mỗi số (byte) là một mẩu của một mệnh lệnh. Máy không giữ lại tên biến hay dòng chữ bạn viết — chỉ còn các con số. Nhớ nhé: máy chạy số, không chạy chữ.

    The core lesson: the program the machine runs is simply a sequence of bytes encoding a series of instructions, and the machine knows very little about the source code that produced them.1 For the multstore function the authors show the object code is exactly 14 bytes long, with the hexadecimal representation 53 48 89 d3 e8 … 5b c3.1

    Bạn gặp nó ở đâuWhere you meet it Lệnh x86-64 dài từ 1 đến 15 byte; cách mã hoá được thiết kế sao cho lệnh phổ biến và ít toán hạng chiếm ít byte hơn.1 Mỗi lệnh dài ngắn khác nhau, từ 1 đến 15 byte. x86-64 instructions range from 1 to 15 bytes; the encoding is designed so common instructions with fewer operands take fewer bytes.1

    Giải mã là duy nhất

    Định dạng lệnh được thiết kế sao cho, từ một vị trí bắt đầu cho trước, có một cách giải mã byte thành lệnh duy nhất — ví dụ chỉ lệnh pushq %rbx mới bắt đầu bằng byte 53.1

    Bộ nhớ ảo, mảng byte

    Máy xem bộ nhớ như một mảng byte rất lớn; địa chỉ là địa chỉ ảo. Trên x86-64 hiện nay, địa chỉ 64-bit nhưng chỉ dùng 48 bit thấp, cho không gian tới 248 byte (64 terabyte).1

  2. processor state

    Trạng thái bộ xử lý — thứ C giấu khỏi bạn Những thứ máy phải nhớ khi chạy Processor state — what C hides from you

    Mã máy để lộ những phần trạng thái mà C thường che giấu: bộ đếm chương trình (PC, gọi là %rip) chỉ địa chỉ lệnh kế tiếp; tệp thanh ghi gồm 16 ô 64-bit; các cờ điều kiện ghi thông tin về phép tính gần nhất để phục vụ rẽ nhánh; và một tập thanh ghi vector chứa dữ liệu số nguyên hoặc dấu phẩy động.12

    Khi chạy, máy phải nhớ bốn thứ: đang ở lệnh nào, nội dung 16 ô nhớ nhanh, vài lá cờ ghi kết quả vừa tính, và mấy ô lớn để tính toán hàng loạt. Ngôn ngữ C giấu hết mấy thứ này đi cho bạn đỡ rối. Nhớ nhé: máy chỉ cần nhớ vài thứ nhỏ gọn.

    Machine code exposes parts of the state that C usually hides: the program counter (PC, called %rip) giving the address of the next instruction; the register file of 16 64-bit locations; the condition codes recording information about the most recent operation for branching; and a set of vector registers holding integer or floating-point data.12

    Bạn gặp nó ở đâuWhere you meet it Mỗi khi bạn dừng chương trình trong trình gỡ lỗi và xem thanh ghi, con trỏ lệnh và cờ, bạn đang nhìn thẳng vào trạng thái bộ xử lý này. Khi bạn mở trình gỡ lỗi và thấy các giá trị thanh ghi. Every time you pause a program in a debugger and inspect the registers, PC and flags, you are looking straight at this processor state.
  3. gcc · as · ld

    Chuỗi biên dịch — bốn chặng tới tệp chạy được Bốn bước dịch từ C ra chương trình The toolchain — four stages to an executable

    Lệnh gcc gọi cả một chuỗi chương trình: tiền xử lý mở rộng #include và macro; trình biên dịch sinh tệp hợp ngữ .s; assembler dịch hợp ngữ thành mã đối tượng nhị phân .o; và linker ghép các tệp đối tượng cùng mã thư viện (như printf) thành tệp thực thi.1 Sách dùng tuỳ chọn tối ưu -Og để mã sinh ra bám sát cấu trúc mã C gốc.1

    Từ mã C đến chương trình chạy được có bốn bước: dọn dẹp mã, dịch sang hợp ngữ, dịch hợp ngữ sang số, rồi dán các mảnh lại với nhau. Một lệnh gcc làm hết cả bốn. Nhớ nhé: bốn chặng — dọn, dịch, đóng gói, dán.

    The gcc command invokes a whole chain of programs: the preprocessor expands #include and macros; the compiler emits an assembly file .s; the assembler turns assembly into a binary object file .o; and the linker merges the object files with library code (such as printf) into an executable.1 The book uses optimization level -Og so the generated code follows the structure of the original C.1

    Bạn gặp nó ở đâuWhere you meet it Tuỳ chọn -S dừng ở hợp ngữ để bạn đọc; tệp mstore.o nặng 1.368 byte dù chỉ chứa 14 byte lệnh, còn prog đã liên kết lên tới 8.655 byte vì gồm cả mã khởi động.1 Dùng gcc -S để xem bước hợp ngữ. The -S option stops at assembly for you to read; the file mstore.o is 1,368 bytes even though its code is 14 bytes, and the linked prog grows to 8,655 bytes because it includes start-up code.1

    Giải mã ngược: objdump

    Để xem nội dung tệp mã máy, ta dùng disassembler như objdump -d; nó dựng lại dạng gần giống hợp ngữ hoàn toàn từ dãy byte, không cần mã nguồn.14

    Hai lối viết: ATT và Intel

    Sách dùng cú pháp ATT (mặc định của gcc): có hậu tố kích thước (movq), tên thanh ghi mang dấu %, và toán hạng nguồn đứng trước đích. Cú pháp Intel (tài liệu Intel, Microsoft) bỏ hậu tố và dấu %, và liệt kê toán hạng theo thứ tự ngược lại.12

Cụm khái niệm 02 Concept cluster 02

Dữ liệu, thanh ghi, toán hạng và di chuyển dữ liệu 16 ô nhớ nhanh và cách lấy, đặt dữ liệu Data, registers, operands and data movement

CPU x86-64 có 16 thanh ghi đa dụng 64-bit, tên đều bắt đầu bằng %r.1 Bộ 8086 nguyên thuỷ chỉ có tám thanh ghi 16-bit (%ax…%bp); IA32 mở lên 32-bit (%eax…), rồi x86-64 mở tám thanh ghi cũ lên 64-bit (%rax…%rbp) và thêm tám thanh ghi mới %r8…%r15.12

Trong máy có 16 ô nhớ cực nhanh gọi là thanh ghi, nhanh hơn bộ nhớ thường rất nhiều. Máy thích để dữ liệu đang dùng trong các ô này. Tên chúng nghe lạ như %rax, %rsp… vì lý do lịch sử. Nhớ nhé: 16 ô siêu nhanh, dữ liệu nóng để ở đây.

An x86-64 CPU has 16 general-purpose registers, each 64 bits, with names all beginning with %r.1 The original 8086 had only eight 16-bit registers (%ax…%bp); IA32 widened them to 32 bits (%eax…), and x86-64 widened the original eight to 64 bits (%rax…%rbp) and added eight new ones, %r8…%r15.12

Về quy ước ghi bytes phần cao. Các byte thấp của mỗi thanh ghi truy cập được riêng (1, 2, 4 hay 8 byte). Khi một lệnh sinh kết quả nhỏ hơn 8 byte, có hai quy ước: lệnh tạo 1 hoặc 2 byte giữ nguyên các byte còn lại, còn lệnh tạo 4 byte lại đặt 4 byte cao về 0.1 Đây là nguồn của nhiều nhầm lẫn và được bàn kỹ ở Phần 3.4.2.

Định nghĩa. Một toán hạng (operand) chỉ ra nguồn hoặc đích của một thao tác, thuộc ba loại: tức thời (hằng, viết $Imm), thanh ghi (nội dung một thanh ghi), và bộ nhớ (giá trị tại một địa chỉ hiệu dụng, effective address).1

Nói đơn giản. Mỗi lệnh phải nói rõ lấy dữ liệu ở đâu: một con số viết thẳng, một ô thanh ghi, hay một ô trong bộ nhớ. Chỉ ba chỗ đó thôi. Nhớ nhé: dữ liệu đến từ ba nơi — số thẳng, thanh ghi, bộ nhớ.

Definition. An operand names the source or destination of an operation, in three types: immediate (a constant, written $Imm), register (the contents of a register), and memory (the value at an effective address).1

eff. addr = Imm + R[rb] + R[ri]·s
  s ∈ {1, 2, 4, 8}
sizes: b=1 w=2 l=4 q=8 byte

địa chỉ = nền + chỉ số × cỡ + lệch
cỡ: b=1 w=2 l=4 q=8 byte

eff. addr = Imm + R[rb] + R[ri]·s
  s ∈ {1, 2, 4, 8}
sizes: b=1 w=2 l=4 q=8 bytes

  1. data formats · b/w/l/q

    Định dạng dữ liệu — và vì sao “word” chỉ 16 bit Mỗi kiểu dữ liệu chiếm mấy byte Data formats — and why a “word” is 16 bits

    Vì nguồn gốc 16-bit, Intel gọi 16-bit là “word”; do đó 32-bit là “double word” và 64-bit là “quad word”.1 Bảng kích thước: char 1 byte (hậu tố b), short 2 byte (w), int 4 byte (l), long và con trỏ 8 byte (q), float 4 byte (s), double 8 byte (l).1

    Mỗi kiểu dữ liệu chiếm một số byte nhất định: một ký tự 1 byte, số nguyên thường 4 byte, số nguyên lớn và con trỏ 8 byte. Chữ “word” ở đây nghĩa là 16 bit — nhỏ hơn bạn tưởng, vì lý do lịch sử. Nhớ nhé: “word” của Intel chỉ 16 bit thôi.

    Because of its 16-bit origins, Intel calls 16 bits a “word”; so 32 bits is a “double word” and 64 bits a “quad word.”1 The sizes: char 1 byte (suffix b), short 2 bytes (w), int 4 bytes (l), long and pointers 8 bytes (q), float 4 bytes (s), double 8 bytes (l).1

    Bạn gặp nó ở đâuWhere you meet it Hậu tố ở cuối tên lệnh (movb/movw/movl/movq) chính là kích thước toán hạng; đọc hậu tố là biết lệnh thao tác trên mấy byte.1 Chữ cái cuối tên lệnh cho biết nó xử lý mấy byte. The suffix on an instruction name (movb/movw/movl/movq) is exactly the operand size; read the suffix to know how many bytes it touches.1
  2. register file · %rsp

    16 thanh ghi — và một chiếc đặc biệt 16 ô nhớ nhanh, một ô có vai riêng The 16 registers — and one special one

    Các thao tác có thể tác động lên byte thấp của 16 thanh ghi ở nhiều kích thước: 1 byte dùng phần thấp nhất, 2 byte, 4 byte, hay cả 8 byte.1 Đa phần thanh ghi dùng khá linh hoạt, nhưng %rsp là con trỏ ngăn xếp (stack pointer), chỉ vị trí cuối của ngăn xếp (stack) lúc chạy và không nên dùng vào việc khác.1 Phần còn lại tuân theo các quy ước lập trình chuẩn về việc truyền tham số và lưu giá trị tạm.13

    16 ô này dùng để giữ dữ liệu đang tính. Mỗi ô có thể dùng nguyên hoặc chỉ dùng phần thấp. Có một ô đặc biệt tên %rsp luôn làm nhiệm vụ riêng: nó đánh dấu đỉnh của “chồng giấy” mà máy dùng khi gọi hàm. Nhớ nhé: %rsp là ô dành riêng, đừng đụng vào.

    Operations can act on the low-order bytes of the 16 registers at several sizes: 1 byte uses the lowest portion, then 2, 4, or the full 8 bytes.1 Most registers are fairly flexible, but %rsp is the stack pointer, marking the end position of the run-time stack and should not be used otherwise.1 The rest follow standard programming conventions for passing arguments and holding temporaries.13

    Bạn gặp nó ở đâuWhere you meet it Ví dụ hàm exchange: tham số xp nằm ở %rdi, y ở %rsi, và giá trị trả về đặt ở %rax — minh hoạ vai trò quy ước của từng thanh ghi.1 Khi gọi hàm, dữ liệu vào ra đều đi qua các ô này. In the exchange function, parameter xp is in %rdi, y in %rsi, and the return value goes in %rax — illustrating each register’s conventional role.1

    Byte thấp có tên riêng

    Mỗi thanh ghi có tên cho phần 4 byte, 2 byte và 1 byte: ví dụ %rax → %eax → %ax → %al; tám thanh ghi mới dùng hậu tố d/w/b, như %r8d, %r8w, %r8b.1

    Quy ước gọi hàm

    Sáu tham số nguyên đầu tiên đi theo thứ tự %rdi, %rsi, %rdx, %rcx, %r8, %r9; một số thanh ghi do hàm gọi giữ (callee-saved), số khác do hàm gọi-đi giữ (caller-saved). Chi tiết thuộc ABI System V.13

  3. operand forms · effective address

    Toán hạng bộ nhớ — một công thức địa chỉ Cách chỉ ra một ô trong bộ nhớ Memory operands — one address formula

    Dạng toán hạng bộ nhớ tổng quát nhất là Imm(rb,ri,s), với địa chỉ hiệu dụng tính bằng Imm + R[rb] + R[ri]·s: một độ lệch hằng, một thanh ghi nền, một thanh ghi chỉ số, và hệ số tỉ lệ s phải là 1, 2, 4 hoặc 8.1 Các dạng khác chỉ là trường hợp đặc biệt khi bỏ bớt thành phần.1

    Muốn chỉ một ô trong bộ nhớ, máy dùng một công thức nhỏ: lấy một địa chỉ gốc, cộng thêm một bước nhảy nhân với cỡ phần tử, cộng một độ lệch. Công thức này rất hợp để duyệt mảng. Nhớ nhé: địa chỉ = gốc + bước × cỡ + lệch.

    The most general memory operand is Imm(rb,ri,s), with effective address Imm + R[rb] + R[ri]·s: a constant displacement, a base register, an index register, and a scale factor s that must be 1, 2, 4, or 8.1 The other forms are just special cases with components omitted.1

    Bạn gặp nó ở đâuWhere you meet it Dạng tổng quát này xuất hiện liên tục khi truy cập phần tử mảng và trường cấu trúc — chủ đề của Phần 3.8 và 3.9.1 Hay gặp nhất khi chương trình chạy qua từng phần tử mảng. This general form shows up constantly when accessing array elements and struct fields — the subject of Sections 3.8 and 3.9.1
  4. mov · lea

    mov và lea — chép dữ liệu, và tính địa chỉ Hai lệnh hay dùng nhất mov and lea — copy data, and compute addresses

    Lớp lệnh MOV (gồm movb/movw/movl/movq) chép dữ liệu từ nguồn sang đích mà không biến đổi; x86-64 cấm cả hai toán hạng cùng là bộ nhớ, nên chép bộ-nhớ-sang-bộ-nhớ cần hai lệnh.1 Riêng movl có đích là thanh ghi sẽ đặt 4 byte cao về 0.1 Lệnh leaq thì “đọc địa chỉ” chứ không đọc bộ nhớ: nó đặt vào đích chính địa chỉ hiệu dụng của toán hạng nguồn.1

    Lệnh mov chỉ chép một giá trị từ chỗ này sang chỗ khác, không đổi gì. Lệnh lea lạ hơn: nó không lấy giá trị, mà tính ra một địa chỉ — và vì công thức địa chỉ có cộng và nhân, nên dân lập trình lợi dụng nó để làm toán nhanh. Nhớ nhé: mov chép giá trị, lea tính địa chỉ.

    The MOV class (movb/movw/movl/movq) copies data from source to destination without transformation; x86-64 forbids both operands being memory, so memory-to-memory copying takes two instructions.1 Note that movl with a register destination sets the upper 4 bytes to 0.1 The leaq instruction “reads an address” rather than memory: it places the effective address of its source operand into the destination.1

    Bạn gặp nó ở đâuWhere you meet it Trình biên dịch dùng leaq để gộp cộng và nhân với hằng nhỏ: hàm scale tính x + 4*y + 12*z được dịch thành một chuỗi ba lệnh leaq.1 Khi thấy phép nhân với 2, 4, 8 biến mất khỏi mã máy. The compiler uses leaq to fold addition and small-constant multiplication: the scale function computing x + 4*y + 12*z compiles into a sequence of three leaq instructions.1

    Mở rộng dấu và mở rộng 0

    Khi chép từ nguồn nhỏ sang đích lớn, lớp MOVZ điền byte còn lại bằng 0, còn lớp MOVS điền bằng bản sao của bit cao nhất (mở rộng dấu).1 Lệnh cltq là dạng gọn của movslq %eax, %rax.1

    leaq không đặt cờ

    Vì sinh ra để tính địa chỉ, leaq không làm thay đổi cờ điều kiện; trong khi hầu hết lệnh số học trong Hình 3.10 thì có.1 Đây là lý do nó được tái dùng cho số học mà không “làm bẩn” trạng thái so sánh.

Cụm khái niệm 03 Concept cluster 03

Điều khiển và thủ tục: cờ, rẽ nhánh, ngăn xếp, call/ret Rẽ nhánh và gọi hàm hoạt động ra sao Control and procedures: flags, branches, the stack, call/ret

Mã máy có hai cơ chế cấp thấp để hiện thực hành vi có điều kiện: nó kiểm tra giá trị dữ liệu rồi đổi luồng điều khiển hoặc luồng dữ liệu theo kết quả.1 Thứ tự thực thi được đổi bằng lệnh nhảy (jump), có thể phụ thuộc kết quả một phép kiểm tra.1

Muốn máy biết “nếu a lớn hơn b thì làm việc này”, nó làm hai bước: so sánh hai số, ghi kết quả vào mấy lá cờ nhỏ; rồi nhìn cờ mà quyết định nhảy đi đâu. Nhớ nhé: so sánh trước, nhìn cờ rồi nhảy.

Machine code has two low-level mechanisms for conditional behaviour: it tests data values and then alters the control flow or the data flow based on the result.1 Execution order is changed with a jump instruction, possibly contingent on the result of a test.1

Định nghĩa. Ngoài thanh ghi số nguyên, CPU giữ các cờ điều kiện một-bit mô tả phép tính gần nhất: CF (nhớ ra ngoài, dùng cho tràn không dấu), ZF (kết quả bằng 0), SF (kết quả âm), OF (tràn số bù hai).1

Nói đơn giản. Sau mỗi phép tính, máy bật tắt bốn lá cờ nhỏ: kết quả có bằng 0 không, có âm không, có bị tràn không. Đó là trí nhớ ngắn hạn để quyết định rẽ nhánh. Nhớ nhé: bốn lá cờ ghi lại “phép vừa rồi ra sao”.

Definition. Besides the integer registers, the CPU keeps single-bit condition codes describing the most recent operation: CF (carry out, used for unsigned overflow), ZF (result was zero), SF (result was negative), OF (two’s-complement overflow).1

cmpq S1,S2 → đặt cờ theo S2−S1
testq S1,S2 → đặt cờ theo S1&S2
jX / setX / cmovX → đọc cờ

so sánh → bật cờ
kiểm tra → bật cờ
nhảy / đặt / chép → đọc cờ

cmpq S1,S2 → set flags from S2−S1
testq S1,S2 → set flags from S1&S2
jX / setX / cmovX → read flags

  1. cmp · test · condition codes

    So sánh mà không lưu kết quả So sánh chỉ để bật cờ Comparing without storing a result

    Phần lớn lệnh số học/logic đặt cờ như một tác dụng phụ, nhưng hai lớp lệnh CMP và TEST sinh ra chỉ để đặt cờ: CMP đặt cờ theo hiệu S2 − S1 như SUB nhưng không ghi đích; TEST đặt cờ theo S1 & S2 như AND nhưng không ghi đích.1

    Có những lệnh chỉ để so sánh, không lưu kết quả đi đâu cả — chúng chỉ bật mấy lá cờ. Ví dụ cmp ngầm làm phép trừ để xem ai lớn hơn, nhưng vứt kết quả trừ đi, chỉ giữ lại cờ. Nhớ nhé: so sánh xong là quên kết quả, chỉ nhớ cờ.

    Most arithmetic/logical instructions set the flags as a side effect, but the CMP and TEST classes exist only to set them: CMP sets flags by the difference S2 − S1 like SUB but writes no destination; TEST sets flags by S1 & S2 like AND but writes no destination.1

    Bạn gặp nó ở đâuWhere you meet it Mẫu testq %rax,%rax rất hay gặp: lặp lại cùng một toán hạng để xem %rax âm, bằng 0 hay dương.1 Hay thấy một thanh ghi được so với chính nó để xem có bằng 0 không. The pattern testq %rax,%rax is common: repeating the same operand to see whether %rax is negative, zero, or positive.1
  2. set · jump

    Ba cách dùng cờ: đặt byte, nhảy, chép có điều kiện Đọc cờ để làm ba việc Three uses of the flags: set, jump, conditional move

    Ta hiếm khi đọc cờ trực tiếp. Có ba cách dùng: (1) lệnh SET đặt một byte về 0 hoặc 1 theo tổ hợp cờ; (2) nhảy có điều kiện sang nơi khác; (3) chuyển dữ liệu có điều kiện.1 Hậu tố của SET/nhảy chỉ điều kiện chứ không chỉ kích thước: setl là “set less”, không phải “set long”.1

    Sau khi cờ đã bật, máy có ba cách dùng: ghi 0 hoặc 1 để trả lời “đúng/sai”, nhảy sang chỗ khác, hoặc chọn một trong hai giá trị. Đây chính là cách “nếu… thì…” thành hình trong mã máy. Nhớ nhé: cờ bật rồi thì đặt, nhảy, hoặc chọn.

    We rarely read the flags directly. There are three uses: (1) a SET instruction sets a byte to 0 or 1 by a combination of flags; (2) a conditional jump to elsewhere; (3) a conditional data transfer.1 The suffix on SET/jump names the condition, not a size: setl means “set less,” not “set long.”1

    Bạn gặp nó ở đâuWhere you meet it Để tính a < b với long, trình biên dịch sinh cmpq, rồi setl đặt byte thấp, rồi movzbl xoá các byte cao để được 0/1 sạch.1 Mỗi phép so sánh trả về true/false trong C. To compute a < b for long, the compiler emits cmpq, then setl for the low byte, then movzbl to clear the upper bytes to a clean 0/1.1

    Có dấu và không dấu khác nhau

    Hậu tố điều kiện phân biệt số có dấu và không dấu: setl/jl (“less”, có dấu) đọc tổ hợp SF ^ OF, còn setb/jb (“below”, không dấu) đọc CF.1 Cùng một phép cmp, chọn nhánh theo kiểu dữ liệu.

    Tên đồng nghĩa

    Một số lệnh máy có nhiều tên: setg (“set greater”) và setnle (“set not less or equal”) là cùng một lệnh; trình biên dịch và disassembler chọn tên tuỳ ý.1

  3. stack · push · pop

    Ngăn xếp — mọc xuống địa chỉ thấp Chồng giấy dùng khi gọi hàm The stack — it grows toward lower addresses

    Cơ chế gọi thủ tục dựa trên một ngăn xếp vào-sau-ra-trước. Trên x86-64, ngăn xếp mọc về phía địa chỉ thấp, và %rsp trỏ phần tử đỉnh (có địa chỉ nhỏ nhất).1 Đẩy một quad word bằng pushq là giảm %rsp đi 8 rồi ghi giá trị vào đỉnh mới; lấy ra bằng popq thì đọc từ đỉnh rồi tăng %rsp thêm 8.1

    Khi gọi hàm, máy dùng một “chồng giấy”: đặt tờ mới lên trên, lấy đi thì lấy tờ trên cùng. Điều lạ: chồng giấy này mọc xuống dưới theo số địa chỉ, nên sách vẽ nó lộn ngược. Đẩy vào thì số địa chỉ giảm. Nhớ nhé: ngăn xếp mọc xuống, đỉnh có số nhỏ nhất.

    The procedure-calling mechanism rests on a last-in, first-out stack. On x86-64 the stack grows toward lower addresses, and %rsp points to the top element (the lowest address).1 Pushing a quad word with pushq decrements %rsp by 8 and writes the value at the new top; popq reads from the top and increments %rsp by 8.1

    Bạn gặp nó ở đâuWhere you meet it pushq %rbp tương đương cặp subq $8,%rsp rồi movq %rbp,(%rsp), nhưng được mã hoá gọn thành một byte.1 Mỗi lần một hàm cần chỗ tạm để nhớ việc. pushq %rbp is equivalent to subq $8,%rsp then movq %rbp,(%rsp), but encoded compactly as a single byte.1
  4. call · ret · stack frame

    call và ret — lưu rồi khôi phục con trỏ lệnh Gọi hàm rồi quay về đúng chỗ call and ret — save then restore the PC

    Truyền điều khiển từ hàm P sang hàm Q nghĩa là đặt PC về đầu Q; để Q biết quay về đâu, lệnh call Q đẩy địa chỉ trở về (địa chỉ lệnh ngay sau call) lên ngăn xếp rồi nhảy vào Q.1 Lệnh ret lấy địa chỉ đó ra khỏi ngăn xếp và đặt PC về đó.1 Vùng ngăn xếp của một lời gọi gọi là khung ngăn xếp (stack frame).1

    Khi gọi hàm, máy phải nhớ “gọi xong thì quay về dòng nào”. Lệnh call ghi địa chỉ quay về lên chồng giấy rồi nhảy đi; làm xong, ret đọc lại địa chỉ đó và quay về đúng chỗ. Nhớ nhé: call ghi chỗ quay về, ret quay về đó.

    Passing control from P to Q means setting the PC to the start of Q; so Q knows where to return, call Q pushes the return address (the instruction right after call) onto the stack and jumps into Q.1 The ret instruction pops that address and sets the PC to it.1 A call’s region of the stack is its stack frame.1

    Bạn gặp nó ở đâuWhere you meet it Trong ví dụ của sách, lệnh call ở địa chỉ 0x400563 gọi multstore; nó đẩy địa chỉ trở về 0x400568 lên ngăn xếp, và ret sau đó lấy chính giá trị ấy ra.1 Mỗi lần một hàm trong chương trình gọi một hàm khác. In the book’s example, the call at address 0x400563 calls multstore; it pushes return address 0x400568, and the matching ret later pops that very value.1

    Hàm lá không cần khung

    Vì tốc độ và tiết kiệm, mỗi hàm chỉ cấp phần khung nó cần. Hàm giữ được mọi biến cục bộ trong thanh ghi và không gọi hàm khác — gọi là hàm lá (leaf procedure) — thậm chí không cần khung ngăn xếp.1

    Tham số thứ bảy trở đi

    Một hàm truyền được tối đa sáu tham số nguyên qua thanh ghi; nếu cần nhiều hơn, các tham số còn lại được hàm gọi-đi đặt trong khung ngăn xếp của nó trước khi gọi.13

Đính chính Corrections

Ba ngộ nhận mà người đọc lần đầu hay mắc Ba chỗ hay hiểu nhầm Three misconceptions first readers hit

Phần này thuộc mức đọc Đầy đủ: nó đối chiếu điều người đọc thường nghĩ với điều bản văn thật sự nói.

Phần này hơi khó, bạn có thể bỏ qua và quay lại sau. Nhớ nhé: hiểu chậm mà chắc vẫn hơn.

This block belongs to the Đầy đủ (full) reading level: it sets what readers usually assume against what the text actually says.

“Mã máy vẫn giữ kiểu và tên biến của tôi” "Machine code still keeps my types and variable names"

Ngộ nhận: sau khi biên dịch, máy vẫn biết đâu là số có dấu, đâu là con trỏ, biến tên gì.

Bản văn nói: mã máy xem bộ nhớ như một mảng byte và không phân biệt số nguyên có dấu/không dấu, các loại con trỏ, hay thậm chí con trỏ với số nguyên; mọi tên biến và kiểu dữ liệu đã bị bóc đi.1

Misconception: after compilation the machine still knows which values are signed, which are pointers, and what the variables were called.

The text says: machine code views memory as a byte array and makes no distinction between signed and unsigned integers, between kinds of pointers, or even between pointers and integers; all variable names and data types have been stripped away.1

“mov luôn đặt đúng số byte vào đích” "mov always touches exactly the bytes you’d expect"

Ngộ nhận: mọi lệnh mov chỉ chạm đúng phần đích được nêu, phần còn lại luôn nguyên vẹn.

Bản văn nói: có hai quy ước khác nhau. Lệnh tạo 1 hoặc 2 byte giữ nguyên các byte cao; nhưng movl với đích thanh ghi lại đặt 4 byte cao về 0 — một ngoại lệ của x86-64.1 Vì vậy không có lệnh movzlq: chỉ cần movl là đủ để mở rộng 0 lên 8 byte.1

Misconception: every mov touches only the stated destination, leaving everything else intact.

The text says: there are two different conventions. An instruction generating 1 or 2 bytes leaves the upper bytes unchanged; but movl with a register destination sets the upper 4 bytes to 0 — an x86-64 exception.1 This is why there is no movzlq: a plain movl already zero-extends to 8 bytes.1

“Ngăn xếp mọc lên, đỉnh ở địa chỉ cao” "The stack grows upward, with its top at a high address"

Ngộ nhận: đẩy thêm dữ liệu vào ngăn xếp thì địa chỉ đỉnh tăng lên.

Bản văn nói: trên x86-64 ngăn xếp mọc về phía địa chỉ thấp; đỉnh ngăn xếp có địa chỉ nhỏ nhất, nên sách vẽ ngăn xếp lộn ngược với “đỉnh” ở dưới. Đẩy vào là giảm %rsp.1

Misconception: pushing more data onto the stack raises the top address.

The text says: on x86-64 the stack grows toward lower addresses; the top of the stack has the lowest address, which is why the book draws the stack upside down with the “top” at the bottom. A push decrements %rsp.1

Tự kiểm tra Self-check

Bạn đã đọc được mức máy chưa? Thử trả lời xem nào Can you read the machine level yet?

Trả lời bằng lời của bạn, không tra sách. Nếu một câu khiến bạn ngập ngừng, đó chính là mục bạn nên đọc lại.

Thử trả lời miệng thôi, không cần giở lại. Câu nào bí thì quay lại đọc phần đó nhé. Nhớ nhé: chỗ bí chính là chỗ cần học.

Answer in your own words, without looking back. Whichever question makes you hesitate points at the section you should reread.

  1. Bốn chặng của chuỗi biên dịch là gì, và chặng nào sinh tệp hợp ngữ .s?

    Gợi ý sâu hơn: công cụ nào giúp bạn đọc ngược mã máy mà không cần mã nguồn?

  2. Bốn phần của trạng thái bộ xử lý mà mã máy để lộ là gì?

    Gợi ý sâu hơn: thanh ghi nào giữ địa chỉ lệnh kế tiếp, và thanh ghi nào là con trỏ ngăn xếp?

  3. leaq khác movq ở điểm nào, và vì sao trình biên dịch thích dùng nó cho số học?

    Gợi ý sâu hơn: leaq 7(%rdx,%rdx,4), %rax đặt gì vào %rax nếu %rdx = x?

  4. Vì sao cmp và test không ghi kết quả vào đâu — và chúng để lại cái gì?

    Gợi ý sâu hơn: hậu tố l trong setl chỉ điều gì, có dấu hay không dấu?

  5. Khi call chạy, cái gì được đẩy lên ngăn xếp, và ret dùng nó ra sao?

    Gợi ý sâu hơn: ngăn xếp mọc về phía địa chỉ cao hay thấp, và %rsp trỏ vào đâu?

  1. Từ mã C đến chương trình chạy được có mấy bước? Kể tên.

  2. Khi chạy, máy cần nhớ những gì? Kể được hai thứ là tốt.

  3. Thanh ghi là gì, và vì sao máy thích để dữ liệu ở đó?

  4. Máy làm sao để thực hiện “nếu a lớn hơn b thì…”?

  5. Khi gọi một hàm, máy nhớ chỗ quay về bằng cách nào?

  1. What are the four stages of the compilation chain, and which one emits the assembly .s file?

  2. What are the four parts of the processor state that machine code exposes?

  3. How does leaq differ from movq, and why does the compiler like it for arithmetic?

  4. Why do cmp and test store no result — and what do they leave behind?

  5. When call runs, what is pushed onto the stack, and how does ret use it?

Khép lại chương Closing the chapter

Mã máy không giữ tên biến hay kiểu dữ liệu của bạn; nó chỉ là byte lệnh tác động lên vài thanh ghi và một mảng bộ nhớ. Nhưng chính vì máy không giấu gì cả, học đọc nó là học thấy những gì trình biên dịch và phần cứng thật sự làm.1

Máy không nhớ chữ bạn viết — nó chỉ chạy các con số điều khiển vài ô nhớ và bộ nhớ. Nhưng vì nó không giấu gì, đọc được mã máy là thấy rõ máy đang làm gì. Nhớ nhé: đọc được mã máy là nhìn thấu bên trong.

Machine code keeps none of your variable names or types; it is just instruction bytes acting on a few registers and an array of memory. But precisely because the machine hides nothing, learning to read it is learning to see what the compiler and the hardware actually do.1