Ghidra色彩重构


Ghidra 色彩重构V1.0

存在问题

原生软件各处的色彩通过直接调用Java 默认Color类调用,色彩被“写死”在各个地方,对颜色的修改或主题切换的管理十分复杂。

解决思路

通过将使用的色彩写入配置文件中,将软件UI使用的色彩统一通过配置文件加载与管理,目前主流的配置文件使用json,xml,properties等,通过在配置文件中以Key-Value形式或字典形式存储16进制色彩,通过构建处理配置文件的Class,实现读写配置文件的基本函数、字符与色彩对象转换函数等功能,最终将软件所有的色彩替换或添加到配置文件中,实现色彩重构!

色彩工具

千通彩

传统配色

RGB色彩对照表

具体方案

配置文件

依据Java原生的Properties类,使用.propertise配置文件来实现色彩重构的存储对象。

配置文件放在Docking目录下:

Ghidra/Framework/Docking/src/main/resources/config/Color.properties

解决Properties嵌套问题

在构建PropertiesHandle.class之前,发现Java原生的Properties.class无法处理属性嵌套(Nested Properties),举个例子:对于各类组件的背景色假如是黑色(#ff000000),如果把每个组件的背景色都写死,显得十分的蠢,例如:

button.background = #ff000000
Table.background = #ff000000
Panel.background = #ff000000
……

我们可以通过引入嵌套,通过${}来调用配置文件的其他Key,上述配置改为:

BLACK = #ff000000
button.background = ${BLACK}
Table.background = ${BLACK}
Panel.background = ${BLACK}
……

这样,对于一些类似的色彩属性可以统一管理。

因此,需要对Java 的Properties.class进行改写,利用正则表达式匹配,从而实现嵌套,构建PropertiesEnhance.class:

package ghidra.util.config;

import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Let Properties be Nested
 * by using ${}
 * */

public class PropertiesEnhance extends Properties {

    @Override
    public String getProperty(String key) {
        String str = super.getProperty(key);

        String pattern = "\\$\\{.*?}";
        Pattern r = Pattern.compile(pattern);
        Matcher m = r.matcher(str);

        while (m.find()) {
            String findKey = m.group();
            String fixKey = findKey.replaceAll("[${}]", "");
            String findValue = super.getProperty(fixKey);
            str = str.replaceAll(escapeExprSpecialWord(findKey), findValue);
        }
        return str;
    }

    /**
     * 转义正则特殊字符 ($()*+.[]?\^{},|)
     */
    public String escapeExprSpecialWord(String keyword) {
        if (keyword != null && keyword.length() > 0) {
            String[] fbsArr = { "\\", "$", "(", ")", "*", "+", ".", "[", "]", "?", "^", "{", "}", "|" };
            for (String key : fbsArr) {
                if (keyword.contains(key)) {
                    keyword = keyword.replace(key, "\\" + key);
                }
            }
        }
        return keyword;
    }
}

PropertiesHandle实现

该类通过实现三个基本函数:

GetValueByKey:通过给定的Key获取Properties文件中对应的Value

GetAllProperties:获取Properties配置文件中所有的Value

WriteProperties:向指定的Properties文件中写入键值对(key-value)

package ghidra.util.config;

import java.io.*;
import java.util.Enumeration;

public class PropertiesHandle {

    /**
     * Find the value according to Key
     * @param filePath Filepath String
     * @param key String
     * @return Value String
     * */
    public static String GetValueByKey(String filePath, String key) {
        PropertiesEnhance pps = new PropertiesEnhance();
        try {
            InputStream in = new BufferedInputStream(new FileInputStream(filePath));
            pps.load(in);
            String value = pps.getProperty(key);
            return value;

        }catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    public static String GetValueByKey(InputStream fileStream, String key) {
        PropertiesEnhance pps = new PropertiesEnhance();
        try {
            InputStream in = fileStream;
            pps.load(in);
            String value = pps.getProperty(key);
            return value;

        }catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * Read all value from properties file
     * @param filePath Filepath String
     * @return Value String
     * */
    public static void GetAllProperties(String filePath) throws IOException {
        PropertiesEnhance pps = new PropertiesEnhance();
        InputStream in = new BufferedInputStream(new FileInputStream(filePath));
        pps.load(in);
        Enumeration en = pps.propertyNames();
        while(en.hasMoreElements()) {
            String strKey = (String) en.nextElement();
            String strValue = pps.getProperty(strKey);
        }

    }

    /**
     * Write value to properties file
     * @param filePath Filepath String
     * @param pKey String
     * @param pValue Sting
     * */
    public static void WriteProperties (String filePath, String pKey, String pValue) throws IOException {
        PropertiesEnhance pps = new PropertiesEnhance();
        InputStream in = new FileInputStream(filePath);
        pps.load(in);
        OutputStream out = new FileOutputStream(filePath);
        pps.setProperty(pKey, pValue);
        pps.store(out, "Update " + pKey + " name");
    }
}

色彩转换实现

实现色彩的十六进制字符表示与Color Object的相互转换:

toHexFromColor:色彩对象转十六进制色彩

toColorFromString:十六进制色彩转色彩对象

实现了读取配置文件中色彩的RGB表示或RGBA表示。

例如:#ffffffff(前两位是透明度信息)和#ffffff(透明度默认100%,不透明)

package ghidra.util.config;

import java.awt.*;

public class ColorHexConvert {

    public static Color color;

    /**
     * Color Object converts to String
     * @param color Color Object
     * @return Hex Color String
     * */
    private static String toHexFromColor(Color color){
        String r,g,b;
        StringBuilder su = new StringBuilder();
        r = Integer.toHexString(color.getRed());
        g = Integer.toHexString(color.getGreen());
        b = Integer.toHexString(color.getBlue());
        r = r.length() == 1 ? "0" + r : r;
        g = g.length() ==1 ? "0" +g : g;
        b = b.length() == 1 ? "0" + b : b;
        r = r.toUpperCase();
        g = g.toUpperCase();
        b = b.toUpperCase();
        su.append("0xFF");
        su.append(r);
        su.append(g);
        su.append(b);
        return su.toString();
    }
    /**
     * String converts to Color Object
     * @param colorStr Hex Color String
     * @return Color Object
     * */
    public static Color toColorFromString(String colorStr){
        if (colorStr.length() == 7){
            colorStr = colorStr.replace("#","0xff");
            color = toColorFromString0(colorStr);
        }
        else if (colorStr.length() == 9){
            colorStr = colorStr.replace("#","0x");
            color = toColorFromString0(colorStr);
        }
        else{
            colorStr = "0xff000000";
            color = toColorFromString0(colorStr);
        }
        return color;
    }

    public static Color toColorFromString0(String colorStr){
        String str_a = colorStr.substring(2, 4);
        String str_r = colorStr.substring(4, 6);
        String str_g = colorStr.substring(6, 8);
        String str_b = colorStr.substring(8, 10);
        int a = Integer.parseInt(str_a, 16);
        int r = Integer.parseInt(str_r, 16);
        int g = Integer.parseInt(str_g, 16);
        int b = Integer.parseInt(str_b, 16);
        Color color =  new Color(r, g ,b , a);
        return color;
    }
}

色彩调用封装

实现直接调用从配置文件中色彩属性的封装

package ghidra.util.config;

import resources.ResourceManager;

import java.awt.*;
import java.io.InputStream;


import static ghidra.util.config.ColorHexConvert.toColorFromString;
import static ghidra.util.config.PropertiesHandle.GetValueByKey;

/**
 * Get Color from Config file
 * */
public class ReadColorFromConfig {

    public static Color findColor(String key){
        return ReadColorFromProperties(key);
    }

    /**
     * ReadColorFromProperties
     * @param key String
     * @return Color Object
     * */
    private static Color ReadColorFromProperties(String key) {
        InputStream ColorConfigFile = ResourceManager.getResourceAsStream("config/Color.properties");
        Color color = toColorFromString(GetValueByKey(ColorConfigFile, key));
        return color;
    }
}

色彩替换

通过调用函数findColor(key)可以从配置文件中直接读取色彩,赋值给Color对象。

例如:

Color DEFAULT_COLOR_REGISTER_MARKERS = findColor("Debugger.default.register.markers");

setBackground(findColor("Button.background.pressed"));

目前配置文件添加了Java原生基础色彩的16进制Key-Value,完全可以实现基础色彩的替换,对于一些自定义色彩,需要后续的替换和添加,主要就是色彩替换工作。

# Color Config File
# Using to Set UI Color

# ---- Java Base Color ----
Pink = #FFC0CB
Crimson = #DC143C
LavenderBlush = #FFF0F5
PaleVioletRed = #DB7093
HotPink = #FF69B4
DeepPink = #FF1493
MediumVioletRed = #C71585
Orchid = #DA70D6
Thistle = #D8BFD8
Plum = #DDA0DD
Violet = #EE82EE
Magenta = #FF00FF
Fuchsia = #FF00FF
DarkMagenta = #8B008B
Purple = #800080
MediumOrchid = #BA55D3
DarkVoilet = #9400D3
DarkOrchid = #9932CC
Indigo = #4B0082
BlueViolet = #8A2BE2
MediumPurple = #9370DB
MediumSlateBlue = #7B68EE
SlateBlue = #6A5ACD
DarkSlateBlue = #483D8B
Lavender = #E6E6FA
GhostWhite = #F8F8FF
Blue = #0000FF
MediumBlue = #0000CD
MidnightBlue = #191970
DarkBlue = #00008B
Navy = #000080
RoyalBlue = #4169E1
CornflowerBlue = #6495ED
LightSteelBlue = #B0C4DE
LightSlateGray = #778899
SlateGray = #708090
DoderBlue = #1E90FF
AliceBlue = #F0F8FF
SteelBlue = #4682B4
LightSkyBlue = #87CEFA
SkyBlue = #87CEEB
DeepSkyBlue = #00BFFF
LightBLue = #ADD8E6
PowDerBlue = #B0E0E6
CadetBlue = #5F9EA0
Azure = #F0FFFF
LightCyan = #E1FFFF
PaleTurquoise = #AFEEEE
Cyan = #00FFFF
Aqua = #00FFFF
DarkTurquoise = #00CED1
DarkSlateGray = #2F4F4F
DarkCyan = #008B8B
Teal = #008080
MediumTurquoise = #48D1CC
LightSeaGreen = #20B2AA
Turquoise = #40E0D0
Auqamarin = #7FFFAA
MediumAquamarine = #00FA9A
MediumSpringGreen = #F5FFFA
MintCream = #00FF7F
SpringGreen = #3CB371
SeaGreen = #2E8B57
Honeydew = #F0FFF0
LightGreen = #90EE90
PaleGreen = #98FB98
DarkSeaGreen = #8FBC8F
LimeGreen = #32CD32
Lime = #00FF00
ForestGreen = #228B22
Green = #008000
DarkGreen = #006400
Chartreuse = #7FFF00
LawnGreen = #7CFC00
GreenYellow = #ADFF2F
OliveDrab = #556B2F
Beige = #6B8E23
LightGoldenrodYellow = #FAFAD2
Ivory = #FFFFF0
LightYellow = #FFFFE0
Yellow = #FFFF00
Olive = #808000
DarkKhaki = #BDB76B
LemonChiffon = #FFFACD
PaleGodenrod = #EEE8AA
Khaki = #F0E68C
Gold = #FFD700
Cornislk = #FFF8DC
GoldEnrod = #DAA520
FloralWhite = #FFFAF0
OldLace = #FDF5E6
Wheat = #F5DEB3
Moccasin = #FFE4B5
Orange = #FFA500
PapayaWhip = #FFEFD5
BlanchedAlmond = #FFEBCD
NavajoWhite = #FFDEAD
AntiqueWhite = #FAEBD7
Tan = #D2B48C
BrulyWood = #DEB887
Bisque = #FFE4C4
DarkOrange = #FF8C00
Linen = #FAF0E6
Peru = #CD853F
PeachPuff = #FFDAB9
SandyBrown = #F4A460
Chocolate = #D2691E
SaddleBrown = #8B4513
SeaShell = #FFF5EE
Sienna = #A0522D
LightSalmon = #FFA07A
Coral = #FF7F50
OrangeRed = #FF4500
DarkSalmon = #E9967A
Tomato = #FF6347
MistyRose = #FFE4E1
Salmon = #FA8072
Snow = #FFFAFA
LightCoral = #F08080
RosyBrown = #BC8F8F
IndianRed = #CD5C5C
Red = #FF0000
Brown = #A52A2A
FireBrick = #B22222
DarkRed = #8B0000
Maroon = #800000
White = #FFFFFF
WhiteSmoke = #F5F5F5
Gainsboro = #DCDCDC
LightGrey = #D3D3D3
Silver = #C0C0C0
DarkGray = #A9A9A9
Gray = #808080
DimGray = #696969
Black = #000000

# ==== Page|Module ====
# ---- Debugger ----
Debugger.default.background.stale = ${LightGrey}
Debugger.default.background.error = #FFBFBF
Debugger.default.register.markers = #BFDFBF
Debugger.default.register.stale = ${Gray}
Debugger.default.register.stale.select = ${LightGrey}
Debugger.default.register.changed = ${Red}
Debugger.default.register.changed.select = #7D0000
Debugger.default.watch.stale = ${Gray}
Debugger.default.watch.stale.select = ${LightGrey}
Debugger.default.watch.changed = ${Red}
Debugger.default.watch.changed.select = #7D0000
Debugger.default.pcode.counter = #BFDFBF



# ==== Components ====
# ---- Button ----
Button.background = #ff404040
# event
Button.background.focus = #0D7A7A7A
Button.background.pressed = #0D9E9E9E

……

待解决问题

Ghidra build后资源被封装成jar包,暂时无法面向用户去修改,对开发者提供色彩设置接口。

Ghidra 色彩重构V2.0

根据V1.0版本的设计,存在无法为用户提供设置色彩的接口(即用户无法自己配置想要的色彩)

针对V1.0版本的问题,提出了V2.0的版本的设计。

改进要点

  • Ghidra启动时动态加载配置文件
  • 配置文件不封装进jar包,作为单独配置文件供用户修改使用

解决思路

Ghidra的启动流程加载GUI过程中:首先需要加载软件布局GhidraApplicationLayout(),具体加载需要先找到软件项目的根目录,根据根目录加载软件属性applicationProperties,其中加载需要属性依据application.properties文件和applicationProperties类来实现(load),最后进行调用软件属性来继续后续流程,那么在加载软件属性后,即可加载配置文件,因此在这里需要实现配置文件加载获取,通过一个ResourceFile变量去存储属性,最后在需要使用色彩的地方从其中读取色彩。

ghidra启动流程

见附录:Ghidra启动分析。

具体方案

配置文件

根据ghidra build的过程,在Ghidra/RuntimeScripts/Common/目录下的文件不回被打包成jar包,同事目录下有Ghidra/RuntimeScripts/Common/support/Ghidra/RuntimeScripts/Common/server目录,在build之后会直接生成supportserver目录,并不会打包成jar包,因此,将色彩重构的配置文件转移到Ghidra/RuntimeScripts/Common/support/Color.properties位置。

同时,在Ghidra/RuntimeScripts/certification.manifest文件中添加一条:

Common/support/Color.properties||GHIDRA||||END|

配置文件类ConfigurationProperties实现

之所以创建取名叫`ConfigurationProperties`而不是`ColorProperties`,为后面可能需要添加其他配置文件提供统一的入口,例如设置组件尺寸等等。

由于启动相关的代码在Ghidra/Framework/Utility/目录下,因此在Ghidra/Framework/Utility/src/main/java/ghidra/framework/ConfigurationProperties.java位置创建配置属性类,用于加载配置文件。

ConfigurationProperties实现如下:通过设置面向开发者的配置文件目录和面向用户的配置文件目录,通过添加开发模式判断来选择配置文件加载路径。并提供了从配置文件中读取色彩的接口ReadColorFromProperties

package ghidra.framework;

import generic.jar.ResourceFile;
import ghidra.util.SystemUtilities;
import ghidra.util.config.PropertiesEnhance;

import java.awt.*;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;

import static ghidra.util.config.ColorHexConvert.toColorFromString;


public class ConfigurationProperties extends PropertiesEnhance {

    /**
     * The name of the Color properties file.
     */
    public static final String COLOR_PROPERTY_NAME = "Color.properties";
    public static final String COLOR_PROPERTY_FILE = "/RuntimeScripts/Common/support/Color.properties";
    public static final String COLOR_PROPERTY_FILE_INS = "/support/Color.properties";

    /**
     * Creates a new configuration properties from the given config properties file.
     *
     * @param configPropertiesFile The application properties file.
     * @throws IOException If there was a problem loading/reading a discovered properties file.
     */
    public ConfigurationProperties(ResourceFile configPropertiesFile) throws IOException {

        if (!configPropertiesFile.exists()) {
            throw new FileNotFoundException(
                    COLOR_PROPERTY_NAME + " file does not exist: " + configPropertiesFile);
        }
        try (InputStream in = configPropertiesFile.getInputStream()) {
            load(in);
        }
    }

    /**
     * Creates a new configuration properties from the configuration properties files found
     * in the given application root directories.  If multiple configuration properties files
     * are found, the properties from the files will be combined.  If duplicate keys exist,
     * the newest key encountered will overwrite the existing key.
     *
     * @param applicationRootDirs The application root directories to look for the properties files in.
     * @throws IOException If there was a problem loading/reading a discovered properties file.
     */
    public ConfigurationProperties(Collection<ResourceFile> applicationRootDirs) throws IOException {
        boolean found = false;
        // Application installation directory
        ResourceFile applicationInstallationDir = applicationRootDirs.iterator().next().getParentFile();
        if (SystemUtilities.isInDevelopmentMode()) {
            for (ResourceFile appRoot : applicationRootDirs) {
                ResourceFile configPropertiesFile = new ResourceFile(appRoot, COLOR_PROPERTY_FILE);
                if (configPropertiesFile.exists()) {
                    try (InputStream in = configPropertiesFile.getInputStream()) {
                        load(in);
                        found = true;
                    }
                }
            }
        }
        else {
            ResourceFile configPropertiesFile = new ResourceFile(applicationInstallationDir, COLOR_PROPERTY_FILE_INS);
            if (configPropertiesFile.exists()) {
                try (InputStream in = configPropertiesFile.getInputStream()) {
                    load(in);
                    found = true;
                }
            }
        }
        if (!found) {
            throw new IOException(COLOR_PROPERTY_NAME + " was not found!");
        }
    }

    /**
     * Get Properties from Color.properties by key
     * 
     * @param key Color.properties key
     * @return Color Object
     * */
   public Color ReadColorFromProperties(String key) {
      Color color = toColorFromString(getProperty(key));
      return color;
   }
}

其中ConfigurationProperties继承自PropertiesEnhance,具体见Ghidra 色彩重构V1.0中,将工具类位置改变到Ghidra/Framework/Utility/src/main/java/ghidra/util/config/目录下:

import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Let Properties be Nested
 * by using ${}
 * */

public class PropertiesEnhance extends Properties {

    @Override
    public String getProperty(String key) {
        String str = super.getProperty(key);

        String pattern = "\\$\\{.*?}";
        Pattern r = Pattern.compile(pattern);
        Matcher m = r.matcher(str);

        while (m.find()) {
            String findKey = m.group();
            String fixKey = findKey.replaceAll("[${}]", "");
            String findValue = super.getProperty(fixKey);
            str = str.replaceAll(escapeExprSpecialWord(findKey), findValue);
        }
        return str;
    }

    /**
     * 转义正则特殊字符 ($()*+.[]?\^{},|)
     */
    public String escapeExprSpecialWord(String keyword) {
        if (keyword != null && keyword.length() > 0) {
            String[] fbsArr = { "\\", "$", "(", ")", "*", "+", ".", "[", "]", "?", "^", "{", "}", "|" };
            for (String key : fbsArr) {
                if (keyword.contains(key)) {
                    keyword = keyword.replace(key, "\\" + key);
                }
            }
        }
        return keyword;
    }
}

除此之外,将色彩转换ColorHexConvert.class也加到该目录下:

package ghidra.util.config;

import java.awt.*;

public class ColorHexConvert {

    public static Color color;

    /**
     * Color Object converts to String
     * @param color Color Object
     * @return Hex Color String
     * */
    private static String toHexFromColor(Color color){
        String r,g,b;
        StringBuilder su = new StringBuilder();
        r = Integer.toHexString(color.getRed());
        g = Integer.toHexString(color.getGreen());
        b = Integer.toHexString(color.getBlue());
        r = r.length() == 1 ? "0" + r : r;
        g = g.length() ==1 ? "0" +g : g;
        b = b.length() == 1 ? "0" + b : b;
        r = r.toUpperCase();
        g = g.toUpperCase();
        b = b.toUpperCase();
        su.append("0xFF");
        su.append(r);
        su.append(g);
        su.append(b);
        return su.toString();
    }
    /**
     * String converts to Color Object
     * @param colorStr Hex Color String
     * @return Color Object
     * */
    public static Color toColorFromString(String colorStr){
        if (colorStr.length() == 7){
            colorStr = colorStr.replace("#","0xff");
            color = toColorFromString0(colorStr);
        }
        else if (colorStr.length() == 9){
            colorStr = colorStr.replace("#","0x");
            color = toColorFromString0(colorStr);
        }
        else{
            colorStr = "0xff000000";
            color = toColorFromString0(colorStr);
        }
        return color;
    }

    public static Color toColorFromString0(String colorStr){
        String str_a = colorStr.substring(2, 4);
        String str_r = colorStr.substring(4, 6);
        String str_g = colorStr.substring(6, 8);
        String str_b = colorStr.substring(8, 10);
        int a = Integer.parseInt(str_a, 16);
        int r = Integer.parseInt(str_r, 16);
        int g = Integer.parseInt(str_g, 16);
        int b = Integer.parseInt(str_b, 16);
        Color color =  new Color(r, g ,b , a);
        return color;
    }
}

同时,重新封装配置读取类ReadConfigProperties,同样通过findColor()函数读取配置文件中的色彩。

package ghidra.util.config;

import ghidra.framework.ConfigurationProperties;
import utility.application.ApplicationLayout;

import java.awt.*;

import static utility.application.ApplicationLayout.getConfigurationProperties;


public class ReadConfigProperties {

//    static ConfigurationProperties configurationProperties = ApplicationLayout.configurationProperties;
    static ConfigurationProperties configurationProperties = getConfigurationProperties();

    public static Color findColor(String key){
        Color color = configurationProperties.ReadColorFromProperties(key);
        return color;
    }

}

动态加载配置文件类

首先在Ghidra/Framework/Utility/src/main/java/ghidra/GhidraApplicationLayout.java

类中的构造函数GhidraApplicationLayout()和重载版本GhidraApplicationLayout(File applicationInstallationDir)中添加:

// Configuration properties
configurationProperties = new ConfigurationProperties(applicationRootDirs);

Ghidra/Framework/Utility/src/main/java/utility/application/ApplicationLayout.java中添加成员:

protected static ConfigurationProperties configurationProperties;

添加Get函数:

/**
 * Gets the configuration properties from the application layout
 *
 * @return The configuration properties.  Should never be null.
 */
public static final ConfigurationProperties getConfigurationProperties() {
   return configurationProperties;
}

后续工作

  • 利用findColor替换软件中写死的色彩,在配置文件中添加色彩的键值。或者选择用户可设置的部分色彩进行替换(字体等),替换方式如下:
Color DEFAULT_COLOR_REGISTER_MARKERS = findColor("Debugger.default.register.markers");

setBackground(findColor("Button.background.pressed"));
  • 细节优化与修改

最终实现:https://github.com/StarCrossPortal/ghidracraft/pull/33

附录:Ghidra启动分析

GhidraLauncher.class

main函数

判断加载类是否实例化

// Make sure our class loader is being used
if (!(ClassLoader.getSystemClassLoader() instanceof GhidraClassLoader)) {
   throw new ClassNotFoundException("ERROR: Ghidra class loader not in use.  " +
      "Confirm JVM argument \"-Djava.system.class.loader argument=" +
      GhidraClassLoader.class.getName() + "\" is set.");
}
  • Instanceof: Java 的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。
  • ClassLoader
什么是ClassLoader

我们写好一个Java程序之后,不是管是CS还是BS应用,都是由若干个.class文件组织而成的一个完整的Java应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的class文件当中,所以经常要从这个class文件中要调用另外一个class文件中的方法,如果另外一个文件不存在的,则会引发系统异常。而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的。

Java默认提供的三个ClassLoader
  1. BootStrap ClassLoader:称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等,可通过如下程序获得该类加载器从哪些地方加载了相关的jar或class文件:
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
        System.out.println(urls[i].toExternalForm());
}

以下内容是上述程序从本机JDK环境所获得的结果:

file:/C:/Program%20Files/Java/jdk1.6.0_22/jre/lib/resources.jar

file:/C:/Program%20Files/Java/jdk1.6.0_22/jre/lib/rt.jar

file:/C:/Program%20Files/Java/jdk1.6.0_22/jre/lib/sunrsasign.jar

file:/C:/Program%20Files/Java/jdk1.6.0_22/jre/lib/jsse.jar

file:/C:/Program%20Files/Java/jdk1.6.0_22/jre/lib/jce.jar

file:/C:/Program%20Files/Java/jdk1.6.0_22/jre/lib/charsets.jar

file:/C:/Program%20Files/Java/jdk1.6.0_22/jre/classes/

其实上述结果也是通过查找sun.boot.class.path这个系统属性所得知的。

System.out.println(System.getProperty("sun.boot.class.path"));

打印结果:C:\Program Files\Java\jdk1.6.0_22\jre\lib\resources.jar;C:\Program Files\Java\jdk1.6.0_22\jre\lib\rt.jar;C:\Program Files\Java\jdk1.6.0_22\jre\lib\sunrsasign.jar;C:\Program Files\Java\jdk1.6.0_22\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.6.0_22\jre\lib\jce.jar;C:\Program Files\Java\jdk1.6.0_22\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.6.0_22\jre\classes

  1. Extension ClassLoader:称为扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar
  2. App ClassLoader:称为系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件。

注意: 除了Java默认提供的三个ClassLoader之外,用户还可以根据需要定义自已的ClassLoader,而这些自定义的ClassLoader都必须继承自java.lang.ClassLoader类,也包括Java提供的另外二个ClassLoader(Extension ClassLoader和App ClassLoader)在内,但是Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。

ClassLoader加载类的原理

ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。

定义自已的ClassLoader

既然JVM已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?

因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的类或jar时,比如:我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现我的业务逻辑。在这样的情况下,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的ClassLoader。

定义自已的类加载器分为两步:

1、继承java.lang.ClassLoader

2、重写父类的findClass方法

读者可能在这里有疑问,父类有那么多方法,为什么偏偏只重写findClass方法?

因为JDK已经在loadClass方法中帮我们实现了ClassLoader搜索类的算法,当在loadClass方法中搜索不到类时,loadClass方法就会调用findClass方法来搜索类,所以我们只需重写该方法即可。如没有特殊的要求,一般不建议重写loadClass搜索类的算法。下图是API中ClassLoader的loadClass方法:

获取软件布局

// Get application layout
GhidraApplicationLayout layout = new GhidraApplicationLayout();
GhidraClassLoader loader = (GhidraClassLoader) ClassLoader.getSystemClassLoader();
  • GhidraApplicationLayout: -> ApplicationLayout
System.getProperty(“java.class.path”)

Properties 类表示了一个持久的属性集。Properties 可保存在流中或从流中加载。属性列表中每个键及其对应值都是一个字符串。

一个属性列表可包含另一个属性列表作为它的“默认值”;如果未能在原有的属性列表中搜索到属性键,则搜索第二个属性列表。

因为 Properties 继承于 Hashtable,所以可对 Properties 对象应用 put 和 putAll 方法。但不建议使用这两个方法,因为它们允许调用者插入其键或值不是 String 的项。相反,应该使用 setProperty 方法。如果在“不安全”的 Properties 对象(即包含非 String 的键或值)上调用 store 或 save 方法,则该调用将失败。类似地,如果在“不安全”的 Properties 对象(即包含非 String 的键)上调用 propertyNames 或 list 方法,则该调用将失败。

将 getProperty(String) 方法使用的当前系统属性集合作为 Properties 对象返回。如果没有当前系统属性集合,则先创建并初始化一个系统属性集合。这个系统属性集合总是包含以下键的值:

系统属性值中的多个路径是用平台的路径分隔符分隔的。

注意,即使安全管理器不允许执行 getProperties 操作,它可能也会选择允许执行 getProperty(String) 操作。

File.pathSeparator与File.separator的区别:

File.pathSeparator指的是分隔连续多个路径字符串的分隔符,例如:

java -cp test.jar;abc.jar HelloWorld

就是指“;”

File.separator才是用来分隔同一个路径字符串中的目录的,例如:

C:/Program Files/Common Files

就是指“/”

与系统有关的默认名称分隔符。此字段被初始化为包含系统属性 file.separator 值的第一个字符。在 UNIX 系统上,此字段的值为 ‘/‘;在 Microsoft Windows 系统上,它为 ‘/‘。

public GhidraApplicationLayout() throws FileNotFoundException, IOException {

   // Application root directories
   applicationRootDirs = findGhidraApplicationRootDirs();

   // Application properties
   applicationProperties = new ApplicationProperties(applicationRootDirs);

   // Application installation directory
   applicationInstallationDir = findGhidraApplicationInstallationDir();


   // User directories
   userTempDir = ApplicationUtilities.getDefaultUserTempDir(getApplicationProperties());
   userCacheDir = ApplicationUtilities.getDefaultUserCacheDir(getApplicationProperties());
   userSettingsDir = ApplicationUtilities.getDefaultUserSettingsDir(getApplicationProperties(),
      getApplicationInstallationDir());

   // Extensions
   extensionInstallationDirs = findExtensionInstallationDirectories();
   extensionArchiveDir = findExtensionArchiveDirectory();

   // Patch directory
   patchDir = findPatchDirectory();

   // Modules
   modules = findGhidraModules();
}
  • GhidraClassLoader:-> URLClassLoader -> SecureClassLoader -> ClassLoader

路径构建

// Build the classpath
List<String> classpathList = new ArrayList<>();
Map<String, GModule> modules = getOrderedModules(layout);
  • getOrderedModules
/**
 * Gets the modules ordered by "class-loader priority".  This ensures that core modules (things 
 * in Framework/Features/Processors, etc) come before user modules (Extensions).  It also
 * guarantees a consistent module order from run to run.
 * 
 * @param layout The layout
 * @return the modules mapped by name, ordered by priority
 */
private static Map<String, GModule> getOrderedModules(ApplicationLayout layout) {

   Comparator<GModule> comparator = (module1, module2) -> {
      int nameComparison = module1.getName().compareTo(module2.getName());

      // First handle modules that are external to the Ghidra installation.
      // These should be put at the end of the list.
      boolean external1 = ModuleUtilities.isExternalModule(module1, layout);
      boolean external2 = ModuleUtilities.isExternalModule(module2, layout);
      if (external1 && external2) {
         return nameComparison;
      }
      if (external1) {
         return -1;
      }
      if (external2) {
         return 1;
      }

      // Now handle modules that are internal to the Ghidra installation.
      // We will primarily order them by "type" and secondarily by name.
      Map<String, Integer> typePriorityMap = new HashMap<>();
      typePriorityMap.put("Framework", 0);
      typePriorityMap.put("Configurations", 1);
      typePriorityMap.put("Features", 2);
      typePriorityMap.put("Processors", 3);
      typePriorityMap.put("GPL", 4);
      typePriorityMap.put("Extensions", 5);
      typePriorityMap.put("Test", 6);

      String type1 = module1.getModuleRoot().getParentFile().getName();
      String type2 = module2.getModuleRoot().getParentFile().getName();
      int priority1 = typePriorityMap.getOrDefault(type1, typePriorityMap.size());
      int priority2 = typePriorityMap.getOrDefault(type2, typePriorityMap.size());
      if (priority1 != priority2) {
         return Integer.compare(priority1, priority2);
      }
      return nameComparison;
   };

   List<GModule> moduleList = new ArrayList<>(layout.getModules().values());
   Collections.sort(moduleList, comparator);
   Map<String, GModule> moduleMap = new LinkedHashMap<>();
   for (GModule module : moduleList) {
      moduleMap.put(module.getName(), module);
   }
   return moduleMap;
}

开发者模式与其他模式路径添加加载

if (SystemUtilities.isInDevelopmentMode()) {
   addModuleBinPaths(classpathList, modules);
   addExternalJarPaths(classpathList, layout.getApplicationRootDirs());
}
else {
   addPatchPaths(classpathList, layout.getPatchDir());
   addModuleJarPaths(classpathList, modules);
}

需要加载的路径添加到Loader中

classpathList = orderClasspath(classpathList, modules);

// Add the classpath to the class loader
classpathList.forEach(entry -> loader.addPath(entry));

GhidraLaunchable

// Make sure the thing to launch is a GhidraLaunchable
Class<?> cls = ClassLoader.getSystemClassLoader().loadClass(args[0]);
if (!GhidraLaunchable.class.isAssignableFrom(cls)) {
   throw new IllegalArgumentException(
      "ERROR: \"" + args[0] + "\" is not a launchable class");
}

// Launch the target class, which is the first argument.  Strip off the first argument
// and pass the rest through to the target class's launch method.
GhidraLaunchable launchable = (GhidraLaunchable) cls.getConstructor().newInstance();
launchable.launch(layout, Arrays.copyOfRange(args, 1, args.length));

源代码

package ghidra;

import java.io.*;
import java.util.*;
import java.util.stream.Collectors;

import generic.jar.ResourceFile;
import ghidra.framework.GModule;
import ghidra.util.SystemUtilities;
import utility.application.ApplicationLayout;
import utility.module.ModuleUtilities;

/**
 * Class to build the Ghidra classpath, add it to the {@link GhidraClassLoader}, and start the 
 * desired {@link GhidraLaunchable} that's passed in as a command line argument.
 */
public class GhidraLauncher {

   /**
    * Launches the given {@link GhidraLaunchable}, passing through the args to it.
    * 
    * @param args The first argument is the name of the class to launch.  The remaining args
    *     get passed through to the class's {@link GhidraLaunchable#launch} method.
    * @throws Exception If there was a problem launching.  See the exception's message for more
    *     details on what went wrong.  
    */
   public static void main(String[] args) throws Exception {

      // Make sure our class loader is being used
      if (!(ClassLoader.getSystemClassLoader() instanceof GhidraClassLoader)) {
         throw new ClassNotFoundException("ERROR: Ghidra class loader not in use.  " +
            "Confirm JVM argument \"-Djava.system.class.loader argument=" +
            GhidraClassLoader.class.getName() + "\" is set.");
      }

      // Get application layout
      GhidraApplicationLayout layout = new GhidraApplicationLayout();
      GhidraClassLoader loader = (GhidraClassLoader) ClassLoader.getSystemClassLoader();

      // Build the classpath
      List<String> classpathList = new ArrayList<>();
      Map<String, GModule> modules = getOrderedModules(layout);

      if (SystemUtilities.isInDevelopmentMode()) {
         addModuleBinPaths(classpathList, modules);
         addExternalJarPaths(classpathList, layout.getApplicationRootDirs());
      }
      else {
         addPatchPaths(classpathList, layout.getPatchDir());
         addModuleJarPaths(classpathList, modules);
      }
      classpathList = orderClasspath(classpathList, modules);

      // Add the classpath to the class loader
      classpathList.forEach(entry -> loader.addPath(entry));

      // Make sure the thing to launch is a GhidraLaunchable
      Class<?> cls = ClassLoader.getSystemClassLoader().loadClass(args[0]);
      if (!GhidraLaunchable.class.isAssignableFrom(cls)) {
         throw new IllegalArgumentException(
            "ERROR: \"" + args[0] + "\" is not a launchable class");
      }

      // Launch the target class, which is the first argument.  Strip off the first argument
      // and pass the rest through to the target class's launch method.
      GhidraLaunchable launchable = (GhidraLaunchable) cls.getConstructor().newInstance();
      launchable.launch(layout, Arrays.copyOfRange(args, 1, args.length));
   }

   /**
    * Add patch jars to the given path list.  This should be done first so they take precedence in 
    * the classpath.
    * 
    * @param pathList The list of paths to add to
    * @param patchDir The application installation directory; may be null
    */
   private static void addPatchPaths(List<String> pathList, ResourceFile patchDir) {
      if (patchDir == null || !patchDir.exists()) {
         return;
      }

      // this will allow for unbundled class files
      pathList.add(patchDir.getAbsolutePath());

      // this is each jar file, sorted for loading consistency
      List<String> jars = findJarsInDir(patchDir);
      Collections.sort(jars);
      pathList.addAll(jars);
   }

   /**
    * Add module bin directories to the given path list.
    * 
    * @param pathList The list of paths to add to.
    * @param modules The modules to get the bin directories of.
    */
   private static void addModuleBinPaths(List<String> pathList, Map<String, GModule> modules) {
      Collection<ResourceFile> dirs = ModuleUtilities.getModuleBinDirectories(modules);
      dirs.forEach(d -> pathList.add(d.getAbsolutePath()));
   }

   /**
    * Add module lib jars to the given path list.
    * 
    * @param pathList The list of paths to add to.
    * @param modules The modules to get the jars of.
    */
   private static void addModuleJarPaths(List<String> pathList, Map<String, GModule> modules) {
      Collection<ResourceFile> dirs = ModuleUtilities.getModuleLibDirectories(modules);
      dirs.forEach(d -> pathList.addAll(findJarsInDir(d)));
   }

   /**
    * Add external runtime lib jars to the given path list.  The external jars are discovered by
    * parsing the build/libraryDependencies.txt file that results from a prepDev.
    * 
    * @param pathList The list of paths to add to.
    * @param appRootDirs The application root directories to search.
    * @throws IOException if a required file or directory was not found.
    */
   private static void addExternalJarPaths(List<String> pathList,
         Collection<ResourceFile> appRootDirs) throws IOException {

      final String LIBDEPS = "build/libraryDependencies.txt";

      // Get "libraryDependencies.txt" file
      ResourceFile libraryDependenciesFile = null;
      for (ResourceFile root : appRootDirs) {
         if (libraryDependenciesFile == null) {
            ResourceFile f = new ResourceFile(root.getParentFile(), LIBDEPS);
            if (f.isFile()) {
               libraryDependenciesFile = f;
            }
         }
      }

      // Make sure we found everything
      if (libraryDependenciesFile == null) {
         throw new FileNotFoundException(LIBDEPS + " file was not found!  Please do a prepDev.");
      }

      // Add the jars to the path list (don't add duplicates)
      Set<String> pathSet = new HashSet<>();
      try (BufferedReader reader =
         new BufferedReader(new FileReader(libraryDependenciesFile.getFile(false)))) {
         String line;
         while ((line = reader.readLine()) != null) {
            String path = line.trim();
            if (!path.startsWith("Module:") && path.endsWith(".jar")) {
               ResourceFile jarFile = new ResourceFile(path);
               if (!jarFile.isFile()) {
                  System.err.println("Failed to find required jar file: " + jarFile);
                  continue;
               }
               pathSet.add(jarFile.getAbsolutePath());
            }
         }
      }

      if (pathSet.isEmpty()) {
         throw new IllegalStateException(
            "Files listed in '" + LIBDEPS + "' are incorrect--rebuild this file");
      }

      pathList.addAll(pathSet);
   }

   /**
    * Searches the given directory (non-recursively) for jars and returns their paths in a list.
    * 
    * @param dir The directory to search for jars in.
    * @return A list of discovered jar paths.
    */
   public static List<String> findJarsInDir(ResourceFile dir) {
      List<String> list = new ArrayList<>();
      ResourceFile[] names = dir.listFiles();
      if (names != null) {
         for (ResourceFile file : names) {
            if (file.getName().endsWith(".jar")) {
               list.add(file.getAbsolutePath());
            }
         }
      }
      return list;
   }

   /**
    * Gets the modules ordered by "class-loader priority".  This ensures that core modules (things 
    * in Framework/Features/Processors, etc) come before user modules (Extensions).  It also
    * guarantees a consistent module order from run to run.
    * 
    * @param layout The layout
    * @return the modules mapped by name, ordered by priority
    */
   private static Map<String, GModule> getOrderedModules(ApplicationLayout layout) {

      Comparator<GModule> comparator = (module1, module2) -> {
         int nameComparison = module1.getName().compareTo(module2.getName());

         // First handle modules that are external to the Ghidra installation.
         // These should be put at the end of the list.
         boolean external1 = ModuleUtilities.isExternalModule(module1, layout);
         boolean external2 = ModuleUtilities.isExternalModule(module2, layout);
         if (external1 && external2) {
            return nameComparison;
         }
         if (external1) {
            return -1;
         }
         if (external2) {
            return 1;
         }

         // Now handle modules that are internal to the Ghidra installation.
         // We will primarily order them by "type" and secondarily by name.
         Map<String, Integer> typePriorityMap = new HashMap<>();
         typePriorityMap.put("Framework", 0);
         typePriorityMap.put("Configurations", 1);
         typePriorityMap.put("Features", 2);
         typePriorityMap.put("Processors", 3);
         typePriorityMap.put("GPL", 4);
         typePriorityMap.put("Extensions", 5);
         typePriorityMap.put("Test", 6);

         String type1 = module1.getModuleRoot().getParentFile().getName();
         String type2 = module2.getModuleRoot().getParentFile().getName();
         int priority1 = typePriorityMap.getOrDefault(type1, typePriorityMap.size());
         int priority2 = typePriorityMap.getOrDefault(type2, typePriorityMap.size());
         if (priority1 != priority2) {
            return Integer.compare(priority1, priority2);
         }
         return nameComparison;
      };

      List<GModule> moduleList = new ArrayList<>(layout.getModules().values());
      Collections.sort(moduleList, comparator);
      Map<String, GModule> moduleMap = new LinkedHashMap<>();
      for (GModule module : moduleList) {
         moduleMap.put(module.getName(), module);
      }
      return moduleMap;
   }

   /**
    * Updates the list of paths to make sure the order is correct for any class-loading dependencies.
    *  
    * @param pathList The list of paths to order.
    * @param modules The modules on the classpath.
    * @return A new list with the elements of the original list re-ordered as needed.
    */
   private static List<String> orderClasspath(List<String> pathList,
         Map<String, GModule> modules) {

      Set<String> fatJars = modules
            .values()
            .stream()
            .flatMap(m -> m.getFatJars().stream())
            .collect(Collectors.toSet());

      List<String> orderedList = new ArrayList<>(pathList);

      for (String path : pathList) {
         if (fatJars.contains(new File(path).getName())) {
            orderedList.remove(path);
            orderedList.add(path);
         }
      }

      return orderedList;
   }
}

文章作者: 杰克成
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 杰克成 !
评论
  目录