Scripting brings a powerful form of dynamic extensibilty to Ghidra, where Java source code is (re)compiled, loaded, and run without exiting Ghidra. When a script grows large or requires external dependencies, it might be worth the effort to split up code into modules.
To support modularity while preserving the dynamic nature of scripts, Ghidra uses OSGi. Without delving too much into terminology, the key things to know are
When a directory is added to the Bundle Manager, it is treated as a source bundle. When enabled, its Java contents are compiled to
<user home>/.ghidra/.ghidra-<version>/osgi/compiled-bundles/<hash>/where
<hash>
is a hash of the source bundle path. These compiled artifacts are then loaded
by the OSGi framework.
Each such subdirectory of compiled-bundles/
is an exploded
jar -- by compressing it, we get a standard OSGi Jar bundle:
jar cMf mybundle.jar -C $HOME/.ghidra/.ghidra_<version>/osgi/compiled-bundles/<hash> .
If there is no manifest in the source directory, Ghidra generates one using bndlib so that:
private
or internal
in its name.@importpackage
meta-data are imported from active bundles.Note: import and export here refer to inter-bundle dependency, see below
If no bundle activator is present, a stub is created and referenced in the generated manifest.
Two types of code dependency are available when developing with OSGi, intra-bundle and inter-bundle.
Classes within a bundle, e.g. source files in a source bundle, can rely one another with
Java's usual package import
.
This kind of dependency is resolved at compile time -- if a class isn't imported or present, compilation will fail.
In the following example, IntraBundleExampleScript
depends on mylib.MyLibrary
without
any special dependency declaration since they are both defined in the same source bundle,
my_ghidra_scripts
:
my_ghidra_scripts/mylib/MyLibrary.java: package mylib; public class MyLibrary { public void doStuff() { // ... } } my_ghidra_scripts/IntraBundleExampleScript.java // Intra-bundle dependency example. //@category Examples import ghidra.app.script.GhidraScript; import mylib.MyLibrary; public class IntraBundleExampleScript extends GhidraScript { @Override public void run() throws Exception { new MyLibrary().doStuff(); } }
To make use of code from other bundles, a bundle must declare its dependencies. When a bundle is activated, the framework attempts to resolve its declared dependencies against active bundles. The first match, in the order of bundle activation, will be "wired" to the dependent.
Note: OSGi bundle dependency is very similar to Java 9 modules. The key difference is that Java 9 modules provide load time resolution, while OSGi provides run time resolution.
@importpackage
in a scriptGhidra's @importpacakge
meta-data tag provides a convenient way to declare inter-bundle
dependencies directly in a script. Set the value to a comma separated list of packages
to import from other bundles.
In the example below, the class InterBundleExampleScript
is implemented in the bundle
my_ghidra_scripts
and uses a class, yourlib.YourLibrary
, defined in another bundle,
your_ghidra_scripts
.
your_ghidra_scripts/yourlib/YourLibrary.java: package yourlib; public class YourLibrary { public void doOtherStuff() { // ... } } my_ghidra_scripts/InterBundleExampleScript.java // Inter-bundle dependency example. //@category Examples //@importpackage yourlib import ghidra.app.script.GhidraScript; import yourlib.YourLibrary; public class InterBundleExampleScript extends GhidraScript { @Override public void run() throws Exception { new YourLibrary().doOtherStuff(); } }
Import-Package
in the manifestThe underlying OSGi mechanism for declaring inter-bundle dependency is via the manifest.
You can find detailed descriptions of manifest attributes used by OSGi, like Import-Package
, here
https://osgi.org/specification/osgi.core/7.0.0/framework.module.html#framework.module.importpackage.
If a Ghidra source bundle has no manifest, Ghidra generates one.
For example, the @importpackage yourlib
in the previous example corresponds to the `yourlib` entry
in the `Import-Package` line of the manifest generated for my_ghidra_scripts
:
<user home>/.ghidra/.ghidra-<version>/osgi/compiled-bundles/ab12cd89/META-INF/MANIFEST.MF: Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Export-Package: mylib Import-Package: ghidra.app.script,yourlib,ghidra.app.plugin.core.osgi Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=11))" Bundle-SymbolicName: ab12cd89 Bundle-Version: 1.0 Bundle-Name: ab12cd89 Bundle-Activator: GeneratedActivatorThe manifest generated for
your_ghidra_scripts
is as follows:
<user home>/.ghidra/.ghidra-<version>/osgi/compiled-bundles/ef34ab56/META-INF/MANIFEST.MF: Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Export-Package: yourlib Import-Package: ghidra.app.plugin.core.osgi Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=11))" Bundle-SymbolicName: ef34ab56 Bundle-Version: 1.0 Bundle-Name: ef34ab56 Bundle-Activator: GeneratedActivator
Notice the Export-Package
line generated for your_ghidra_scripts
. The generated
manifest exports all packages not including private
or internal
.
For full control, users can include a manifest with their bundle's source,
e.g. my_ghidra_scripts/META-INF/MANIFEST.MF
.
For a bundle's contents to be available (e.g. for loading classes), its
OSGi state must be "ACTIVE
". Ghidra generally takes care of this, but
the following provides more details about the underlying OSGi for debugging.
Ghidra activates a bundle when added, enabled, or when a script contained within is run.
When enabled, the root directory of a source bundle is also scanned for Ghidra scripts and any found are added to the script manager.
When disabled, any dependents of a bundle are stopped/deactivated first, then the bundle itself is stopped. Its scripts are then removed from the script manager.
The color of each bundle path reflects state as follows:
The normally hidden column "OSGi State" is also available. In addition to the Bundle state, this column will report
Adding a directory to the bundle manager will also enable it, so scripts within become available in the script manager.
Removing a bundle disables it, so its scripts will be removed from the script manager and its dependents will become inactive.
When Ghidra builds a source bundle, the results are written to the
directory
<user home>/.ghidra/.ghidra-<version>/osgi/compiled-bundles/<hash>
.
These files can then be loaded by the OSGi framework.
A clean deactivates then wipes this subdirectory for each selected bundle and clears its build summary.
If a source bundle's imports aren't available during build, some source files can produce errors. In order to force Ghidra to recompile, one must either modify the files with errors or clean the bundle then re-enable.
Refresh will deactivate, clean, then reactivate every enabled bundle.