Tải bản đầy đủ (.pdf) (24 trang)

TÓM TẮT LUẬN VĂN Nghiên cứu kỹ thuật phân tích chương trình tĩnh trong việc nâng cao chất lượng phần mềm

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (282.24 KB, 24 trang )


ĐẠI HỌC QUỐC GIA HÀ NỘI
TRƯỜNG ĐẠI HỌC CÔNG NGHỆ




TRẦN MẠNH ĐÔNG





NGHIÊN CỨU KỸ THUẬT PHÂN TÍCH CHƯƠNG TRÌNH
TĨNH TRONG VIỆC NÂNG CAO CHẤT LƯỢNG PHẦN MỀM



Ngành: Công nghệ thông tin
Chuyên ngành: Công nghệ phần mềm
Mã số: 60 48 10




TÓM TẮT LUẬN VĂN THẠC SĨ CÔNG NGHỆ THÔNG TIN













Hà Nội – 2013
1
MỞ ĐẦU
Sự tiến hóa nhanh chóng của các thiết bị phần cứng trong hơn 30
năm qua đã đưa đến hệ quả về sự phát triển theo cấp số nhân của kích
cỡ các chương trình phần mềm chạy trên đó. Quy mô của những ứng
dụng cực lớn này (khoảng từ 1 tới 40 triệu dòng mã lệnh) vẫn tiếp tục
gia tăng trong thời gian tới. Những phần mềm như thế cần phải được
thiết kế với chi phí vừa phải trong khi vẫn phải bảo trì, nâng cấp trong
toàn bộ vòng đời của chúng, tầm 20 năm. Một thực tế là quy mô và
hiệu quả của những nhóm lập trình và bảo trì chúng không thể tăng
theo tỉ lệ như vậy. Với hoàn cảnh đó, tỉ lệ giả định 1 lỗi trong 1000
dòng lệnh đối với những phần mềm như vậy là quá lạc quan và sẽ
không thể đạt được trong những hệ thống đòi hỏi độ an toàn cực cao.
Do đó, vấn đề về độ tin cậy của phần mềm (software reliability) chắc
chắn là một mối quan tâm và thách thức đối với xã hội hiện đại ngày
càng phụ thuộc vào các dịch vụ do máy tính đem lại. Nhiều kỹ thuật
kiểm chứng phần mềm (software verification) và các công cụ hỗ trợ
đi kèm đã được phát triển để thực thi hoặc giả lập chương trình trên
nhiều môi trường khác nhau. Tuy nhiên, gỡ rối mã dịch hoặc giả lập
mô hình của mã nguồn các chương trình không thể mở rộng quy mô
và thường chỉ xét được mức độ bao phủ hạn chế các hành vi động của
chương trình. Các phương pháp hình thức trong kiểm chứng chương

trình (formal methods) cố gắng chứng minh một cách tự động rằng
chương trình sẽ thực thi đúng đắn trên mọi môi trường được đặc tả.
Mảng nghiên cứu này bao gồm các phương pháp suy dẫn (deductive
methods), kiểm chứng mô hình (model checking), định kiểu chương
trình (program typing) và phân tích chương tr ình tĩnh (static program
analysis). Ba nhóm đầu tập trung vào việc kiểm chứng phần mềm tại
2
mức mô hình, trong khi nhóm cuối cùng xử lý phần mềm tại mức mã
nguồn. Phân tích chương trình tĩnh thu hút sự quan tâm nhất do nền
tảng lý thuyết hình thức của nó cũng như mục đích của nó đối với các
ứng dụng của nó trong thực tế. Kỹ thuật này phát hiện tính chất/hành vi
của một chương trình mà không yêu cầu chạy chương trình đó. Ngoài
ra, một số lỗi chương trình như việc khởi tạo/sử dụng biến chương
trình, biến con trỏ NULL, có thể được phát hiện bởi kỹ thuật này.
Chương 1: Giới thiệu
1.1 Giới thiệu về phân tích chương trình
Phân tích chương trình tĩnh là kỹ thuật xác định tính chất/hành
vi của một chương trình mà không cần phải chạy chương trình đó.
Phân tích tĩnh được xây dựng dựa trên lý thuyết diễn giải trừu tượng
(abstract interpretation) để chứng minh tính chính xác của các phân
tích liên quan đến ngữ nghĩa của một ngôn ngữ lập trình.
1.2 Điểm mạnh và điểm yếu
Phân tích chương trình tĩnh có những ưu điểm sau:
• Chỉ ra lỗi tại vị trí chính xác trong chương trình
• Dễ dàng thực hiện bởi những chuyên gia kiểm định chất lượng
phần mềm hiểu rõ về mã nguồn
• Khoảng thời gian ngắn từ lúc phát hiện tới khi sửa lỗi
• Có thể tự động hóa nhanh (thông qua các bộ công cụ hỗ trợ ví
dụ: SOOT, Astree, TVLA, )
• Lỗi được phát hiện sớm trong qui trình phát triển phần mềm nên

chi phí sửa lỗi thấp.
3
Một số điểm yếu không khắc phục được:
• Mất thời gian nếu phải thực hiện bằng tay
• Việc tự động hóa chỉ hướng vào một ngôn ngữ lập trình (ví dụ:
SOOT chỉ kiểm tra mã nguồn chương trình viết bằng ngôn ngữ
Java)
• Thiếu nhân lực có thể hiểu và phân tích được chương trình
• Có thể sinh ra nhiều lời cảnh báo lỗi không chính xác
• Không phát hiện được lỗi chỉ xuất hiện khi chạy chương tr ình
(run-time error).
1.3 Các công nghệ phân tích chương trình tĩnh
Những kỹ thuật phân tích chương trình tĩnh đã và đang thu hút
nhiều nghiên cứu trên thế giới, hiện có nhiều kỹ thuật nhưng tựu chung
có thể phân theo 4 nhóm chính như sau:
- Kỹ thuật phân tích chương trình tĩnh dựa trên phân tích luồng dữ liệu
(data flow analysis).
- Kỹ thuật liên quan tới xấp xỉ ngữ nghĩa được gọi là diễn giải trừu
tượng (abstract interpretation).
- Kỹ thuật liên quan tới mô hình được gọi là kỹ thuật kiểm chứng mô
hình (Model checking).
- Kỹ thuật phân tích biểu trưng (symbolic analysis).
Hai nhóm đầu tập trung vào việc nâng cao chất lượng phần mềm tại
mức mã nguồn, trong khi hai nhóm sau xử lý phần mềm tại mức trừu
tượng cao hơn – mô hình. Luận văn sẽ tập trung vào xu thế thứ nhất,
đó là kiểu phân tích luồng dữ liệu dựa trên đồ thị luồng dữ liệu.
1.4 Nền tảng
4
1.4.1 Đồ thị luồng điều khiển
Đồ thị luồng điều khiển (Control-Flow Graph-CFG) là một đồ thị

có hướng, trong đó các nút tương ứng là các điểm trong chương trình
và các cạnh thể hiện cho luồng điểu khiển. Một CFG luôn luôn có một
điểm vào, ký hiệu là entry, và một điểm ra, ký hiệu exit. Ngoài ra, nếu
v là một nút trong CFG thì những ký hiệu pred(v) là tập các nút kế
trước (predecessor) và succ(v) là tập các nút kế sau (successor).
CFG cho các lệnh
• Các lệnh cơ bản
id = E printf(E) return E int id
Hình 1: CFG cho các lệnh cơ bản.
• Các lệnh tuần tự
S1
S2
S1 S2
Hình 2: CFG cho các lệnh tuần tự.
• Các lệnh cấu trúc điều khiển
5
E E
S
S1 S2
if(E) S; if(E) S1; else S2;
Hình 3: CFG cho các lệnh if, if-else.
E
S
E1
E2
S
E3
for(E1; E2; E3;) S;while(E) S;
Hình 4: CFG cho các lệnh while, for.
Ví dụ CFG của một chương trình

Sử dụng các cách xây dựng CFG cho từng lệnh ở trên, ta xây dựng
CFG cho một ví dụ chương trình hàm tính giai thừa viết bằng ngôn
ngữ Java:
int iterative(int n) {
int f;// khai báo biến (f là kiểu int)
int uu_f;
f = 1;
uu_f = 0;// biến này không là biến sống
6
while (n > 0){
f = f*n;
n = n - 1;
}
return f;
}
Và được biểu diễn thành CFG như sau:
n > 0
n=n-1
int uu_f
f = 1
f = f*n
return f
int f
uu_f = 0
Hình 5: CFG của chương trình tính giai thừa.
1.4.2 Lý thuyết Dàn
7
Định nghĩa Dàn
Một thứ tự bộ phận (partial order) là một cấu trúc toán học: L =
(S, ⊑), với S là một tập và ⊑ là quan hệ hai ngôi trên tập S , thỏa mãn

