CONSIDERAÇÕES SOBRE ESPECIFICAÇÃO E

Propaganda
CONSIDERAÇÕES SOBRE ESPECIFICAÇÃO E VERIFICAÇÃO FORMAL DE
SISTEMAS EMBARCADOS UTILIZANDO JML
Antonio Augusto¹ , David Deharbe¹, Ivan Saraiva¹, Luigi Carro²
[email protected], [email protected], [email protected],
[email protected]
¹Universidade Federal do Rio Grande do Norte
²Universidade Federal do Rio Grande do Sul
SUMMARY
In this work we are going to present the JML language for the formal verification of embedded systems, and using as
a case study the SASHIMI tool for synthesis of Java programs. This tool receives a Java program and generates some
VHDL files that are ready to be synthesized, and have as target, the FemtoJava microcontroller that can interpret
Java programs. To perform our tests we implemented a system for the SASHIMI, specified it using JML and made
the verification using the Krakatoa and ESC/java tools. In our work we made some considerations about the
situations we have faced, so this can be a start point for those who intend to use JML for specification and
verification of embedded systems made in Java.
Key words: Formal verification, formal specification, Java, JML
RESUMO
Nesse trabalho será apresentada a linguagem JML para a verificação de sistemas embarcados usando como um
estudo de caso a ferramenta de síntese SASHIMI, que tem por finalidade transformar um programa Java em uma
serie de arquivos VHDL prontos para serem sintetizados, o SASHIMI possui como plataforma alvo o
microcontrolador FemtoJava, que é capaz de interpretar programas feitos em Java. Para fazer nossos testes foi
implementado um sistema para o SASHIMI e ele foi especificado utilizando-se JML e verificado usando as
ferramentas Krakatoa e ESC/Java. No trabalho são feitas algumas considerações sobre algumas situações com as
quais nos deparamos durante o processo de desenvolvimento, visando assim poder passar uma visão sobre a
utilização de JML para sistemas embarcados para aqueles que têm interesse em usa-lo.
Palavras chave: Verificação formal, especificação formal, Java, JML
CONSIDERAÇÕES SOBRE ESPECIFICAÇÃO E VERIFICAÇÃO FORMAL DE
SISTEMAS EMBARCADOS UTILIZANDO JML
Antonio Augusto¹ , David Deharbe¹, Ivan Saraiva¹, Luigi Carro²
[email protected], [email protected], [email protected],
[email protected]
¹Universidade Federal do Rio Grande do Norte
²Universidade Federal do Rio Grande do Sul
ABSTRACT
A verificação formal de sistemas a muito já se encontra
disponível para uso, mas poucos foram aqueles que
aderiram a ela. Uma das razões para isso é a suposta
dificuldade que existe na utilização dessa metodologia
em sistemas comercias, além do atraso que ela impõe
no processo de desenvolvimento.
No entanto esse cenário começou a mudar com o
surgimento de sistemas embarcados, sistemas pequenos
e que requerem o máximo possível de confiabilidade.
Esses sistemas se mostram como um caso ideal para o
uso de especificação e verificações formais, já que o
gasto de tempo será mínimo e o retorno oferecido
(segurança) é alto.
Nesse cenário aparece JML, uma linguagem de
especificação de sistemas escritos em Java que está
sendo amplamente utilizada para a verificação de
aplicações em JavaCard que oferece ao seu utilizador a
grande facilidade da sintaxe de Java e o suporte de um
grande conjunto de ferramentas.
Neste artigo apresenta-se um estudo de caso,
baseado na ferramenta SASHIMI de síntese de sistemas
embarcados descritos em Java. O estudo de caso tem
como finalidade verificar as reais funcionalidades de
JML, compreendendo as dificuldades que poderão ser
encontradas pelo projetista. Para a realização do estudo
de caso foram utilizados, além do JML, os
verificadores Krakatoa e ESC/Java.
1. INTRODUÇÃO
O conceito de especificações formais já é
conhecido há algum tempo, e é uma das melhores
maneiras de se desenvolver um sistema seguro e
funcional, mas apesar disso seu uso não é tão difundido
quando o desejado devido à complexidade que se supõe
inerente a eles. Sendo assim, as especificações formais
atualmente são usadas quase que somente em sistemas
críticos, onde a necessidade de confiabilidade é
extrema.
No entanto esse cenário está começando a se
alterar, já que recentemente foi desenvolvida uma
linguagem de especificação de programas Java
chamada JML (Java Modeling Language). O ponto
principal em que JML difere das outras linguagens de
especificações (como Z ou B, por exemplo) é o fato de
sua sintaxe ser baseada na linguagem Java, o que nos
garante uma curva de aprendizado menor, já que Java é
uma linguagem de domínio da maioria dos
programadores, o que pode se mostrar extremamente
benéfico para as empresas.
A ênfase que vem se dando a JML atualmente
é a de especificação de programas escritos em Java
para cartões (JavaCard), e para sistemas embarcados
desenvolvidos usando J2ME, dois nichos que se
encaixam perfeitamente na idéia de sistemas pequenos
e de confiabilidade.
Um outro ponto forte a se destacar em JML é a
grande quantidade de ferramentas já disponíveis que
suportam a linguagem. A verificação de programas
descritos em JML pode ser feita usando uma dentre
várias ferramentas como, por exemplo, ESC/Java,
Krakatoa, LOOP, Jack, etc.
No entanto cada uma dessas ferramentas tem
um escopo de utilização diferente, o ESC/Java, por
exemplo,
é
uma
ferramenta
completamente
automatizada, onde dada a especificação ela diz quais
os erros encontrados, no entanto, essa automação tem
um preço: ele é extremamente unsound e incomplete,
ou seja, ele pode acusar vários falsos erros bem como
pode não encontrar algumas falhas que realmente
existam.
Já o Krakatoa é uma ferramenta que pretende
ser mais completa, abrangendo uma gama maior de
verificações que o ESC/Java, mas para isso
necessitando que o usuário realize parte das provas da
verificação do programa, tornando necessário o
conhecimento do provador de teoremas Coq, além de
demandar um tempo maior para a verificação.
O objetivo desse trabalho é usar um estudo de
caso especificando-se parte da ferramenta de síntese
automática SASHIMI[6] para tentarmos ter uma visão
de onde uma ferramenta se sai melhor que a outra,
tanto na quantidade de erros encontrados como em
tempo gasto para fazer a verificação.
O resto desse trabalho se divide da seguinte forma:
na seção 2 será dada uma pequena introdução a JML
cobrindo alguns de seus aspectos básicos. No entanto é
recomendada a leitura de [7, 9, 10] para se obter uma
visão melhor da linguagem, bem como de algumas
especificações não detalhadas aqui. A seção 3
apresenta as ferramentas ESC/Java e Krakatoa,
mostrando
as
idéias
que
motivaram
seu
desenvolvimento. Na seção 4 será dada uma rápida
visão sobre a ferramenta SASHIMI bem como detalhes
da aplicação que iremos especificar. A seção 5 trata de
algumas questões sobre as especificações feitas no
nosso estudo, enquanto a 6 mostra o resultado da
verificação usando ambas as ferramentas. Finalmente
na seção 7 são apresentadas algumas conclusões a
partir dos resultados que obtivemos.
2. SINTAXE DE JML
Nessa sessão nós daremos uma pequena introdução
sobre o que é JML e como ela pode ser aplicada na
especificação de programas Java, a utilização das
ferramentas para a verificação será discutida mais
tarde.
JML (Java Modeling Language) é uma
linguagem de especificação de interfaces e
comportamentos
(BISL,
Behavioral
Interface
Specification Language) desenvolvida para a
especificação de módulos de programas Java (classes,
interfaces e métodos). Como linguagem JML herda
características de algumas outras, como um estilo de
sintaxe semelhante ao de Eiffel, e um a semântica
baseada em modelos de VDM e Larch.
Quando se diz que JML é uma BISL
queremos dizer que com ela podemos especificar tanto
a interface de um programa para o seu usuário como
também detalharmos o comportamento que vai ser
oferecido para esse cliente. A interface é definida
através de assinatura dos métodos em Java e o
comportamento utilizando-se JML.
Em especial, no que toca ao comportamento,
nos podemos usar JML para fazer Design By Contract
(DBC) que é uma técnica que nos permite fazer um
“contrato” entre o usuário de uma determinada classe
ou função e o seu desenvolvedor. Esse contrato diz que
se o usuário usar corretamente a função (passando
corretamente seus parâmetros) o desenvolvedor garante
que ele vai ter um resultado coerente. Ao passo que o
desenvolvedor se reserva ao direito de dar um resultado
coerente somente quando a função for usada
corretamente. Esse contrato é conseguido através das
pré- e pós-condições em JML, que serão apresentadas
no próximo tópico.
Uma diferença entre JML e as demais
linguagens de especificação (como Z ou B, por
exemplo) é que a especificação é escrita juntamente
com o código fonte do programa em questão, o que já é
uma vantagem, pois não é necessário ficarmos
trabalhando com vários arquivos, além do que, desse
modo nós sabemos exatamente a qual parte da
implementação uma especificação diz respeito.
Uma segunda preocupação durante o
desenvolvimento dessa linguagem foi que se
mantivesse a linguagem o mais fácil possível, e para
isso se optou por oferecer uma sintaxe que se
assemelhasse a da própria linguagem Java, com a qual
a maioria já está acostumado. O objetivo dessa
similaridade é diminuir a sua curva de aprendizado,
para, assim, ela se tornar de uso mais comum.
Linguagens como Z e B geralmente oferecem uma
notação mais matemática baseado em teoria de
conjuntos, enquanto outras, como CASL trabalham
com uma lógica algébrica.
As principais vantagens oferecidas por JML,
como são colocadas pelos desenvolvedores da
linguagem, são que por ela ser uma linguagem formal
pode ser usada para a automação da verificação de
código e que ela oferece uma grande facilidade para a
documentação. Nesse trabalho nos vamos nos dedicar
somente ao caso de verificação, mas é importante notar
também que, de fato, as especificações escritas em
JML são um grande avanço no sentido de uma
documentação precisa e formal das decisões de
implementação, quando comparadas com os esquemas
de comentários em linguagem natural.
No resto dessa seção nos vamos mostrar um
pouco mais de JML através de alguns exemplos, que
visam cobrir os aspectos básicos da linguagem, como
pré- e pós-condições, invariantes e constraints. Além
disso, como JML foi feita para se adequar às
necessidades de Java ela apresenta algumas
características que facilitam essa especificação, como
por exemplo herança e tratamento de exceções, que
também serão apresentados aqui.
2.1. Pré- Pós condições e afins
As pré- e pós-condições são as peças chave
para a especificação do comportamento de uma função,
são elas que determinam o que uma função espera para
funcionar corretamente bem como o que vai acontecer
no final de sua execução. As condições são expressões
boleanas, do tipo x >0 ou x == y, por exemplo, e é esse
valor verdade que tem de ser verdadeiro antes (para as
pré-condições) e depois (nas pós-condições) da
execução do corpo da função.
Por exemplo:
1 //@ requires x >= 0.0;
2 //@ ensures
JMLDouble.approximatelyEqualTo(x, \result *
\result, eps);
3 protected double internalSqrt(double x) {
4 return Math.sqrt(x);
}
Nesse exemplo nas linhas um e dois temos a
especificação em JML para a função internalSqrt, que
nada mais é do que uma função para encapsular o
método Math.sqrt().
Em primeiro lugar note que as especificações
estão dentro de comentários (//) e por isso são
ignoradas pelo compilador Java. As especificações em
JML são iniciadas sempre por //@ , para as
especificações em uma única linha, ou /*@ e @*/ ,
para múltiplas linhas.
A primeira linha diz que para um
funcionamento normal a função necessita que o x seja
maior ou igual a 0, já que não podemos ter uma raiz de
um número negativo.
Logo em seguida é colocada a pós-condição.
Ela garante que o resultado ao quadrado (\result *
\result) vai ser igual a aproximadamente o valor de x.
Além dessas duas temos também as clausulas
invariant e constraint. A clausula invariant funciona
como uma pré- e uma pós-condição incluída em todas
as funções (incluindo o construtor) enquanto que
constraint introduz uma relação entre os estados depois
e antes da execução dos métodos.
1 public abstract class ConstInvar {
2 int a;
3 //@ constraint a == \old(a);
4 boolean[] b;
5 //@ invariant b != null;
6 //@ constraint b.length == \old(b.length)
;
7 boolean[] c;
8 //@ invariant c != null;
9 //@ constraint c.length >= \old(c.length)
;
10 ConstInvar (int bLength, int cLength) {
11 b = new boolean[bLength];
12 c = new boolean[cLength];
13 }
14 }
Nesse segundo exemplo temos uma classe
abstrata criada para demonstrar o uso de invariant e
contraints. Na linha três é definida uma constraint que
determina que o valor de a após a execução deve ser
igual ao valor anterior (antes da execução), ou seja, ele
cria uma relação entre os valores novos e antigos da
variável a.Um outro uso de constraint poderia ser, por
exemplo, para definirmos que sempre após a execução
de todos os métodos o valor de b seria igual ao valor
antigo acrescido de um, o que nós daria algo do tipo:
int b;
//@constraint b == \old(b) + 1;
O funcionamento do invariante é bem parecido
com o de uma constraint, e é exemplificado na linha
cinco. Nesse caso dizemos que a variável b tem de ser
diferente de null. Essa clausula é assumida daí então
em todas as funções, tanto na pré como na póscondição. A única exceção a isso são os construtores
que por serem os métodos que vão ser chamados
inicialmente não precisam ter ele como pré-condição.
Mas são esses mesmos construtores que garantem que
todos os invariantes são válidos a partir da chamada
dos outros métodos.
É importante notar que tudo que é
demonstrado para o uso de constraint e invariant
também pode ser obtido colocando-se pré- e póscondições em todas as funções com as quais estamos
trabalhando, o que, a depender da quantidade de
funções, poderia se tornar extremamente cansativo e
propenso a erros.
2.2. Herança e Visibilidade
Como linguagem de programação orientada a
objetos Java permite o uso de heranças e usa o conceito
de visibilidade para fazer o encapsulamento em suas
classes. Pensando nisso os desenvolvedores de JML
incluíram essas características em sua linguagem.
A primeira noção que nos veremos é a de
visibilidade. Em JML, assim como em Java, cada
especificação pode ter uma visibilidade, que varia de
public a private, passando por protected e default. As
especificações devem seguir uma regra básica para a
utilização de visibilidade: uma especificação não pode
ter maior visibilidade que a sua implementação. Ou
seja, nós nunca poderíamos ter uma especificação
publica para um método privado, e nem uma
especificação protected para um método privado. O
que é bem claro, já que seria muito estranho termos
uma especificação de um método que não podemos
usar. Um resumo dessa noção pode ser visto na Tabela
1.
Tabela 1 - Relação entre visibilidade em Java e JML
JML Public
JAVA
Public
Sim
Protected
Não
Default
Não
Private
Não
Protected
Default
Private
Sim
Sim
Não
Não
Sim
Sim
Sim
Não
Sim
Sim
Sim
Sim
Para tornar essa noção mais clara temos um
exemplo mais prático. No exemplo que segue são
definidas quatro variáveis, cada uma com uma
visibilidade, e logo em seguida são feitas
especificações para cada uma delas usando-se os quatro
tipos de visibilidade, o que nos da um total de dezesseis
especificações. As especificações que estão corretas
são comentadas no final com o texto legal enquanto as
incorretas tem illegal no seu final.
1 public class PrivacyDemoLegalAndIllegal {
2 public int pub;
3 protected int prot;
4 int def;
5 private int priv;
6 //@ public invariant pub > 0; // legal
7 //@ public invariant prot > 0; // illegal!
8 //@ public invariant def > 0; // illegal!
9 //@ public invariant priv < 0; // illegal!
10 //@ protected invariant pub > 1; // legal
11 //@ protected invariant prot > 1; // legal
12 //@ protected invariant def > 1; // illegal!
13 //@ protected invariant priv < 1; //
illegal!
14 //@ invariant pub > 1; // legal
15 //@ invariant prot > 1; // legal
16 //@ invariant def > 1; // legal
17 //@ invariant priv < 1; // illegal!
18 //@ private invariant pub > 1; // legal
19 //@ private invariant prot > 1; // legal
20 //@ private invariant def > 1; // legal
21 //@ private invariant priv < 1; // legal
}
JML também suporta o conceito de herança
(as clausula extends e implements de Java). Se uma
classe B estiver estendendo uma outra classe A, então
todas as especificações para os métodos de A
continuam sendo válidas na classe B. No entanto é
possível redefinir métodos numa operação de herança.
No ponto de vista da especificação isso nos leva a dois
possíveis casos: 1) a especificação vai ser totalmente
reescrita; 2)a especificação anterior ainda é válida, e
nós pretendemos apenas estende-la.
No caso 1 basta refazermos a especificação do
método que está sendo reescrito normalmente. Já no
segundo caso para não termos de redigitar a
especificação anterior novamente JML nos fornece o
construtor also que indica que o novo método, além de
satisfazer as especificações da classe pai, também vai
satisfazer as que estão sendo escritas na nova classe,
por exemplo:
1 public class Object {
2
3 /*@ protected normal_behavior
4 @ requires Cloneable.class.isInstance(
this );
5 @ assignable objectState;
6 @ ensures \result != null && (* \result is
a clone of this *);
7 @*/
8 protected Object clone() throws
CloneNotSupportedException;
9}
10
11 public interface BoundedThing {
12 /*@ also
13 @ public behavior
14 @ assignable \nothing;
15 @ ensures \result instanceof
BoundedThing
16 @ && size ==
((BoundedThing)\result).size;
17 @ signals
(CloneNotSupportedException) true;
18 @*/
19 public Object clone () throws
CloneNotSupportedException;
20 }
Nesse exemplo nós temo s duas classes, a
classe Object e uma outra chamada BoundedThing. Na
classe Object nós temos o método clone e algumas
especificações feitas para ele. Já a classe
BoundedTHing é uma interface que, por padrão,
estende a classe Object, e redefine o método clone, com
algumas especificações especificas para essa classe.
Note na linha 12 a palavra also antes de começar a
especificação, com isso dizemos que além das
especificações colocadas nesse arquivo, aquelas que
foram definidas na classe Object também devem ser
respeitadas.
2.3. Exceções
O tratamento de exceções é outra
característica marcante da linguagem Java, e não podia
ser deixada de fora no desenvolvimento de JML. Para
fazer esse tratamento usa-se de duas soluções a
clausula
singals
ou
a
especificação
exceptional_behavior.
Usando a clausula signals nós definimos o que
vai acontecer quando um determinado sinal (exceção)
for detectado, colocando o que deve ser válido quando
o método finalizar com essa exceção. Por exemplo, no
caso da raiz quadrada mostrada no início dessa seção
nos poderíamos fazê-la de tal modo que quando o
parâmetro passado fosse menor que zero um exceção
do tipo IllegalArgumentException fosse disparada,
como mostra o código abaixo.
1 /*@ ensures x >= 0
2 @ &&
JMLDouble.approximatelyEqualTo(x, \result *
\result, eps);
3@
4 @ signals (IllegalArgumentException e)
5 @ e.getMessage() != null && !(x > 0.0);
6 @*/
7 public double sqrt(double x) {
8 if (x >= 0) {
9 return internalSqrt(x);
10 } else {
11 throw new IllegalArgumentException("x is
negative: " + x);
12 }
13 }
Nesse código nós podemos notar a clausula
signals na linha quatro, que define o comportamento
que o método deve ter quando for detectada uma
exceção do tipo IllegalArgumentException, e associa
um objeto e a ela. Na linha cinco estão as
características que devem ser mantidas quando esse
exceção ocorrer. No caso, o objeto e.getMessage() deve
ser diferente de null e o valor de x deve ser menor ou
igual a zero.
Uma outra possibilidade para se tratar
exceções é fazendo uma especificação do tipo
exceptional_behavior. Nas especificações feitas até
agora nós não especificamos qual tipo de
comportamento estávamos tratando (normal ou
excepcional). Essa diferença é introduzida utilizando os
construtores normal_behavior e exceptional_behavior.
Com isso nós podemos definir clausulas requires e
ensures para cada um desses dois comportamentos.
No exemplo abaixo essa diferenciação é feita
para o método pop de uma pilha, que pode disparar
uma exceção BoundedStackException, quando a pilha
está vazia. Nas linhas um a quatro são definidas as
características para o método executar normalmente,
com suas respectivas pré e pós-condições. O mesmo se
aplica para as linhas seis a nove, que contêm os
requisitos e garantias caso ocorra uma exceção.
1 /*@ public normal_behavior
2 @ requires !theStack.isEmpty();
3 @ assignable size, theStack;
4 @ ensures
theStack.equals(\old(theStack.trailer()));
5 @ also
6 @ public exceptional_behavior
7 @ requires theStack.isEmpty();
8 @ assignable \nothing;
9 @ signals (BoundedS tackException);
10 @*/
11 public void pop( ) throws
BoundedStackException;
3. FERRAMENTAS UTILIZADAS
Como JML é uma linguagem formal, nós
podemos automatizar algumas tarefas, usando essa
linguagem. Essa atualmente vem sendo sua principal
função.
As mais diversas ferramentas têm sido geradas
usando JML como base para as mais diversas
finalidades, como, por exemplo, geração de units de
testes compatíveis com a classe JUnit, geração de uma
documentação (javadoc) estendida que inclui as
especificações feitas, programas criados para se achar
invariantes, entre outros.
Dentre tantos programas os que mais nos
interessam no momento são aqueles criados para se
fazer a verificação de programas anotados em JML.
Para esse serviço existem muitas ferramentas já
disponíveis, embora a maioria ainda esteja nos seus
estágios iniciais. Nesse trabalho apresentaremos as
ferramentas ESC/Java e Krakatoa. A primeira foi
desenvolvida pela equipe do Compaq Research Center,
como um dos primeiros esforços nesse sentido, a
segunda foi desenvolvida no Laboratoire de Recherche
en Informatique, na França, e tem como base o uso de
duas ferramentas auxiliares para fazer a verificação, o
Why e Coq.
3.1ESC/Java
ESC/Java é o segundo extended static
checking (checagem estática estendida) da Compaq, o
primeiro tratava da linguagem Modula-3. Essa
checagem é dita estática, pois trabalha em cima do
código fonte do programa e não em tempo de execução
(alguns verificadores de JML trabalham com código
executável, como o jmlc) e dita estendida, pois tenta
verificar erros que não são comumente alcançados
pelos compiladores normais (deferências a variáveis
nulas, indexação de arrays fora dos limites, etc).
Uma característica peculiar do ESC/Java é que
ele não tenta ser nem sound nem complet, ou seja, ele
não tenta detectar todos os erros que existam num
programa nem garante que os avisos que ele vai dar
sejam todos verdadeiros. Essa característica foi feita
propositalmente pelos desenvolvedores, pois, do ponto
de vista deles, seria muito caro ter um verificador que
satisfizesse essas duas características, e acharam que a
quantidade de erros reais que fosse detectada já seria
suficiente para compensar pelo tempo e esforço gasto,
durante.o processo de especificação e verificação.
Outro fato a ser notado no ESC/Java é que ele
não usa a linguagem JML como seu modelo de
especificação, mas sim uma linguagem muito similar a
essa. Isso se da ao fato de que o ESC/Java foi criado
em conjunto com os desenvolvedores de JML ainda no
começo do desenvolvimento da linguagem e por isso
algumas de suas características não foram ser
incorporadas ao verificador. É interessante notarmos
que a última versão disponível do ESC/Java data de
outubro de 2001, enquanto as últimas documentações
sobre JML datam de setembro de 2003. Esperasse uma
nova versão do ESC/Java (ESC/Java2) totalmente
compatível com JML para um futuro próximo.
Essa diferença na sintaxe, no entanto, não
impede que usemos o ESC/Java em nossos
experimentos já que quase tudo que vimos até agora
pode ser aplicado a ele, com exceção das
especificações de visibilidade e das clausulas
normal_behavior e exceptional_behavior.
O ESC/Java pode fazer checagem em códigos
que não estejam comentados usando JML, mas desse
modo ele gera mais avisos falsos do que o comum. A
anotação dos programas serve para mostrar para o
ESC/Java as decisões tomadas pelo desenvolvedor e
ajudá-lo a melhor verificar o código.
Para fazer a verificação dos programas o
ESC/Java usa um verificador de teoremas chamado
Simplify, tornando assim totalmente automatizada a
checagem dos programas como veremos adiante. Para
uma informação mais completa sobre o funcionamento
do ESC/Java recomenda-se a releitura de [1, 2, 3].
3.2. Krakatoa
A idéia do Krakatoa é ligeiramente diferente
daquela apresentada pelo ESC/Java, ele verifica o
arquivo fonte do programa e gera alguns arquivos que
mais tarde vão ser passados para duas ferramentas
auxiliares (Why e Coq) para que a partir daí se posa
efetivar a verificação.
O Why é uma ferramenta para a geração de
condições de verificação, que gera saídas para diversos
assistentes de provas (Coq, PVS, entre outros) e para
provadores automáticos (haRVey), dada uma entrada
em ML, C ou Java. Com o uso do Krakatoa o Why
pode gerar as obrigações de prova em Coq da
semântica de Java. Essa semântica é apresentada ao
Why na forma de programas em ML que gerados a
partir do Krakatoa.
O uso do Krakatoa se torna bem diferente do
ESC/Java no ponto que toca à automação. Como
mencionado anteriormente o ESC/Java utiliza um
provador de teoremas chamado Simplify para realizar
suas provas, enquanto o Krakatoa trabalha com um
assistente de provas chamado Coq, que requer, na
maioria dos casos, a intervenção do usuário para
concluir as provas.
O Coq trabalha com formulas lógicas e utiliza
o método de dedução natural e um conjunto de táticas
para provar essas fórmulas. Essas táticas são na
verdade teorias que são aplicadas às fórmulas por
etapas para tentar resolvê-las.
A maior parte do contato que o usuário tem
quando fazendo verificações com o Krakatoa é com o
próprio Coq, no entanto devido a grande quantidade de
táticas apresentadas por ele e a necessidade de
conhecimento de lógica e, em especial, do método de
dedução natural, sua utilização se torna um tanto
complexa. Por isso mesmo nós não vamos nos deter a
explicações sobre ele, pois isso fugiria do escopo desse
trabalho. Recomenda-se a leitura de [12, 13] para se
familiarizar com esse provador caso realmente se
deseje entender o funcionamento das provas
demonstradas aqui.
4. SASHIMI
O SASHIMI é uma ferramenta que se destina
à síntese de sistemas microcontrolados descritos em
linguagem Java. O alvo dos sistemas sintetizados pelo
SASHIMI é o microcontrolador FemtoJava, que é
gerado de forma particular para cada aplicação que é
desenvolvida. O FemtoJava é um microcontrolador
capaz de executar programas Java e foi desenvolvido
pela mesma equipe do SASHIMI.
O SASHIMI foi escolhido como alvo para o
nosso teste, pois ele representa um dos nichos para o
qual JML foi criada, os sistemas embarcados. A partir
do programa feito em Java o SASHIMI verifica quais
as chamadas da API serão necessárias à execução do
programa e criam um controlador FemtoJava especial
que cobre somente essas funções. Depois disso gera a
parte que vai ser a ROM do microcontrolador e que é o
programa Java que foi escrito.
Daí pode se perceber a importância que é a
certeza de que todos os métodos que serão usados nesse
microcontrolador sejam validados e seu perfeito
funcionamento assegurado.
Para a criação da classe que vai ser passado
pelo SASHIMI nós necessitamos implementar as
interfaces IOInterface, TimerInterface e IntrInterface,
que especificam a estrutura do modelo de simulação
quanto à utilização do sistema de E/S, temporização e
interrupção, respectivamente A interface IOInterface
especifica a estrutura do modelo se simulação através
dos métodos int read(int) e void write(int,int)). A
interface TimerInterface através dos métodos void
tf0Method() e void tf1Method() e a interface
IntrInterface através dos métodos void int0Method(),
void int1Method e void spiMethod(). Além disso, é
necessário um método initSystem(), que é de onde o
código utilizado para a síntese será extraído. Este é o
método de onde o sistema irá iniciar a
simulação/execução.
A aplicação que nós implementamos nesse
trabalho é uma que consta em [6] e corresponde a um
fatorial. No entanto algumas alterações foram feitas em
relação ao exemplo original.
Na nova versão da aplicação nos temos uma
única classe responsável tanto pela definição do
método de calculo do fatorial (calculate) como da
implementação das demais interfaces necessárias ao
SASHIMI.
Na classe nos contamos com dois arrays de
inteiros do mesmo tamanho, inputValues e
outputValues. No início da execução os valores do
array inputValues correspondem ao seu índice mais um
(inputValues[0] == 1, por exemplo) e todos os valores
de outputValues são igual a um. Ao final da execução
do método initSystem os valores de outputValeus serão
iguais ao fatorial dos valores de inputValues
(outputValues[i] == inputValues[i]! ).
A obtenção dos valores do array inputValues
é feita através do método read e a escrita em
outputValues através do método write, e o controle das
posições nos arrays é dado pelas variáveis idx_r e
idx_w, respectivamente.
5. AS ESPECIFICAÇÕES
As especificações detalhadas aqui tentam ser o
mais completas possível, incluindo sempre as pré- e
pós-condições, com exceção das interfaces de
interrupção, que não possuem nenhum código de
implementação.
Como a sintaxe dos dois verificadores é
ligeiramente diferente foi necessário gerar duas
especificações distintas para cada caso. No entanto a
diferença da capacidade de verificação das duas
ferramentas se mostrou maior do que o esperado, nos
forçando a fazer algumas mudanças mais profundas.
Por exemplo, o Krakatoa não suporta o uso da
clausula also e por isso todas as especificações tiveram
de ser reescritas na própria classe Factorial, o que não
ocorre com o ESC/Java, que permitiu que
modularizassemos a especificação.
O método calculate foi fruto de mais duas
diferenças. Em primeiro lugar o ESC/Java não suporta
a definição de métodos puros, o que nos forçou a uma
mudança na definição da pós-condição do método
calculate entre uma implementação e outra. O outro
caso foi que só conseguimos realizar a prova desse
método no Krakatoa através de uma solução recursiva,
solução essa que não pode ser verificada com o
ESC/Java, e por isso tivemos de voltar à solução
interativa.
Inicialmente a classe Factorial não possuía
nenhum construtor, usando assim o construtor padrão
oferecido pelo Java. No entanto o ESC/Java detectou
que esse construtor não respeitava os invariantes com
que estávamos trabalhando. Esse fato não ocorreu com
o Krakatoa, já que ele verifica somente os métodos que
passamos para ele, e não os implícitos.
As demais funções que tratavam das
interrupções, por não apresentarem corpo nenhum,
foram especificadas de forma padrão usando-se
//@ requires true
//@ ensures true
para afirmar que elas são sempre verdadeiras.
Restasse salientar que o processo de
especificação foi incremental, à medida que íamos
fazendo as especificações verificávamos os resultados
e, quando necessário, alterávamos as especificações ou
a implementação para nos dar mais recursos para
podermos trabalhar no momento da prova.
6. VERIFICAÇÕES
Como dito anteriormente o processo de
especificação foi incremental, e em diversos pontos
mudanças tiveram de ser feitas para que pudéssemos
ter um sistema que funcionasse conforme o que era
esperado e ainda assim fosse passível de ser verificado.
Em grande parte os esforços foram
satisfatórios, e conseguimos verificar as especificações
propostas, enquanto outros ficaram falhos.
Como era esperado, as provas utilizando o
Krakatoa consumiram mais tempo do que as feitas com
o ESC/Java e ainda assim algumas não puderam ser
totalmente verificadas.
Quanto a verificação de cada um dos métodos
especificados na classe Factorial temos o seguinte:
O método calculate foi o único que teve de
ser feito completamente diferente entre uma
especificação e outra, mas ele pode ser provado tanto
com o Krakatoa quanto com ESC/Java, considerandose as alterações já mencionadas anteriormente. No caso
do Krakatoa achamos que seria possível realizar a
prova da implementação interativa do mesmo
utilizando-se de invariantes de loop, mas, infelizmente,
nossas tentativas nesse sentido decorreram sem
sucesso. Já para a versão recursiva do mesmo não
cremos que o ESC/Java possa ser suficiente para
realizar essa prova.
Um outro ponto a ser notado durante nossa
experiência com a especificação e prova do método
calculate foi o fato de que a sua implementação, tal
qual segue abaixo, se mostrou extremante complicada
para ser provada utilizando-se o Krakatoa
public /*@ pure @*/ int calculate(int n ){
if (n == 0 || n == 1) return 1;
else return n * calculate(n 1);
}
No entanto se somente tirarmos o ou lógico e
incluirmos mais uma estrutura do tipo if ... else, a prova
se da diretamente.
public /*@ pure @*/ int calculate(int n ){
if (n == 0) return 1;
else if (n == 1) return 1;
else return n *
calculate(n - 1);
}
Apesar da diferença ser mínima no ponto de
vista do programador já foi suficiente para
impossibilitar uma das provas. Mas apesar disso não
podemos concluir se existe algum estilo de codificação
que facilite mais ou menos as provas de um dado
problema em alguma ferramenta específica.
O método initSystem inicialmente havia sido
especificado de forma mais robusta, com a utilização
do método calculate em sua especificação, mas, como
já dito antes, o ESC/Java não suporta tal tipo de
construção e por isso optamos por deixar as duas
versões iguais. O ESC/Java provou o método
perfeitamente, enquanto o Krakatoa gerou algumas
provas que não conseguimos verificar. Como esse
método foi provado correto pelo ESC/Java supomos
que as teorias geradas pelo Krakatoa estão corretas,
faltando na realidade, alguma “habilidade” para que
possamos prová-las.
O método read pode ser provado trivialmente
pelos dois verificadores e não demonstrou nenhum
problema durante nossos testes.
O método write, no entanto, foi o único
método para o qual ambos os provadores falharam. Em
ambos os casos nos deparamos com uma situação em
que os provadores, aparentemente, não podem garantir
se existe ou não um alias entre as variáveis inputValues
e outputValues, e por isso não conseguem provar que a
atribuição
outputValues[idx_w] = valor;
conseguimos com ele provar tudo que nós foi possível
provar com o Krakatoa, além de algo mais, com um
esforço muito menor. No entanto é bom notarmos que
esse foi apenas um estudo de caso, e os resultados aqui
obtidos podem não se mostrar os mesmos em outros
experimentos.
Um outro fato que devemos notar é que nesse
trabalho foram utilizadas apenas duas ferramentas,
quando na verdade já existem muitas outras disponíveis
e que talvez se mostrem melhor que qualquer uma das
duas aqui apresentadas.
Outro ponto importante é que o Krakatoa
ainda está em desenvolvimento e novas versões já estão
sendo trabalhadas, inclusive uma que pretende gerar
saída para o provador de teoremas Simplify (o mesmo
utilizado pelo ESC/Java), o que deve facilitar em muito
a prova das especificações. Nós inclusive temos planos
de num trabalho em conjunto com os desenvolvedores
do Krakatoa, num futuro não muito longe, para incluir
a geração de saídas compatíveis com haRVey no
Krakatoa.
Além desses avanços ainda temos o
ESC/Java2 que também deverá estar disponível em
algum tempo, e com certeza trará grandes avanços em
relação à versão com a qual estamos trabalhando.
mantém o invariante
8. BIBLIOGRAFIA
(\forall int k; 0 <= k && k <
inputValues.length; inputValues[k] == k + 1);
pois, caso haja um alias o valor de
inputValues[idx_w] será alterado para valor, o que
realmente violaria o invariante.
Essa não é uma questão totalmente estranha,
já que o caso de aliases é coberto por [4]. No entanto
tentamos aplicar as técnicas por ele apresentadas e não
obtivemos sucesso nessa prova.
Os demais métodos, por não possuírem corpo,
são provados trivialmente, e não foram considerados
nesse trabalho.
7. CONCLUSÕES
O objetivo desse trabalho era apresentar a
linguagem JML e como ela pode ser usada para a
especificação de sistemas embarcados, e analisar as
ferramentas de verificação Krakatoa e ESC/Java e
tentar verificar em quais pontos uma se mostraria
melhor que a outra, e optamos por fazer uso do
SASHIMI para esses testes pois esse é um ambiente
para geração de sistemas embarcados especiais que
utilizam o microcontrolador Femtojava.
Através do esforço depreendido durante a
realização desse trabalho nós podemos constatar que,
pelo menos para as situações com as quais nos
deparamos, o ESC/Java, apesar da sua falta de
atualizações, se mostra uma melhor opção já que
[1] Extended Static Checking for Java, Cormac
Flanagan, K. Rustan M. Leino, Mark Lillibridge,Greg
Nelson, James B. Saxe, Raymie Stata
[2] ESC/Java Quick Reference, Silvija Seres
[3] ESC/Java User's Manual, K. Rustan M. Leino, Greg
Nelson, and James B. Saxe
[4] The Krakatoa Tool Version 0.55, Claude Marché,
Christine Paulin, Xavier Urbain
[5] The Krakatoa Tool for Certification of
Java/JavaCard Programs annotated in JML, Claude
Marché, Christine Paulin, Xavier Urbain
[6] SASHIMI v0.8b Manual do Usuário
[7] JML: A Notation for Detailed Design, Gary T.
Leavens, Albert L. Baker, and Clyde Ruby
[8] Design by Contract with JML, Gary T. Leavens and
Yoonsik Cheon
[9] JML Reference Manual
[10] Preliminary Design of JML: A Behavioral
Interface Specification Language for Java, Gary T.
Leavens, Albert L. Baker, and Clyde Ruby
[11] An overview of JML tools and applications, Lilian
Burdy, Yoonsik Cheon, David Cok, Michael Ernst, Joe
Kiniry, Gary T. Leavens, K. Rustan M. Leino, Erik Poll
[12] The Coq Proof Assistant - Reference Manual, The
Coq Development Team
[13] The Coq Proof Assistant - A Tutorial, Gérard
Huet, Gilles Kahn and Christine Paulin-Mohring
[14] Why a multi-language multi-prover verification
tool, Jean-Christophe Filliâtre
Download