Check alle échte Black Friday-deals Ook zo moe van nepaanbiedingen? Wij laten alleen échte deals zien
Toon posts:

Decompilatie van Java bytecode: loop-type bepalen

Pagina: 1
Acties:

Verwijderd

Topicstarter
Momenteel ben ik bezig met een tool die java bytecode analyseert. Tijdens deze analyze is het zeer wenselijk om het type van een loop te kunnen bepalen aan de hand van de bytecode (do of while). De vraag is echter of dit altijd mogelijk is. Om dit onderscheid te kunnen maken moet je in de bytecode in feite zoeken naar een de loop-condition (als aanwezig) en de loop-body. In de meeste gevallen is een loop-condition wel te vinden ahv control flow analyse (als de loop-constructie slechts een code block bevat met een exit-edge (een tak die de loop verlaat), dan is dat het blok met de loop-condition). Als er meerdere code-blokken met een dergelijke exit-edge bestaan dan kan er blijkbaar ook vanuit de body uit de loop gesprongen worden. Exceptions daargelaten kan dat alleen met een return of een break statement. Een return is gemakkelijk op te sporen, maar breaks zijn een lastig verhaal. In deze situatie is het vaak nog mogelijk een onderscheid tussen loop-condition en loop-body te maken door te kijken naar het type van de laatste bytecode-instructie in elk blok, aangezien een loop-condition altijd met een IF-instructie eindigt, en een loop-body doorgaans niet; er zijn echter uitzonderingen, zie de code hieronder:

Gegeven bijvoorbeeld de volgende methode*:

*Let ff niet op de flauwe betekenis van deze methode, ik heb hem puur geschreven als voorbeeld

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void earlyBreakWhile() {
    System.out.print("....");
    int i = 0;

    while (i <= 5) {
        System.out.print(i);
    
        if (i++ >= 10)
            break;
    }

    System.out.println();
}


Javac compileert dit naar de volgende bytecode:

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void earlyBreakWhile();
  Code:
   0:   getstatic   #7; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:   ldc #32; //String ...: 
   5:   invokevirtual   #29; //Method java/io/PrintStream.print:(Ljava/lang/String;)V
   8:   iconst_0
   9:   istore_0

   10:  iload_0
   11:  iconst_5
   12:  if_icmpgt   34

   15:  getstatic   #7; //Field java/lang/System.out:Ljava/io/PrintStream;
   18:  iload_0
   19:  invokevirtual   #30; //Method java/io/PrintStream.print:(I)V

   22:  iload_0
   23:  iinc    0, 1
   26:  bipush  10
   28:  if_icmplt   10

   31:  goto    34

   34:  getstatic   #7; //Field java/lang/System.out:Ljava/io/PrintStream;
   37:  invokevirtual   #31; //Method java/io/PrintStream.println:()V
   40:  return


In de bytecode is de while-loop terug te vinden in de instructies 10 t/m 28. Aan de hand van de source code is makkelijk te achterhalen dat de loop-condition bestaat uit de instructies 10 t/m 12 en de loop-body beslaat dan 15 t/m 28, eindigend op een IF-statement. Het enige teken dat de ontsnapping aan de loop in instructie 28 door een break wordt veroorzaakt is de goto-instructie van regel 31. Het is echter niet ondenkbaar dat deze instructie door een andere compiler in een simpele optimalisatie-stap verwijderd wordt, aangezien deze goto-instructie naar de daaropvolgende instructie verwijst. Als we deze goto-instructie even wegdenken dan kunnen we de bytecode net zo goed als een do-loop decompileren:

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void earlyBreakDo() {
    System.out.print("...");
    int i = 0;

    do {
         if (i > 5)
            break;
        
        System.out.print(i);
    } while (i++ < 10);

    System.out.println();
}


Heb ik zojuist ontdekt dat het bepalen van het loop-type vanuit java bytecode, doch niet geheel willekeurig, een niet algemeen oplosbaar probleem is, of zie ik details over het hoofd?

  • Soultaker
  • Registratie: September 2000
  • Nu online
