Draft
Conversation
91420a8 to
eaec91e
Compare
39ff202 to
7dd602d
Compare
cad4e77 to
61724f0
Compare
2da1c77 to
5af0610
Compare
7f3c528 to
1102795
Compare
The previous implementation temporarily placed the .kamek (hooks) section into the simulated Wii memory just after the .bss section. Any relocations that applied to this address range were assumed to apply to the hook data, which was fine because there was no way to violate that assumption at the time. However, with the new code-injection feature, it's now possible to inject code into arbitrary static address ranges. If building in -static mode, that means that .kamek is given a fixed, absolute address range, which can potentially conflict with one or more injected code sections. It's a rare edge case, but it can happen. When it does, Kamek can no longer distinguish between relocations meant to apply to the injected section(s) and ones meant to apply to the hook data, which can lead to exceptions or incorrect behavior. To fix this, this commit makes Kamek keep track of the .kamek section data *separately*, outside of the Wii address space entirely. This somewhat complicates the logic, unfortunately, but it fixes the root cause of the issue.
1102795 to
30cbc0a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Status of this PR: Tested and working in both NSMBW-Updated and a linkage stress-test I made, which will be added to #52 once this PR is merged. I also have some optimizations in mind, but I plan to save those for follow-up PRs.
This PR adds a new family of macros that let you "inject" code directly into a specified address range:
C++ (kamek.h):
kmWriteDefAsm(startAddr[, endAddr[, flags[, pad]]]) { ... }kmWriteDefCpp(startAddr, endAddr, returnType, ...) { ... }kmWriteNops(startAddr, endAddr);Assembly (kamek_asm.S):
kmWriteDefStart startAddr[, endAddr[, flags[, pad]]]+kmWriteDefEndkmWriteNops startAddr, endAddrThis can make certain types of patches cleaner, as well as help reduce patches' memory footprints.
For example, this patch from NSMBW-Updated:
Can now be written as simply:
Or as a larger example, this patch:
Can be rewritten as:
Compared to assembling instructions by hand, as in the above examples, not only is this more convenient, but since it's properly integrated with Kamek, relocations are supported. That means your injected code can reference game addresses and other custom-code addresses, and Kamek will ensure everything is linked and translated appropriately, just like with other patch types.
For whole-function replacements that were previously done with
kmBranchand related macros, usingkmWriteDefCpp/kmWriteDefAsmsaves memory because no additional memory is required at all.1 The downside, of course, is that replacement functions cannot be larger than the originals when patched in this way.Usage Details
C++ macros
kmWriteDefAsm(startAddr[, endAddr[, flags[, pad]]]) { ... }Inject assembly code directly into the target executable, overwriting whatever's there.
startAddrandendAddrare the addresses of the first and last instructions to replace. IfendAddris omitted, it defaults tostartAddr.flagsspecifies options:KM_INJECT_STRIP_BLR_PAST: If the specified address range has size N, and the compiled code has size exactly N+4, and the last four bytes are ablrinstruction, delete the instruction instead of raising a link-time error.Rationale
This flag makes it possible to write simple patches likeThis is toggleable because implicit
blrstripping may be undesirable in some cases.KM_INJECT_ADD_PADDING: If the specified address range has size N, and the compiled code has size < N, pad the remaining space with the provided pad value. (If this flag isn't set, the remaining space is left untouched.)Rationale
This flag makes it convenient to replace large chunks of code with a smaller amount of replacement code, without having to carefully count the number of instructions and add an appropriate amount of explicit nops to reach the right length.This is toggleable because if the user intends to (say) replace an entire function, padding is unnecessary and would only serve to bloat the compiled patch file size.
The default is for both of the above flags to be enabled.
padis the value to fill extra space with ifKM_INJECT_ADD_PADDINGis set. Default is nop (0x60000000).In most cases,
startAddrandendAddrwill be all you need.flagsandpadare expected to be used only rarely.kmWriteDefCpp(startAddr, endAddr, returnType, ...) { ... }Inject a C++ function defined directly underneath into the target executable, overwriting whatever function is already there.
startAddrandendAddrare the same as inkmWriteDefAsm, and the remaining argument(s) are the same as inkmBranchDefCpp/kmCallDefCpp.Regarding
These values are not configurable for this macro, and are both hardcoded to 0. This is because I can't think of any non-contrived use cases for them, and unlike inflagsandpadkmWriteDefAsm, they would always muddy up the macro signature: they can't be added as optional arguments, because the macro already uses optional arguments to collect function arguments.If a use case is found in the future, we can add a
kmWriteDefCppExmacro that includes the extra arguments, or something. But for now, I don't see the point.kmWriteNops(startAddr, endAddr);This is syntactic sugar for (conceptually)
kmWriteDefAsm(startAddr, endAddr) { }.To write a single nop, use
kmWriteNop(addr)(already in Kamek, not added by this PR).Assembly macros
kmWriteDefStart startAddr[, endAddr[, flags[, pad]]]+kmWriteDefEndEquivalent to
kmWriteDefAsmin C++.The use of an "end" macro is unfortunately required for technical reasons.
kmWriteNops startAddr, endAddrEquivalent to
kmWriteNopsin C++.kmWriteNopexists, too.Kamekfile format and loader changes
To support this new feature, the loader is updated to use the new Kamekfile v3 format, which adds a new
kWriteRangecommand type. The updated loader also now requires game harnesses (e.g. nsmbw.cpp) to supply amemcpypointer in theloaderFunctionstable.It's worth pointing out that #59 also increments the Kamekfile version field, currently to 3. Whichever PR is merged second will have its version number changed to 4 instead.
Implementation notes
Use of sections
Injected functions are placed in their own sections called
.km_inject_<unique identifier>.Unlike other hook types, which use structs in the
.kameksection to declare their metadata, each injected function declares its metadata in another bespoke section,.km_inject_<unique identifier>_meta. This is done to simplify the implementation. Kamek needs to determine every section's base address extremely early in the linking process, and for injection sections, this is part of the hook metadata. On the other hand, hooks in the.kameksection are the last thing the Linker class processes: it requires relocations to have already been processed, which requires symbols to have already been processed, which requires all sections to already have their base addresses assigned.In earlier versions of this PR, I experimented with techniques like using multiple processing "passes" or adding stripped-down versions of the symbol-, relocation-, and hook-processing functions so that injection metadata could be parsed earlier. These all ended up much more complicated and ugly than the final approach. Raw section data (apart from relocations, which we don't need for this) is available right at the start of linking, so using extra sections like this is a simple way to convey the information in a way that can be easily parsed when we need to. And just like the
.kameksection, these extra metadata sections are discarded during the linking process, so this design choice doesn't affect the final output at all.Not planned
Using symbol names as targets instead of explicit addresses. For example:
Rationale
Not only would supporting this make the implementation more complex, but the only possible use case for it would be to replace a function with ablr...Discussion: End-address inclusivity/exclusivity
There's an argument to be made for making
endAddrexclusive rather than inclusive. Essentially, instead of thinking of it as "the last instruction to be overwritten", it would instead refer to "the next instruction to be run". This would parallel very nicely withkmBranchDefAsmand itsexitPointparameter.A much older version of this branch, from before this PR was made, implemented it that way. However, after thinking about it for a while, and discussing with others, it was decided that the exclusive-upper-bound semantics would be too unintuitive, even if it would match
kmBranchDefAsmbetter. So it was changed.Footnotes
After the loading process is completed during game boot, at least. Also, some memory might still be required for things like static local variables and string constants, if the new function uses any. ↩