Lột quần áo “lỗi tràn số” trong lập trình

SHARE ME NOW

Howdy,

Có một câu hỏi mà trc giờ khá nhiều ng hỏi mình một câu hỏi về lĩnh vực lập trình, nhưng vấn đề xảy ra lại là không hẳn là do lĩnh vực này mà lại là liên qua mật thiết đến một lĩnh vực khác. Thực chất thì câu hỏi này liên quan đến lỗi tràn số của những kiểu dữ liệu cơ sở có trong hầu hết những ngôn ngữ lập trình có đặc điểm ràng buộc rõ ràng, chặt chẽ giữa các kiểu dữ liệu như C++, Delphi, Java,…etc. Đối với ng chưa hiểu rõ, chưa nắm vững kiến thức thì nó khá phức tạp nhưng đối với ng đã hiểu rồi thì thấy nó cũng đơn giản và cơ bản.

Một câu hỏi gần đây nhất từ một bạn gái giấu chim. Câu hỏi nsau:

——————————-
Lần 1:
Với đoạn code dưới, n sẽ chuyển từ số dương thành số âm, rồi lại từ số âm sang số dương.


#include 

int main()
{
    int n = 2;
    for (int i = 0, temp = (sizeof(n) * 8); i < temp; i++, n <<= 1)
            printf("0");
        else
            printf("1");
    printf("\n");
    return 0;
}

Kết quả chương trình sẽ trả ra

00000000000000000000000000000010

Cho em hỏi tại sao n đang thành dương sao đó chyển thành âm. Cái này có phải do miền giá trị k ạ?
n từ âm chuyển thành dương thì e lại k biết.
——————————-
Lần 2:
Cảm ơn bạn vì những chưa sẻ rất bổ ích đây. Nhưng mình vẫn chưa hiểu chỗ này mong bạn giải thích hô t
Nếu thay n=166 thì khi
i=23: n= 1,392,508,928
i=24: n= -1,509,949,440
i=25: n= 1,275,068,416

Theo như cách bạn nói thì mình vẫn k hiểu là n đang âm nếu nhân 2 vào thì vượt quá giới hạn kiểu int
nhưng n lại ra số dương là sao?
——————————-
Trc khi tl thì mình có nhận xét ntn: Câu hỏi này khá hay. Thực tế, rất ít lập trình viên biết và để ý đến lỗi này nếu ko để ý kỹ, hay ko hiểu rõ bản chất. Trc đã có một số ng hỏi nhưng mình bận chưa tl đc. Hôm nay rảnh rỗi nên tl luôn cho những ai chưa biết. Mặc dù câu hỏi này ngắn nhưng để hiểu rõ vđề này thì ko có cách tl nào có thể ngắn để hiểu rõ chi tiết đc. Vđề này ko thuộc phạm vi lập trình nhiều mà nó lquan mật thiết đến lĩnh vực khai thác lỗ hổng phần mềm.

Để gthích rõ, mình sẽ nói cụ thể hơn. Máy tính thực ra nó ko dử dụng hệ thập phân (Decan – 10) như con ng hay sử dụng mà thay vào đó nó sử dụng hệ thập lục (Hexan – 16). Chính vì vậy nó ko có quy tắc dấu âm, dấu dương vậy nên miền giá trị của nó cũng khác so với con ng.
*Chú ý: Mình sử dụng cả hệ thập lục biểu diễn số cho dễ hiểu vì nó đủ số byte sẽ dễ hình dung hơn. Ví dụ số int 932.740.237 kích thước 4 bytes nên biểu diễn số này là 4 cặp số 37.98.7C.8Dh.

Mình sẽ lấy ví dụ nhẹ nhàng hơn cho các bạn dễ hiểu và dễ hình dung, nv dễ dàng hơn là cứ theo đoạn code kia.
Mình lấy ví dụ về kiểu char có kích thước 1 byte thay vì kiểu int 4 byte kia.
Thông tin sơ lược về kiểu này nsau:
Tên kiểu dữ liệu: char
Kích thước: 1 Byte
Miền giá trị: -128..127 (-128..-1: Biểu diễn cho miền gtrị âm, 0..127: Biểu diễn cho miền gtrị dương).

