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
%rip, 16 thanh ghi, cờ điều kiện, thanh ghi vector
Đang ở lệnh nào, 16 ô nhớ nhanh, mấy lá cờ
PC %rip, 16 registers, condition codes, vector registers
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
-
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ân53 48 89 d3 e8 … 5b c3.1Chươ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
multstorefunction the authors show the object code is exactly 14 bytes long, with the hexadecimal representation53 48 89 d3 e8 … 5b c3.1Bạ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.1Giả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 %rbxmới bắt đầu bằng byte53.1Bộ 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
-
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.12Khi 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.12Bạ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. -
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
gccgọi cả một chuỗi chương trình: tiền xử lý mở rộng#includevà 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.1Từ 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
gcclàm hết cả bốn. Nhớ nhé: bốn chặng — dọn, dịch, đóng gói, dán.The
gcccommand invokes a whole chain of programs: the preprocessor expands#includeand 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 asprintf) into an executable.1 The book uses optimization level-Ogso the generated code follows the structure of the original C.1Bạn gặp nó ở đâuWhere you meet it Tuỳ chọn-Sdừng ở hợp ngữ để bạn đọc; tệpmstore.onặng 1.368 byte dù chỉ chứa 14 byte lệnh, cònprogđã liên kết lên tới 8.655 byte vì gồm cả mã khởi động.1 Dùnggcc -Sđể xem bước hợp ngữ. The-Soption stops at assembly for you to read; the filemstore.ois 1,368 bytes even though its code is 14 bytes, and the linkedproggrows to 8,655 bytes because it includes start-up code.1
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
-
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:
char1 byte (hậu tốb),short2 byte (w),int4 byte (l),longvà con trỏ 8 byte (q),float4 byte (s),double8 byte (l).1Mỗ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:
char1 byte (suffixb),short2 bytes (w),int4 bytes (l),longand pointers 8 bytes (q),float4 bytes (s),double8 bytes (l).1Bạ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 -
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
%rsplà 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.1316 ô 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
%rspluô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é:%rsplà ô 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
%rspis 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.13Bạn gặp nó ở đâuWhere you meet it Ví dụ hàmexchange: tham sốxpnằ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 theexchangefunction, parameterxpis in%rdi,yin%rsi, and the return value goes in%rax— illustrating each register’s conventional role.1Byte 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 -
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ằngImm + 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ệsphả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.1Muố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 addressImm + R[rb] + R[ri]·s: a constant displacement, a base register, an index register, and a scale factorsthat must be 1, 2, 4, or 8.1 The other forms are just special cases with components omitted.1Bạ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 -
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ồmmovb/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êngmovlcó đích là thanh ghi sẽ đặt 4 byte cao về 0.1 Lệnhleaqthì “đọ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.1Lệnh
movchỉ chép một giá trị từ chỗ này sang chỗ khác, không đổi gì. Lệnhlealạ 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é:movchép giá trị,leatính địa chỉ.The
MOVclass (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 thatmovlwith a register destination sets the upper 4 bytes to 0.1 Theleaqinstruction “reads an address” rather than memory: it places the effective address of its source operand into the destination.1%rdx = x→ leaq 7(%rdx,%rdx,4), %rax%rax = 5x + 7Bạn gặp nó ở đâuWhere you meet it Trình biên dịch dùngleaqđể gộp cộng và nhân với hằng nhỏ: hàmscaletínhx + 4*y + 12*zđược dịch thành một chuỗi ba lệnhleaq.1 Khi thấy phép nhân với 2, 4, 8 biến mất khỏi mã máy. The compiler usesleaqto fold addition and small-constant multiplication: thescalefunction computingx + 4*y + 12*zcompiles into a sequence of threeleaqinstructions.1Mở 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ớpMOVSđiền bằng bản sao của bit cao nhất (mở rộng dấu).1 Lệnhcltqlà dạng gọn củamovslq %eax, %rax.1leaq không đặt cờ
Vì sinh ra để tính địa chỉ,
leaqkhô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
-
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
CMPvàTESTsinh ra chỉ để đặt cờ:CMPđặt cờ theo hiệuS2 − S1nhưSUBnhưng không ghi đích;TESTđặt cờ theoS1 & S2nhưANDnhưng không ghi đích.1Có 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ụ
cmpngầ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
CMPandTESTclasses exist only to set them:CMPsets flags by the differenceS2 − S1likeSUBbut writes no destination;TESTsets flags byS1 & S2likeANDbut writes no destination.1Bạn gặp nó ở đâuWhere you meet it Mẫutestq %rax,%raxrấ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 patterntestq %rax,%raxis common: repeating the same operand to see whether%raxis negative, zero, or positive.1 -
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ủaSET/nhảy chỉ điều kiện chứ không chỉ kích thước:setllà “set less”, không phải “set long”.1Sau 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
SETinstruction 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 onSET/jump names the condition, not a size:setlmeans “set less,” not “set long.”1Bạn gặp nó ở đâuWhere you meet it Để tínha < bvớilong, trình biên dịch sinhcmpq, rồisetlđặt byte thấp, rồimovzblxoá các byte cao để được 0/1 sạch.1 Mỗi phép so sánh trả về true/false trong C. To computea < bforlong, the compiler emitscmpq, thensetlfor the low byte, thenmovzblto clear the upper bytes to a clean 0/1.1Có 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ợpSF ^ OF, cònsetb/jb(“below”, không dấu) đọcCF.1 Cùng một phépcmp, 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 -
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à
%rsptrỏ phần tử đỉnh (có địa chỉ nhỏ nhất).1 Đẩy một quad word bằngpushqlà giảm%rspđi 8 rồi ghi giá trị vào đỉnh mới; lấy ra bằngpopqthì đọc từ đỉnh rồi tăng%rspthêm 8.1Khi 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
%rsppoints to the top element (the lowest address).1 Pushing a quad word withpushqdecrements%rspby 8 and writes the value at the new top;popqreads from the top and increments%rspby 8.1 -
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 saucall) lên ngăn xếp rồi nhảy vào Q.1 Lệnhretlấ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).1Khi gọi hàm, máy phải nhớ “gọi xong thì quay về dòng nào”. Lệnh
callghi đị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é:callghi chỗ quay về,retquay về đó.Passing control from P to Q means setting the PC to the start of Q; so Q knows where to return,
call Qpushes the return address (the instruction right aftercall) onto the stack and jumps into Q.1 Theretinstruction pops that address and sets the PC to it.1 A call’s region of the stack is its stack frame.1Bạn gặp nó ở đâuWhere you meet it Trong ví dụ của sách, lệnhcallở địa chỉ0x400563gọimultstore; nó đẩy địa chỉ trở về0x400568lên ngăn xếp, vàretsau đó 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, thecallat address0x400563callsmultstore; it pushes return address0x400568, and the matchingretlater pops that very value.1Hà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
Đí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.
-
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?
-
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?
-
leaqkhácmovqở đ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%raxnếu%rdx= x? -
Vì sao
cmpvàtestkhô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ố
ltrongsetlchỉ điều gì, có dấu hay không dấu? -
Khi
callchạy, cái gì được đẩy lên ngăn xếp, vàretdù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à
%rsptrỏ vào đâu?
Từ mã C đến chương trình chạy được có mấy bước? Kể tên.
Khi chạy, máy cần nhớ những gì? Kể được hai thứ là tốt.
Thanh ghi là gì, và vì sao máy thích để dữ liệu ở đó?
Máy làm sao để thực hiện “nếu a lớn hơn b thì…”?
Khi gọi một hàm, máy nhớ chỗ quay về bằng cách nào?
-
What are the four stages of the compilation chain, and which one emits the assembly
.sfile? -
What are the four parts of the processor state that machine code exposes?
-
How does
leaqdiffer frommovq, and why does the compiler like it for arithmetic? -
Why do
cmpandteststore no result — and what do they leave behind? -
When
callruns, what is pushed onto the stack, and how doesretuse 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