các điều kiện sau:
• Phản xạ: ∀x ∈ S : x ⊑ s
• Phản xứng: ∀x, y ∈ S : x ⊑ y ∧ y ⊑ x ⇒ x = y
• Bắc cầu: ∀x, y, z ∈ S : x ⊑ y ∧ y ⊑ z ⇒ x ⊑ z
Biểu diễn Dàn thông qua biểu đồ Hasse
Ví dụ, biểu diễn Dàn (2
{x,y,x}
, ⊆) (Hình 6 (a)) hoặc Dàn đảo
ngược (Dàn được sắp bởi thứ tự ngược của các tập con và quan hệ hai
ngôi ⊑ được định nghĩa là ⊇) (2
{x,y,x}
, ⊇) (Hình 6 (b)) bằng biểu đồ
Hasse:
{x}
{x,y,z}
{x,y} {x,z} {y,z}
{}
{y} {z}
{}
{y} {z}
{x,y,z}
{x,z} {y,z}
{x,y}
{x}
(a) (b)
Hình 6: Biểu đồ Hasse biểu diễn Dàn.
Cận trên, cận dưới
Cho X ⊆ S . Ta nói rằng y ∈ S là một cận trên của X, ký hiệu X ⊑ y,
nếu ∀x ∈ X : x ⊑ y. Tương tự, y ∈ S là một cận dưới của X, ký hiệu
y ⊑ X, nếu ∀x ∈ X : y ⊑ x. Một cận trên nhỏ nhất, ký hiệu ⊔X, được

định nghĩa bởi:
8
X ⊑ ⊔X ∧ ∀y ∈ X : X ⊑ y ⇒ ⊔X ⊑ y
Bên cạnh đó, một cận dưới lớn nhất, ký hiệu ⊓X, được định nghĩa bởi:
⊓X ⊑ X ∧ ∀y ∈ X : y ⊑ X ⇒ y ⊑ ⊓X
Một Dàn là một thứ tự bộ phận trong đó ⊔X và ⊓X tồn tại cho tất cả
X ⊆ S .
Phần tử lớn nhất, phân tử nhỏ nhất
Nếu x = ⊔X ∈ X thì x được gọi là phần tử lớn nhất của X, ký hiệu
là ⊤ và định nghĩa là ⊤ = ⊓S . Từ tính phản xạ của quan hệ thứ tự ta
suy ra rằng ⊤ nếu tồn tại là duy nhất.
Tương tự ta có, nếu y = ⊓X ∈ X thì y được gọi là phần tử nhỏ nhất
của X, ký hiệu là ⊥ và định nghĩa là ⊥ = ⊔S . Từ tính phản xạ của quan
hệ thứ tự ta suy ra rằng ⊥ nếu tồn tại là duy nhất.
Do tính duy nhất của các phần tử trong ⊤ và ⊥.
Độ cao của dàn
Độ cao của một Dàn được tính bằng chiều dài từ phần tử nhỏ nhất
⊥ tới phần tử lớn nhất ⊤. Dàn L = (S , ⊑) là hữu hạn (chiều cao của
Dàn là hữu hạn) nếu tập S chứa hữu hạn số phần tử.
Điểm cố định (Fixed-Point)
Hàm đơn điệu
Ánh xạ f : L → L được gọi là hàm đơn điệu khi ∀x, y ∈ S : x ⊑
y ⇒ f (x) ⊑ f (y). Chú ý rằng thuộc tính này không có nghĩa hàm f là
hàm tăng (∀x ∈ S : x ⊑ f (x)); Ví dụ, tất cả các hàm hằng là hàm đơn
điệu, những hàm ⊔ và ⊓ là những đơn điệu trong cả hai trường hợp.
Lưu ý rằng những phép hợp của những hàm đơn điệu là hàm đơn điệu.
9
Điểm cố định
Điều mà ta cần chính là việc tìm ra điểm cố định của một hàm.
Theo lý thuyết điểm cố định [? ], trong một Dàn L với độ cao hữu hạn,

mọi hàm đơn điệu f có duy nhất điểm cố định nhỏ nhất được định
nghĩa:
f ix( f ) =

i≥0
f
i
(⊥)
Thuộc tính đóng
Nếu L
1
, L
2
, , L
n
là những Dàn với độ cao hữu hạn, từ đó một phép
toán tích (product):
L
1
× L
2
× × L
n
= {(x
1
, x
2
, , x
n
)|x

i
∈ L
1
}
Tương tự ta có, một phép toán cộng (sum):
L
1
+ L
2
+ + L
n
= {(i, x
i
)|x
i
∈ L
i
\ {⊥, ⊤}} ∪ {⊥, ⊤}
Nếu Dàn L là một Dàn với độ cao hữu hạn, khi đó li f t(L), độ cao
của Dàn L là: height(li f t(L)) = height(L) + 1
Nếu A là một tập hữu hạn (A không cần thiết phải là một Dàn), khi đó
f lat( A) là một Dàn. Cuối cùng, nếu A và L được định nghĩa như trên,
khi đó chúng ta thu được một Dàn ánh xạ (map) với độ cao như sau:
A → L = {[a
1
x
1
, , a
n
→ x

n
]|x
i
∈ L}
Phương trình và bất phương trình
Cho L là một Dàn với độ cao hữu hạn. Một hệ phương trình được
biểu diễn:
x
1
= F
1
(x
1
, , x
n
)
x
2
= F
2
(x
1
, , x
n
)

