Khi chƣơng trình chạy dƣới sự kiểm soát của Memcheck, tất cả các lời gọi đọc và ghi của bộ nhớ sẽ đƣợc kiểm tra và phát hiện các lỗi nhƣ sau:
Sử dụng bộ nhớ chƣa khởi tạo
Đọc/Ghi vào bộ nhớ sau khi khối nhớ đã đƣợc giải phóng Đọc/Ghi vào cuối của khối nhớ malloc
Rò rỉ bộ nhớ.
Lỗi sử dụng không đúng giữa malloc/ new/ new[] với free/ delete/ delete[]
Giải phóng bộ nhớ sai (lặp lại hoặc không đúng cách).
Đơn giản là Memcheck thực thi một CPU mô phỏng giống hệt CPU thật. Trong đó, mỗi byte trong bộ nhớ đƣợc thể hiện bởi một bit định địa chỉ (A), chỉ ra rằng chƣơng trình có thể truy cập byte đó hợp lệ hay không. Bit A đƣợc cập nhật bằng các thao tác khởi tạo hoặc giải phóng bộ nhớ. Chúng đƣợc sử dụng để xác định, tại mỗi một byte, nếu có sự truy cập nào làm ảnh hƣởng bộ nhớ thì là truy cập không nên.
Mỗi byte trong thanh ghi và bộ nhớ đƣợc thể hiện bởi 8 bit giá trị (V), chỉ ra rằng giá trị tại bit đó đã đƣợc định nghĩa hay chƣa, theo sự xấp xỉ của máy tính. Bit V đƣợc sử dụng để xác định những vấn đề sau đây khi nó không đƣợc khởi tạo đầy đủ: Kiểm tra điều kiện đầu vào; đọc khối bộ nhớ bởi lời gọi hệ thống; hoặc địa chỉ đƣợc sử dụng trong truy cập bộ nhớ. Mỗi khối heap đƣợc phân bổ, Memcheck ghi lại địa chỉ của nó và hàm phân bổ (malloc()/calloc()/realloc(), new, new[]) nó dùng. Khi một khối heap đƣợc giải phóng, thông tin này đƣợc sử dụng để xác định rằng con trỏ thông qua chức năng giải phóng thực sự trỏ tới một khối heap, và cũng xác định hàm giải phóng (free(), delete, delete[]) tƣơng ứng đƣợc sử dụng có đúng không. Dữ liệu này cũng đƣợc sử dụng khi chƣơng trình kết thúc, để xác định thất thoát bộ nhớ; bất kỳ khối heap nào mà không
đƣợc giải phóng và không có con trỏ nào trỏ đến nó là thất thoát bộ nhớ. Bit A sẽ đƣợc tái sử dụng ở đây.
Với sự tiện lợi và chính xác Memcheck đƣợc sử dụng để kiểm tra các sản phẩm lớn nhƣ: OpenOffice, Firefox, Opera, MySQL ... Tuy nhiên, Memcheck chỉ đƣợc sử dụng với một số bài toán, với mục đích cụ thể là các lỗi mà Memcheck có thể kiểm tra nhƣ ở trên. Và nó cũng bộc lộ một số hạn chế sau:
Chạy chậm hơn khoảng 20 - 30 lần so với bình thƣờng. Sử dụng nhiều bộ nhớ hơn.
Không kiểm tra trên stack/ mảng tĩnh (là bộ nhớ không đƣơc phân bổ bởi malloc)
Một hạn chế nghiêm trọng khi sử dụng Memcheck là việc kiểm tra chƣơng trình một cách tự đông - nó kiểm tra suốt quá trình thực thi thật sự của chƣơng trình có bất kì lỗi bộ nhớ nào thực sự xảy ra hay không. Tức là, nếu chạy Memcheck với một tập giá trị đầu vào, và tập này không gây ra bất kì vấn đề nào về bộ nhớ, thì Memcheck sẽ thông báo chƣơng trình không có lỗi mặc dù chƣơng trình có chứa đựng lỗi. Điều này phụ thuộc vào tập đầu vào vào ngƣời dùng đƣa ra có đủ độ bao phủ hết các lỗi hay không.
Ví dụ: { }
Nếu chuỗi kí tự nhập vào có kích thƣớc lớn hơn 10, thì chƣơng trình có lỗi tràn bộ nhớ. Tuy nhiên, nếu chuỗi kí tự nhập nhỏ hơn 10, thì đối với trƣờng hợp này không có lỗi tràn bộ nhớ, Memcheck cũng không phát hiện ra lỗi. Vì thế đã bỏ qua trƣờng hợp chuỗi kí tự có kích thƣớc lớn hơn 10 khi chƣơng trình đƣợc đƣa vào ứng dụng thực tế.
Điều này cho thấy Memcheck (hay Valgrind) yêu cầu sử dụng nhiều không gian bộ nhớ hơn, độ bao phủ trong bài toán quản lý bộ nhớ cũng không rộng rãi bằng kỹ thuật mà luận văn đề cập tới ở chƣơng 3.
4.1.2. Biên dịch chương trình với Memcheck
Ta biết có 4 giai đoạn chính để thực thi một chƣơng trình C: Pre-processing (Tiền xử lý)
Compilation (Biên dịch) Assembly (Hợp ngữ) Linking (Liên kết)
Trong Valgrind biên dịch chƣơng trình bằng gcc với tùy chọn –g để đảm bảo kết quả đầu ra của Memcheck có số dòng, nhãn – Wall để tất cả các cảnh bảo lỗi đƣợc thông báo đến ngƣời dùng. Cụ thể biên dịch một chƣơng trình C với Valgrind nhƣ sau:
Bƣớc 1: Mở của sổ terminal
Bƣớc 2: Dẫn tới thƣ mục Valgrind
Bƣớc 3: Lệnh tạo môi trƣờng kiểm tra chƣơng trình C trong TepChuongTrinh.c
gcc TepChuongTrinh.c –o TepChuongTrinh –g –Wall
Bƣớc 4: Thực hiện lệnh kiểm tra lỗi mà ngƣời dùng mong muốn kiểm tra chƣơng trình C đó. Xem chi tiết ở phần 4.1.3.
4.1.3. Lệnh kiểm tra trong Memcheck
Trong Memcheck có rất nhiều loại lệnh kiểm tra đƣợc sử dụng để kiểm tra các lỗi có thể có trong chƣơng trình C. Tuy nhiên trong phạm vi luận văn chỉ đƣa ra một số lệnh phổ biến, thƣờng xuyền đƣợc dùng trong Memcheck.
--leak-check = <no/summary|yes|full> [mặc định: summary]
Lệnh đƣợc sử dụng khi muốn kiểm tra sự rò rỉ bộ nhớ khi chƣơng trình kết thúc. Nếu thiết lập dạng summary thì cho biết có bao nhiêu rò rỉ xảy ra. Nếu thiết lập dạng full hoặc yes thì ngoài chỉ ra các rò rỉ còn cung cấp các chi tiết có thể có của các rò rỉ đó.
--track-origins = <yes|no> [mặc định: no]
Lệnh đƣợc sử dụng khi Memcheck cần theo dõi nguồn gốc của các giá trị không đƣợc khởi tạo. Mặc định là no thì không có, tức là mặc dù có đƣa ra một giá trị không đƣợc khởi tạo đƣợc sử dụng không đúng. Nhƣng không chỉ cho ta biết giá trị không đƣợc khởi tạo đó từ vị trí nào.
Nếu thiết lập dạng yes, Memcheck truy vết nguồn gốc của tất các giá trị không đƣợc khởi tạo. Sau đó, mỗi khi có lỗi giá trị không khởi tạo đƣợc cảnh báo, Memcheck cố gắng để đƣa ra nguồn gốc của giá trị đó. Nguồn gốc này có thể là một trong bốn nơi sau: một khối heap, sự phân bổ một stack, một yêu cầu client, hoặc nơi khác.
--show-mismatched-frees=<yes|no> [mặc định: yes]
Lệnh đƣợc sử dụng khi Memcheck cần kiểm tra xem khối nhớ heap đƣợc giải phóng bằng câu lệnh có phù hợp với câu lệnh phân bổ heap đó không. Tức là yêu cầu free đƣợc sử dụng để giải phóng khối bộ nhớ đƣợc khởi tạo bằng malloc, delete để giải phóng khối bộ nhớ khởi tạo bằng new, và
delete[] để giải phóng khối bộ nhớ khởi tạo bằng new[]. Nếu có một sự không phù hợp nào đƣợc xác định, thì một lỗi đƣợc cảnh báo. Điều này rất quan trọng vì trong một số môi trƣờng sự không phù hợp giữa lệnh giải phóng và lệnh khởi tạo có thể gây ra nhiều nguy hại cho chƣơng trình. Tuy nhiên có một trƣờng hợp mà không tránh đƣợc sự không phù hợp này. Đó là khi ngƣời dùng cung cấp cài đặt dạng new/new[] cho lời gọi malloc
và delete/delete[] cho lời gọi free, và các lệnh này không đối xứng trong hàng (inlined). Ví dụ, delete[] là trong hàng nhƣng new[] thì không. Kết quả là Memcheck cho rằng tất cả các delete[] là lời gọi trực tiếp tới free, thậm chí ngay cả khi chƣơng trình không chứa bất kì lời gọi không phù hợp nào. Điều này dẫn tới nhiều cảnh báo khó hiểu và không liên quan. --show- mismatched-frees=no vô hiệu hóa tất cả các trƣờng hợp này.
--show-leak-kinds=<set> [mặc định: definite,possible]
Để xác định các loại rò rỉ đƣợc chỉ ra đầy đủ trong việc tìm kiếm sự rò rỉ, là một trong những cách sau đây:
o Một danh sách đƣợc ngăn cách dấu phẩy của một hoặc nhiều của definite indirect possible reachable.
o Để xác định một tập hoàn chỉnh (tất cả loại lỗi rò rỉ). Nó tƣơng đƣơng với --show-leak-kinds = definite, indirect, possible, reachable.
o none là tập rỗng
--show-reachable=<yes|no> , --show-possibly-lost=<yes|no>
Lệnh này là một cách khác để xác định các loại rò rỉ bộ nhớ:
o --show-reachable=no --show-possibly-lost=yes tƣơng đƣơng với --show-leak-kinds=definite, possible.
o --show-reachable=no --show-possibly-lost=no tƣơng đƣơng với
--show-leak-kinds=definite.
o --show-reachable=yes tƣơng đƣơng với --show-leak-kinds=all.
4.2. Phân tích heap của chƣơng trình trong Valgrind
Trong phần này luận văn sẽ thực nghiệm với một số phân tích ví dụ thể hiện một số lỗi đƣợc phát hiện với Memcheck.
4.2.1. Rò rỉ bộ nhớ
Ví dụ: Chương trình C được ghi trong tệp test.c
{ }
Đoạn ví dụ trên ta nhận thấy rằng có một khối bộ nhớ đƣợc khởi tạo cho biến x, nhƣng khi kết thúc chƣơng trình nó lại không đƣợc giải phóng.
Các bước phân tích trong Valgrind như sau:
Bƣớc 1: Applications -> accessories -> terminal
Bƣớc 2: cd dẫn vào thƣ mục chạy valgrind:
cd Desktops
Bƣớc 3: Lệnh tạo môi trƣờng để kiểm tra tệp test.c
gcc test.c -o test -g –Wall
Bƣớc 4: Lệnh kiểm tra rò rỉ bộ nhớ
valgrind --leak-check=yes ./test
Hình 4.1. Kết quả ví dụ rò rỉ bộ nhớ trong Memcheck
Thông báo rò rỉ bộ nhớ:
Truy vết ngăn xếp chỉ ra nơi mà bộ nhớ bị rò rỉ đã đƣợc khởi tạo. Memcheck không thể chỉ ra tại sao bộ nhớ bị rò rỉ.
Có vài kiểu lỗi rò rỉ bộ nhớ. Trong đó hai loại quan trọng nhất là :
« definitely lost« : chƣơng trình của bạn bị rò rỉ => hãy kiểm tra nó
« probably lost « : chƣơng trinh của bạn bị rò rỉ, trừ khi bạn làm một điều gì đó đúng với con trỏ (chẳng hạn di chuyển chúng trỏ tới giữa khối heap)
4.2.2. Sử dụng bộ nhớ không được khởi tạo
Ví dụ: Chương trình C được ghi trong tệp chuacapphat-mem.c
{ }
Đoạn ví dụ trên ta nhận thấy rằng biến con trỏ p đƣợc khai báo, nhƣng chƣa đƣợc khởi tạo bộ nhớ ban đầu.
Các bước phân tích trong Valgrind như sau:
Bƣớc 1: Applications -> accessories -> terminal
Bƣớc 2: cd dẫn vào thƣ mục chạy valgrind:
cd Desktop
Bƣớc 3: Lệnh tạo môi trƣờng để kiểm tra tệp chuacapphat-mem.c
gcc chuacapphat-mem.c -o chuacapphat-mem -g –Wall
Bƣớc 4: Lệnh kiểm tra sử dụng bộ nhớ không đƣợc khởi tạo
valgrind --track-origins=yes ./ chuacapphat-mem
Hình 4.2. Kết quả ví dụ sử dụng bộ nhớ chưa được khởi tạo trong Memcheck sử dụng –track-origins
Với lệnh --track-origins=yes Memcheck chỉ ra lỗi rất cụ thể, kể cả vị trí nguồn gốc của giá trị không đƣợc khởi tạo.
Nếu ta chỉ dùng lệnh –tool = memcheck tƣơng đƣơng với --track- origins=no thì ta không thể nhận đƣợc một cảnh báo lỗi chi tiết nhƣ trên. Chú ý phần khoanh đỏ ở hình 4.3.
Hình 4.3.Kết quả ví dụ sử dụng bộ nhớ chưa được khởi tạo trong Memcheck không sử dụng –track-origins
4.2.3. Lỗi sử dụng không đúng giữa malloc/ new/ new[] với free/ delete/ delete[]
Ví dụ: Chương trình C được ghi trong tệp mismath-mem.c
{ }
Đoạn ví dụ trên ta nhận thấy rằng biến con trỏ đƣợc phân bổ bởi lệnh
malloc, nhƣng khi giải phóng bộ nhớ con trỏ lại dùng lệnh delete thay vì lệnh
free.
Các bước phân tích trong Valgrind như sau:
Bƣớc 1: Applications -> accessories -> terminal
Bƣớc 2: cd dẫn vào thƣ mục chạy valgrind:
cd Desktop
Bƣớc 3: Lệnh tạo môi trƣờng để kiểm tra tệp mismath-mem.c
gcc mismath -mem.c -o mismath -mem -g –Wall
Bƣớc 4: Lệnh kiểm tra sử dụng bộ nhớ không đƣợc khởi tạo
valgrind –tool=memcheck --show-mismatched-frees=yes ./ mismath –mem
Kết quả Memcheck sinh ra như hình 4.4 cho ta thấy
Hình 4.4.Kết quả ví dụ lỗi sử dụng không đúng giữa malloc/new/new[] với free/delete/delete[]
Thông báo sử dụng không phù hợp giữa malloc và delete
4.2.4. Thực nghiệm trên chương trình có mã nguồn lớn
Sau đây là bản thực nghiệm chạy Valgrind trên một tập chƣơng trình có mã nguồn tƣơng đối lớn và có nhiều lỗi về bộ nhớ nhƣ sử dụng biến con trỏ sau khi đã giải phóng, sử dụng khối bộ nhớ ngoài khối bộ nhớ đƣợc cấp phát, giải phóng hai lần một khối bộ nhớ đƣợc cấp phát
Các bước phân tích trong Valgrind như sau:
Bƣớc 1: Applications -> accessories -> terminal
Bƣớc 2: cd dẫn vào thƣ mục chạy valgrind:
cd Desktop
Bƣớc 3: Lệnh tạo môi trƣờng để kiểm tra tệp large-test.c
gcc large-test.c -o large-test -g
Bƣớc 4: Lệnh kiểm tra sử dụng bộ nhớ không đƣợc khởi tạo
valgrind –tool=memcheck –track-origins=yes ./ large-test
Kết quả Memcheck sinh ra như hình cho ta thấy
Hình trên cho ta thấy lỗi sử dụng khối bộ nhớ đã giải phóng và lỗi giải phóng 2 lần một khối bộ nhớ. Tất cả có 3 lỗi bộ nhớ Valgrind thống kê đƣợc.
Trên đây là một ví dụ thực nghiệm Valgrind trên một chƣơng trình có số lƣợng mã nguồn lớn và có nhiều lỗi bộ nhớ gặp phải, sau khi sử dụng valgrind để test lỗi chƣơng trình ta thấy đƣợc kết quả cụ thể các lỗi bộ nhớ và vị trí lỗi bộ nhớ trên các dong lệnh mà không phải chạy chƣơng trình.
KẾT LUẬN Nội dung đã đạt đƣợc
Đề tài đã tiếp cận và tìm hiểu lý thuyết cơ bản của các phƣơng pháp hình thức, cụ thể là áp dụng phƣơng pháp phân tích chƣơng trình tĩnh vào giải bài toán kinh điển liên quan đến con trỏ, tiến hành khảo sát và cập nhật đƣợc những xu thế trên thế giới về các phƣơng pháp hình thức, các lĩnh vực ứng dụng kỹ thuật này trong phát triển và kiểm soát chất lƣợng phần mềm.
Luận văn tập trung chủ yếu vào nghiên cứu cơ bản, vì vậy tác giả chú trọng nghiên cứu lý thuyết trong việc xác định các tính chất của heap từ mã nguồn chƣơng trình. Cuối cùng, dựa trên các lý thuyết đã tìm hiểu tiến hành áp dụng cho bài toán phân tích hình dạng heap của chƣơng trình và thực nghiệm trên công cụ Valgrind.
Hạn chế
Hiện tại việc áp dụng lý thuyết vào thực tế còn nhiều khó khăn, các công cụ hỗ trợ hầu hết chỉ dừng ở mức thử nghiệm cho nghiên cứu.
Hƣớng phát triển tiếp theo
Thực hiện áp dụng, cải tiến lý thuyết để tăng khả năng ứng dụng. Sử dụng các bộ công cụ mã nguồn mở có sẵn làm nền tảng để thực hiện việc cải tiến đƣa vào ứng dụng thực tế.
TÀI LIỆU THAM KHẢO
[1]. Buytaert D, Venstermans K, Eeckhout L, BosschereK. D, Garbage Collection Hints. ELIS Department, Ghent University – HiPEAC Member, St.-Pietersnieuwstraat 41, B-9000 Gent, Belgium.
[2]. Cousot P. (2001), Abstract interpretation based formal methods and futurechallenges, Lecture Notes in Computer Science 2000, (138–170). [3]. Cousot P. Abstract Interpretation. Web page maintained by P. Cousot, 2008 [4]. Cousot P. Constructive Design of a Hierarchy of Semantics of a Transition
System by Abstract Interpretation. In Electronic Notes in Theoretical Computer Science, Volume 6, 1997.
[5]. Clarke E.M, Grumberg O, Peled D.A. (1999), Model Checking. The MIT Press.
[6]. Jon Rafkind, Adam Wick, John Regehr, Matthew Flatt. Precise Garbage Collection for C.
[7]. Julian Seward, Nicholas Nethercote. Using Valgrind to detect undefined value errors with bit-precision
[8]. Nielson F., Nielson H.R., Hankin C. (2004), Principles of Program Analysis, Springer.
[9]. Rice H.G. (1953), Classes of recursively enumerable sets and their decisionproblems, Transactions of American Math. Society 74, (358-366). [10]. Schwartzbach M.I. (2008), Lecture notes on static analysis. Technical
report, BRICS, Dept. of Computer Science - University of Aarhus
[11]. Sagiv M, Reps T, and Wilhelm R. Parametric shape analysis via 3-valued logic. ACM Transactions on Programming Languages and Systems (TOPLAS), 24(3):217–298, 2002.
[12]. Thomas Reps, Mooly Sagiv, Reinhard Wilhelm, Static Program Analysis via 3-Valued Logic*.
[13]. http://valgrind.org/
[14]. Wogerer W (2005), A Survey of Static Program Analysis Techniques, Technische Universitat Wien.
PHỤ LỤC A
Giải hệ phƣơng trình cho ví dụ về thuật toán Andersen
Ta có hệ phƣơng trình: { } ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ { } ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧
Hệ phƣơng trình tƣơng đƣơng với: { } ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ { } ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧
Hệ phƣơng trình tƣơng đƣơng với: { } ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧
{ } ⟦ ⟧ ⟦ ⟧
Hệ phƣơng trình tƣơng đƣơng với: