Hoàng Mạnh Khôi Trường Đại học Công nghệ Luận văn Thạc sĩ ngành: Công nghệ phần mềm; Mã số: 60 48 10 Người hướng dẫn: PGS.TS Nguyễn Việt Hà Năm bảo vệ: 2012
Kiểm chứng từng phần cho chương trình C Hoàng Mạnh Khôi Trường Đại học Công nghệ Luận văn Thạc sĩ ngành: Công nghệ phần mềm; Mã số: 60 48 10 Người hướng dẫn: PGS.TS Nguyễn Việt Hà Năm bảo vệ: 2012 Abstract: Trình bày cơ sở lý luận về kiểm chứng. Trình bày các khái niệm cơ bản liên quan như các khái niệm về mô hình chuyển trạng thái được gán nhãn Hệ chuyển trạng thái gán nhãn (LTS), các phương pháp biểu diễn LTS, khái niệm về trừu tượng hóa hành vi của hệ thống Trìu tượng hóa thủ tục (PA), cũng như các khái niệm cần thiết trong kĩ thuật kiểm chứng … Trình bày nội dung chính của Kiểm thử từng phần cho chương trình C: nêu cách xây dựng mô hình LTS biểu diễn hành vi của hệ thống từ mã nguồn bắt đầu bằng việc xây dựng sơ đồ luồng xử lý Otomat luồng điều khiển (CFA); sơ đồ luồng xử lý mở rộng (Expanding Control flow Automata) của chương trình có sử dụng các LTS giả thiết; giới thiệu phương pháp trừu tượng mệnh đề để xây dựng được mô hình LTS biểu diễn hành vi của mã nguồn từ sơ đồ luồng xử lý mở rộng; nêu cách kiểm chứng mô hình LTS của phần cài đặt có đảm bảo với mô hình LTS của đặc tả. Đưa ra ứng dụng của phương pháp bằng cách giới thiệu các công cụ Copper. Đầu vào của công cụ này là tập file mã nguồn C của chương trình và các đặc tả của các thuộc tính cần kiểm chứng, đầu ra là kết luận phần cài đặt đã đúng với đặc tả của nó hoặc đưa ra phản ví dụ chứng minh cài đặt không đúng với đặc tả. Giới thiệu một vài ứng dụng đơn giản được áp dụng thực tế trên công cụ bằng cách nêu chi tiết cách xây dựng các file đặc tả cũng như cách xây dựng các PA giả thiết bằng ví dụ. Keywords: Công nghệ phần mềm; Kiểm chứng mô hình; Mã nguồn C Content Chương 1: Giới thiệu Đảm bảo chất lượng phần mềm là một trong những giai đoạn quan trọng bậc nhất trong quy trình phát triển phần mềm. Có rất nhiều phương pháp được sử dụng trong việc đảm bảo chất lượng phần mềm, trong đó kiểm chứng mô hình [6] là một trong những cách tiếp cận hiệu quả nhất, và ngày càng được sử dụng rộng rãi, đặc biệt là trong các hệ thống phần mềm đòi hỏi độ chính xác cao. Kiểm chứng mô hình là một nhóm các kĩ thuật kiểm định tự động một mô hình với các đặc tả tính năng của mô hình đó. Mô hình là một hệ thống bao gồm tập hợp có giới hạn các trạng thái và tập hợp các bước chuyển tiếp giữa các trạng thái đó. Kiểm chứng mô hình là xác minh tính đúng đắn của mô hình bằng cách xác định xem thuộc tính mà người dùng mong muốn có được thõa mãn bởi mô hình đó hay không [6]. 2 Trong kiểm chứng mô hình phần mềm có hai bài toán được quan tâm chính đó là kiểm chứng đặc tả và kiểm chứng mã nguồn. Bài toán kiểm chứng tự động mã nguồn được xem là ứng dụng đầu tiên của kiểm chứng mô hình trong việc đảm bảo chất lượng phần mềm. Dù đã có từ lâu nhưng đến nay nó vẫn là vấn đề mở và chưa có giải pháp thỏa đáng, cho nên nó vẫn đang nhận được sự quan tâm rộng rãi. Như chúng ta đã biết một chương trình C có mã nguồn lớn, sẽ tồn tại nhiều lời gọi hàm hay nhiều lời gọi đến các thư viện hàm. Việc kiểm chứng theo mô hình một chương trình như vậy sẽ gặp khó khăn và phức tạp vì hoặc có thể chúng ta chưa có được đầy đủ mã nguồn của các thư viện hàm hoặc dễ gây ra bùng nổ không gian trạng thái. Trong luận văn này tôi xin giới thiệu một phương pháp mới trong kiểm chứng tự động để kiểm chứng một cài đặt của chương trình C có mã nguồn lớn và có nhiều thành phần. Cách tiếp cận của phương pháp là chúng ta đưa việc kiểm chứng một chương trình phần mềm lớn về việc kiểm chứng các thành phần con nhỏ hơn và đơn giản hơn bằng cách trừu tượng hóa hành vi [3] (procedure abtraction-PA) của các thành phần con (hay các hàm thư viện) theo một khái niệm đặc tả của máy hữu hạn trạng thái đó là hệ thống chuyển trạng thái được gán nhãn LTS (Label Transition System) [4]. Phương pháp cho phép chúng ta tự định nghĩa các hành vi của các hàm thư viện (chưa có mã nguồn hoặc chưa rõ hành vi) và sử dụng chúng như là giả thiết trong quá trình xây dựng mô hình kiểm chứng. Nội dung chính của luận văn là giới thiệu phương pháp kiểm chứng phần cài đặt của một hệ thống viết bằng ngôn ngữ C có đảm bảo đúng với đặc tả của nó, để làm được điều đó trước hết từ mã nguồn C chúng ta phải xây dựng được mô hình LTS biểu diễn hành vi của hệ thống, sau đó sử dụng kĩ thuật kiểm chứng để thẩm định xem nó có đảm bảo đúng với mô hình LTS của đặc tả hay không. Nội dung của luận văn được trình bày trong 4 chương: Chương 1 giới thiệu về đề tài, trình bày tổng quan về nội dung phương pháp được nghiên trong đề tài, mục tiêu của đề tài và cấu trúc của luận văn. Chương 2 trình bày các khái niệm cơ bản phục vụ cho đề tài, chương này đưa ra các khái niệm về mô hình chuyển trạng thái được gán nhãn LTS, khái niệm về trừu tượng hóa hành vi của hệ thống PA, cũng như các khái niệm cần thiết trong kĩ thuật kiểm chứng … Chương 3 trình bày nội dung chính của luận văn, đó là nêu cách xây dựng mô hình LTS biểu diễn hành vi của hệ thống từ mã nguồn bắt đầu bằng việc xây dựng sơ đồ luồng xử lý CFA (Control Flow Automata) [3] và sơ đồ luồng xử lý mở rộng (Expanding Control flow Automata) [3] của chương trình có sử dụng các LTS giả thiết. Nêu cách kiểm chứng mô hình LTS của phần cài đặt có đảm bảo với mô hình LTS của đặc tả. Chương 4 luận văn đưa ra ứng dụng thực tế của phương pháp bằng cách giới thiệu các công cụ Copper [2]. Đầu vào của công cụ này là tập file mã nguồn C của chương trình và các 3 đặc tả của các thuộc tính cần kiểm chứng, đầu ra là kết luận phần cài đặt đã đúng với đặc tả của nó hoặc đưa ra phản ví dụ chứng minh cài đặt không đúng với đặc tả. Chương 2: Một Số Khái Niệm Cơ Bản Trong chương này chúng ta sẽ tìm hiểu một số khái niệm cần thiết như máy hữu hạn trạng thái, hệ chuyển trạng thái được gán nhãn và khái niệm về trừu tượng hóa hành vi một chương trình… 2.1 Hệ chuyển trạng thái được gán nhãn - LTS Định nghĩa 2.1: Hệ thống chuyển trạng thái được gán nhãn (Labeled Transition System – LTS[4]) Một hệ chuyển trạng thái được gán nhãn M là một bộ có thứ tự gồm 4 thành phần trong đó: S là một tập khác rỗng các trạng thái của M là trạng thái khởi tạo Act là tập các hành động quan sát được là hàm chuyển trạng thái Ta kí hiệu nếu có một hành động chuyển hệ thống từ trạng thái S sang trạng thái S’. Trạng thái kết thúc STOP là trạng thái mà ở đó không có hành động để chuyển sang một trạng thái nào khác, tức với . 4 Chú ý 2.1: Chúng ta dùng để kí hiệu trạng thái lỗi đặc biệt của hệ thống, và để biểu diễn LTS <{π}, Act, , π>. Ví dụ 2.1: Ở trên là một ví dụ về hệ chuyển trạng thái được gán nhãn M = , trong đó: S = {S0, S1, STOP} Act = {lock, return {0}, return {1}} T = { (S0, lock, S1), (S1, return {0}, STOP), (S0, return {1}, STOP)} S0 là trạng thái khởi đầu. Định nghĩa 2.2: Dẫn xuất của một hệ chuyển trạng thái được gán nhãn M. Dẫn xuất của một hệ chuyển trạng thái được gán nhãn M = là một chuỗi hữu hạn các hành động với , (i = 1, ,n). Như vậy dẫn xuất của hệ chuyển trạng thái được gán nhãn M là một chuỗi các hành động quan sát được mà M có thể thực hiện từ trạng thái khởi tạo . Chú ý 2.2: Ta ký hiệu ↑Σ là một dẫn xuất thu được bằng cách loại bỏ khỏi tất cả các hành động a mà a Σ. Tập tất cả các dẫn xuất của M được gọi là ngôn ngữ của M, ký hiệu L(M). Một dẫn xuất = a1a2 an là một dẫn xuất hữu hạn trên hệ chuyển trạng thái được gán nhãn M. Ta ký hiệu hệ chuyển trạng thái được gán nhãn Mσ = (S, S0, , ) với S = {s0, s1, , sn} và = { (si-1, ai, si)} với i=1, ,n. Ta nói rằng một hành động a được chấp nhận từ một trạng thái s S nếu tồn tại s S sao cho (s, a, s ) . Tương tự vậy ta nói rằng một dẫn xuất a1a2 …an được chấp nhận từ trạng thái s S nếu tồn tại một dãy các trạng thái s0, s1, …, sn với s0 = S0 sao cho i= thì (si-1, ai, si) . Định nghĩa 2.3: Cho một dẫn xuất = và hai trạng thái s, t của hệ chuyển trạng thái được gán nhãn M = . Ta nói rằng t có thể đi đến được từ s thông qua dẫn xuất (viết là ) nếu tồn tại một tập các trạng thái với s = và t = sao cho . 2.2 Trừu tượng hóa thủ tục (Procedure Abstraction-PA) Để kiểm chứng một chương trình từ mã nguồn chúng ta phải trừu tượng hóa được các hành vi của chương trình và các hàm thư viện của nó bằng các đặc tả LTS. Trong một chương trình C, 5 một hàm thư viện có thể thực hiện những chức năng khác nhau tùy thuộc vào tham số đầu vào hay ngữ cảnh thực hiện, trong cách tiếp cận của phương pháp này chúng ta đưa ra khái niệm trừu tượng hóa thủ tục (procedure abstraction - PA) [3] cho phép nhiều đặc tả LTS biểu diễn cho một thủ tục hàm. PA (procedure abstraction) của một thủ tục hàm proc là một tập hữu hạn các cặp ,… trong đó: là điều kiện (guard) ràng buộc trên các tham số của proc. là LTS trừu tượng hóa hành vi của proc ứng với điều kiện đạt giá trị true. Chú ý 2.3: Trong quá trình kiểm chứng, đối các hàm thư viện không có mã nguồn thì các PA của nó được người dùng tự định nghĩa và cung cấp như là các giả thiết trong việc xây dựng mô hình kiểm chứng, ta xem đó là các PA giả thiết. Chương 3: Phương Pháp Kiểm Chứng Mục đích chính của chúng ta là cần phải kiểm chứng xem phần cài đặt mã nguồn của một chương trình C có thõa mãn với đặc tả của nó (được biểu diễn bằng một LTS) hay không? Để làm được việc đó thì trước hết từ phần cài đặt chúng ta phải xây dựng được một mô hình (biểu diễn bằng một LTS) mô tả hành vi của phần cài đặt, sau đó sử dụng kĩ thuật kiểm chứng để kiểm định sự thõa mãn của với . 3.1 Xây dựng mô hình Cho một chương trình C và một tập các mệnh đề logic (predicates) P, hệ trạng thái được gán nhãn =( ) là đặc tả của chương trình và các PAs giả thiết . Trong phần này sẽ trình bày cách xây dựng từ mã nguồn của chương trình bằng cách sử dụng các PA giả thiết, các điều kiện (Guard) và tập một các mệnh đề logic P. Việc xây dựng dựa trên các nguyên tắc sau: Mỗi trạng thái của được mô hình từ một trạng thái của chương trình trong quá trình thực hiện, vì vậy mỗi trạng thái sẽ bao gồm một thành phần điều khiển (control component) và một thành phần dữ liệu (data component). Thành phần điều khiển là đại diện trực quan cho các giá trị của chương trình và nó thu được từ otomat luồng điều khiển (CFA) của chương trình. Thành phần dữ liệu là đại diện trừu tượng cho trạng thái các biến của chương trình, được tính toán dựa trên tập các mệnh đề logic P. Các bước chuyển trạng thái trên tương ứng với các bước chuyển trạng thái trên CFA. 6 Không mất tính tổng quát ta giả thiết trong chương trình có 5 loại lệnh gán, gọi hàm, rẽ nhánh if –then-else, return và lệnh goto. Ta cũng gọi Stmt là tập các câu lệnh của chương trình và Exp là tập tất cả các biểu thức logic (ví dụ như các điều kiện rẽ nhánh) trên các biến của chương trình. 3.1.1 Otomat luồng điều khiển Việc xây dựng mô hình từ mã nguồn của một chương trình C bắt đầu bằng việc xây dựng otomat luồng điều khiển (CFA) của chương trình theo nguyên tắc: Mỗi trạng thái của CFA là một điểm điều khiển (control location) trong chương trình (tương ứng với một câu lệnh trong chương trình). Mỗi bước chuyển trạng thái trong CFA tương ứng với một bước chuyển giữa hai điểm điều khiển (hai câu lệnh) trong chương trình. Định nghĩa 3.1: CFA của một chương trình C CFA của một chương trình là một bộ gồm 4 thành phần trong đó: là tập các trạng thái. là trạng thái khởi tạo. là tập các chuyển đổi trạng thái. là hàm gán nhãn các trạng thái của CFA. {Final} là trạng thái kết thúc duy nhất của CFA. là lệnh khởi tạo của chương trình, và nếu và chỉ nếu một trong các điều kiện sau đây được thõa mãn: Nếu là lệnh gán, lệnh gọi hàm hoặc lệnh goto và là lệnh kế tiếp duy nhất của nó trong π. Nếu là lệnh rẽ nhánh và là lệnh kế tiếp nó sau then hoặc else trong π. Nếu là lệnh return và = {Final}. Như vậy CFA là mô hình đơn giản nhất của chương trình tuy nhiên nó chỉ mới mô hình được luồng điều khiển của chương trình mà chưa trừu tượng hóa được dữ liệu các biến tại các trạng thái của chương trình. Để làm được điều này chúng ta sử dụng đến tập các mệnh đề logic P và một phương pháp gọi là predicate abstraction [3] để xây dựng một otomat luồng điều khiển mở rộng. 3.1.2 Otomat luồng điều khiển mở rộng Mô hình otomat luồng điều khiển mở rộng là sự kết hợp giữa mỗi trạng thái s của CFA với một tập con của Exp thu được từ P gọi là (P thường là các điều kiện rẽ nhánh trong chương trình). Như vậy nếu có k phần tử mỗi phần tử nhận giá trị là true hoặc false thì lúc đó mỗi 7 trạng thái trong CFA sẽ tương ứng với trạng thái trong CFA mở rộng. Việc xây dựng LTS thực hiện theo các bước như sau: 1) Xây dựng CFA 2) Xây dựng một CFA mở rộng theo nguyên tắc: Với mỗi trạng thái trong CFA, chúng ta bổ sung trạng thái trong , mỗi trạng thái trong CFA mở rộng là tổ hợp trạng thái trong CFA và giá trị của tập các mệnh đề logic đang xem xét. Xem xét một cạnh trong CFA. Lúc đó mỗi và sẽ tương ứng với trạng thái trong tập các trạng thái . Như vậy có khả năng chuyển đổi trạng thái tương ứng trong . Tuy nhiên không phải tất cả các khả năng chuyển trạng thái đều thuộc . Chúng ta sẽ sử dụng kĩ thuật theorem prover [4] để quyết định xem phép chuyển đổi nào là thực sự được chấp nhận. Chúng ta cũng sẽ chỉ loại bỏ những chuyển trạng thái nào bị loại trừ bởi theorem prover. 3) là mô hình chính xác hơn so với CFA. Tuy nhiên nó cũng không mô hình hóa được hành vi của các hàm thư viện mà thủ tục proc gọi đến. Để làm được việc đó thì chúng ta phải kết hợp các PAs giả thiết với . LTS thu được sau khi kết hợp với các PAs giả thiết chính là . 3.1.3 Phương pháp trừu tượng mệnh đề Trừu tượng mệnh đề [8] là phương pháp tiếp cận nhằm mô hình hóa trạng thái của một chương trình dựa trên một tập các mệnh đề logic (predicates). Ở đây ta cũng sử dụng các kí hiệu và để thay thế cho những kí hiệu của phép toán logic tương đương trong C đó là &&, || và !. Gọi π là một chương trình C và P là một tập các mệnh đề logic (predicates) của các biến trong π, ta kí hiệu A(π,P) là một mô hình trừu tượng của π theo P (hay A(π,P) chính là CFA mở rộng của π). Ta cũng định nghĩa Stmt là tập các câu lệnh của π và Exp là tập tất cả các biểu thức trên các biến của π. Giả sử rằng π chỉ bao gồm 1 khối mã nguồn và không có các con trỏ hàm hay các gọi hàm đệ quy. Như vậy không mất tính tổng quát ta giả thiết trong π có 4 loại là lệnh gán, lệnh gọi hàm, lệnh rẽ nhánh if-then-else, lệnh return và lệnh goto. Như ta đã biết thì CFA là mô hình đơn giản nhất của một chương trình và từ cách xây dựng CFA ta có thể thấy CFA tương đương với A(π, ). Mô hình A(π,P) là sự kết hợp giữa mỗi trạng thái s của CFA với một tập con của Exp thu được từ P, gọi là . Việc xây dựng từ P sẽ được miêu tả bằng thuật toán Predicate 8 Inference sau đây với chú ý là nếu s là trạng thái kết thúc Final hoặc là một lệnh return và P là một tập con của các lệnh rẽ nhánh trong π. Trước hết ta tìm hiểu khái niệm Weakest Preconditon WP của một biểu thức logic p theo một câu lệnh a trong π. Định nghĩa 3.1: Weakest Precondition WP Cho một câu lệnh a và là một biểu thức logic của C ( ) thì WP của đối với a gọi là sẽ được định nghĩa như sau: Nếu a là một lệnh gán có dạng v = e thì thu được từ bằng cách thay thế tất cả v xuất hiện trong bằng e. Nếu a là một lệnh gán có dạng *v = e. là tập các biến xuất hiện ở trong và với , là lệnh gán . Lúc đó = ( ) ( ) Ví dụ 3.1: Cho là biểu thức (x==5), và nếu a là lệnh gán x = e, ta có = = . Còn nếu a là lệnh gán có dạng *x = e lúc đó ta có = ( ) = ( ) . Thuật toán Predicate Inference: Input: Tập các lệnh rẽ nhánh P trong π. Output: Tập các được kết hợp với tương ứng từng trạng thái s trong CFA của π. Khởi tạo: ∀ s ∈ S CF , P s = ∅ Do forever For each s ∈ S CF do IF (ℒ(s) == lệnh gán and ℒ(s′) là lệnh tiếp theo của nó) THEN. For each p′ ∈ P s′ thêm WP(p ′ , ℒ(s)) vào trong P s . Else (If ℒ(s) == lệnh rẽ nhánh với điều kiện rẽ nhánh là c) THEN IF (ℒ(s) ∈ P) THEN thêm c vào P s . IF (ℒ(s′) là lệnh kế tiếp ℒ(s) sau then hoặc else) THEN P s := P s ∪ P s′ . Else IF (ℒ(s) là lệnh goto và ℒ(s′) là lệnh tiếp theo của nó) THEN P s := P s ∪ P s′ . IF (không tồn tại P s để tiếp tục chỉnh sửa trong vòng lặp) THEN EXIT. 9 Trạng thái và tập các bước chuyển trạng thái trong mô hình Mỗi trạng thái của mô hình A(π,P) (hay trong CFA mở rộng) sẽ tương ứng với một trạng thái trong CFA kết hợp với tập các giá trị của tập mệnh đề logic tại các trạng thái đó. Cho một CFA của một chương trình và một trạng thái s trên CFA đó, giả sử thì giá trị của là một véc tơ kiểu Boolean . Gọi là tập tất các các giá trị của thì hàm concretization của được xác định như sau: Với thì trong đó = và = . Chú ý thì và . Ví dụ 3.2: Giả sử với , và lúc đó ta có: . . Việc xây dụng các bước chuyển trạng thái trong mô hình yêu cầu phải sử dụng theorem prover. Trước hết ta tìm hiểu hai khái niệm được chấp nhận (admisible) và không được chấp nhận (inadmisible). Cho hai biểu thức bất kì chúng ta nói rằng 2 biểu thức là được chấp nhận (admissible) nếu theorem prover cho giá trị TRUE hoặc UNKNOWN trên biểu thức , kí hiệu là Adm( ), ngược lại các trường hợp khác là không được chấp nhận (inadmissible), kí hiệu là . Như vậy với CFA của một chương trình là ta có thể định nghĩa CFA mở rộng (hay mô hình A(π,P)) như sau đây. Mô hình A(π,P) là một bộ gồm 3 thành phần trong đó: là tập các trạng thái. là tập các trạng thái khởi tạo. là tập các bước chuyển đổi trạng thái và được xây dựng như sau nếu và chỉ nếu và một trong các điều kiện sau được thõa mãn : 1. là lệnh gán và Adm( ). 2. là lệnh rẽ nhánh với điều kiện rẽ nhánh là c, là lệnh kế tiếp của nó sau THEN, Adm( ) và Adm( ). 10 3. là lệnh rẽ nhánh với điều kiện rẽ nhánh là c, là lệnh kế tiếp của nó sau ELSE, Adm( ) và Adm( ). 4. là lệnh goto và Adm( ). 5. là lệnh return và là trạng thái kết thúc (FINAL). 3.2 Kiểm chứng Như vậy chúng ta đã trình bày cách xây dựng mô hình LTS từ mã nguồn của một chương trình C. Trong phần này chúng ta sẽ trình bày cách kiểm chứng xem có thõa mãn đặc tả của hệ thống hay không. 3.2.1 Phép ghép nối song song Một toán tử ghép nối song song[7], ký hiệu || là một phép toán ghép nối 2 thành phần phần mềm (được biểu diễn bằng 2 hệ trạng thái được gán nhãn) bằng cách đồng bộ các hành vi chung trên bảng chữ cái và đan xen các hành động còn lại. Giả sử có 2 hệ chuyển trạng thái được gán nhãn là M1= (S1, S01, Act1, T1) và M2= (S2, S02, Act2, T2), ghép nối song song giữa M1 và M2, ký hiệu M1 || M2 được định nghĩa như sau: Nếu M1 = hoặc M2 = thì M1|| M2 = . Ngược lại, M1|| M2 = (S, S0, Act, T) trong đó: S= S1S2, Act= Act1 Act2, S0 = (S01, S02) và hàm T được xác định như sau: Với mọi (s 1 ,a,s 2 ) ∈ T 1 và (s 1 ′, a, s 2 ′) ∈ T 2 thì ( (s 1 , s 1 ′), a, (s 2 , s 2 ′) ) ∈ T Với (s 1 ,a,s 2 ) ∈ T 1 , a ∉ Act 2 thì s′ ∈ S 2 ta có ( (s 1 ,s′), a, (s 2 ,s′) ) ∈ T Với (s 1 ′, a, s 2 ′) ∈ T 2 , a ∉ Act 1 thì s ∈ S 1 ta có ( (s, s 1 ′), a, (s, s 2 ′) ) ∈ T Ví dụ 3.3: Ghép nối song song hai LTS Input và Output như trên hình 3.1 Hình 3.1: LTS Input và Output Khi ghép nối hai mô hình trên, hai hành động send và ack là đồng bộ, các hành động còn lại đan xen nhau. Theo các quy tắc trên ta xác định được hệ chuyển trạng thái song song được gán nhãn M′ = (S′, S 0 ′, Act′, T′) trong đó: S′ = S 1 x S 2 = {(0,a), (0,b), (0,c), (1,a), (1,b), (1,c), (2,a), (2,b), (2,c)} Act′ = {in, send, out, ack} Input 0 1 2 in send ack a b c send out ack Output