Interpretazione astratta in pratica Un analizzatore generico per Java

Riassunto puntate precedenti Java Bytecode Semantica Concreta Semantica a tracce Punto fisso
Interpretazione astratta in pratica
Un analizzatore generico per Java
Pietro Ferrara
Università Ca’ Foscari di Venezia
I-30170 Venezia (Italy)
[email protected]
Ecole Polytechnique, Paris
F-91128 Palaiseau (France)
[email protected]
April 21, 2008
Pietro Ferrara
Interpretazione astratta in praticaUn analizzatore generico per Ja
Abbiamo visto
Concetti basilari di matematica
C senza puntatori
C con puntatori
Java
Cos’è
Java viene compilato in un linguaggio intermedio: il bytecode
Perchè?
Compilo una volta, eseguo su diverse piattaforme, sistemi
operativi, ...
codice Java
javac
bytecode Java
virtualmachine
esecuzione
La Java Virtual Machine è specifica per la piattaforma e il sistema
operativo!
Idee di base
Linguaggio intermedio
Più semplice di Java
Un po’ più ad alto livello dell’assembly
Maggiori differenze
Valori passati attraverso uno stack apposito
Praticamente spariscono le variabili
Codice non strutturato!
Un esempio
public void prova(int i) {
if(i >= 0)
System.out.println(”Ok”);
else System.out.println(”No”);
}
0 iload 1
1 iflt 15 (+14)
4 getstatic #2 < java/lang/System.out >
7 ldc #3 < Ok >
9 invokevirtual #4 < java/io/PrintStream.println >
12 goto 23 (+11)
15 getstatic #2 < java/lang/System.out >
18 ldc #5 < No >
20 invokevirtual #4 < java/io/PrintStream.println >
23 return
0 iload 1
1 iflt 15 (+14)
4 getstatic #2 < java/lang/System.out >
7 ldc #3 < Ok >
9 invokevirtual #4 < java/io/PrintStream.println >
12 goto 23 (+11)
15 getstatic #2 < java/lang/System.out >
18 ldc #5 < No >
20 invokevirtual #4 < java/io/PrintStream.println >
23 return
entry point
iload 1 Q
QQQ
QQQ>=0
QQQ
<0
QQQ
QQ(
15 getstatic #2
18 ldc #5 < No >
20 invokevirtual #4
7 ldc #3 < Ok >
9 invokevirtual #4
lll
lll
l
l
ll
lll
l
v ll
23 return
Overview dei comandi
Lettura e scritture sulle variabili locali: iload, aload,
iload 1, istore, ...
Operazioni aritmetiche: iadd, dmul, ...
Costanti: iconst 0, ldc, ...
Eccezioni: throw
Oggetti: new, putfield, getfield
Istruzioni condizionali: goto, ifnull, ifeq, ifge, if icmplt,
...
Etc.!
Stato di esecuzione
Due componenti principali
Lo stack delle operazioni
L’array di valori locali
Un valore può essere
Un numero
intero long
intero short
byte
...
non esiste il tipo booleano!
Un indirizzo
Definition (Val)
Val = Ref ∪ N
Stack
Lo stack è usato per passare valori
Ad esempio: add
Prende i due valori più in alto nello stack
Li somma
Mette sullo stack il risultato
Definition (Op)
Op = ST(Val)
Array di valori locali
Utilizzato per immagazzinare e leggere valori locali
Intuitivamente corrisponde alle variabili locali di un metodo
Ad esempio: iload 1
Legge un valore intero dall’elemento n.1 dell’array
Mette sullo stack tale valore
Definition (LV)
LV = AR(Val)
Heap
In ciascun indirizzo un elemento di array di valori locali
array di valori locali ≈ ambiente!
Oggetto
Immagazzinato nello heap
Ogni volta che si accede un campo:
Si passa tramite stack l’indirizzo da accedere
putfield e getfield hanno un indice per sapere quale
elemento dell’array accedere
Definition (H)
H = [Ref → LV]
Frame
Lo heap è unico
Stack e array sono locali al metodo
Ad ogni invocazione di metodo viene creato un frame, ovvero
una coppia composta da uno stack e un array
Definition (F)
F = Op × LV
Stato concreto
Stack di frame
Heap
Definition (Σ)
Σ = ST(F) × H
Un esempio
public class Classe {
int n = 0;
public void prova(int i) {
Classe v = new Classe();
if(i >= 0)
v.n = i + 1;
else v.n = 0;
}
}
Un esempio
0 new #3 < Classe >
3 dup
4 invokespecial #4 < Classe. < init >>
7 astore 2
8 iload 1
9 iflt 22 (+13)
12 aload 2
13 iload 1
14 iconst 1
15 iadd
16 putfield #2 < Classe.n >
19 goto 27 (+8)
22 aload 2
23 iconst 0
24 putfield #2 < Classe.n >
27 return
0 aload 0
1 invokespecial #1 < java/lang/Object. < init >>
4 aload 0
5 iconst 0
6 putfield #2 < Classe.n >
9 return
Supponiamo che l’indirizzo di this sia #0, che il suo campo n sia
uguale a 5, e che il valore passato al metodo sia 1
Stack di valori:
Valori locali:
0
1 2
#0 1
Heap:
Add. Index
#0
2
Value
5
0 new #3 < Classe >
Stack di valori:
#1
Valori locali:
0
1 2
#0 1
Heap:
Add. Index
#0
2
#1
Value
5
3 dup
Stack di valori:
#1
#1
Valori locali:
0
1 2
#0 1
Heap:
Add. Index
#0
2
#1
Value
5
4 invokespecial #4 < Classe. < init >>
0 aload 0
1 invokespecial #1 < java/lang/Object. < init >>
4 aload 0
5 iconst 0
6 putfield #2 < Classe.n >
9 return
Stack di valori:
#1
#1
Valori locali:
0
1 2
#0 1
Heap:
Add. Index
#0
2
#1
2
Value
5
0
7 astore 2
8 iload 1
Stack di valori:
#1 1
Valori locali:
0
1 2
#0 1 #1
Heap:
Add. Index
#0
2
#1
2
Value
5
0
9 iflt 22 (+13)
Stack di valori:
1
Valori locali:
0
1 2
#0 1 #1
Heap:
Add. Index
#0
2
#1
2
Value
5
0
12 aload 2
Stack di valori:
#1
Valori locali:
0
1 2
#0 1 #1
Heap:
Add. Index
#0
2
#1
2
Value
5
0
13 iload 1
Stack di valori:
1
#1
Valori locali:
0
1 2
#0 1 #1
Heap:
Add. Index
#0
2
#1
2
Value
5
0
14 iconst 1
Stack di valori:
1
1
#1
Valori locali:
0
1 2
#0 1 #1
Heap:
Add. Index
#0
2
#1
2
Value
5
0
15 iadd
Stack di valori:
1
21
#1
Valori locali:
0
1 2
#0 1 #1
Heap:
Add. Index
#0
2
#1
2
Value
5
0
16 putfield #2 < Classe.n >
Stack di valori:
2
#1
Valori locali:
0
1 2
#0 1 #1
Heap:
Add. Index
#0
2
#1
2
Value
5
2
19 goto 27 (+8)
Stack di valori:
Valori locali:
0
1 2
#0 1 #1
Heap:
Add. Index
#0
2
#1
2
Value
5
2
27 return
Stack di valori:
Valori locali:
0
1 2
#0 1 #1
Heap:
Add. Index
#0
2
#1
2
Value
5
2
Esecuzione
Fin qui abbiamo visto il dominio concreto
Ma come viene utilizzato questo per avere informazione
sull’esecuzione?
Funzione di trasferimento
Per ogni istruzione del bytecode definiamo i suoi effetti su uno
stato del dominio
Funzione di trasferimento
(s0 , v) = popOS (s), (s00 , r) = popOS (s0 ),
h0 = h[r 7→ h(r)[i 7→ v]]
C~putfield #i(hs, h) → hs00 , h0 ii
(s0 , r) = popOS (s), (v, t, pc, c, m0 ) = h(r)(i ),
s00 = pushOS (s0 , v)
C~getfield #i(hs, hi) → hs00 , hi
(s0 , v1 ) = popOS (s), (s00 , v2 ) = popOS (s), s000 = pushOS (s00 , v1 + v2 )
C~iadd(hs, hi) → hs00 , hi
Semantica a tracce
Ma come gestiamo goto e if...?
La domanda è un’altra:
Come rappresentiamo una singola esecuzione?
Come una sequenza ordinata di stati!
Se ad esempio l’esecuzione inizia con uno stato s0 , prosegue
con s1 , e poi con s2 , lo rappresentiamo come s0 → s1 → s2
Le istruzioni di salto (condizionale o no) vengono trattate a
questo livello
Nello stato infatti c’è solo la memoria e non lo stato del
controllo
Un esempio
0 new #3 < Classe >
⇓
3 dup
⇓
4 invokespecial #4 < Classe. < init >>
⇓
7 astore 2
⇓
8 iload 1
⇓
9 iflt 22 (+13)
⇓
12 aload 2
⇓
13 iload 1
⇓
14 iconst 1
⇓
15 iadd
⇓
16 putfield #2 < Classe.n >
⇓
19 goto 27 (+8)
⇓
27 return
Esecuzioni parziali
Abbiamo già definito
Dominio
Funzione di trasferimento
Manca
Semantica del programma!
Costruiamo istruzione per istruzione la traccia di esecuzione
Attraverso la funzione di trasferimento
In forma di punto fisso
Esecuzioni parziali!
Un esempio
All’inizio si parte da una traccia vuota Prima iterazione
{, 0 new #3 < Classe >}
Seconda iterazione
{, 0 new #3 < Classe >, 0 new #3 < Classe > → 3 dup}
Terza iterazione
{, 0 new #3 < Classe >, 0 new #3 < Classe > →
3 dup, 0 new #3 < Classe > → 3 dup →
4 invokespecial #4 < Classe. < init >>}
... Finchè non si arriva a un punto fisso!
Dominio
Dominio composto da un insieme di tracce di esecuzioni
parziali
Diversi input
Numeri casuali
Interleaving casuali di diversi threads
...
Costruiamo incrementalmente la tracce di esecuzione
Finchè abbiamo costruito tutte le possibili esecuzioni parziali...
... Punto fisso!
Solo esecuzioni finite
⊆ operatore di ordinamento, ∪ operatore di upper bound, ...