Phân Tích Hợp Ngữ Mã Máy: Các Phép Toán Số Học và Gán Biến trong C++

Khi một chương trình C++ được biên dịch, các phép toán số học và gán biến mà chúng ta viết trong mã nguồn sẽ được chuyển đổi thành các lệnh hợp ngữ tương ứng. Việc xem xét mã hợp ngữ được tạo ra có thể tiết lộ cách trình biên dịch tối ưu hóa mã của chúng ta, đặc biệt là sự khác biệt giữa các bản dựng Debug và Release.

1. Phép Cộng và Gán

Phép cộng là một trong những phép toán cơ bản. Trình biên dịch thường thực hiện tối ưu hóa hằng số (constant folding) ngay tại thời điểm biên dịch nếu tất cả các toán hạng là hằng số.

Ví dụ C++:

int calculateSum() {
    int valA = 10;
    int valB = 20;
    int sum = valA + valB; // Phép cộng biến với biến
    int fixedSum = 5 + 3;  // Hằng số được tính toán tại thời điểm biên dịch
    int adjustedSum = sum + 7; // Phép cộng biến với hằng số
    return adjustedSum;
}

Phân tích hợp ngữ (ví dụ minh họa):

Trong chế độ Debug: Trình biên dịch thường tạo ra mã hợp ngữ tường minh cho từng bước, giúp dễ gỡ lỗi. Các giá trị thường được lưu trữ trong bộ nhớ trước khi được sử dụng.

; int valA = 10;
mov dword ptr [ebp-4], 0Ah ; Gán 10 vào biến valA
; int valB = 20;
mov dword ptr [ebp-8], 14h ; Gán 20 vào biến valB

; int sum = valA + valB;
mov eax, dword ptr [ebp-4] ; Lấy giá trị valA vào EAX
add eax, dword ptr [ebp-8] ; Cộng EAX với valB
mov dword ptr [ebp-0Ch], eax ; Lưu kết quả vào biến sum

; int fixedSum = 5 + 3;
; Trình biên dịch sẽ tính toán 5+3 thành 8 ngay lập tức
mov dword ptr [ebp-10h], 08h ; Gán trực tiếp 8 vào fixedSum

; int adjustedSum = sum + 7;
mov eax, dword ptr [ebp-0Ch] ; Lấy giá trị sum vào EAX
add eax, 07h                 ; Cộng EAX với hằng số 7
mov dword ptr [ebp-14h], eax ; Lưu kết quả vào adjustedSum

Trong chế độ Release: Trình biên dịch thực hiện tối ưu hóa mạnh mẽ hơn. Các biến có thể được giữ trong thanh ghi (register) để truy cập nhanh hơn, và các phép tính hằng số được thực hiện hoàn toàn tại thời điểm biên dịch.

; Giả sử valA, valB, sum được giữ trong các thanh ghi
; (ví dụ: valA trong EAX, valB trong EBX, sum trong EAX)

; int valA = 10;
mov eax, 0Ah ; valA = 10
; int valB = 20;
mov ebx, 14h ; valB = 20

; int sum = valA + valB;
add eax, ebx ; EAX = EAX + EBX (sum = valA + valB)

; int fixedSum = 5 + 3; (Kết quả 8 được tính toán trước)
; (Biến fixedSum có thể không cần lưu vào bộ nhớ nếu không được dùng lại)

; int adjustedSum = sum + 7;
add eax, 07h ; EAX = EAX + 7 (adjustedSum = sum + 7)
; Kết quả cuối cùng (adjustedSum) được trả về trong EAX

2. Phép Trừ

Phép trừ cũng tương tự như phép cộng, sử dụng lệnh sub. Trình biên dịch vẫn áp dụng các tối ưu hóa tương tự như đối với phép cộng.

Ví dụ C++:

int subtractValues(int param1, int param2) {
    int result = param1 - 5;       // Trừ biến với hằng số
    result = result - param2;      // Trừ biến với biến
    result = result + 10 - 2;      // Hỗn hợp cộng trừ
    return result;
}

Phân tích hợp ngữ (ví dụ minh họa):

Trong chế độ Debug:

; int result = param1 - 5;
mov eax, dword ptr [ebp+8]   ; Lấy param1 vào EAX
sub eax, 05h                 ; EAX = EAX - 5
mov dword ptr [ebp-4], eax   ; Lưu vào result

; result = result - param2;
mov eax, dword ptr [ebp-4]   ; Lấy result vào EAX
sub eax, dword ptr [ebp+0Ch] ; EAX = EAX - param2
mov dword ptr [ebp-4], eax   ; Lưu vào result

; result = result + 10 - 2;
mov eax, dword ptr [ebp-4]   ; Lấy result vào EAX
add eax, 0Ah                 ; EAX = EAX + 10
sub eax, 02h                 ; EAX = EAX - 2
mov dword ptr [ebp-4], eax   ; Lưu vào result

