2012-05-21 18 views
11

Devo modificare una costante di stringa in un programma Java distribuito, vale a dire il valore all'interno dei file compilati .class. Può essere riavviato, ma non facilmente ricompilato (anche se è un'opzione scomoda se questa domanda non dà risposte). È possibile?Modifica costante della stringa in una classe compilata

Aggiornamento: ho appena guardato il file con un editor esadecimale e sembra che possa facilmente cambiare la stringa lì. Funzionerebbe, non invaliderebbe alcun tipo di firma del file? La vecchia e la nuova stringa sono alfanumeriche e possono avere la stessa lunghezza se necessario.

Aggiornamento 2: l'ho risolto. Poiché la classe specifica che dovevo cambiare era molto piccola e non cambiava nella nuova versione del progetto, potevo semplicemente compilarla e prendere la nuova classe da lì. Ancora interessato a una risposta che non implichi la compilazione, per scopi didattici.

+0

Ci sono un paio di librerie di manipolazione bytecode, come ASM e BCEL, che consentono di modificare i file di classe a proprio piacimento. La soluzione migliore, IMO, è estrarre la costante come una proprietà e passare attraverso l'inconveniente della ricompilazione il tempo necessario per l'estrazione. –

+0

@ NathanD.Ryan Certamente lo estrarrò e lo inserirò in un file di configurazione per il futuro, ma in questo caso specifico sarebbe molto scomodo ricompilare la versione distribuita, che è piuttosto vecchia. –

+0

La stringa in questione è una costante in fase di compilazione? –

risposta

7