Het is een heel verhaal, maar het klopt helemaal: Java broncode en JVM bytecode staan los van elkaar, dus je kunt niet zomaar van de ene naar de andere transformeren. De transformatie van Java naar bytecode is al niet precies bepaald, dus het zou je niet moeten verbazen dat de terugtransformatie ook niet vaststaat. (En je kunt ook niet alle denkbare bytecode converteren naar Java broncode!)

Als je een decompiler voor gebruik met een specifieke compiler wil schrijven dan kun je al wel heel veel verder komen; hoe ver precies hangt af van wat voor code die compiler genereert. Je geeft zelf terecht aan dat optimalisaties op bytecode nivo roet in het eten gooien, maar gelukkig wordt op meestal niet geoptimaliseerd (hoewel de compiler wel dingen als constant expression folding moet doen, geloof ik).

Kort samengevat zul je dus moeten accepteren dat je niet altijd precies de oorspronkelijke broncode kunt reconstrueren, hoewel je meestal wel equivalente broncode kunt genereren.

Verwijderd

Topicstarter
Ik ben het geheel met je eens dat je over het algemeen van bytecode die wordt geschreven door een compiler en al dan niet geoptimaliseerd, verschillende equivalente source code kunt genereren. Ik heb echter wat papers gelezen (Decompiling Java Using Staged Encapsulation, J. Miecznikowski) die min of meer beweren een solide methode te hebben om in ieder geval onderscheid te kunnen maken tussen do- en while-loops. Ik denk echter dat, gezien mijn voorbeeld hierboven, zo'n sluitend algoritme niet bestaat.

Alhoewel ik blij ben dat je mijn verhaal bevestigt, is het wel vervelend dat dit niet mogelijk is. Mijn doel is namelijk om op basis van code-instrumentatie te bepalen 'hoe vaak' een loop wordt doorlopen. Ik zou dit kunnen definieren als in 'hoe vaak de loop-conditie achtereens true wordt ge-evalueerd'. Echter, als ik geen sluitende manier heb om te bepalen waar de loop-conditie staat wordt dit vrij lastig (lees: onmogelijk). Het ziet er naar uit dat ik hier omheen moet gaan werken...

  • Voutloos
  • Registratie: Januari 2002
  • Niet online
Verwijderd schreef op dinsdag 21 augustus 2007 @ 16:38:
Mijn doel is namelijk om op basis van code-instrumentatie te bepalen 'hoe vaak' een loop wordt doorlopen. Ik zou dit kunnen definieren als in 'hoe vaak de loop-conditie achtereens true wordt ge-evalueerd'.
Bij zowel i > 5 als i++ >= 10 wordt de loop zonder verdere side-effects beëindigd. Kan je dan niet gewoon beiden als loop conditie zien en in dit geval het minimum pakken?
Als je die getallen 5 en 10 in de code van plek verwisseld, wil je dan 10 als antwoord terwijl er stiekem altijd al halverwege de iteraties een break plaatsvindt?

{signature}


Verwijderd

Topicstarter
Bij zowel i > 5 als i++ >= 10 wordt de loop zonder verdere side-effects beëindigd. Kan je dan niet gewoon beiden als loop conditie zien en in dit geval het minimum pakken?
Ik begrijp je punt, maar dat is wat ik eigenlijk juist niet wil doen. Ik wil me echt richten op het onderscheid tussen loop-conditie en eventuele break-condities omdat ik voor test-doeleinden precies wil kunnen meten of een loop-conditie bijvoorbeeld 0, 1, 2 tot 10 of meer dan 10 keer wordt uitgevoerd. Zodoende kan ik als een soort path-coverage tool aangeven dat de tester er goed aan doet om nog wat testcases te schrijven die de betreffende loop een ander aantal keer uitvoeren dan de huidige set testcases, en dit aantal keer mag alleen betrekking hebben op de 'echte' loop-conditie.

  • Soultaker
  • Registratie: September 2000
  • Nu online