Trong chế độ Release: Các phép toán có thể được xâu chuỗi và thực hiện trực tiếp trên các thanh ghi, và các hằng số như 10 - 2 sẽ được tính thành 8 tại thời điểm biên dịch.

; Giả sử param1 trong EAX, param2 trong ECX
; int result = param1 - 5;
sub eax, 05h ; EAX = EAX - 5

; result = result - param2;
sub eax, ecx ; EAX = EAX - ECX

; result = result + 10 - 2; (tối ưu thành +8)
add eax, 08h ; EAX = EAX + 8
; Trả về kết quả trong EAX

3. Phép Nhân

Phép nhân có thể được thực hiện bằng lệnh imul (integer multiply) hoặc bằng các phép dịch bit (shift) nếu toán hạng là lũy thừa của 2.

Ví dụ C++:

int multiplyValues(int baseValue) {
    int val1 = baseValue * 15; // Nhân với hằng số không phải lũy thừa của 2
    int val2 = baseValue * 16; // Nhân với hằng số là lũy thừa của 2
    int val3 = baseValue * 4 + 3; // Kết hợp nhân và cộng
    int val4 = baseValue * baseValue; // Nhân biến với biến
    return val1 + val2 + val3 + val4;
}

Phân tích hợp ngữ (ví dụ minh họa):

Trong chế độ Debug:

; int val1 = baseValue * 15;
mov eax, dword ptr [ebp+8] ; Lấy baseValue vào EAX
imul eax, 0Fh              ; EAX = EAX * 15
mov dword ptr [ebp-4], eax ; Lưu vào val1

; int val2 = baseValue * 16;
mov eax, dword ptr [ebp+8] ; Lấy baseValue vào EAX
shl eax, 4                 ; EAX = EAX << 4 (nhân 16)
mov dword ptr [ebp-8], eax ; Lưu vào val2

; int val3 = baseValue * 4 + 3;
mov eax, dword ptr [ebp+8] ; Lấy baseValue vào EAX
lea ecx, [eax*4 + 3]       ; LEA có thể dùng để nhân với lũy thừa của 2 và cộng.
                           ; ECX = (EAX * 4) + 3
mov dword ptr [ebp-0Ch], ecx ; Lưu vào val3

; int val4 = baseValue * baseValue;
mov eax, dword ptr [ebp+8] ; Lấy baseValue vào EAX
imul eax, dword ptr [ebp+8] ; EAX = EAX * baseValue
mov dword ptr [ebp-10h], eax ; Lưu vào val4

Trong chế độ Release: Trình biên dịch tìm kiếm các cơ hội tối ưu hóa. Phép nhân với 15 có thể được viết lại thành phép dịch bit và trừ.

; Giả sử baseValue trong EAX
; int val1 = baseValue * 15;
; Tối ưu hóa: baseValue * 16 - baseValue
mov ecx, eax       ; Lưu baseValue tạm vào ECX
shl eax, 4         ; EAX = EAX * 16
sub eax, ecx       ; EAX = EAX - baseValue (EAX = baseValue * 15)
; (val1 được giữ trong EAX hoặc lưu vào bộ nhớ nếu cần)

; int val2 = baseValue * 16;
; Lại sử dụng baseValue gốc trong thanh ghi khác (hoặc tải lại)
; Giả sử baseValue được tải vào EBX
mov ebx, [baseValue] ; Tải lại baseValue vào EBX
shl ebx, 4           ; EBX = EBX * 16

; int val3 = baseValue * 4 + 3;
; Giả sử baseValue được tải vào EDX
mov edx, [baseValue] ; Tải lại baseValue vào EDX
lea ecx, [edx*4 + 3] ; ECX = (EDX * 4) + 3

; int val4 = baseValue * baseValue;
; Giả sử baseValue được tải vào EDI
mov edi, [baseValue] ; Tải lại baseValue vào EDI
imul edi, edi        ; EDI = EDI * EDI

4. Phép Chia và Chia lấy dư (Modulo)

Phép chia số nguyên có dấu (signed integer division) sử dụng lệnh idiv. Đối với các lũy thừa của 2, trình biên dịch có thể sử dụng phép dịch bit phải (sar - shift arithmetic right) để tối ưu hóa.

Ví dụ C++:

int divideValues(int dividend, int divisor) {
    int res1 = dividend / divisor; // Chia biến cho biến
    int res2 = dividend / 2;       // Chia cho 2
    int res3 = dividend / 7;       // Chia cho hằng số không phải lũy thừa của 2
    int remainder = dividend % 7;  // Chia lấy dư
    int res4 = dividend / 8;       // Chia cho 8
    return res1 + res2 + res3 + remainder + res4;
}