x
n
= F
n

(x
1
, , x
n
)
10
với x
i
là những biến và F
i
: L
n
→ L là một tập những hàm đơn điệu.
Mỗi hệ chỉ có nghiệm nhỏ nhất duy nhất, nó được gọi là điểm cố định
nhỏ nhất của hàm F : L
n
→ L được định nghĩa bởi:
F(x
1
, x
2
, , x
n
) = (F
1
(x
1
, x
2
, , x

n
), , F
n
(x
1
, x
2
, , x
n
))
1.4.3 Thuật toán điểm cố định
Thuật toán lặp chaotic
x
1
= ⊥; x
n
= ⊥;
do {
t
1
= x
1
; t
n
= x
n
;
x
1
= F

1
(x
1
, , x
n
);

x
n
= F
n
(x
1
, , x
n
);
} while (x
1
 t
1
∨ ∨ x
n
 t
n
);
Thuật toán work-list
x
1
= ⊥; x
n

= ⊥;
W = {1, , n};
while (W  ∅) {
i = W.removeNext();
y = Fi(x
1
, , x
n
);
if (y  x
i
) {
for (v
j
∈ dep(v
i
))W.add( j);
x
i
= y;
}
}
Chương 2: Phân tích chương trình tĩnh
2.1: Phân tích luồng dữ liệu nội thủ tục Phân tích luồng dữ liệu hay
còn gọi là khung đơn điệu (monotone framework), là kỹ thuật phân tích
chương trình tĩnh nhằm thu thập các hành vi của chương trình và phát
11
hiện lỗi thông qua đồ thị luồng dữ liệu (CFG) và Dàn L có độ cao hữu
hạn.
Các bước để phân tích luồng dữ liệu bao gồm:

• Bước 1: Khởi tạo CFG của chương trình: Gọi V = v
1
, v
2
, là
tập các nút trên CFG.
• Bước 2: Khởi tạo Dàn hữu hạn L thông qua các tập biến, biểu
thức trong chương trình.
• Bước 3: Xây dựng các hệ phương tr ình là các ràng buộc luồng
dữ liệu trong chương trình: với mỗi nút v trên CFG ta gán một
biến v ∈ L, với mỗi khởi tạo trong ngôn ngữ lập trình, môt kết
hợp ràng buộc luồng dữ liệu (dataflow constraint) được xác định
liên quan đến giá trị biến của các nút tương với các nút khác, đặc
biệt là các nút láng giếng.
2.2.1 Phân tích quay lại (backward)
Với mỗi điểm của chương trình (nút trên CFG), phân tích quay lại
(backward analysis) là phân tích thông tin về hành vi trong tương
lai. Do đó, trong vế phải của các phương trình chỉ phụ thuộc vào
các nút kế sau nó (successor) trên CFG. Phân tích lùi được bắt
đầu từ nút exit của CFG và di chuyển quay lại trong CFG. Một
số phân tích điển hình: Phân tích tính sống của biến (Liveness),
phân tích biểu thức bận rộn (Busy Expression) Với mỗi điểm
của chương trình (nút trên CFG), phân tích quay lại (backward
analysis) là phân tích thông tin về hành vi trong tương lai. Do
đó, trong vế phải của các phương trình chỉ phụ thuộc vào các nút
kế sau nó (successor) trên CFG. Phân tích lùi được bắt đầu từ
nút exit của CFG và di chuyển quay lại trong CFG. Một số phân
tích điển hình: Phân tích tính sống của biến (Liveness), phân tích
biểu thức bận rộn (Busy Expression)
Phân tích tính sống của biến

