Thị hình dạng heap sau vòng lặp

Một phần của tài liệu (LUẬN văn THẠC sĩ) kỹ thuật phân tích chương trình tĩnh cho bài toán phân tích hình dạng bộ nhớ heap (Trang 36 - 44)

Từ kết quả này ta có thể kết luận rằng vùng heap của 2 biến con trỏ và luôn luôn rời nhau.

Chú ý rằng phân tích hình dạng cũng có thể tính toán nhƣ phân tích points – to luồng ngữ cảnh, với mỗi điểm định nghĩa:

{ ⟦ ⟧}

Phân tích này chính xác hơn thuật toán Andersen, nhƣng rõ ràng thực hiện nó cũng tốn kém hơn.

Nhƣ ví dụ sau:

Sau các câu lệnh này thuật toán Andersen cho ra kết quả là { } trong khi đó phân tích hình dạng tính toán ra kết quả { }

tại điểm kết thúc chƣơng trình.

3.2.2. Đánh giá

Nhƣ đã trình bày ở trên, phân tích này chính xác hơn, nhƣng rõ ràng là nó cũng tốn kém để thực hiện hơn so với các thuật toán phân tích points – to khác nhƣ thuật toán Andersen hay thuật toán Streensgaard. Tuy nhiên, ta có thế tăng độ chính xác hơn của phân tích hình bằng cách sử dụng một cách tiếp cận nào đó của phân tích points – to để giảm số đích đƣợc xem xét trong các hàm left

right.

Kết quả của phân tích hình dạng có nhiều ứng dụng. Chúng hữu ích và rất cần thiết đối với một số dạng kiểm chứng chƣơng trình nhƣ: Xác định có tham chiếu con trỏ null trong chƣơng trình hay không. Hoặc cho 2 nút, bắt đầu từ một nút liệu có thể tới đƣợc nút còn lại thông qua các kết nối con trỏ không. Nhìn chung, ta quan tâm tới hình dạng của một heap vì đó là một đặc tính của cấu trúc dữ liệu của nó. Đối với một chƣơng trình thao tác với danh sách móc nối đơn, ta có thể tiến hành xác định xem heap của nó có bao gồm các danh sách hoặc chu trình hay không. Tuy nhiên, vì giới hạn luận văn, chúng tôi chƣa đề cập đến phân tích xem heap có bao gồm chu trình hay không ở báo cáo này.

Đối với chƣơng trình nhỏ, các kỹ thuật này thực hiện thủ công bằng tay. Tuy nhiên với các chƣơng trình lớn phức tạp hơn thì cần phải có các công cụ tự động để thuận tiện cho việc phân tích và quản lý bộ nhớ. Hiện nay, có một số công cụ quản lý bộ nhớ heap cho chƣơng trình viết bằng ngôn ngữ Java nhƣ PMAT, Jmap, Jprofiler. Đối với chƣơng trình viết bằng ngôn ngữ C, C++ có công cụ Valgrind.

3.3. Kết luận chƣơng

Chƣơng đã tìm hiểu và phân tích chi tiết về bài toán phân tích hình dạng bộ nhớ heap, bắt đầu từ nguồn biến thể của nó là phân tích con trỏ. Luận văn đƣa ra các thuật toán, thực hiện phân tích, đánh giá và so sánh kết quả thu đƣợc từ các thuật toán.

Trong chƣơng 4, luận văn sẽ thực hiện thực nghiệm dựa trên công cụ Valgrind để mô phỏng bài toán phân hình dạng bộ nhớ heap.

CHƢƠNG 4. THỰC NGHIỆM 4.1. Tổng quan về Valgrind