Phân tích hợp ngữ (ví dụ minh họa):

Trong chế độ Debug:

; int res1 = dividend / divisor;
mov eax, dword ptr [ebp+8]   ; Lấy dividend vào EAX
cdq                          ; Mở rộng dấu của EAX vào EDX (EDX:EAX chứa toán hạng 64-bit)
idiv dword ptr [ebp+0Ch]     ; Chia EDX:EAX cho divisor (thường là EBX hoặc ECX)
                             ; Thương lưu vào EAX, dư lưu vào EDX
mov dword ptr [ebp-4], eax   ; Lưu thương vào res1

; int res2 = dividend / 2;
mov eax, dword ptr [ebp+8]   ; Lấy dividend vào EAX
cdq                          ; Mở rộng dấu
sub eax, edx                 ; Điều chỉnh cho phép chia làm tròn về 0 với số âm (của C/C++)
sar eax, 1                   ; Dịch phải số học 1 bit (chia 2)
mov dword ptr [ebp-8], eax   ; Lưu vào res2

; int res3 = dividend / 7;
mov eax, dword ptr [ebp+8]   ; Lấy dividend vào EAX
cdq                          ; Mở rộng dấu
mov ecx, 7                   ; Đặt hằng số 7 vào ECX
idiv ecx                     ; Chia EDX:EAX cho ECX
mov dword ptr [ebp-0Ch], eax ; Lưu thương vào res3

; int remainder = dividend % 7;
; Các lệnh idiv sẽ được thực hiện lại hoặc sử dụng kết quả EDX từ phép chia trước.
mov eax, dword ptr [ebp+8]
cdq
mov ecx, 7
idiv ecx                     ; Sau phép idiv, EDX chứa phần dư
mov dword ptr [ebp-10h], edx ; Lưu phần dư vào remainder

; int res4 = dividend / 8;
mov eax, dword ptr [ebp+8]   ; Lấy dividend vào EAX
cdq                          ; Mở rộng dấu
and edx, 7                   ; EDX sẽ là 0 nếu số dương, hoặc 7 nếu số âm (cho số âm, thêm (divisor - 1))
add eax, edx                 ; Điều chỉnh cho phép chia làm tròn về 0
sar eax, 3                   ; Dịch phải số học 3 bit (chia 8)
mov dword ptr [ebp-14h], eax ; Lưu vào res4

Trong chế độ Release: Trình biên dịch áp dụng các kỹ thuật tối ưu hóa phức tạp hơn cho phép chia bằng hằng số, thường biến phép chia thành phép nhân với một "số ảo" (magic number) và một vài phép dịch bit. Điều này nhanh hơn đáng kể so với lệnh idiv.

; Giả sử dividend trong EDI
; int res1 = dividend / divisor;
; Vẫn sử dụng idiv nếu divisor là biến
mov eax, edi      ; dividend vào EAX
cdq               ; Mở rộng dấu
mov ecx, [divisor_var] ; divisor vào ECX
idiv ecx          ; EAX = EAX / ECX (thương)

; int res2 = dividend / 2;
; Giả sử dividend được giữ trong EDI
mov eax, edi      ; Tải lại dividend vào EAX
cdq               ; Mở rộng dấu
sub eax, edx      ; Điều chỉnh cho làm tròn về 0
sar eax, 1        ; Chia 2 bằng dịch bit

; int res3 = dividend / 7;
; Tối ưu hóa phép chia cho hằng số (magic number algorithm)
; (Ví dụ minh họa, mã thực tế phức tạp hơn)
mov eax, edi                 ; dividend vào EAX
mov ecx, 0x92492493          ; Ví dụ một "số ảo" cho phép chia 7 (32-bit)
imul ecx                     ; nhân EAX với "số ảo" (kết quả 64-bit trong EDX:EAX)
mov eax, edx                 ; Thương có thể nằm trong EDX
sar eax, 2                   ; Dịch phải thêm để có kết quả cuối cùng

; int remainder = dividend % 7;
; (Sẽ tính toán lại từ thương hoặc sử dụng EDX từ phép idiv nếu không bị tối ưu hóa hoàn toàn)
; Nếu tối ưu thành phép nhân với số ảo, việc tính remainder sẽ phức tạp hơn,
; thường là r = a - q * b (remainder = dividend - quotient * divisor).

; int res4 = dividend / 8;
mov eax, edi      ; Tải lại dividend vào EAX
cdq               ; Mở rộng dấu
and edx, 07h      ; Lấy bit dấu của phần dư
add eax, edx      ; Điều chỉnh
sar eax, 3        ; Chia 8 bằng dịch bit

Thẻ: C++ reverse engineering assembly Disassembly Compiler Optimization

Đăng vào ngày 17 tháng 7 lúc 09:47