Trong một chương trình việc xác định tính sống của biến tại mỗi
điểm của chương trình là rất cần thiết, việc này giúp chương trình
12
xác định và loại bỏ được các biến chết giúp tối ưu hóa bộ nhớ/
tối ưu hoá chương trình dịch và làm tăng tốc độ tính toán của
chương trình. Một biến được gọi là biến sống tại một điểm của
chương trình ( liveness) nếu giá trị hiện tại của nó được đọc trong
nút hiện tại hoặc được đọc trong một số nút kế tiếp (không được
ghi ở nút hiện tại). Thuộc tính này có thể được xấp xỉ bởi phân
tích tĩnh dựa trên CFG với một dàn là một tập các biến trong
chương trình. Gọi các biến ràng buộc cho mỗi nút v trên CFG là
v, khi đó các ràng buộc cho tính sống của biến tại các nút với
các cấu trúc lệnh trong chương trình được xác định như sau:
– Đối với nút exit, có ràng buộc là:
exit = ∅
– Đối với những lệnh điều kiện, lệnh return và lệnh printf(E),
ràng buộc là:
v =

w∈succ(v)
w ∪ vars(E)
– Đối với những phép gán id = E, ràng buộc là:
v =

w∈succ(v)
w \ {id} ∪ vars(E)
– Đối với một khai báo biến intid
1
, , id
n

, ràng buộc là:
v =

w∈succ(v)
w \ {id
1
, , id
n
}
– Cuối cùng, đối với những nút khác, ràng buộc là:
v =

w∈succ(v)
w
Hàm vars(E) là tập các biến trong E và vế phải của các ràng
buộc trên là các hàm đơn điệu.
Với quan sát trên CFG của một chương tr ình, một biến là sống
nếu nó được đọc trong nút hiện tại, hoặc nó được đọc trong một
số nút kế tiếp trừ khi nó được ghi ở nút hiện tại.
Xét ví dụ Mục 1.4.1, dàn ở đây là L = (2
{f, uu_f, n}
, ⊆).
Các ràng buộc được tạo ra như sau:
13
entry = int f
int f = int uu_f \ {f}
int uu_f = f = 1 \ {uu_f}
f = 1 = uu_f = 0 \ {f}
uu_f = 0 = n > 0 \ {uu_f}
n > 0 = (return f ∪ f = f*n) ∪ {n}

return f = exit ∪ {f}
f = f*n = (n = n-1 \ {f}) ∪ {n,f}
n = n-1 = (n > 0 \ {n}) ∪ {n}
exit = ∅
Hệ phương trình trên là để giải thông qua Dàn: L = (2
{f, uu_f, n}
, ⊆
). Hơn nữa, nó dễ dàng thấy rằng tất cả vế phải ràng buộc của
phương trình định nghĩa những hàm đơn điệu. Theo kết quả,
lý thuyết của điểm cố định nhỏ nhất có thể được áp dụng [? ].
Nghiệm nhỏ nhất cho hệ phương trình (Được giải trong Phụ lục
A) là duy nhất là:
entry = ∅
int f = ∅
int uu_f = ∅
f = 1 = {n}
uu_f = 0 = {n,f}
n > 0 = {n,f}
return f = {f}
f = f*n = {n,f}
n = n-1 = {n,f}
exit = ∅
Phân tích biểu thức bận rộn
Việc tính toán các biểu thức trong chương trình làm tăng bộ nhớ
và làm chậm thời gian chạy kết quả của chương trình. Do vậy,
trong chương trình nếu hạn chế tính toán lại một biểu thức trong
tương lai sẽ giúp chương trình chạy nhanh hơn và giúp bộ nhớ
tối ưu hơn.
14
Một biểu thức E là bận rộn (busy) tại điểm p của chương trình

nếu tất cả các đường (path) xuất phát từ một điểm chương trình p
phải được đánh giá E trước khi một biến bất kỳ trong biểu thức E
thay đổi. Hoặc, ta cũng có thể hiểu là giá trị của biểu thức được
đánh giá tại thời điểm hiện tại hoặc sẽ được đánh giá trong tất cả
các nút trong tương lai trừ khi một phép gán làm thay đổi giá trị
của nó. Để xấp xỉ thuộc tính này, ta cần Dàn là tập các biểu thức
trong chương trình. Đối với phân tích này, ta xác định được các
ràng buộc luồng dữ liệu cho các cấu trúc lệnh như sau:
– Ràng buộc cho lệnh exit:
exit = {}
– Các ràng buộc cho các lệnh điều kiện và output là:
v =

w∈succ(v)
w ∪ exps(E)
– Ràng buộc cho các phép gán là:
v =

w∈succ(v)
w ↓ id ∪ exps(E)
– Với tất cả các nút còn lại có ràng buộc là:
v =

w∈succ(v)
w
2.1.2 Phân tích chuyển tiếp (forward)
Với mỗi điểm của chương trình (nút trên CFG), phân tích chuyển
tiếp (forward analysis) là phân tích thông tin về hành vi trong quá
khứ. Do đó, trong vế phải của các phương trình chỉ phụ thuộc
vào các nút kế trước nó (predecessor) trên CFG. Phân tích tiến