Valgrind là một bộ công cụ đƣợc giới thiệu đầu tiên vào năm 2002, cung cấp một số công cụ gỡ lỗi và điều chỉnh giúp chạy nhanh hơn và chính xác hơn. Valgrinds hỗ trợ chạy trên nền tảng LINUX. Valgrind đƣợc phân bổ gồm các công cụ gỡ lỗi và điều chỉnh nhƣ: Memcheck, Cachegrind, Callgrind, Massif, Helgrind, DRD, Lackey, Nulgrind và vài công cụ nhỏ khác [13].

Memcheck là công cụ phổ biến và mặc định của Valgrind. Nó xác

định nhiều vấn đề liên quan đến bộ nhớ heap, và chủ yếu hƣớng tới chƣơng trình viết bằng C và C++ [7].

Cachegrind là lƣu trữ bộ nhớ cache. Nó thực hiện mô phỏng chi tiết

hóa bộ nhớ cache của L1, D1 và D2 trong CPU và vì thế có thể xác định chính xác nguồn gốc của cache bị lỗi trong mã nguồn. Nó xác định số lƣợng cache bị lỗi, tham chiếu bộ nhớ và hƣớng dẫn thực hiện cho từng dòng của mã nguồn, với từng hàm, từng moodul và từng tóm tắt chƣơng trình. Nó hữu ích với chƣơng trình đƣợc viết bằng bất kì chƣơng trình nào. Cachegrind chạy chậm hơn 20 -100 lần so với chƣơng trình bình thƣờng.

Callgrind đƣợc phát triển bởi Josef Weidendorfer, là mở rộng của Cachegrind. Nó cung cấp tất cả các các thông tin mà Cachegrind có, cộng thêm thông tin về Callgraph. Nó đƣợc phân bố chính thức trong phiên bản Valgrind 3.2.0.

Massif là hồ sơ heap. Nó thực hiện chi tiết hóa hồ sơ heap bằng cách

chụp ảnh thƣờng xuyên heap của chƣơng trình. Nó đƣa ra đồ thị cho thấy cách sử dụng heap theo thời gian, bao gồm thông tin về một phần chƣơng trình mà cấp phát bộ nhớ nhiều nhất. Đồ thì đƣợc bổ sung bởi flie văn bản HTML bao gồm nhiều thông tin để xác định nơi mà bộ nhớ đƣợc khởi tạo nhiều nhất. Massif chạy chƣơng trình chậm hơn 20 lần bình thƣờng.

Helgrind là bộ gỡ lỗi luồng, nó tìm luồng dữ liệu trong chƣơng trình

đa luồng. Nó nhìn vào bộ nhớ phân bổ mà đƣợc truy cập bởi nhiều hơn một luồng, nhƣng thƣờng xuyên ko tìm thấy khóa đƣợc sử dụng. Sự phân bổ nhƣ vậy là thể hiện sự không đồng bộ giữa các luồng, và gây khó khăn cho việc tìm các vấn đề phụ thuộc thời gian. Nó rất hữu ích cho bất kì chƣơng trình nào sử dụng đa luồng. Nó là một phần của công cụ thử nghiệm. vì thế cần phản hồi ngƣời dùng.

DRD là công cụ để xác định lỗi trong chƣơng trình C và C++ đa luồng. công cụ làm việc với bất kì chƣơng trình nào sử dụng luồng nguyên thủy POSIX hoặc sử dụng luồng khái niệm xây dựng dựa trên luồng POSIX nguyên thủy. Trong khi Helgrind có thể xác định các khóa vi phạm thứ tự, thì với hầu hết chƣơng trình DRD cần ít bộ nhớ để thực hiện phân tích của nó.

Lackey, Nulgrind Cũng đƣợc bao gồm trong Valgrind. Chƣa đƣợc sử

dụng nhiều, và mục đích là chứng minh.  Các công cụ khác.

4.1.1. Công cụ Memcheck

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

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

Một phần của tài liệu (LUẬN văn THẠC sĩ) kỹ thuật phân tích chương trình tĩnh cho bài toán phân tích hình dạng bộ nhớ heap (Trang 36 - 44)