Se si hanno le fonti per questa classe, quindi il mio approccio è:

  • ottenere il file JAR
  • Prendi la sorgente per la singola classe
  • compilare il sorgente con il JAR nel classpath (in questo modo, non devi compilare nient'altro, non fa male che il JAR contenga già il binario). È possibile utilizzare la versione Java più recente per questo; basta effettuare il downgrade del compilatore usando -source e -target.
  • Sostituire il file di classe nel vaso con uno nuovo utilizzando jar u o un task Ant

Esempio per un compito Ant:

 <jar destfile="${jar}" 
      compress="true" update="true" duplicate="preserve" index="true" 
      manifest="tmp/META-INF/MANIFEST.MF" 
     > 
      <fileset dir="build/classes"> 
       <filter /> 
      </fileset> 
      <zipfileset src="${origJar}"> 
       <exclude name="META-INF/*"/> 
      </zipfileset> 
     </jar> 

Qui ho anche aggiornare il manifesto. Metti prima le nuove classi e poi aggiungi tutti i file dal JAR originale. duplicate="preserve" farà in modo che il nuovo codice non venga sovrascritto.

Se il codice non è firmato, è anche possibile provare a sostituire i byte se la nuova stringa ha la stessa lunghezza di quella precedente. Java esegue alcuni controlli sul codice ma c'è no checksum in the .class files.

È necessario conservare la lunghezza; altrimenti il ​​loader di classe si confonderà.

+0

L'ho risolto con la fonte (vedi domanda), ma sono ancora interessato a trovare una risposta senza fonte. Durante l'ispezione della classe in un editor esadecimale, ho visto che la lunghezza della stringa è memorizzata in uno o più byte prima di essa. Se aggiorno anche quello, potrei cambiare la lunghezza della stringa, o ci sono altri campi di lunghezza che devono essere modificati? –

+0

Se si modifica la lunghezza della stringa, è necessario inserire/eliminare tanti byte dopo la stringa perché il lettore di classi leggerà la stringa e quindi si aspetta che il successivo elemento valido -> crash –

+1

@Aaron, non sia corretto. Finché si aggiorna il campo lunghezza all'inizio della stringa, tutto funzionerà correttamente. Non è come se il classloader avesse offset magici in codice. – Antimony

1

È possibile modificare .class utilizzando molte librerie di ingegneria bytecode. Ad esempio, utilizzando javaassist.

Tuttavia, se si sta tentando di sostituire un membro finale statico, potrebbe non fornire l'effetto desiderato, poiché il compilatore dovrebbe allineare questa costante ovunque venga utilizzata.

codice

esempio utilizzando javaassist.jar

//ConstantHolder.java

public class ConstantHolder { 

public static final String HELLO="hello"; 

public static void main(String[] args) { 
    System.out.println("Value:" + ConstantHolder.HELLO); 
} 
} 

//ModifyConstant.java

import java.io.IOException; 

import javassist.CannotCompileException; 
import javassist.ClassPool; 
import javassist.CtClass; 
import javassist.CtField; 
import javassist.NotFoundException; 

//ModifyConstant.java 
public class ModifyConstant { 
public static void main(String[] args) { 
    modifyConstant(); 
} 

private static void modifyConstant() { 
    ClassPool pool = ClassPool.getDefault(); 
    try { 
    CtClass pt = pool.get("ConstantHolder"); 
    CtField field = pt.getField("HELLO"); 
    pt.removeField(field); 
    CtField newField = CtField.make("public static final String HELLO=\"hell\";", pt); 
    pt.addField(newField); 
    pt.writeFile(); 
    } catch (NotFoundException e) { 
    e.printStackTrace();System.exit(-1); 
    } catch (CannotCompileException e) { 
    e.printStackTrace();System.exit(-1); 
    } catch (IOException e) { 
    e.printStackTrace();System.exit(-1); 
    } 
} 
} 

In questo caso, il programma modifica correttamente il valore di CIAO da "Ciao" a "Inferno". Tuttavia, quando si esegue la classe ConstantHolder, verrà comunque stampato "Value: Hello" a causa dell'inallineamento da parte del compilatore.

Spero che aiuti.

+0

Cosa significa per una firma? –

+0

per favore spieghi. Non ho capito la tua domanda. – krishnakumarp

4

Gli unici dati aggiuntivi richiesti quando si modifica una stringa (tecnicamente un elemento Utf8) nel pool costante è il campo lunghezza (2 byte big endian che precede i dati). Non ci sono checksum o offset aggiuntivi che richiedono modifiche.

ci sono due accorgimenti:

  • La stringa può essere utilizzato in altri luoghi. Ad esempio, "Codice" viene utilizzato per un attributo del codice del metodo, quindi cambiandolo si interromperà il file.
  • La stringa è memorizzata nel formato Utf8 modificato. Quindi i byte null e i caratteri unicode al di fuori del piano di base sono codificati in modo diverso. Il campo lunghezza è il numero di byte, non i caratteri ed è limitato a 65535.

Se si intende fare molto questo, è meglio ottenere uno strumento di editor di file di classe, ma l'editor esadecimale è utile per rapidi cambiamenti.

+0

Strumento di esempio per la modifica di una classe compilata: http://sourceforge.net/projects/classeditor/ (è necessario selezionare un file .class, non è possibile aprire i jar). – Marcin

2

Recentemente ho scritto il mio mapper ConstantPool perché ASM e JarJar hanno avuto i seguenti problemi:

  • a rallentare
  • non supportava la riscrittura senza tutte le dipendenze classe
  • non supportava lo streaming
  • Non supportato Remapper in modalità API Albero
  • Ha dovuto espandere e comprimere StackMaps

ho finito con il seguente:

public void process(DataInputStream in, DataOutputStream out, Function mapper) throws IOException { 
    int magic = in.readInt(); 
    if (magic != 0xcafebabe) throw new ClassFormatError("wrong magic: " + magic); 
    out.writeInt(magic); 

    copy(in, out, 4); // minor and major 

    int size = in.readUnsignedShort(); 
    out.writeShort(size); 

    for (int i = 1; i < size; i++) { 
     int tag = in.readUnsignedByte(); 
     out.writeByte(tag); 

     Constant constant = Constant.constant(tag); 
     switch (constant) { 
      case Utf8: 
       out.writeUTF(mapper.apply(in.readUTF())); 
       break; 
      case Double: 
      case Long: 
       i++; // "In retrospect, making 8-byte constants take two constant pool entries was a poor choice." 
       // See http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.4.5 
      default: 
       copy(in, out, constant.size); 
       break; 
     } 
    } 
    Streams.copyAndClose(in, out); 
} 

private final byte[] buffer = new byte[8]; 

private void copy(DataInputStream in, DataOutputStream out, int amount) throws IOException { 
    in.readFully(buffer, 0, amount); 
    out.write(buffer, 0, amount); 
} 

E poi

public enum Constant { 
    Utf8(1, -1), 
    Integer(3, 4), 
    Float(4, 4), 
    Long(5, 8), 
    Double(6,8), 
    Class(7, 2), 
    String(8, 2), 
    Field(9, 4), 
    Method(10, 4), 
    InterfaceMethod(11, 4), 
    NameAndType(12, 4), 
    MethodHandle(15, 3), 
    MethodType(16, 2), 
    InvokeDynamic(18, 4); 

public final int tag, size; 

Constant(int tag, int size) { this.tag = tag; this.size = size; } 

private static final Constant[] constants; 
static{ 
    constants = new Constant[19]; 
    for (Constant c : Constant.values()) constants[c.tag] = c; 
} 

public static Constant constant(int tag) { 
    try { 
     Constant constant = constants[tag]; 
     if(constant != null) return constant; 
    } catch (IndexOutOfBoundsException ignored) { } 
    throw new ClassFormatError("Unknown tag: " + tag); 
} 

Ho pensato di mostrare le alternative senza librerie come è un bel posto abbastanza per iniziare l'hacking da. Il mio codice è stato ispirato dal codice sorgente javap