được bắt đầu từ nút entry của CFG và di chuyển chuyển tiếp
trong CFG. Một số phân tích điển hình: Phân tích biểu thức có
sẵn(Available Expression), phân tích định nghĩa tới được(Reaching
Denitions)
15
Phân tích biểu thức có sẵn
Một biểu thức không bình thường (nontrivial) trong một chương
trình là có sẵn (available) tại một điểm trong chương trình nếu
giá trị của nó đã được tính toán sẵn trước đó trong khi thực thi.
Việc xác định các biểu thức đã có sẵn trước khi thực thi sẽ giúp
cho việc tính toán nhanh và đơn giản hơn. Do vậy, trong phân
tích này chúng ta sử dụng các thông tin về hành vi trong quá
khứ. Và, Dàn cho phân tích này là tập hợp các biểu thức xảy ra
cho tất cả các điểm chương trình và được sắp bởi các tập con đảo
ngược (reverse). Đối với mỗi nút v trên CFG tương ứng với một
biến ràng buộc v trên Dàn L chứa các tập con của các biểu thức
mà nó được đảm bảo luôn luôn có sẵn tại điểm chương trình kế
sau nút đó. Ví dụ, biểu thức a+ b là có sẵn ở điều kiện trong vòng
lặp, nhưng nó không phải là có sẵn tại các phép gán trong vòng
lặp. Phân tích đưa ra sẽ bảo toàn kể từ khi thiết lập tính toán có
thể là quá nhỏ. Từ đó, có các ràng buộc luồng dữ liệu cho các
cấu trúc lệnh trong phân tích như sau:
– Với mỗi nút entry ta có ràng buộc:
entry = {}
– Nếu v chứa một điều kiện E hoặc lệnh output E, khi đó
ràng buộc là:
v =

w∈pred(v)
w ∪ exps(E)

– Nếu v chứa một phép gán id = E, khi đó ràng buộc là:
v = (

w∈pred(v)
w ∪ exps(E)) ↓ id
– Đối với tất cả các lệnh khác của các nút, có ràng buộc là:
v =

w∈pred(v)
w
16
Với hàm ↓ id loại bỏ tất cả các biểu thức có chứa một tham chiếu
đến biến id, và các hàm exps được định nghĩa là:
exps(intconst) = ∅
exps(id) = ∅
exps(input) = ∅
exps(E
1
opE
2
) = {E
1
opE
2
} ∪ exps(E
1
) ∪ exps(E
2
)
Với op là phép toán nhị phân bất kỳ. Ta thấy rằng một biểu thức

là có sẵn trong v nếu nó có sẵn từ tất cả các cạnh hoặc được tính
toán trong nút v, trừ khi giá trị của nó đã được hủy bởi lệnh gán.
Một lần nữa, phía vế phải của những ràng buộc là những hàm
đơn điệu.
Phân tích định nghĩa tới được
Trong lĩnh vực kiểm thử và đảm bảo chất lượng phần mềm, việc
xác định đồ thị def-use là việc làm quan trọng trong việc hạn
chế các mã chết (dead code) và những mã chuyển động (code
motion). Vì vậy, mục đích của kỹ phân tích định nghĩa tới được
là xác định mối quan hệ của các biến tại có các điểm của chương
trình với nhau.
Các định nghĩa tới được (reaching denitions) cho những điểm
của chương trình là những phép gán mà có thể được xác định là
giá trị hiện tại của các biến. Đối với phân tích này, chúng ta cần
một dàn là tập tất cả các phép gán xảy ra trong chương trình. Với
mọi nút v trên CFG biến v là tập các phép gán mà có thể xác
định giá trị của biến tại điểm chương trình. Ta có các ràng buộc
cho các lệnh chương trình trong phân tích như sau:
– Với những phép gán có ràng buộc:
v = (

w∈pred(v)
w) ↓ id ∪ {v}
17
– Với tất cả các nút khác, ràng buộc là:
v =

w∈pred(v)
w
Trong đó, hàm


