Getting Started
Megaton is a build tool and library for linking and patching NX modules at runtime.
Credits:
- exlaunch for the hooking and patching framework megaton is based on.
Prerequisites
Make sure you have these installed:
git
- Rust toolchain (
cargo
)
These are just for installing the tool. The tool itself requires other tools to function, which you can install later.
Install
- First, clone the repository to where you want to install the tool
git clone https://github.com/Pistonite/megaton path/to/install cd path/to/install
- Build the tool
cargo run --release --bin megaton-buildtool -- install
- Add the
bin
directory to yourPATH
. For example, add the following to your shell profile:export PATH=$PATH:path/to/install/bin
- Finish the installation
megaton install
Update
To update the tool in the future, you can run install
with -u
/--update
flag:
megaton install -u
This will git pull
the repository and rebuild the tool.
Check Environment
To check that you have the necessary tools installed, run
megaton checkenv
This will check for additional tools required to build projects
If any tool is missing, you will need to install it manually.
Rerun megaton checkenv
after installing the tool to verify.
Create Project
To initialize a new project, run the following command in an empty directory:
megaton init
This generates the following structure:
- src/
- main.cpp
- Megaton.toml
- .clang-format
- .clangd
- .gitignore
Build
To build the project the first time, you need to add the --lib
flag
to also build libmegaton
. You don't need to do this for future builds
of this and other projects, unless you updated the tool and need to rebuild
the library
Most of the time you will want to use the megaton library.
However if you don't, you can set build.libmegaton
to false
in the config, and skip it here
megaton build --lib
This should put the output NSO at target/megaton/none/<name>.nso
The build command also generates a clangd
-compatible compile DB (compile_commands.json
).
The generated .clangd
config already references this file, along
with other useful settings to make it work out of the box for you
Megaton is optimized for incremental build performance, which means
it sometimes has compromised correctness and requires a megaton clean
build. See Incremental Build
for more information
Entrypoint
Open src/main.cpp
and you will see the following:
#include <megaton/prelude.h>
extern "C" void megaton_main() {
// Your code here
}
The megaton/prelude.h
include file includes primitive type definitions
like i32
and u32
as well as panic_
macros to help interfacing
with the panic system. megaton_main
is the main function of your module.
Libmegaton provides an entry point __megaton_module_entry
that initializes
the library and calls your main function. The entry point is called by
RTLD when the module is loaded.
Incremental Build
Megaton is optimized for incremental builds, i.e. the following cases:
- Rebuild after changing a source file
- Rebuild after creating/removing source files
- Rebuild after changing the build configuration
Megaton also uses GCC's deps file and OS timestamps to make sure the correct changes are detected and rebuilt for incremental builds.
However, there are some other types of change that may require a full rebuild, but isn't detected:
- Upgrading the toolchain (i.e. DevKitPro) or a tool used in the build
- Run
megaton checkenv
andmegaton clean
to clear env cache and build cache
- Run
- Updating the
megaton
tool or library- Run
megaton build --lib
to rebuild the library
- Run
- Outputs/timestamps are manually changed
- Run
megaton clean
to clear build cache
- Run
Build Config
The Megaton.toml
file is used to configure the build options of your module.
Module options
Open Megaton.toml
in your project root. You should see:
[module]
name = "..."
title-id = 0xFFFFFFFFFFFFFFFF
[build]
sources = ["src"]
includes = ["src"]
name
should already be auto-filled with the name of the folder when
you run megaton init
. You need to replace title-id
with the title ID
of the target game.
Build options
libmegaton
and entry
By default, the build tool will link your module with libmegaton
and use its entry point __megaton_module_entry
, which calls
your megaton_main
function or your main function in Rust.
If you don't want to use libmegaton
, you can set:
[build]
libmegaton = false
entry = "your_main_function"
your_main_function
will be called by RTLD instead.
sources
Sources are directories where C, C++, or assembly source files are located. Nested directories will be automatically included.
includes
Include directories for C and C++ source files.
The default config assumes you will be putting headers and sources in the same
src
directory (common for non-library projects).
Using -I
flags have the same effect. However, includes
is preferred
because it lets your specify relative paths. The build tool will convert
the paths to absolute paths for the compiler.
libpaths
and libraries
These options are useful if you are linking with additional libraries.
libpaths
is akin to -L
flags, and libraries
is akin to -l
flags.
[build]
libpaths = ["path/to/lib"]
libraries = ["name"]
Like includes
, paths are relative to the project root. Also,
the build tool will detect when libraries are updated to invoke the linker
again, even when nothing else has changed.
ldscripts
This option is used to specify additional linker scripts. For example, if you need to provide addresses to external functions
[build]
ldscripts = ["path/to/script.ld"]
Build Flags
See Build Flags for configuring build flags.
Cargo/Rust
This is not available yet
Build Flags
You can customize build and linker flags applied to your C, C++, and assembly
source files in Megaton.toml
, using the build.flags
object.
There are 5 properties that can be set:
common
: Common flags for all processesc
: Flags for compiling C sources.c
cxx
: Flags for compiling C++ sources.cpp
,.cc
,.cxx
,.c++
as
: Flags for compiling assembly sources.s
,.asm
ld
: Flags for linking
Each flag property is an array of strings:
[build.flags]
common = ["<default>"]
c = ["<default>", "-DDEBUG"]
cxx = ["<default>"]
as = ["<default>"]
ld = ["<default>"]
C++ compiler is used for linking. Flags for ld
should be specified as -Wl,--flag
instead of --flag
.
The string <default>
is a special token used to include the
default flags that are maintained by the megaton build tool
plus extending from another set of flags (for example, the
default flags of cxx
include all c
flags).
The default flags can be found here, and the flag extension behavior is detailed below.
Property | <default> includes |
---|---|
common | None |
c | common |
cxx | c |
as | cpp |
ld | common |
Each property is also optional. If it's not set, it's equivalent
to ["<default>"]
. Setting a property to an empty array []
means
not add any flags.
Includes and Libraries
Includes and libraries should be specified in build.includes
,
build.libpaths
, and build.libraries
respectively. See here for more details.
The build tool will also automatically include the headers for
libmegaton
and link with your project's rust code if the [rust]
top-level section
is configured.
Profiles
The build flags also support the profile system.
The profile-specific flags for a profile foo
is defined at build.profiles.foo.flags
.
The flags are merged with flags from the base build.flags
in the following way:
- If a property is not specified, it will be the same as the ones in
build.flags
- If a property is specified, it will be appended to the ones in
build.flags
For example:
[build.flags]
c = ["<default>", "-DDEBUG"]
[build.profiles.foo.flags]
c = ["-DFOO=1"]
The C flags for the base profile will be the default flags plus -DDEBUG
,
while the C flags for the foo
profile will be the default flags plus -DDEBUG -DFOO=1
.
There isn't a way to remove flags from the base profile. Typically, when you find yourself needing to remove a flag, you can restructure the flags and configure profile defaults to get the desired behavior for selecting profile with command line.
For example, if you want to build with -DDEBUG
by default,
and change it to -DNODEBUG
for the release
profile, you can do:
[profile]
default = "debug"
allow-base = false
[build.flags]
[build.profiles.debug.flags]
c = ["<default>", "-DDEBUG"]
[build.profiles.release.flags]
c = ["<default>", "-DNODEBUG"]
With this configuration megaton build
will build with -DDEBUG
and megaton build -p release
will build with -DNODEBUG
.
Profiles
The profile system allows you to have different configurations/targets for your project. An example is having different build flags and dependencies when targeting different versions of a game.
The profile system applies to the following top-level sections:
[build]
[rust]
[check]
Profiles for a top-level section like [build]
are specified in the profiles
object,
which is a map of profile names to the configuration.
For example, [build.profiles.foo]
has the same
schema as [build]
, and should contain configuration for
the foo
profile.
[build]
sources = ["src"]
[build.profiles.foo]
sources = ["src_foo"]
Profiles can be selected when building by using
megaton build --profile PROFILE
.
For the example above, running megaton build --profile foo
will include both src
and src_foo
as source directories.
none
is a reserved profile name for the base profile
and cannot be used as a profile name. Likewise, megaton build --profile none
will use the base profile, which is the default behavior
When both the base profile and a custom profile specify the same
property, like the example above with the sources
property,
the values are merged in the following manner:
- If the property is an array, the values from the custom profile is appended to the base profile
- Otherwise, value from the custom profile overrides the base
Nested map properties like build.flags
are recursively merged,
and should be specified like [build.profiles.foo.flags]
, NOT [build.flags.profiles.foo]
Configure Defaults
The profile.default
optional property can be used to:
- Specify which profile to use when
megaton build
is run without-p
/--profile
- Require a profile to be specified when running
megaton build
When the property is not specified, megaton build
will use the base profile (i.e. the none
profile)
When the property is set to empty string, a profile must be specified when running megaton build
.
This is useful when multiple profiles are present, but none of them makes sense to be the default.
[module]
name = "example"
[profile]
default = ""
# Run with `megaton build -p PROFILE`
When there makes sense to be a default, you can set default
to that
so megaton build
will select that profile when -p
is not specified
[module]
name = "example"
default = "foo"
# Run with `megaton build` -> use foo
# Run with `megaton build -p none` -> use none (base profile)
If a profile other than none
should be the default for clangd
,
also change the CompileDatabase
property in .clangd
to the desired
output location
If the base profile is not meant to be run, set the allow-base
property to false
[module]
name = "example"
default = "foo"
allow-base = false
# Run with `megaton build` -> use foo
# Run with `megaton build -p none` -> error
Check
The build tool can check the built ELF to help catch issues early, saving you time debugging crashes.
Dynamic Symbols
Your module is built as a shared object, which means any unresolved symbols are treated as dynamic symbols imported at runtime by RTLD. However, if no such symbol exists, RTLD will abort the process.
The build tool allows you to check the ELF against a list of known
dynamic symbols that exist at runtime. The symbol listing
can be obtained by running objdump -T
on the other libraries
loaded at runtime.
Once you have the symbols dumped, add the following check
configuration
to Megaton.toml
:
[check]
symbols = [
"path/to/symbol-file.syms",
# ... add more symbol files here
]
Rebuild the project, and the checker should run after linking the ELF
megaton build
If there are false positives, you can add them to the ignore
list:
[check]
ignore = [
"__magic_symbol",
# ... add more symbols to ignore here
]
symbols = [
"path/to/symbol-file.syms",
# ... add more symbol files here
]
Disallowed Instructions
By default, the checker also checks the instructions in the ELF
to make sure there aren't any instructions that will crash
when executed, such as privileged instructions or hlt
.
You can add more instructions to the disallowed list, by specifying a regex pattern for the mnemonic of the instruction:
[check]
disallowed-instructions = [
# ... add more instructions here
]
C/C++ API
The Megaton C/C++ library offers tools for accessing the state of runtime modules, as well as for patching and hooking. A significant portion of the systems for patching and hooking has been derived from exlaunch.
Prelude Includes
Include the <megaton/prelude.h>
header to include common types and macros.
This is sometimes unnecessary if you are including other megaton
API headers.
Private Headers
Headers from megaton/__priv
are internal headers and should not be included
in your project. The functionality is exposed through the public headers,
which provide more ergonomic and secure APIs.
Naming Convention
The megaton public API has the following naming convention:
PascalCase
for structs and classessnake_case
for functionssnake_case_
for macros (with trailing underscore)
Defines
In your project C/C++ sources, you can use these defines provided by megaton
build tool.
If the defines are not resolved correctly by your IDE/LSP server, try megaton build --compdb
to rebuild the compile database and restart your IDE/LSP server.
MEGART_NX_MODULE_NAME
String literal containing the name of the module, for example "my_module"
.
MEGART_NX_MODULE_NAME_LEN
Length of the module name, for example 8
.
MEGART_TITLE_ID
Title ID of the module as a numeric literal, for example 0x0100000000000000
.
MEGART_TITLE_ID_HEX
Hex string literal of the title ID, for example "0100000000000000"
.
Module (C/C++)
Module Information
The <megaton/module.h>
header provides information about the module.
- Module name:
const char* megaton::module_name()
- Length of module name:
size_t megaton::module_name_len()
- Title ID:
u64 megaton::title_id()
- Title ID as hex string:
const char* megaton::title_id_hex()
Module Layout
The <megaton/module_layout.h>
header provides information about
runtime memory layout of the module
See the source file for details
Patching in C++
The C++ Patching API provides a stream-like interface for patching the main binary.
All patching should be done in the main function (i.e. megaton_main
),
since editing the executable is not thread safe.
Stream
The main_stream
function constructs a patching stream starting
at an offset from the main binary. The <<
operator is used
to patch the instructions. The write head will automatically advance
by the number of instructions written.
#include <megaton/patch.h>
void patch() {
megaton::patch::main_stream(0x02345678)
<< 0x1E2E1008 // fmov S8, #1.0
<< 0xBD045BEC; // str, S12, [SP, #0x458]
}
The exl::armv8
types to generate instructions with CPP classes
are deprecated. They will be replaced with something more ergonomic
in the future.
For now, you can use tools like https://armconverter.com to generate the instructions. Make sure to get the correct endianness.
The stream implements the RAII pattern. The cache will be flushed when the stream is destroyed or goes out of scope.
Branching
Use the megaton::patch::b
and megaton::patch::bl
functions
to create branch instructions to a function pointer. The relative
offset is calculated automatically.
#include <megaton/patch.h>
void my_func() {
// Your code here
}
void patch() {
megaton::patch::main_stream(0x02345678)
<< megaton::patch::b(my_func);
}
Skip
Use megaton::patch::skip(n)
to skip n
instructions (i.e. advance
the write head without changing the instruction).
#include <megaton/patch.h>
void patch() {
megaton::patch::main_stream(0x02345678)
<< 0x1E2E1008 // fmov S8, #1.0
<< megaton::patch::skip(2) // skip 2 instructions
<< 0xBD045BEC; // str, S12, [SP, #0x458]
}
Repeat
Use megaton::patch::repeat(n, insn)
to repeated write an instruction n
times.
#include <megaton/patch.h>
void patch() {
megaton::patch::main_stream(0x02345678)
// write 3 nops
<< megaton::patch::repeat(3, exl::armv8::inst::Nop());
}
Hooking in C++
The C++ Hooking API allows injecting custom callbacks into functions in the main module.
There are 3 types of hooks:
- Replace: Replace the original function with your implementation
- Trampoline: Inject a trampoline callback in the original function. You can call the original function in the trampoline.
- Inline: Inject a trampoline callback at an instruction to inspect or modify the registers
To get started, include the <megaton/hook.h>
header, then declare
a hook as a struct with provided macros. The following example
declares a hook to replace a function:
#include <megaton/hook.h>
struct hook_replace_(foo) {
target_offset_(0x00345678)
static int call(int a, int b) {
return a + b;
}
};
Note:
hook_replace_
is macro for defining the hook struct. It can be replaced withhook_trampoline_
orhook_inline_
for a different hook type- The
call
function defines the hook callback (i.e. code to run when the hooked function or code is called). It must bestatic
- For replace and trampoline hooks, the
call
function's signature should match the signature of the hooked function. - The
target_offset_
macro declares the target offset in the main module where the hook will be installed. This is optional.
When hooking a (non-static) member function, the callback should still
be a static function, with the this
pointer passed as the first argument.
For inline hooks, the call
function should take in an InlineCtx
pointer,
which can be used to access the registers
#include <megaton/hook.h>
struct hook_inline_(foobar) {
target_offset_(0x00345678)
static void call(InlineCtx* ctx) {
// Your code here
// read X20
u64 x20 = ctx->x<20>();
// set w8
ctx->w<8>() = 0x12345678;
}
};
Installing the Hook
Once the hook struct is defined, it needs to be installed when your module loads. Your main function's code path should at some point call one of the install functions:
// should be called by your main function
void install_my_hooks() {
foo::install();
foobar::install();
}
The install
function is defined by the target_offset_
macro, and
will install the function at an offset in the main module.
Without target_offset_
, you can also manually specify the hook target
at install time:
foobar::install_at_offset(0x00345678); // install at offset to main module
This is useful if the offset is not known at compile time.
Pointer-to-member-function (PTMF) pitfalls
It's recommended to always use offsets for hooking, as function pointers can have unexpected behavior with compilers. For example, the following code will NOT work:
struct hook_replace_(foo_hook) {
static void call(void* this, int b) {
// ...
}
};
struct Foo {
void foo(int b); // member function to hook, defined in the main module
};
void install_my_hooks() {
foo_hook::install_at(reinterpret_cast<uintptr_t>(&Foo::foo));
}
This is because Foo::foo
is a member function. Pointer-to-member-functions,
or PTMFs, have compiler-specific implementation and may not be a simple
(narrow) pointer. This is especially true for virtual functions.