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.

Warning

Currently, the installation is only tested on Linux.

Install

  1. 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
    
  2. Build the tool
    cargo run --release --bin megaton-buildtool -- install
    
  3. Add the bin directory to your PATH. For example, add the following to your shell profile:
    export PATH=$PATH:path/to/install/bin
    
  4. 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

Warning

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

Info

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

Tip

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

Note

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.

Info

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 and megaton clean to clear env cache and build cache
  • Updating the megaton tool or library
    • Run megaton build --lib to rebuild the library
  • Outputs/timestamps are manually changed
    • Run megaton clean to clear build cache

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.

Info

Paths are relative to the project root (where Megaton.toml is located).

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).

Info

Paths are relative to the project root (where Megaton.toml is located).

Tip

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 processes
  • c : 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>"]

Warning

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
commonNone
ccommon
cxxc
ascpp
ldcommon

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.

Danger

Rust support is not yet available

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]

Danger

Rust support is not yet available

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.

Note

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

Info

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:

  1. Specify which profile to use when megaton build is run without -p/--profile
  2. 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)

Tip

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
]

Info

Like other paths in Megaton.toml, paths are relative to the project root.

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
]

Tip

Any symbol that starts with . are automatically ignored.

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 classes
  • snake_case for functions
  • snake_case_ for macros (with trailing underscore)

Defines

In your project C/C++ sources, you can use these defines provided by megaton build tool.

Tip

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.

Tip

Most of the defines can be accessed through a C++ API instead of macros.

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.

Warning

The actual flag passed to compiler is the value in decimal, not hex.

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]
}

Note

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 with hook_trampoline_ or hook_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 be static
  • 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.

Warning

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.