Tác dụng của ngữ nghĩa hoạt động với câu lệnh được mô tả bởi một tập các bản cập nhật công thức xác định giá trị của mỗi vị từ sau hành động của câu lệnh đó. Định lý nhúng cho phép ta đánh giá lại các các công thức trong các cấu trúc trừu tượng và cho biết kết quả cung cấp là một ngữ nghĩa trừu tượng bảo toàn. Nếu không có công thức cập nhật được quy định cho một vị từ, thì nó được giữ nguyên bởi các hành động.
Trong hình 3.8, hiệu quả của các action Get_Next_L(x=x->n) được tính bằng cách sử dụng công thức cập nhật sau đây: (i) ( ) = ∃ 1: ( 1) ∧
( 1, ), (ii) [ , ]( ) = [ , ]( ) ∧ ( [ ]( ) ∨ ¬ ( )). Công thức cập nhật (i) biến là phần tử tiếp theo với gốc là . Công thức (ii) cập nhật thông tin về những nút có thể truy cập từ sau mỗi action.
Một nút là có thể đạt từ sau hành động nếu nó là có thể đạt từ trước hành động , trừ trường hợp các nút trực tiếp được trỏ đến bởi (trừ khi
64
xuất hiện trên một chu trình n, trong trường hợp các nút được trỏ đến bởi x là
vẫn có thể truy cập mặc dù chúng ta tiến đến phần tử kế tiếp của nó). Đối
với Sf2, công thức cập nhật cho x được gán giá trị là 1 cho v→ u.1 và 0 đối với tất cả các nút khác so với u.1. Do đó, sau action các kết quả cấu trúc So2 có thể là ( . 1) = 1 nhưng ( . 0) = 0 và ( 0) = 0.
Giai đoạn cuối cùng của việc tính toán là hoạt động Coerce, trong đó sử dụng một tập các quy tắc phi mâu thuẫn để làm cho cấu trúc chính xác hơn bằng cách loại bỏ các giá trị vô hạn không cần thiết và loại bỏ các cấu trúc không khả thi. Tập hợp các quy tắc phi mâu thuẫn được sử dụng là độc lập với hành động hiện tại đang được thực hiện. Trong phần 3..3.6 cho một mô tả chi tiết về các thuật toán Coerce được sử dụng trong TVLA và cách TVLA tự động tạo ra các quy tắc phi mâu thuẫn từ các vị từ đo và các hàm thuộc tính của các vị từ.
Ví dụ, hình 3.8 cho thấy cách thức vận hành Coerce để cải thiện độ
chính xác. Cấu trúc So0 là không khả thi bởi vì các nút u phải được truy cập từ y (khi [ , ] ( ) = 1) và đây không phải là trường hợp của So0. Trong cấu
trúc So1, u không còn là một nút tóm tắt bởi vì x là duy nhất; u của vòng lặp được loại bỏ bởi vì u đã có trường n trỏ đến và nó không đại diện cho một
phần danh sách chia sẻ ( [ ] ( ) = 0).
Tương tự như vậy, trong cấu trúc So2, u.1 không còn là một nút tóm tắt; Ngoài ra, các phần tử danh sách đại diện bởi u.1 đã có một trường n trỏ đến
và nó không được chia sẻ ( [ ] ( . 1) = 0), do đó u.1 được xóa bỏ. Tương tự ta cũng có, cạnh vô hạn n từ u.0 tới u.1 được xóa bỏ.
Để đảm bảo rằng việc phân tích là dừng đối với chương trình có chứa vòng lặp thì đối với mỗi phân tích, chúng ta chọn một tập các vị từ đơn
nguyên được gọi là vị từ trừu tượng. Trong cấu trúc bị chặn, hai nút u1, u2 được trộn với nhau nếu pS(u1) = pS(u2) cho mỗi vị từ trừu tượng p.
Khi các nút được trộn, các giá trị vị từ cho vị từ không trừu tượng của chúng sẽ được tham gia (tức là, kết quả là 1/2 nếu giá trị của chúng là khác
65
nhau). Các hoạt động của việc tính toán loại cấu trúc này được gọi là bị chặn mờ (Blur). Sự lựa chọn của các vị trừu tượng là rất quan trọng đối với sự cân bằng giữa không gian bộ nhớ và độ chính xác. TVLA cho phép người dùng lựa chọn các vị từ trừu tượng. Theo mặc định, tất cả các vị đơn nguyên là vị từ trừu tượng, như trong ví dụ trong phần 3.2.5.
Ví dụ trong hình 3.7, các nút u0 và u được phân biệt bởi thực tế là
( ) = 1, trong khi ( ) = 0 (tất cả vị từ khác là 0.) Nếu x không phải là một vị từ trừu tượng, thì các cấu trúc bị chặn thích hợp S’4 sẽ có một nút
duy nhất, khi đó ta nói rằng u với ( ) = 1/2 và ( , ) = 1/2 . 3.3.6. Thuật toán Coerce xác định hệ ràng buộc
Ràng buộc có tác dụng trong việc cải thiện độ chính xác của phân tích. Ràng buộc (còn gọi là quy tắc phi mâu thuẫn) là bất biến toàn cục trong
chương trình. Ví dụ, các ràng buộc trạng thái nếu nút u không được chia sẻ bởi trường n (is[n]S(u)=0) và không có một đường đi tới thông qua trường n. Vì vậy, nếu có tồn tại u1≠u2 như vậy thì nS(u1, u) = nS(u2, u) = 1 là cấu trúc
không đại diện cho bất kỳ cấu trúc cụ thể hợp lệ và có thể được loại bỏ.
∃ 1: (¬ [ ]( ) ∧ ( 1, ) ∧ 1 ≠ 2) ⊳ ¬ ( 2, )
Tương tự như vậy, nếu có tồn tại u1≠u2 sao cho n(u1, u)=1 và n(u2, u)=1/2, chúng ta có thể từng bước điều chỉnh n để n(u2, u) = 0. Ví dụ, trong hình 3.8, chúng ta sử dụng ràng buộc này trên cấu trúc So1 để loại bỏ vòng lặp
vô hạn trên chính u, và trên So2 để loại bỏ các vòng lặp vô hạn trên chính u.1 và cạnh vô hạn từ u.0 tới u.1.
Nói chung một ràng buộc được đưa ra với hình thức ⊳ , khi mà là một ký hiệu; gọi là thân của ràng buộc và được gọi là đầu của các
ràng buộc. Với một ràng buộc như vậy là hợp lý trên một cấu trúc S nếu cho
từng phép gán mà phần thân ( ) được đánh giá là 1 (không phải là 1/2), phần đầu cũng phải đánh giá là 1, ký hiệu: ⊨ 1 ⊳ 2. Ta nói rằng ràng buộc là vi phạm ký hiệu: ⊭ 1 ⊳ 2.
66
Định nghĩa 3.4: Cho cấu trúc S và một tập các ràng buộc XF, các thực thi
Coerce(S, XF) trả về cực đại S’, vì vậy ⊑ , = , và S’ không vi phạm
bất cứ ràng buộc trong XF. Coerce là không xác định nếu không tồn tại S’.
Định nghĩa 3.5: Đối với một mệnh đề Horn mở rộng φ, chúng ta xác định bao đóng của φ, biểu điễn bằng closure(φ) là tập các ràng buộc:
Bảng các ràng buộc sinh ra từ thuộc tính hàm thể hiện trong hình 3.7.
Đối với một vị từ đo p được định nghĩa bởi công thức φ các ràng buộc
sau đây được tạo ra: (i) ⊳ , (ii) ¬ ⊳ ¬ , và (iii) bao đóng của → , và
¬ → ¬ .
Hệ thống TVLA sử dụng một tinh chỉnh thứ tự của ràng buộc cho phép chúng ta có nhiều trường hợp để tính toán Coerce bằng cách qua các ràng buộc
trên. Một ràng buộc c2 phụ thuộc vào ràng buộc c1 nếu trong khi Coerce sửa chữa c1 trên một cấu trúc mà bước đầu đáp ứng c2 có thể gây ra một vi phạm của c2.
Định nghĩa 3.6: Nếu tồn tại một cấu trúc S sao cho ⊨ 2 và
( , 1) ⊭ 2 thì ta nói rằng c2 phụ thuộc vào c1.
Chúng ta định nghĩa một đồ thị phụ thuộc bắt đầu giữa các ràng buộc, khi có một cạnh hướng từ ≡ ⊳ ( , . . . , ) đến ≡ ⊳ nếu vị
67
từ ( , . . . , ) xuất hiện ở dạng DNF của l2. Nó không khó để thấy rằng
nếu không có cạnh từ c1 đến c2 sau đó c2 không phụ thuộc vào c1. [13]
Bảng các ràng buộc được sinh ra từ các vị từ đo thể hiện trong hình 3.7 Bổ đề: Đối với một cấu trúc S sao cho
3.3.7. Thuật toán Focus giải hệ ràng buộc
Các tính toán Focus là vấn đề cốt lõi cho phân tích tính chính xác. Tuy nhiên, nó có thể tạo ra một số lượng lớn các cấu trúc. Thuật toán trình bày ở đây sử dụng các hàm thuộc tính của các vị từ để giảm số lượng của cấu trúc được tạo ra.
Thuật toán Focus như sau:
Focus(S: structure, φ: formula) : Set of structures conj1 ∨ ... ∨ conjn:=DNF(QuatifierFree(TCEliminate(φ))) XS0:= {S}
68
For i in 1..n do
XSi := FocusConjunction(XSi−1, conji) od
return XSn
Định nghĩa 3.7: Cho một tập các công thức F, một toán tử Focus cho F là một toán tử có thể chuyển đổi tất cả các cấu trúc S vào một tập hợp các cấu trúc XS sao cho
+ XS và S đại diện cho cùng cấu trúc cụ thể,
+ Mỗi công thức ∈ có có giá trị nhất định trong từng cấu trúc trong
XS trên từng phép gán có thể.
Thuật toán bắt đầu bằng các công thức tổng quát đưa vào trong một tập hợp các liên từ của các ký hiệu vị từ. Mỗi kết hợp được phân tích thành từng bước để tìm ra nguồn gốc của phép gán không xác định. Đối với mỗi phép gán không xác định như vậy, cấu trúc được thay thế bằng một tập hợp các cấu trúc nhúng cùng với một tập các cấu trúc cụ thể mà phép gán là không còn hiện tượng không xác định.
Chuẩn hóa công thức Focus
Focus bắt đầu bằng cách chuẩn hóa các công thức tập trung vào trong một tập hợp các phép hội (conjunctions) của các ký hiệu vị từ. Focus trên kết quả các liên từ trả ra là các cấu trúc trong đó các công thức gốc là được xác định.
FocusConjunction(XS0:Set of structs, φ1 ∧ ... ∧φn : conj. of literals) : Set of structs
For i in 1. . . n do
XSi := FocusLiteral(XSi−1, φ1∧ ... ∧φi-1, φi) od
return XSn
FocusLiteral(XS: Set of structures, φ: formula, φ’: literal) : Set of structures
AnswerSet :=ϕ While XS≠ϕ do
69
select and remove S from XS
if exists Zsuch that [[φ∧φ’]]S(Z) = ½ do XS := XS ∪ FocusAssignment (S, φ’, Z) else AnswerSet := AnswerSet ∪ {S} fi od return AnswerSet
Function FocusAssignment(S: structure, φ: Literal, Z assignment): Set of structures
sw3Ditch φ c3Dase φ≡1/2
error (Illegal focus formula) c3Dase φ≡ v1 = v2
error (Focusing on equality of a summary node Z(v1) may create infinite number of structures)
c3Dase φ≡p(v1,v2, . . . , vk) Let ui =Z(vi)
S0:= S[pS(u1,u2, . . . , uk).→ 0] S1:= S[pS(u1,u2, . . . , uk).→ 1] XS :={S0,S1}
i3Df there exists i, s.t., smS(ui) = ½ then i3Df there exists j≠i, s.t., smS(uj) = ½ then
error(Focusing on a formula φ with two summary nodes ui and uj may create an infinite number of structures)
fi
let u.0 and u.1 be individuals not in US S’:= Expand(S, ui, u.0, u.1)
S’’:=S’[pS’(u1,...,ui−1,u.0,ui+1,...,uk).→0,pS’(u1,...,ui−1, u.1,ui+1,..., uk)→1] XS := XS∪{S’’} fi return XS c3Dase φ≡¬φ’ return FocusAssignment(S, φ’, Z) esac
Hàm trả về một tập hợp các cấu trúc có thể được nhúng vào trong S sao cho là ước lượng một giá trị nhất định cho phép gán Z.
70
Đầu tiên, TC_Eliminate loại bỏ tất cả toán tử bao đóng phản xạ bắc cầu để đảm bảo rằng các biến của các công thức con là được giải phóng và được giữ lại là duy nhất. Sau đó Focus trên kết quả công thức cho mỗi phép gán để ước lượng công thức con TC tới một giá trị nhất định, do đó bao đóng bắc cầu của công thức con cũng phải được ước lượng là một giá trị nhất định. Như vậy chúng ta có thể Focus vào công thức sau khi TC_Eliminate thực hiện thay vì công thức ban đầu.
Tiếp theo, QuatifierFree loại bỏ tất cả các lượng từ. Focus vào kết quả đảm bảo rằng tất cả các công thức cho phép gán để ước lượng công thức đến một giá trị nhất định, do đó các công thức ban đầu mà lượng từ trên của một số các biến cũng phải đánh giá một giá trị nhất định. Do đó, chúng ta có thể Focus vào công thức sau khi QuatifierFree thay vì công thức ban đầu.
Cuối cùng DNF được sử dụng để chuyển đổi các công thức tự do định lượng tới DNF. [13].
3.4. Thử nghiệm
3.4.1. Đặc tả hệ thống trong TVLA
Chương trình được đặc tả trong TVLA thông qua hai định dạng.
+ TVS (Three Valued logical Structure): mô tả một trạng thái cụ thể của heap sử dụng cấu trúc 3-valued logic. Được sử dụng để đặc tả trạng thái của heap đầu vào và heap đầu ra.
+ TVP (Three Valued Program) chứa định nghĩa các vị từ, các actions. Từ đó kết hợp với CFG để đặc tả ngữ nghĩa thực thi của chương trình. Để đơn giản trong đặc tả, ngữ nghĩa thực thi được chỉ định riêng biệt theo từng loại dữ liệu.
Một quan sát quan trọng là đặc tả TVP luôn được nghĩ thông qua các khái niệm dạng 2-valued hơn là 3-valued: lý thuyết nhúng (Embedding) đảm bảo tính mạnh của việc giải thích lại công thức trong miền trừu tượng. Đây là ứng dụng của giải thích trừu tượng trong phân tích tĩnh, theo đó một phân tích tĩnh luôn bắt đầu với ngữ nghĩa thực thi cụ thể của chương trình.
71 3.4.2. Giới thiệu bài toán
Trong phần thử nghiệm này chúng ta phân tích các chương trình thao tác với danh sách liên kết đơn có cấu trúc như sau.
typedef struct node { struct node *n; int data;
}*L;
Mục tiêu là áp dụng các kỹ thuật giải thích trừu tượng đã trình bày vào phân tích để xác minh các thuộc tính sau.
+ Không thất thoát bộ nhớ (không có phần tử nào tồn tại mà không đạt đến được từ biến của chương trình);
+ Danh sách đầu ra là không chia sẻ (không có phần tử nào bị tham chiếu tới bởi nhiều hơn hai phần tử khác).
Để đơn giản hóa phân tích trong TVLA, các hàm C nguyên bản được tiền xử lý bằng phương pháp sau.
+ Lệnh gán với một trường dữ liệu được thực hiện trung gian qua lệnh gán trường đó bằng giá trị NULL trước. Ví dụ x−> n = y; được thay thế bởi x−> n = NULL; x− > n = y;
+ Các lệnh giải phóng bộ nhớ cũng được thực hiện qua lệnh gán NULL trung gian với các trường giá trị. Ví dụ, free( x); được thay thế bằng x− > n = NULL; free( x);
+ Các lệnh sao chép giá trị từ một trường đến một trường khác được đơn giản bởi việc sử dụng biến trung gian.Ví dụ, x−> n=y−>n
được thay thế bởi t=y−>n; x−>n=NULL; x−>n=t; t=NULL;
Các điều kiện rẽ nhánh trong chương trình được mô hình bởi hai action, một cho nhánh true và một cho nhánh false. [13]
3.4.3. Đặc tả dữ liệu đầu vào
3.4.3.1. Tệp TVS mô tả heap đầu vào
Tệp tin TVS (input.tvs) mô tả các heap đầu vào như sau [13]:
// An acyclic singly-linked list with two or more elements pointed by program x.
72 %n = {head, tail} %p = { sm = {tail:1/2} n = {head->tail:1/2, tail->tail:1/2} x = {head}
t[n] = {head->head, head->tail, tail->tail:1/2} r[n,x] = {head, tail}
}
// An acyclic singly-linked list with a single element pointed by x. %n = {head} %p = { x = {head} t[n] = {head->head} r[n,x] = {head} }
// An empty list (x points to NULL). %n = {}
%p = {}
3.4.3.2. Tệp TVP định nghĩa các vị từ
Tệp tin TVP (predicates.tvp) mô tả các vị từ được sử dụng có nội dung như sau [13]:
// Core Predicates
// For every program variable z there is a unary predicate that holds for
// list elements pointed by z.
// The unique property is used to convey the fact that the predicate can hold
// for at most one individual.
// The pointer property is a visualization hint for graphical renderers.
foreach (z in PVar) {
%p z(v_1) unique pointer }
// The predicate n represents the n field of the list data type.
%p n(v_1, v_2) function acyclic
///////////////////////////////////////////// // Instrumentation (i.e., derived) predicates
// The is[n] predicate holds for list elements pointed by two different
// list elements.
%i is[n](v) = E(v_1, v_2) (v_1 != v_2 & n(v_1, v) & n(v_2, v))
// The t[n] predicate records transitive reflexive reachability between
73
// list elements along the n field.
%i t[n](v_1, v_2) = n*(v_1, v_2) transitive reflexive // Integrity constraints for transitive reachability %r !t[n](v_1, v_2) ==> !n(v_1, v_2)
%r !t[n](v_1, v_2) ==> v_1 != v_2
%r E(v_1) (t[n](v_1, v_2) & t[n](v_1, v_3) & !t[n](v_2, v_3)) ==> t[n](v_3, v_2)
// For every program variable z the predicate r[n,z] holds for individual
// v when v is reachable from variable z along the n field (more formally,
// the corresponding list element is reachable from z). foreach (z in PVar) {
%i r[n,z](v) = E(v_1) (z(v_1) & t[n](v_1, v)) }
3.4.3.3. Tệp TVP định nghĩa các Actions
Tệp tin TVP (actions.tvp) mô tả các actions được sử dụng có nội dung như sau [13]: %action uninterpreted() { %t "uninterpreted" } %action skip() { %t "skip" }
// Actions for statements manipulating pointer variables and pointer fields %action Set_Null_L(lhs) { %t lhs + " = NULL" { lhs(v) = 0 } } %action Copy_Var_L(lhs, rhs) { %t lhs + " = " + rhs %f { rhs(v) } { lhs(v) = rhs(v) } } %action Malloc_L(lhs) { %t lhs + " = (L) malloc(sizeof(struct node)) " %new { lhs(v) = isNew(v)
74 t[n](v_1, v_2) = (isNew(v_1) ? v_1 == v_2 : t[n](v_1, v_2)) r[n, lhs](v) = isNew(v) foreach(z in PVar-{lhs}) { r[n,z](v) = r[n,z](v) } is[n](v) = is[n](v) } } %action Free_L(lhs) { %t "free(" + lhs + ")" %f { lhs(v) }
%message (E(v, v_1) lhs(v) & n(v, v_1)) ->
"Internal Error! " + lhs + "->" + n + " != NULL" { t[n](v_1, v_2) = t[n](v_1, v_2) foreach(z in PVar) { r[n,z](v) = r[n,z](v) } is[n](v) = is[n](v) } %retain !lhs(v) } %action Get_Next_L(lhs, rhs) {