id
loại bỏ tất cả phép gán chứa biến
id
.
2.2 Phân tích luồng dữ liệu liên thủ tục
Trong phân tích liên thủ tục, các vấn đề được luận văn tiếp cận
là việc xây dựng CFG cho toàn chương trình, xem xét tính nhạy
cảm luồng dữ liệu (sensitivity) và ứng dụng của kỹ thuật phân
tích này.
2.2.1 Xây dựng đồ thị luồng dữ liệu
Để xây dựng CFG cho chương trình liên thủ tục, ta giả sử có một
tập hợp các biến ẩn (shadow) như sau:
– Với mỗi hàm f ta đặt là biến ret − f tương ứng cho các giá
trị được trả về của hàm;
– Với các lệnh gọi hàm trong chương trình đặt là biến call−i,
trong đó i là chỉ mục biểu thị các giá trị đã được tính toán
bằng lời gọi hàm;
– Với mỗi biến cục bộ hoặc tham số chính thức x trong hàm
gọi cho mọi câu lệnh gọi hàm, một biến save − i − x để bảo
toàn giá trị của biến đó trước khi thực hiện việc gọi hàm;
– Cuối cùng, cho mỗi tham số chính thức x trong hàm bị gọi
và mọi điểm gọi, giới thiệu một biến tạm temp − i − x.
Để đơn giản, ta giả định rằng tất cả các lời gọi hàm được thực
hiện trong kết nối với các tham số:
x = f (E
1
, , E
n
)

Bây giờ, xem xét các CFG cho lời gọi và hàm được gọi, nếu các
tham số chính thức của hàm gọi là a
1
, , a
n
và của hàm bị gọi là
b
1
, , b
m
, thì lời gọi hàm được chuyển thành đồ thị như sau:
18
call-i=ret-f;
bj=save-i-bj;
xj=save-i-xj;
x=call-I;
int x1,x2,…xk;
save-i-bj=bj;
save-i-xj=xj;
temp-i-aj=Ej;
aj=temp-i-aj;
ret-f=E;
Hình 7: Ví dụ CFG tổng quát cho chương trình có chứa lời gọi hàm.
2.2.2 Tính cảm ngữ cảnh (context sensitivity)
Một vấn đề được quan tâm trong phân tích liên thủ tục là phân
tích ngữ cảnh phụ thuộc của sự gọi hàm. Đối với một hàm chỉ
được gọi một lần ta gọi là đơn biến (monovariant), và đối với
hàm được sử dụng nhiều lần gọi là đa biến (polyvariant). Xét ví
dụ cho phân tích đa biến cho chương trình sau:
int f() {

int x;
x=test(20)
return x;
}
int g() {
int y;
y=test(10)
return y;
}
int test(int z){
19
return z+1;
}
Xây dựng CFG cho chương trình như đã giới thiệu trong Mục
2.2.1 như hình dưới đây:
save-1-x=x
z=20
int x
call-1=ref-test
save-2-y=y
z=10
int y
call-2=ref-testref -test=z
save-2-y=ysave-1-x=x
y=call-2x=call-1
ret-g=yret-f=x
Hình 8: CFG đơn biến.
Với cách xây dựng CFG, nếu ta phân tích sự lan truyền hằng số
một cách tuần tự khi đó giá trị trả về trong cả hai hàm f và g
có thể bị sai do cả hai đều cùng gọi hàm test. Để giải quyết ngữ

cảnh nhạy cảm này, ta sử dụng phân tích đa biến, tạo ra các sao
chép CFG hàm được gọi cho mỗi lời gọi hàm. Việc xác định số
bản sao chép đơn giản nhất là mỗi lời gọi sẽ tạo ra một sao chép
như sau:
20
save-1-x=x
z=20
int x
call-1=ref-test
save-2-y=y
z=10
int y
call-2=ref-testref -test=z
save-2-y=ysave-1-x=x
y=call-2x=call-1
ret-g=yret-f=x
ref -test=z
Hình 9: CFG đa biến.
Tuy nhiên, đối với những hàm đệ quy thì số bản sao chép có thể vô hạn.
2.2.3 Ứng dụng phân tích luồng dữ liệu liên thủ tục
Một ứng dụng cho phân tích liên thủ tục được gọi là rung cây (tree
shaking), đây là ứng dụng để xác định và loại bỏ những hàm không bao
giờ được gọi hay hàm chết và từ đó có thể loại bỏ nó từ chương trình
mà vẫn đảm bảo tính an toàn. Điều này thực sự hữu ích nếu chương
trình thực hiện với thư viện hàm lớn.
Với CFG có thể được xây dựng như phần trên đã trình bày, ở đây
ta sử dụng Dàn là tập các tên hàm trong chương trình này. Với mỗi nút
v trên CFG, đưa vào một biến hạn chế v biểu thị tập các hàm có thể
được gọi đến trong tương lai. Tiếp theo, sử dụng ký hiệu entry(id) cho
các nút vào (entry) của hàm id. Khi đó, các ràng buộc:

21
• Cho các phép gán, điều kiện và output là:
v =

