# MID # TN Câu 1: Tất cả thông tin trong hệ thống máy tính đều được biểu diễn dưới dạng gì? * Đáp án: Bit * Giải thích: Ở cấp độ vật lý thấp nhất, máy tính chỉ hiểu hai trạng thái 0 và 1 (tương ứng với điện áp thấp/cao hoặc tắt/mở). Bit (Binary Digit) là đơn vị cơ bản nhất để biểu diễn thông tin. Byte, Word hay mã ASCII đều là các tập hợp hoặc quy ước được xây dựng từ các Bit. Câu 2: Kết quả trả về sau khi thực thi đoạn mã Assembly (tính giá trị biến y tại 12(%ebp))? * Đáp án: Lỗi * Giải thích: Trong kiến trúc tập lệnh (ISA) của x86/IA32, các lệnh số học và logic (như ADD, SUB, AND, XOR, MOV...) có một hạn chế phần cứng nghiêm ngặt: Không được phép có cả hai toán hạng (nguồn và đích) đều là địa chỉ bộ nhớ (Memory-to-Memory operation). * Lệnh dòng 2: subl 8(%ebp), 12(%ebp) * Toán hạng nguồn: 8(%ebp) $\rightarrow$ Memory. * Toán hạng đích: 12(%ebp) $\rightarrow$ Memory. * $\rightarrow$ **Vi phạm quy tắc phần cứng**. Trình hợp dịch (Assembler) sẽ báo lỗi ngay lập tức và không tạo ra mã máy, hoặc nếu cố tình nạp mã máy sai, CPU sẽ sinh ra ngoại lệ (Illegal Instruction). Câu 3: Trong hệ thập lục phân (Hexadecimal), ký tự 'E' đại diện cho giá trị thập phân nào? * Đáp án: 14 * Giải thích: Hệ Hex sử dụng các ký tự: 0-9 và A(10), B(11), C(12), D(13), E(14), F(15). Câu 4: Trong kiến trúc x86-64, kích thước của kiểu dữ liệu char là bao nhiêu? * Đáp án: 1 byte * Giải thích: Theo chuẩn C và kiến trúc x86, kiểu char luôn có kích thước là 1 byte (8 bit) để biểu diễn các ký tự (thường là mã ASCII). Câu 5: Trong ngôn ngữ C, toán tử nào thực hiện phép toán Boolean Exclusive-Or (XOR)? * Đáp án: ^ * Giải thích: * | là toán tử OR (Bitwise OR). * & là toán tử AND (Bitwise AND). * ~ là toán tử NOT (Bitwise NOT/Đảo bit). * ^ là toán tử XOR (Bitwise Exclusive-OR). Câu 6: Quy ước "lưu trữ các byte theo thứ tự giảm dần của trọng số" (Most to Least Significant Byte)? * Đáp án: Big-Endian... (byte có trọng số cao nhất nằm ở địa chỉ thấp nhất) ![image](https://hackmd.io/_uploads/S1_od2j7be.png) ![image](https://hackmd.io/_uploads/Sy6q_hiQWx.png) * Giải thích: có 2 đáp án big-endian, cần đọc kĩ mô tả. * Big-Endian: Byte đầu tiên (địa chỉ thấp nhất) chứa byte quan trọng nhất (MSB - Most Significant Byte). Ví dụ số 0x12345678 lưu là 12 34 56 78. Đây là cách con người thường viết số. * Little-Endian (x86 dùng): Byte đầu tiên chứa byte ít quan trọng nhất (LSB). Số trên sẽ lưu là 78 56 34 12. * Đề bài mô tả việc lưu từ "Most to Least" (Cao xuống thấp), tức là Big-Endian. Câu 7: Thanh ghi nào là Program Counter (PC)? * Đáp án: %eip * Giải thích: Trong kiến trúc IA32 (32-bit), thanh ghi con trỏ lệnh (Instruction Pointer) là %eip. Nó chứa địa chỉ của lệnh tiếp theo sẽ được thực thi. (Trên 64-bit là %rip). Câu 8: Lệnh movl chuyển bao nhiêu byte dữ liệu? * Đáp án: 4 byte * Giải thích: Trong cú pháp AT&T, hậu tố (suffix) quy định kích thước: * b (byte) = 1 byte. * w (word) = 2 byte. * l (long word) = 4 byte (32-bit integer). * q (quad word) = 8 byte. Câu 9: Lệnh nào thực hiện Dest = Dest + 1? * Đáp án: leal 1(%eax), %eax * Giải thích: * leal D(B), R: Tính địa chỉ hiệu dụng $Base + Displacement$ rồi lưu vào Register. Ở đây là lấy giá trị %eax cộng thêm 1, sau đó lưu ngược lại vào %eax. Đây là một cách phổ biến để thực hiện phép cộng mà không ảnh hưởng đến các cờ (flags). * A sai vì nó gán %eax = 1. * B và D thực hiện phép trừ.![image](https://hackmd.io/_uploads/BkYSY2j7-l.png) ![image](https://hackmd.io/_uploads/SyvIFhoQ-x.png) Câu 10: Các Cờ điều kiện (Condition Codes) được lưu trữ trong thanh ghi nào? * Đáp án: C. %eflags * Giải thích: Thanh ghi trạng thái trong IA32 được gọi là EFLAGS. Nó chứa các cờ như ZF (Zero), SF (Sign), OF (Overflow), CF (Carry)... để phục vụ cho các lệnh nhảy có điều kiện. Câu 11: Trong hệ Bù hai w-bit, trọng số của bit trọng số cao nhất ($x_{w-1}$) là bao nhiêu? * Đáp án: D. $-2^{w-1}$ * Giải thích: Đây là định nghĩa cơ bản của số nguyên có dấu dạng Bù hai (Two's Complement). Bit cao nhất (MSB) là bit dấu, mang giá trị âm lớn nhất. Ví dụ trong 4-bit, bit đầu tiên có trọng số là $-2^3 = -8$. Câu 12: Công cụ chuyển chương trình hợp ngữ (.s) thành file Relocatable object (.o)? * Đáp án: Assembler * Giải thích: Quy trình biên dịch: 1. Preprocessor (Tiền xử lý). 2. Compiler (Biên dịch): .c $\to$ .s (Assembly). 3. Assembler (Hợp dịch): .s $\to$ .o (Mã máy dạng Object). 4. Linker (Liên kết): .o $\to$ file thực thi. Câu 13: Mục đích chính của lệnh leal (Load Effective Address)? * Đáp án: Tính toán địa chỉ hiệu dụng và gán địa chỉ đó vào thanh ghi * Giải thích: Mặc dù tên là "Load Address", lệnh lea thường được dùng để thực hiện các phép tính số học dạng $x + k*y + d$ lưu kết quả tính toán trực tiếp vào thanh ghi đích. Câu 14: Khi dùng movl ghi vào thanh ghi 64-bit (ví dụ %rax), điều gì xảy ra với 4 byte cao? * Đáp án: Chúng được đặt thành 0 * Giải thích: Đây là quy tắc của x86-64. Bất kỳ lệnh nào ghi vào thanh ghi 32-bit (ví dụ %eax) sẽ tự động xóa 32 bit cao của thanh ghi 64-bit tương ứng (%rax) về 0 (Zero-extension). Câu 15: Hệ 4 bit bù hai, tính 5 + 5? * Đáp án: -6, có tràn số * Giải thích: * Hệ 4 bit bù hai biểu diễn được từ $-2^{3}$ đến $2^3 - 1$, tức là từ -8 đến +7. * $5 + 5 = 10$. Giá trị 10 nằm ngoài khoảng biểu diễn $\to$ Tràn số (Overflow). * Tính toán nhị phân: $0101 + 0101 = 1010_2$. * Trong hệ bù hai 4 bit, bit đầu là 1 nghĩa là số âm. Giá trị = $-8 + 2 = -6$. (xem lại câu trọng số $-2^{w-1}$) Câu 16: Trong phép dịch phải toán học (sar), bit nào điền vào bên trái? * Đáp án: Bit dấu (Sign bit) * Giải thích: Dịch phải toán học (sar) dùng cho số có dấu để bảo toàn dấu của số đó. Nếu số ban đầu là âm (bit dấu = 1), nó điền 1 vào bên trái. Nếu dương, điền 0. (Khác với shr là dịch logic, luôn điền 0).![image](https://hackmd.io/_uploads/BkWqZTo7We.png) Câu 17: Lệnh nhảy je (Jump if Equal) sử dụng cờ nào? * Đáp án: ZF (Zero Flag) * Giải thích: Lệnh je kiểm tra nếu ZF=1 thì thực hiện nhảy.![image](https://hackmd.io/_uploads/H1vSG6iQZg.png) Câu 18: Thanh ghi dành riêng cho quản lý stack? * Đáp án: %esp * Giải thích: %esp (Stack Pointer) luôn trỏ đến đỉnh của ngăn xếp. Câu 19: Tính toán leaq (%rdx,%rcx,4), %rax? * Đáp án: 0xF400 * Giải thích: * Công thức: $Dest = Base + Index * Scale$. * $Dest = 0xF000 + 0x0100 * 4$ * $Dest = 0xF000 + 0x0400 = 0xF400$. Câu 20: Cấu trúc while (Test) Body chuyển thành Assembly bằng phương pháp nào? * Đáp án: A. Phương pháp kiểm tra điều kiện trước rồi sử dụng do-while * Giải thích: * Trình biên dịch thường sử dụng phương pháp Jump-to-middle (không có đáp án) * Phương pháp A (Kiểm tra trước rồi do-while hay còn gọi là Guarded-Do) là một kỹ thuật tối ưu hóa cao hơn (thường ở mức -O1 trở lên), nhưng phương pháp C là cách dịch cơ bản và trực tiếp nhất cho vòng lặp while. **=> do đó chọn A** ![image](https://hackmd.io/_uploads/rkW84TjXZl.png) # TL **Câu 1001:** Phân tích đoạn mã Assembly IA32 sau đây và chuyển đổi logic của nó thành một biểu thức C tương đương. **Giả định:** Tham số x được truyền vào nằm ở ô nhớ 8(%ebp) và tham số y nằm ở 12(%ebp). Kết quả cuối cùng được lưu trữ trong thanh ghi %eax trước khi lệnh ret được thực thi. ``` Dòng Mã Assembly (AT&T syntax) 1 movl 8(%ebp), %eax 2 subl 12(%ebp), %eax 3 sarl $31, %eax 4 movl %eax, %edx 5 andl 12(%ebp), %eax 6 notl %edx 7 andl 8(%ebp), %edx 8 orl %edx, %eax 9 ret ``` Yêu cầu: 1. Viết biểu thức C tương đương (chỉ cần một biểu thức duy nhất) cho giá trị cuối cùng được tính toán và trả về (lưu trong %eax). * Đoạn mã này thực hiện logic tìm giá trị lớn nhất (Maximum) giữa hai số x và y. * Biểu thức C tương đương là: `(x > y) ? x : y` (Hoặc viết đầy đủ theo logic bitwise của mã nguồn: `( (x-y)>>31 & y ) | ( ~((x-y)>>31) & x ) )` * Lưu ý ở câu này, ***nếu viết bitwise*** phải bỏ ngoặc chính xác, do trong C, thứ tự ưu tiên các phép toán là khác nhau. (giáo trình trang 88) ![image](https://hackmd.io/_uploads/r1wLw6sXZg.png) 2. Giải thích ngắn gọn chức năng của các dòng 3 và 4 (sarl $31, %eax và movl %eax, %edx) và vai trò của các lệnh này trong logic tổng thể. * Hai dòng này đóng vai trò trong việc tạo ra cơ chế "chọn" giá trị mà không cần dùng lệnh if. * Dòng 3: sarl $31, %eax * Chức năng: Dịch phải số học 31 bit kết quả của phép trừ (x - y). * Vai trò: Tạo ra Bit mask dựa trên dấu của hiệu số: * Nếu x < y (Hiệu âm): Kết quả là toàn bit 1 (tức là -1 hay 0xFFFFFFFF). * Nếu x >= y (Hiệu dương hoặc bằng 0): Kết quả là toàn bit 0 (tức là 0 hay 0x00000000). * Thanh ghi %eax lúc này chứa mask. * Dòng 4: movl %eax, %edx * Chức năng: Sao chép mask từ %eax sang %edx. * Vai trò: Chúng ta cần sử dụng mask này hai lần cho hai mục đích ngược nhau: 1. Dùng %eax để giữ lại y (nếu mask là -1) qua lệnh andl 12(%ebp), %eax. 2. Dùng %edx (sau khi đảo bit bằng lệnh notl) để giữ lại x (nếu mask là 0 ban đầu) qua lệnh andl 8(%ebp), %edx. * *Việc sao chép giúp bảo toàn giá trị mask để xử lý song song cho cả hai biến x và y.* * <span style="color:red">chỉ dịch lệnh sang tiếng Việt chứ chưa giải thích được tại sao đoạn mã lại làm như vậy sẽ không đạt điểm tối đa.</span> **Câu 1010:** Phân tích đoạn mã Assembly IA32 sau đây và chuyển đổi logic của nó thành một biểu thức C tương đương. Giả định: Tham số x được truyền vào nằm ở ô nhớ 8(%ebp) và tham số y nằm ở 12(%ebp). Kết quả cuối cùng được lưu trữ trong thanh ghi %eax trước khi lệnh ret được thực thi. ``` Dòng Mã Assembly (AT&T syntax) 1 movl 8(%ebp), %eax 2 subl 12(%ebp), %eax 3 sarl $31, %eax 4 movl %eax, %edx 5 andl 8(%ebp), %eax 6 notl %edx 7 andl 12(%ebp), %edx 8 orl %edx, %eax 9 ret ``` Yêu cầu: 1. Viết biểu thức C tương đương (chỉ cần một biểu thức duy nhất) cho giá trị cuối cùng được tính toán và trả về (lưu trong %eax). Tương tự như cách trả lời của câu trước. Biểu thức C tương đương là: * ```(x < y) ? x : y``` * ```( ((x - y) >> 31) & x ) | ( ~((x - y) >> 31) & y )``` 2. Giải thích ngắn gọn chức năng của các dòng 3 và 4 (sarl $31, %eax và movl %eax, %edx) và vai trò của các lệnh này trong logic tổng thể. * Giải thích tương tự câu trước...... * Nếu x < y $\rightarrow$ mask = 11..1 $\rightarrow$ Lấy x | 0 $\rightarrow$ x * Nếu x >= y $\rightarrow$ mask = 00..0 $\rightarrow$ Lấy 0 | y $\rightarrow$ y **Câu 1011:** Phân tích đoạn mã Assembly IA32 sau đây và chuyển đổi logic của nó thành một biểu thức C tương đương. Giả định: Tham số x được truyền vào nằm ở ô nhớ 8(%ebp) và tham số y nằm ở 12(%ebp). Kết quả cuối cùng được lưu trữ trong thanh ghi %eax trước khi lệnh ret được thực thi. ``` Dòng Mã Assembly (AT&T syntax) 1 movl 8(%ebp), %eax 2 subl 12(%ebp), %eax 3 movl %eax, %edx 4 sarl $31, %edx 5 xorl %edx, %eax 6 subl %edx, %eax 7 ret ``` Yêu cầu: 1. Viết biểu thức C tương đương (chỉ cần một biểu thức duy nhất) cho giá trị cuối cùng được tính toán và trả về (lưu trong %eax). * Biểu thức C tương đương: ```abs(x - y)``` * bitwise: ```((x - y) ^ ((x - y) >> 31)) - ((x - y) >> 31))``` 2. Giải thích ngắn gọn chức năng của các dòng 3 và 4 (movl %eax, %edx và sarl $31, %eax) và vai trò của các lệnh này trong logic tổng thể. * Giải thích tương tự câu trước nhé.... * Hai lệnh này biến %edx thành 0 nếu hiệu số là dương, và -1 nếu hiệu số là âm. * Giá trị mask này sau đó được dùng ở dòng 5 và 6 để thực hiện logic đảo dấu số âm thành dương (theo quy tắc bù 2: đảo bit rồi cộng 1) mà không cần dùng lệnh if. **Câu 1100:** Phân tích đoạn mã Assembly IA32 sau đây và chuyển đổi logic của nó thành một biểu thức C tương đương. Giả định: Tham số x được truyền vào nằm ở ô nhớ 8(%ebp) và tham số y nằm ở 12(%ebp). Kết quả cuối cùng được lưu trữ trong thanh ghi %eax trước khi lệnh ret được thực thi. ``` Dòng Mã Assembly (AT&T syntax) 1 movl 8(%ebp), %eax 2 movl %ecx, %eax 3 andl $1, %eax 4 shll $31, %eax 5 sarl $31, %eax 6 andl 12(%ebp), %eax 7 addl %ecx, %eax 8 ret ``` Yêu cầu: 1. Viết biểu thức C tương đương (chỉ cần một biểu thức duy nhất) cho giá trị cuối cùng được tính toán và trả về (lưu trong %eax). Biểu thức C tương đương: (bài này có 1 giá trị lưu trong %ecx, các bạn có thể đặt tên nó là c hoặc z hoặc gì cũng đc nhé,....) * ~~```(x & 1) ? (x + y) : x```~~ * ```(ecx & 1) ? (ecx + y) : ecx``` hoặc * ~~```x + (y & -(x & 1))```~~ * ```ecx + (y & -(ecx & 1))``` 2. Giải thích ngắn gọn chức năng của các dòng 4 và 5 (shll $31, %eax và sarl $31, %eax) và vai trò của các lệnh này trong logic tổng thể. * Tương tự giải thích câu đầu tiên .... * Vai trò tổng thể: Cặp lệnh này chuyển đổi giá trị boolean 0 hoặc 1 (từ phép kiểm tra chẵn lẻ) thành một mask 0 hoặc -1. mask này sau đó dùng lệnh AND để quyết định xem có cộng thêm y hay không mà không cần dùng lệnh nhảy if/else. **Câu 2:** Trong kiến trúc x86-64 (Little-Endian), giả sử biến 4-byte int D có giá trị 0x00A0B0C0. Nếu D được lưu trong bộ nhớ bắt đầu tại địa chỉ 0x1F4, hãy biểu diễn các byte tại các địa chỉ 0x1F4 đến 0x1F7 (câu này thì tụi em ít sai) | Địa chỉ bộ nhớ | Giá trị (Hex) | Vai trò | | -------- | -------- | -------- | | 0x1F4 | C0 | Byte thấp nhất (LSB) | 0x1F5 | B0 | | 0x1F6 | A0 | | 0x1F7 | 00 | Byte cao nhất (MSB) **Câu 3:** Phân tích đoạn mã Assembly IA32 sau đây và chuyển đổi logic của nó thành một hàm C tương đương (kiểu void). Giả định: Đây là mã của hàm void a_x(int *A, int len). • Tham số A (địa chỉ cơ sở mảng) nằm ở ô nhớ 8(%ebp). • Tham số len (độ dài mảng) nằm ở ô nhớ 12(%ebp). • Biến cục bộ i (chỉ mục) nằm ở ô nhớ -4(%ebp). ``` Dòng Mã Assembly (IA32 AT&T syntax) 1 movl $0, -4(%ebp) 2 jmp .L2 3 .L3: 4 movl -4(%ebp), %ecx 5 movl 8(%ebp), %ebx 6 sall $1, %ecx 7 movl -4(%ebp), %edx 8 movl %ecx, (%ebx,%edx,4) 9 incl -4(%ebp) 10 .L2: 11 movl -4(%ebp), %eax 12 cmpl 12(%ebp), %eax 13 jl .L3 14 ret ``` Yêu cầu: 1. Viết hàm C (void a_x(int *A, int len)) có chức năng tương đương với đoạn mã assembly trên. ``` void a_x(int *A, int len) { int i; // Dòng 1 khởi tạo i = 0 // Dòng 10-13 kiểm tra điều kiện lặp: i < len // Dòng 9 tăng i lên 1 (i++) for (i = 0; i < len; i++) { // Dòng 4-6: Tính giá trị i * 2 (dịch trái 1 bit) // Dòng 7-8: Gán giá trị đó vào phần tử mảng A[i] A[i] = i * 2; // A[i] = i << 1; } } ``` * *<span style="color:red">Ở câu này một số bạn viết ```A[4*i] = i * 2``` là sai địa chỉ mảng A.</span>* 2. Giải thích lệnh ở dòng 8 (movl %ecx, (%ebx,%edx,4)). Cụ thể, lệnh này sử dụng chế độ đánh địa chỉ nào và nó thực hiện thao tác gì? * Lệnh này sử dụng chế độ Scaled Indexed Addressing (Câu này để kiểm tra các bạn có đọc tài liệu giáo trình không) * Hình 3.3 trang 209 ![image](https://hackmd.io/_uploads/BJS1Caim-l.png) * Lệnh thực hiện việc chép dữ liệu từ thanh ghi vào bộ nhớ với các thành phần cụ thể sau: * %ecx: Chứa giá trị 2 * i (do trước đó dòng 4 nạp i vào %ecx và dòng 6 dịch trái 1 bit). * (%ebx,%edx,4): * Base (%ebx): Chứa địa chỉ cơ sở của mảng A (được nạp từ dòng 5). * Index (%edx): Chứa giá trị của biến đếm i (được nạp từ dòng 7). * Scale (4): Là hệ số tỉ lệ. Vì mảng kiểu int (4 bytes), nên cần nhân chỉ số i với 4 để tìm đúng offset byte. * $\rightarrow$ Địa chỉ tính được: Địa chỉ A + (i * 4). Đây chính xác là địa chỉ của phần tử A[i]. --- # Các câu hỏi được đặt trong khoá học ## I. Về Biểu diễn Dữ liệu & Endianness ### 1. Tại sao lại cần 2 chuẩn Big Endian và Little Endian? Tại sao không thống nhất? * Lịch sử & Kỹ thuật: Đây là cuộc chiến giữa các thiết kế phần cứng. * Big Endian (BE): Tự nhiên với con người (đọc từ trái sang phải, byte quan trọng nhất đứng đầu). Dễ so sánh dấu (chỉ cần nhìn byte đầu). Thường dùng trong giao thức mạng (Network Byte Order). * Little Endian (LE): Tự nhiên với máy tính khi thực hiện tính toán. Khi cộng trừ, máy tính làm từ hàng đơn vị (bit thấp) lên. Với LE, byte thấp nằm ở địa chỉ thấp, CPU đọc byte đầu tiên là tính được ngay mà không cần nhảy đến cuối số. * Tại sao hiện nay dùng Little Endian (x86)? Vì dòng chip Intel x86 thống trị thị trường PC/Server và nó chọn LE. Các hệ thống phải theo để tương thích phần cứng. ### 2. Trọng số của byte là gì? Tại sao chuỗi (string) lưu kiểu khác số nguyên? * Trọng số: Là giá trị đóng góp của byte đó vào tổng giá trị số. Ví dụ số 0x1234: Byte 12 là MSB (Most Significant Byte - trọng số cao nhất), Byte 34 là LSB (Least Significant Byte - trọng số thấp nhất). * Chuỗi (String) vs Số: * Số (int, float): Là một đơn vị dữ liệu đa byte, nên cần quy ước thứ tự (Endianness) để máy biết ghép lại thế nào. * Chuỗi: Là một mảng các ký tự (array of bytes). Quy tắc mảng là: phần tử chỉ số 0 luôn ở địa chỉ thấp, chỉ số 1 ở địa chỉ cao hơn. Do đó chuỗi luôn trông giống như Big Endian (viết xuôi) vì đó là thứ tự của mảng, không bị ảnh hưởng bởi Endianness của CPU. ### 3. Floating-point dùng Endian nào? Tại sao Exponent cần cộng bias (127)? * Endian: Floating-point tuân theo quy tắc Endian của hệ thống đó (giống như số nguyên). Nếu máy là Little Endian, các byte của số float cũng lưu ngược. Chuẩn IEEE 754 quy định cấu trúc logic của số thực (float) gồm 3 phần: [Sign bit] + [Exponent] + [Mantissa/Fraction]. * Ví dụ: Số float 32-bit (4 byte). Dù logic là vậy, nhưng khi lưu 4 byte này xuống RAM, máy tính phải quyết định byte nào lưu trước. Hãy lấy ví dụ số Pi ($\pi \approx 3.14159...$). Trong mã hex, nó được biểu diễn là: 0x40490FDB. * Byte cao nhất (MSB): 40 (Chứa dấu và một phần mũ). * Byte thấp nhất (LSB): DB (Chứa phần cuối của đuôi thập phân). Khi lưu vào bộ nhớ bắt đầu từ địa chỉ 0x100: | Hệ thống | Địa chỉ 0x100 | Địa chỉ 0x101 | Địa chỉ 0x102 | Địa chỉ 0x103 | |--------|--------|--------|--------|--------| | Big Endian | 40 | 49 | 0F | DB | | Little Endian | DB | 0F | 49 | 40 | * Bias (127): Để biểu diễn số mũ âm mà không cần dùng bit dấu riêng cho phần mũ. * Ví dụ: Mũ thực tế là $-1$, cộng bias 127 thành $126$ (số dương). * Tại sao không dùng Bù 2 (Two's Complement) như số nguyên mà phải dùng số Biased (số dịch chuyển)? * Vấn đề nếu dùng Bù 2: * Giả sử ta dùng Bù 2 cho phần mũ (Exponent) 8-bit. * Mũ dương $2^1$: 00000001 * Mũ âm $2^{-1}$: 11111111 (trong bù 2, đây là -1) * Nếu CPU so sánh thô (coi chuỗi bit là số không dấu) để xem số nào lớn hơn: Nó sẽ thấy 11111111 > 00000001.$\rightarrow$ Máy tính sẽ hiểu nhầm là $2^{-1}$ lớn hơn $2^1$. SAI! * Giải pháp Bias * Người thiết kế chuẩn IEEE 754 muốn các giá trị mũ phải tăng dần theo thứ tự tự nhiên của chuỗi bit. Họ cộng thêm 127 (với float 32-bit) để đẩy toàn bộ dải mũ từ âm sang dương (không dấu). * Mũ thực tế: từ -126 đến +127. * Mũ lưu trữ (cộng 127): từ 1 đến 254. * Ví dụ so sánh lại: * Mũ thực $2^{-1}$ (-1): Lưu là $-1 + 127 = 126$ $\rightarrow$ Nhị phân: 01111110 * Mũ thực $2^1$ (+1): Lưu là $1 + 127 = 128$ $\rightarrow$ Nhị phân: 10000000 * Bây giờ so sánh thô chuỗi bit: 10000000 (128) > 01111110 (126). $\rightarrow$ Máy tính hiểu đúng là $2^1 > 2^{-1}$ mà không cần thêm mạch phần cứng giải mã gì cả. * Công thức tổng quát giá trị của số thực là: $$Value = (-1)^S \times (1 + Fraction) \times 2^{(Exponent - 127)}$$ * Ví dụ: Giải mã số 0xC0C80000 Giả sử trong bộ nhớ, gặp giá trị Hex: 0xC0C80000. Nó là số mấy? * Bước 1: Bung từ Hex ra Nhị phân (Binary) Đầu tiên, hãy viết nó dưới dạng 32 bit nhị phân: C = 1100, 0 = 0000, C = 1100, 8 = 1000... Dãy bit đầy đủ: 1100 0000 1100 1000 0000 0000 0000 0000 * Bước 2: Cắt bit theo khuôn mẫu (S - E - M) Chuẩn IEEE 754 32-bit chia làm 3 phần: 1. Bit dấu (Sign - S): 1 bit đầu tiên. 2. Mũ (Exponent - E): 8 bit tiếp theo. 3. Phần đuôi/Thập phân (Mantissa/Fraction - M): 23 bit còn lại. Phân tách ví dụ trên: * S (1 bit): 1 * E (8 bit): 1000 0001 * M (23 bit): 100 1000 0000 0000 0000 0000 * Bước 3: Giải mã từng phần 1. Bit dấu (S): * S = 1 $\rightarrow$ Đây là số Âm. * (Nếu S=0 thì là số Dương). 2. Phần Mũ (Exponent - E): * Giá trị nhị phân: 1000 0001 * Đổi sang thập phân: $128 + 1 = 129$. * Trừ đi Bias (127):