Verwijderd schreef op dinsdag 21 augustus 2007 @ 16:38:
Ik heb echter wat papers gelezen (Decompiling Java Using Staged Encapsulation, J. Miecznikowski) die min of meer beweren een solide methode te hebben om in ieder geval onderscheid te kunnen maken tussen do- en while-loops. Ik denk echter dat, gezien mijn voorbeeld hierboven, zo'n sluitend algoritme niet bestaat.
Ik heb de paper die je noemt niet in detail gelezen, maar ik zie niet direct claims dat ze do- en while-loops altijd kunnen onderscheiden (en dat kan volgens mij ook niet).
Echter, als ik geen sluitende manier heb om te bepalen waar de loop-conditie staat wordt dit vrij lastig (lees: onmogelijk). Het ziet er naar uit dat ik hier omheen moet gaan werken...
Dat zal sowieso moeten, denk ik. Misschien kun je backward jumps tellen als loop-iteraties? Dat werkt waarschijnlijk ook voor continue statements. (In theorie kunnen backward jumps ook in andere situaties voorkomen, maar ik weet niet of een Java compiler die in de praktijk ook genereert. In IA-32 assembly is het wel enigzins gebruikelijk, maar Java bytecode wordt een stuk minder geoptimaliseerd.)

Ook kun je (dacht ik) de compiler annotaties laten genereren, waardoor je kunt zien bij welke regel van de broncode gegenereerde bytecode hoort. Misschien kun je daar ook iets mee? (Lijkt me dat je sowieso verschillende/geneste loops moet onderscheiden.)

Verwijderd

Topicstarter
Ik heb de paper die je noemt niet in detail gelezen, maar ik zie niet direct claims dat ze do- en while-loops altijd kunnen onderscheiden (en dat kan volgens mij ook niet).
Ik ben bang dat je gelijk hebt. Wat ze feitelijk zeggen is dat bepaalde heuristieken gebruiken om tot een bepaald loop-type te komen, waarbij ze uiteraard proberen in ieder geval source code te genereren die een loop bevat die equivalent is aan het origineel. Wat hun algoritme ook precies is, voor hun zou het decompileren van bovenstaande bytecode zowel een while- als een do-loop mogen opleveren, aangezien ze in dit geval equivalent zijn. Voor mij geldt dit dus niet.
Ook kun je (dacht ik) de compiler annotaties laten genereren, waardoor je kunt zien bij welke regel van de broncode gegenereerde bytecode hoort. Misschien kun je daar ook iets mee?
Alhoewel ik een hekel heb om afhankelijk te zijn van zulke meta-informatie denk ik dat je hier best een goed punt heb. In source code is het natuurlijk zo dat de loop-condition dikwijls een regel boven(for/while) of een regel onder de body (do) staat. Echter, alhoewel je er echt een beroerde coding-stijl op nahoudt als dit niet het geval is, hoeft dit niet per se waar te zijn (alles op één regel bv). Logischerwijs komt het erop neer dat hoe meer aannames je doet (bepaalde compiler, wel/geen optimalisatie, wel/geen codingstijl en annotaties) hoe verder je kunt komen met decompilatie maar hoe minder flexibel je tool wordt, en ik hou van flexibel. ;)

Desalniettemin zal ik het ermee moeten doen, en ik denk dat ik de guldenmiddenweg ga kiezen. Ondanks mijn eigen voorbeeld denk ik dat deze situatie niet zoveel voor zal komen en dat ik in 99% vd gevallen met 100% zekerheid kan vaststellen wat de loop-conditie en wat de loop-body was. In de overige gevallen zal ik een willekeurige keuze moeten maken maar bij de meting een kanttekening moeten plaatsen dat de meting niet geheel nauwkeurig of betrouwbaar hoeft te zijn. Aangezien dit onderdeel van mijn afstudeeropdracht is zullen ze het probleem wat ik heb geschetst wel onderkennen en accepteren.
Pagina: 1