w∈succ(v)
w ∪ f unc s(E) ∪

f ∈ f uncs(E)
entry( f )
• Và cho các trường hợp khác là:
v =

w∈succ(v)
w
Trong đó, hàm funcs được định nghĩa như sau:
funcs(id) = f uncs(const) = f uncs(input) = ∅
funcs(E
1
opE
2
) = f uncs(E
1
) ∪ f uncs (E
2
)
funcs(id(E
1
, , E
2
)) = {id} ∪ f uncs(E

1
) ∪ ∪ f uncs(E
n
)
Trong phân tích này, vế phải trong các ràng buộc là các hàm đơn
điệu. Khi đó, với cách xây dựng CFG cho cả chương trình như trên và
các ràng buộc cho ứng dụng như trên ta sẽ có hệ phương trình ràng
buộc và việc giải nghiệm cố định nhỏ nhất (Mục ) để tìm ra các hàm
tất cả các hàm không được sử dụng trong chương trình. Như vậy, ta có
thể loại bỏ các hàm này mà vẫn đảm bảo sự an toàn cho chương trình.
Chương 3: Thực nghiệm
Trong phần thực nghiệm này, luận văn tiến hành dựa trên công cụ
mã nguồn mở SOOT, hỗ trợ cho chương tr ình viết bởi ngôn ngữ Java,
và là một plugin tích hợp vào Eclipse.
3.1 Tổng quan về SOOT
SOOT là một framework tối ưu hóa cho Java do một nhóm nghiên
cứu tại đại học McGill (Canada), năm 2000. Đây là phần mềm nguồn
mở.
Các bước phân tích trong SOOT
• Bước 1: Cài đặt kiểu phân tích: BackwarkFlowAnalysis(), For-
warkFlowAnalysis()
22
• Bước 2: Cài đặt các trừu tượng: merge(), copy()
• Bước 3: Cài đặt hàm luồng: flowThrough()
• Bước 4: Cài đặt khởi tạo các giá trị: newInitialFlow() và entryIni-
tialFlow()
• Bước 5: Xây dựng phân tích: doAnalysis()
3.2 Phân tích chương trình ví dụ với Soot
Trong phần này, luận văn thực nghiệm với phân tích tính sống của
biến.

23
KẾT LUẬN
Phân tích chương trình tĩnh là kỹ thuật đang thu hút sự quan tâm
của thế giới bởi những ứng dụng của nó mang lại. Trong quá tr ình thực
hiện luận văn này, tôi đã tìm hiểu những kiến thức nền tảng về phân
tích chương trình - là một khâu vô cùng quan trọng giúp phát hiện lỗi
và tối ưu hóa chương trình để nhằm nâng cao chất lượng phần mềm.
Cụ thể, luận văn đã đạt trình bày tổng quan về phân tích chương trình
tĩnh. Trong đó, nêu ra các khái niệm liên quan đến phân tích chương
trình tĩnh, những điểm mạnh và mặt yếu, các kỹ thuật chính về phân
tích chương trình tĩnh đang được giới học thuật nghiên cứu. Tiếp theo
đó là các lý thuyết nền tảng liên quan đến phân tích chương trình tĩnh
như cách xây dựng đồ thị luồng dữ liệu, các lý thuyết toán học như lý
thuyết Dàn, và các thuật toán tìm điểm cố định. Từ những kiến thức
nền tảng đó, luận văn đã tiếp cận nghiên cứu kỹ thuật phân tích chương
trình tĩnh ở mức mã nguồn, đó là phân tích dựa trên phân tích luồng dữ
liệu. Từ các kết quả thu được từ kỹ thuật phân tích này giúp cho việc
phân tích các thuộc tính/hành vi có thể xảy ra trong chương trình. Cuối
cùng, luận văn tiến hành thực nghiệm kỹ thuật phân tích tĩnh trên công
cụ mã nguồn mở SOOT, sử dụng phân tích cho ngôn ngữ lập trình Java.
Tuy nhiên, việc phân tích tĩnh là rất khó và việc phân tích tĩnh dựa
trên phân tích luồng dữ liệu là một kỹ thuật được đề cập trong khuôn
khổ luận văn tốt nghiệp này. Trong tương lai, một số vấn đề cần tiếp tục
nghiên cứu đó là phân tích khoảng, phân tích luồng điều khiển (phân
tích con trỏ) và phát triển công cụ phân tích chương trình tĩnh cho các
ngôn ngữ khác như C, C++, Java, nhằm tối ưu hóa chương trình dịch,
giảm chi phí và nâng cao chất lượng phần mềm.

×