Ghidra 色彩重构V1.0
存在问题
原生软件各处的色彩通过直接调用Java 默认Color类调用,色彩被“写死”在各个地方,对颜色的修改或主题切换的管理十分复杂。
解决思路
通过将使用的色彩写入配置文件中,将软件UI使用的色彩统一通过配置文件加载与管理,目前主流的配置文件使用json,xml,properties
等,通过在配置文件中以Key-Value形式或字典形式存储16进制色彩,通过构建处理配置文件的Class,实现读写配置文件的基本函数、字符与色彩对象转换函数等功能,最终将软件所有的色彩替换或添加到配置文件中,实现色彩重构!
色彩工具
具体方案
配置文件
依据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
之后会直接生成support
和server
目录,并不会打包成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
- 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
- Extension ClassLoader:称为扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar。
- 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;
}
}