$$Mũ thực tế = 129 - 127 = 2$$
 * $\rightarrow$ Vậy ta sẽ nhân với $2^2$. 3. Phần Đuôi (Mantissa - M): * Chuỗi bit đuôi: 1001000... (các số 0 phía sau không quan trọng). * Quy tắc "Số 1 ngầm định" (Implicit Leading 1): Máy tính quy ước dạng chuẩn hóa luôn là $1.xxxxx$. Vì số 1 ở đầu luôn tồn tại nên máy không lưu nó để tiết kiệm bộ nhớ. Khi đọc, bạn phải tự thêm "1." vào trước chuỗi bit M. * Giá trị nhị phân chưa chuẩn hóa: 1.1001 * Bước 4: Lắp ghép và Tính toán Bây giờ ta ghép các mảnh lại: * Dấu: Âm (-) * Giá trị: $1.1001_2 \times 2^2$ Dịch dấu phẩy sang phải 2 hàng (do nhân với $2^2$): $1.1001_2 \times 2^2 \rightarrow 110.01_2$ Đổi từ nhị phân sang thập phân: * Phần nguyên 110: $4 + 2 + 0 = 6$ * Phần thập phân .01: * Bit đầu tiên sau dấu phẩy là $2^{-1} (0.5)$ (ở đây là 0). * Bit thứ hai sau dấu phẩy là $2^{-2} (0.25)$ (ở đây là 1). * $\rightarrow 0 + 0.25 = 0.25$ * Kết quả cuối cùng: -6.25 ## II. Lập trình C, Toán tử & Tối ưu hóa ### 4. Tại sao kích thước con trỏ 32-bit là 4 bytes, 64-bit là 8 bytes? * Con trỏ lưu địa chỉ ô nhớ. * Hệ 32-bit quản lý không gian địa chỉ $2^{32}$ bytes $\rightarrow$ cần 32 bits (4 bytes) để đánh số hết các ô nhớ. * Hệ 64-bit quản lý $2^{64}$ bytes $\rightarrow$ cần 64 bits (8 bytes). ### 5. Tại sao dùng phép toán bit (Bitwise) để tối ưu? * Tốc độ phần cứng: Các phép bit (AND, OR, XOR, SHIFT) được thực hiện trực tiếp bởi các cổng logic trong ALU, thường chỉ tốn 1 chu kỳ máy (cycle). * Tối ưu nhân/chia: * Nhân/Chia cho lũy thừa của 2 (ví dụ: x * 8) có thể thay bằng dịch bit (x << 3). Phép nhân (MUL) và chia (DIV) truyền thống tốn nhiều chu kỳ máy hơn rất nhiều so với phép dịch (SAL, SAR). ### 6. Ép kiểu Signed/Unsigned và các lỗi tiềm ẩn? * Cơ chế: Khi cộng một số signed (âm) với unsigned, C sẽ ngầm định chuyển số signed sang unsigned rồi mới tính. * Ví dụ: -1 (signed) + 1 (unsigned). -1 trong bit là 11...11, khi coi là unsigned nó trở thành số cực lớn (UMAX). * Hậu quả: Gây lỗi logic nghiêm trọng trong các vòng lặp (loop) hoặc điều kiện so sánh (ví dụ: if (a < sizeof(b)) với a là số âm). Có thể gây ra buffer overflow do tính sai kích thước. * Tại sao vẫn ra kết quả đúng (đôi khi)? Vì biểu diễn nhị phân (Two's Complement) của phép cộng và trừ cho số có dấu và không dấu là giống hệt nhau ở cấp độ bit. Máy tính chỉ cộng bit, việc hiểu nó là âm hay dương là do lệnh in ra màn hình (printf %d hay %u). ### 7. Tại sao dùng unsigned char *? * char có thể là signed hoặc unsigned tùy trình biên dịch. * Khi muốn thao tác với dữ liệu thô (raw memory, binary data), ta dùng unsigned char để đảm bảo dải giá trị từ $0 \to 255$, tránh việc các bit cao bị hiểu nhầm là dấu âm khi tính toán hoặc chuyển đổi. ### 8. Mức độ ưu tiên toán tử Bit? * Mức ưu tiên thấp hơn các toán tử so sánh (==, !=). * Ví dụ: x & 1 == 0 sẽ được hiểu là x & (1 == 0). * Lời khuyên: Luôn dùng ngoặc đơn () khi code bitwise. Ví dụ: (x & 1) == 0. ## III. Kiến trúc Máy tính & Assembly (x86) ### 9. Tại sao có 2 cú pháp AT&T và Intel? * AT&T: Xuất phát từ hệ điều hành UNIX (công cụ GNU as, gcc). Đặc điểm: Có dấu %, $, thứ tự Nguồn -> Đích. * Intel: Xuất phát từ tài liệu của Microsoft/Intel. Đặc điểm: Gọn hơn, thứ tự Đích <- Nguồn. * Khi nào dùng: * Dùng AT&T khi làm việc trên Linux, GDB, GCC (đây là mặc định của môi trường học thuật và nguồn mở). * Dùng Intel khi làm việc trên Windows, Reverse Engineering (IDA Pro), hoặc nếu bạn thấy nó dễ đọc hơn. ### 10. Tại sao không có lệnh movl memory, memory? (câu này th không chắc) * Đây là giới hạn của kiến trúc tập lệnh (ISA). Để chuyển dữ liệu từ RAM sang RAM, CPU phải: 1. Phát địa chỉ đọc. 2. Đợi dữ liệu về. 3. Phát địa chỉ ghi. 4. Đẩy dữ liệu đi. * Làm tất cả việc này trong 1 lệnh đơn (mov) sẽ làm phần cứng quá phức tạp và chu kỳ lệnh quá dài. CPU bắt buộc phải dùng thanh ghi làm trung gian: RAM -> Register -> RAM. ### 11. Cờ CF (Carry) và OF (Overflow) khác nhau thế nào? * CF (Carry Flag): Dùng cho số Unsigned. Bật khi kết quả bị tràn khỏi độ rộng bit (ví dụ: cộng 2 số lớn vượt quá $2^{32}$). * OF (Overflow Flag): Dùng cho số Signed (Bù 2). Bật khi kết quả cộng 2 số dương ra số âm, hoặc 2 số âm ra số dương. * Trình biên dịch dùng 2 cờ này để quyết định lệnh nhảy nào (ja cho unsigned, jg cho signed). ### 12. Assembly không có mảng/struct? * Assembly là ngôn ngữ cấp thấp, nó chỉ hiểu: Địa chỉ ô nhớ và kích thước dữ liệu (byte, word, long). * Mảng trong Assembly chỉ là một dãy ô nhớ liên tiếp. * Struct chỉ là việc truy cập ô nhớ cơ sở + offset (độ lệch). Trình biên dịch C chịu trách nhiệm tính toán các offset này. ### 13. Vai trò của lea và scale factor không phải lũy thừa 2? * Lệnh lea (Load Effective Address) thường dùng để tính toán số học dạng $x + k*y + d$. * Nếu $k$ (scale) không phải 1, 2, 4, 8 (giới hạn phần cứng x86), assembler sẽ báo lỗi, phải dùng lệnh nhân (imul) riêng. ### 14. Lệnh CALL và RET hoạt động ra sao? * CALL: 1. Đẩy địa chỉ lệnh kế tiếp (Return Address) vào Stack (giảm %esp). 2. Nhảy đến địa chỉ hàm được gọi (%eip = address hàm). * RET: 1. Lấy (Pop) giá trị trên đỉnh Stack ra. 2. Gán giá trị đó vào %eip (Con trỏ lệnh) để quay về chỗ cũ. ### 15. Phân biệt Segment - Offset (DS, ES)? * Đây là kiến thức của chế độ Real Mode (16-bit cổ điển). * Bộ nhớ được chia thành các đoạn (Segment) 64KB. * DS (Data Segment): Trỏ đến vùng dữ liệu. * ES (Extra Segment): Vùng dữ liệu phụ (thường dùng khi copy chuỗi). ![image](https://hackmd.io/_uploads/S1hg91aXbl.png) ## IV. Quản lý Bộ nhớ, Stack & Linking ### 16. Buffer Overflow trên Stack và Return Address? * Stack phát triển từ địa chỉ cao xuống thấp. * Các biến cục bộ (buf) nằm ở địa chỉ thấp hơn Return Address. * Khi ghi tràn mảng (gets), dữ liệu sẽ ghi dần lên địa chỉ cao $\rightarrow$ đè lên Saved EBP $\rightarrow$ đè lên Return Address. * Khi hàm chạy lệnh RET, nó tin tưởng bốc giá trị tại chỗ Return Address bỏ vào %eip. Nếu giá trị này bị thay đổi thành địa chỉ mã độc, CPU sẽ nhảy tới đó. ### 17. Tại sao phải lưu %rbp (Base Pointer) và tại sao cần move giá trị? * %rbp được dùng làm mốc cố định để truy cập tham số và biến cục bộ trong suốt hàm. %rsp (Stack Pointer) thay đổi liên tục khi push/pop. * Mỗi khi gọi hàm mới, ta phải lưu %rbp của hàm cũ (để lát nữa quay về còn biết mốc cũ ở đâu) rồi mới thiết lập %rbp mới cho hàm hiện tại. * Lưu ý: Khi biên dịch (-O2) thường bỏ qua %rbp (fomit-frame-pointer) và chỉ dùng %rsp để tiết kiệm thanh ghi. ### 18. Padding và Alignment trong Struct? * CPU đọc bộ nhớ theo khối (word/chunk), ví dụ 4 hoặc 8 bytes một lần. ![image](https://hackmd.io/_uploads/H1MAcJ6mZg.png) * Nếu biến int (4 byte) nằm vắt ngang giữa 2 khối bộ nhớ, CPU phải đọc 2 lần rồi ghép lại $\rightarrow$ Rất chậm. * Compiler chèn byte thừa (Padding) để đảm bảo dữ liệu nằm gọn trong các ranh giới địa chỉ đẹp (chia hết cho 4 hoặc 8), giúp truy xuất nhanh nhất ("Tối ưu thời gian hy sinh không gian"). ### 19. Strong vs Weak Symbols (Linking)? * Strong: Hàm hoặc biến toàn cục đã khởi tạo (int x = 10;). * Weak: Biến toàn cục chưa khởi tạo (int x;) hoặc có từ khóa __weak. * Quy tắc: 1. Không được có 2 Strong cùng tên (Lỗi: Multiple definition). 2. 1 Strong + n Weak $\rightarrow$ Chọn Strong. 3. n Weak $\rightarrow$ Chọn bất kỳ (Rất nguy hiểm, trình biên dịch có thể gộp chung biến x và y ở 2 file khác nhau vào cùng 1 địa chỉ nếu cùng tên và đều là weak). ### 20. User Mode, Kernel Mode và System Calls? * User Mode: Chương trình bình thường, bị giới hạn quyền, không được đụng vào phần cứng trực tiếp. * Kernel Mode: Hệ điều hành, toàn quyền sinh sát. * System Call (Lời gọi hệ thống): Là cánh cửa duy nhất. Khi user cần đọc file/in ra màn hình, nó gọi lệnh đặc biệt (ví dụ syscall hoặc int 0x80) để nhờ OS làm hộ. OS kiểm tra quyền, làm xong trả kết quả về.