Như mình nói ở trên, thay vì sử dụng số thập phân máy sẽ sử dụng phương pháp bù số 2 để chuyển gtrị sang hệ thập lục. Nếu bạn học qua môn Kiến trúc máy tính và hệ điều hành rồi bạn sẽ hiểu rõ phương pháp này, hoặc bạn có thể tìm hiểu phương pháp này trên Google*.
Một phương pháp nữa là sử dụng cách sau cho đơn giản hoá việc chuyển đổi số âm giữa hai hệ số theo kích thước kiểu dữ liệu:
X’ = 2^8n – |X| // #1.Miền âm sang miền dương
X = -(2^8n – X’) // #2. Miền dương sang miền âm
Trong đó n là kích thước của kiểu dữ liệu, X là số thuộc miền âm muốn chuyển, X’ là số thuộc miền dương muốn chuyển.
Công thức này mình chỉ tự nghĩ ra để đơn giản hoá đi thôi.

Ví dụ:
n = sizeof(char) = 1
N = -12 -> N’ = 2^8 – |-12| = 244 (F4h)
N’ = 235 -> N = -(2^8 – 235) = -21 (EBh)

Vậy nên ta có miền gtrị tương ứng của kiểu dữ liệu char theo máy tính như sau: 80h..FFh (Miền âm, -128..-1), 0..7F (Miền dương, 0..127).

Bây giờ ta có một ví dụ về tràn số mà bạn đang chưa hiểu ở trên. Ta có đoạn code ví dụ sau:
{
char N = 123;
N *= 2;
cout << N << endl;
N *= 13;
cout << N << endl;
}
Ở dòng N *= 2, sau khi lệnh này thực thi kết quả sẽ bị tràn số vì 123 * 2 = 246 (F6h). Do bị vượt quá miền gtrị dương [0..7Fh] sang miền âm [80h..FFh]. Dựa vào #1 thì F6 = -10. Vậy nên thay vì N = 246 thì thực tế N = -10.
Vấn đề 1 đc giải quyết: Từ một số nguyên dương bị chuyển thành số nguyên âm
Tiếp đến dòng N *= 13, sau khi lệnh này thực thi kết quả cũng sẽ bị tràn số vì lúc này N = -10 -> -10 * 13 = -130 (7Eh) vượt qua miền gtrị âm [80F..FFh] sang miền gtrị dương là [0..7Fh]. Dựa vào #1 nên thực tế N = 126.
Vấn đề 2 đc giải quyết: Từ một số nguyên âm bị chuyển thành số nguyên dương


Áp dụng với kquả bạn đưa:

n = 166*2^i
i=23: n= 1,392,508,928;
i=24: n= -1,509,949,440
i=25: n= 1,275,068,416

n là kiểu int, kích thước 4 byte miền gtrị [-2.147.483.648..-1, 0..2.147.483.647] tương đương miền gtrị máy sử dụng là [0..FFFF.FFFFh]. Miền âm [8000.0000h..FFFF.FFFFh], miền dương [0..7FFF.FFFFh].

i= 23 -> 166 * 2^23 = 1,392,508,928 (5300.0000h) vẫn thuộc miền dương nên ko bị tràn.
i = 24 -> 166 * 2^24 = 2.785.017.856 (A600.0000h) tràn khỏi miền dương sang miền âm. Dựa vào #2 thì: N’ = -(2*32 – |2.785.017.856|) = -1.509.949.440.
i = 25 -> 166 * 2^25 = 5.570.035.712 (1.4C00.0000h) một số kích thước 33 bit, tràn hẳn ra khỏi kiểu dữ liệu int giới hạn 32 bit nhưng biến n lúc này chỉ nhận đủ 32 bit nên 1.4C00.0000h sẽ chỉ còn là 4C00.0000h = 1.275.068.416.
Đó là lý do xảy ra nguyên nhân như trên.
Qua đây hi vọng những ai chưa biết hiểu để sau lập trình hình dung, xử lý kiểu dữ liệu tốt hơn. Những lỗi này rất nguy hiểm và dễ bị khai khác gây hậu quả nghiêm trọng!

Leave a Reply

Your email address will not be published. Required fields are marked *