Chƣơng nêu tổng quan về nền tảng lý thuyết của phân tích ngữ nghĩa chƣơng trình. Trong chƣơng 3 luận văn sẽ giới thiệu về bài toán phân tích hình dạng và các kỹ thuật giải bài toán đó.
CHƢƠNG 3. PHÂN TÍCH HÌNH DẠNG HEAP 3.1. Phân tích con trỏ
Trong khoa học máy tính, phân tích con trỏ (pointer analysis), hay gọi là
points – to analysis, là kỹ thuật phân tích mã nguồn tĩnh xác định một tập các đối tƣợng là các con trỏ, hoặc các tham chiếu heap gọi là các đích, có thể trỏ tới bởi các biến hoặc nơi lƣu trữ. Một kỹ thuật có liên quan chặt chẽ khác là phân tích hình dạng (shape analysis).
Tất nhiên có vô hạn đích có thể chọn trong quá trình thực thi, vì vậy ta phải chọn các đại diện hữu hạn. Cách chọn kinh điển nhất là tạo một đích cho mọi biến tên và đích malloc – i, với là duy nhất, cho mỗi vị trí đƣợc cấp phát khác nhau (điểm mà chƣơng trình thực hiện thao tác malloc). Sử dụng
Targets để biểu diễn tập tất cả các đích con trỏ của chƣơng trình [10].
Phân tích points – to đƣợc thực hiện dựa trên cây phân tích cú pháp. Kết quả cuối cùng của phân tích points – to là một hàm mà với mỗi biến (con trỏ) trả lại một tập của các đích con trỏ mà nó có thể trỏ tới. Tất nhiên ta phải thực hiện một phân tích bảo toàn, vì thế các tập này sẽ rất lớn.
Với thông tin này, nếu ta muốn biết liệu các biến con trỏ và có thể là các
aliases (cặp con trỏ cùng trỏ tới một nút), khi đó phải thực hiện kiểm tra sao cho không rỗng.
Ta có thể phân tích points – to bằng một số thuật toán Andersen, Atreengaard.
3.1.1. Thuật toán Andersen
Một cách tiếp cận để phân tích points – to khá giống với phân tích luồng điều khiển (Control flow analysis). Với mỗi biến con trỏ ta khởi tạo một tập biến ⟦ ⟧ là tập tất cả các đích của con trỏ trong chƣơng trình.
Phân tích giả sử rằng chƣơng trình đã đƣợc chuẩn hóa để mỗi thao tác con trỏ là một trong sáu loại sau:
Với mỗi thao tác con trỏ sinh ra một ràng buộc nhƣ sau:
{ } ⟦ ⟧ (với i thể hiện cho các vị trí đƣợc cấp phát khác nhau)
{ } ⟦ ⟧ ⟦ ⟧ ⟦ ⟧
⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧
Hai ràng buộc cuối đƣợc sinh ra với mọi biến tên , nhƣng trong thực tế chỉ cần xem xét cá biến thực sự có địa chỉ trong chƣơng trình. Ở đây bỏ qua phép gán bằng null, vì nó tƣơng ứng với ràng buộc ⟦ ⟧. Những ràng buộc này kết hợp với thuật toán Cubic có thể giải trong thời gian . Kết quả có hàm
points – to nhƣ sau: ⟦ ⟧ Xét ví dụ sau:
Áp dụng thuật toán Andersen ta có các ràng buộc sau: { } ⟦ ⟧
⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧
⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ { } ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧
Hệ phƣơng trình đƣợc giải (chi tiết ở Phụ lục A) thu đƣợc các giá trị không rỗng trong nghiệm nhỏ nhất là:
⟦ ⟧ { } ⟦ ⟧ { }
3.1.2. Thuật toán Steensgaard
Đây là một phân tích thô về cơ bản bằng cách xem sự việc theo hai hƣớng. Sử dụng một tập hợp gồm các thẻ malloc – i và hai thẻ có dạng và cho mỗi biến tên . Với chƣơng trình đã chuẩn hóa ở trên, nhƣng ở đây sinh ràng buộc tƣơng đƣơng với các thẻ nhƣ sau:
Các ràng buộc tạo ra một quan hệ tƣơng đƣơng trên các thẻ, có thể tinh toán trong thời gian tuyến tính. Kết quả hàm points – to nhƣ sau:
{ } { }
Sau đó ta có thể tự điều chỉnh những trƣờng hợp xảy ra trong chƣơng trình. Nếu chúng ta chỉ xem xét một loại chƣơng trình nhất định, thì có thể tiếp tục loại bỏ các đích con trỏ không phù hợp.
Xét ví dụ ở phần 3.1.1. Thuật toán Steengaard sinh ra các ràng buộc sau:
Các ràng buộc này bao gồm một quan hệ tƣơng đƣơng nhƣ sau:
Hình 3.1.Quan hệ tương đương giữa các ràng buộc
Điều này chỉ ra rằng:
{ }
Rõ ràng kém chính xác hơn thuật toán Andersen. Điều chỉnh các địa chỉ thực sự thực hiện ta có:
{ }
Ta thấy trƣờng hợp kết quả trƣờng hợp tƣơng ứng với thuật toán
Andersen. Nhƣng vẫn sai với trƣờng hợp .
Nhƣ trên luận văn đã nêu một số thuật toán để phân tích con trỏ. Cách giải quyết chung của 2 thuật toán là dựa trên lý thuyết giàn để tạo ra một phƣơng trình ràng buộc, sau đó áp dụng thuật toán điểm cố định giải và thu đƣợc tập đích của con trỏ. Tuy nhiên ta có thể phân tích con trỏ một cách tối ƣu hơn bằng cách phân tích hình dạng bộ nhớ heap. Tức là nó có thể xác định tập point – to
chính xác hơn các kỹ thuật trên.
3.2. Phân tích con trỏ liên thủ tục
Nếu hàm con trỏ đƣợc tham chiếu từ các con trỏ khác, thì ta có thể thực hiện phân tích points – to liên thủ tục bằng cách đầu tiên tính toán liên thủ tục CFG và sau đó chạy một trong hai thuật toán Andersen hoặc Steengaard. Tuy nhiên, nếu hàm con trỏ có tham chiếu gián tiếp thì khi đó ta cần thực hiện phân tích
luồng điều khiển và phân tích points – to đồng thời để giải quyết cho ví dụ với lời gọi hàm:
Để thể hiện kết hợp các thuật toán, chúng ta thực hiện đơn giản hóa là tất cả các lời gọi hàm có dạng:
trong đó và là các biến. Tƣơng tự, tất cả các biểu thức trả lại đƣợc giả thiết là các biến.
Thuật toán Andersen đã tƣơng đồng với phân tích luồng dữ liệu, và nó có thể đƣợc mở rộng đơn giản với các ràng buộc xấp xỉ. Một tham chiếu tới một hàm hằng tạo ra ràng buộc :
{ } ⟦ ⟧ Lời gọi hàm tính toán sinh ra ràng buộc sau:
⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ Với mọi xảy ra trong định nghĩa hàm:
{ }
Điều này sẽ đảm bảo sự chính xác của phân tích luồng điều khiển. Ngƣợc lại, thuật toán Steensgaard có thể đƣợc mở rộng với các ràng buộc
Kết quả có thể mất đi đáng kể độ chính xác, vì mọi hàm đối số đƣợc coi là một mục tiêu của lời gọi hàm.
3.3. Phân tích hình dạng bộ nhớ heap
Phân tích hình dạng là việc xác định tĩnh và tự động các tính chất của heap
của một chƣơng trình (còn đƣợc gọi là Store). Mỗi trạng thái của heap có thể đƣợc biểu diễn thông qua một đồ thị gồm các nút đại diện cho các khối bộ nhớ đã đƣợc cấp phát và các cạnh biểu diễn con trỏ giữa các khối bộ nhớ, cùng với các biến của chƣơng trình cũng có thể trỏ tới các nút trong heap [8]. Hiện nay, phân tích hình dạng bộ nhớ heap có thể tiếp cận theo nhiều cách khác nhau, và có thể thực hiện ở nhiều mức (mức cụ thể, mức trừu tƣợng). Nhƣng đều sử dụng lý thuyết giàn ngữ nghĩa của chƣơng trình. Từng nút trong mã nguồn sẽ đƣợc gắn một giá trị ngữ nghĩa (semantic). Ngữ nghĩa của một nút chịu sự ràng buộc trong mối quan hệ với các nút ở trƣớc và sau nó trong mã nguồn chƣơng trình
tùy theo ngữ nghĩa của các câu lệnh đối với thuộc tính cần xem xét. Từ sự ràng buộc về ngữ nghĩa của các nút trong mã nguồn sẽ tạo ra một hệ phƣơng trình ràng buộc. Thuật toán lặp tìm điểm cố định nhỏ nhất đƣợc áp dụng giải hệ phƣơng trình ràng buộc này. Kết quả sẽ là một tập các trạng thái heap có thể có tại mỗi nút (điểm) của chƣơng trình.
Mục tiêu của phân tích hình dạng là xác định, tại mỗi điểm của chƣơng trình, một tập đồ thị hình dạng thể hiện tất cả các cấu trúc heap có thể xảy ra trong quá trình thực thi chƣơng trình tại thời điểm đó. Từ đó rất nhiều thông tin hữu ích ta có thể thu đƣợc qua phân tích.
Một câu hỏi phổ biến là xác định tính tới đƣợc của các nút trong heap (reachability – một nút heap có thể tới đƣợc từ một biến).
Cho hai nút và 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.
Một số tính chất khác nhƣ xem xét việc tồn tại của chu trình:
o Tính rời nhau (disjointness – hai cấu trúc dữ liệu có chung phần tử không),
o Tính chia sẻ (sharing – có nhiều hơn một biểu thức con trỏ tham chiếu tới nút của heap) cũng rất quan trọng.
Nhìn chung, ngƣời ta quan tâm tới hình dạng của một heap, đó 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, điều này có nghĩa chúng ta có thể tiến hành xác định tĩnh xem heap của nó bao gồm các danh sách hoặc chu trình hay không. Ngoài ra cũng có một số cấu trúc dữ liệu phổ biến đƣợc quan tâm nhƣ danh sách móc nối kép, cấu trúc cây, DAGs.
Kết quả của phân tích hình dạng có nhiều ứng dụng. Chúng hữu ích hoặc rất cần thiết đối với một số dạng kiểm chứng chƣơng trình (ví dụ xác định có tham chiếu con trỏ null trong chƣơng trình), tối ƣu hóa và tự động song song (các cấu trúc dữ liệu tách rời có thể đƣợc xử lý song song).
Hiện nay, phân tích hình dạng bộ nhớ heap có thể tiếp cận theo nhiều cách khác nhau nhƣ phân tích dựa trên 3 - giá trị logic (Program Analysis via 3 – Valued Logic) [8] [11] [12]. Ở luận văn tập trung đề cập tới kỹ thuật phân tích hình dạng heap dựa trên việc thiết lập và giải các ràng buộc để tìm ra đƣợc kết quả là hình dạng cuối cùng của heap.
3.2.1. Kỹ thuật phân tích
Trong phân tích hình dạng bộ nhớ heap, đồ thị hình dạng shape graph dùng để mô tả hình dạng bộ nhớ heap. Đồ thị hình dạng là đồ thị có hƣớng với nút là các con trỏ đích, cạnh là các tham chiếu có thể. Con trỏ đích đóng vai trò là sự trừu tƣợng của tất cả các ô nhớ có thể đƣợc tạo ra trong suốt quá trình thực thi, và cạnh là thể hiện cho sự chứa tham chiếu giữa hai ô nhớ đƣợc đại diện bởi nút nguồn và nút đích. Tổng quát, giàn đƣợc sắp thứ tự theo các tập con và có dạng sau:
Và phân tích giả sử rằng chƣơng trình viết bằng ngôn ngữ C, đƣợc chuẩn hóa để mỗi thao tác con trỏ là một trong sáu loại sau:
Khi đó, với mọi nút của CFG ( ) dựa trên lý thuyết giàn ta tạo một biến ràng buộc ⟦ ⟧ để mô tả hình dạng heap sau khi chƣơng trình đi qua nút . Với các nút tƣơng ứng với thao tác con trỏ có các ràng buộc:
⟦ ⟧ { } ⟦ ⟧ { } ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ với tất cả các nút khác có ràng buộc: ⟦v⟧ = JOIN (v) Trong đó,
⟦ ⟧ là hợp của tất cả hình dạng heap tại các nút phía trƣớc .
{ } ⋃ { } ⋃ { } { { } ⋃ { } { } ⋃ { }
Ví dụ, với thao tác gán con trỏ trỏ tới ô nhớ con trỏ đang trỏ tới: . Khi đó, hình dạng của heap sau khi đi qua nút sẽ là hợp của tất cả hình dạng heap tại các nút phía trƣớc , trừ đi định nghĩa của con trỏ , hợp với cạnh tham chiếu từ con trỏ trỏ tới ô nhớ con trỏ đang trỏ tới. Hay với thao tác gán giá trị của ô nhớ trỏ bởi con trỏ id1 cho con trỏ : . Khi đó, hình dạng của heap sau khi đi qua nút này sẽ là hợp tất cả các hình dạng heap tại các nút phía trƣớc , hợp với cạnh tham chiếu từ ô nhớ trỏ bởi con trỏ tới ô nhớ trỏ bởi con trỏ .
Ví dụ Ta có thể theo dõi kỹ thuật một cách chi tiết hơn qua thực nghiệm sau. Giả sử có đồ thị heap nhƣ hình 1, trong đó x, y và z là các biến chƣơng trình. Ta sẽ tìm cách trả lời câu hỏi về tính rời nhau của cấu trúc chứa đựng các biến trong chƣơng trình. Trong ví dụ trên x và y là không rời nhau còn y và z rời nhau
Hình 3.2. Đồ thị heap
{ }
Áp dụng phƣơng trình tạo ràng buộc của kỹ thuật phân tích ta có hệ phƣơng trình: ⟦ ⟧ { } ⟦ ⟧ { } ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ { } ⟦ ⟧ { } ⟦ ⟧ ⟦ ⟧ ⟦ ⟧ ⟦ ⟧
⟦ ⟧
Sau đó áp dụng thuật toán lặp tìm điểm cố định để giải hệ phƣơng trình ràng buộc (đƣợc giải chi tiết trong phụ lục B) trên ta thu đƣợc kết quả nhƣ sau:
{
}
Sau khi lặp, kết quả nhƣ sau:
{
}
Kết quả có thể mô tả nhƣ hình sau:
Hình 3.3.Đồ thị hình dạng heap sau vòng lặp
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 và
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