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