Skip to content

Commit f90daef

Browse files
authored
Merge pull request #72 from BrianPugh/header-meta-data
store firmware metadata in vector table
2 parents 6c1cb0e + 832fddc commit f90daef

File tree

2 files changed

+84
-2
lines changed

2 files changed

+84
-2
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,22 @@ Main stages to developing a feature:
131131
3. Implement your own function, possibly in `Core/Src/main.c`. There's a good chance your custom function will call the function in (2). You will also probably have to add `-Wl,--undefined=my_custom_function` to `LDFLAGS` in the Makefile so that it doesn't get optimized out as unreachable code.
132132
4. Add a patch definition to `patches/patches.py`.
133133

134+
### Protocols
135+
We store information about how much external flash this firmware uses at internal-flash offset `0x01B8`.
136+
This location is in the HDMI-CEC handler in the vector-table; this is unused for all game-and-watch purposes, so serves as a safe, standardized spot to put a bit of data.
137+
At this location we store the following data:
138+
139+
```C
140+
struct {
141+
external_flash_size: 24; // This value * 4096 is the amount of external flash used by this firmware.
142+
must_be_4: 4; // "magic" value that would never be valid in any real hdmi-cec firmware. 0x4 is in the hardware perhipheral space.
143+
is_mario: 1; // This is the mario firmware.
144+
is_zelda: 1; // This is the zelda firmware.
145+
_unused: 2; // Reserved for future use. All 0 for now.
146+
}
147+
```
148+
149+
134150
# Journal
135151
This is my first time ever developing patches for a closed source binary. [I documented my journey in hopes that it helps other people](docs/journal.md). If you have any recommendations, tips, tricks, or anything like that, please leave a github issue and I'll update the documentation!
136152

patches/firmware.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import hashlib
2+
import struct
3+
from dataclasses import dataclass
24

35
from colorama import Fore, Style
46
from Crypto.Cipher import AES
@@ -12,7 +14,7 @@
1214
ParsingError,
1315
)
1416
from .patch import FirmwarePatchMixin
15-
from .utils import round_down_word, round_up_word
17+
from .utils import round_down_word, round_up_page, round_up_word
1618

1719

1820
def _val_to_color(val):
@@ -40,6 +42,55 @@ def __repr__(self):
4042
return "\n".join(substrs)
4143

4244

45+
METADATA_MAGIC = 0x4 # chosen because no real executable code starts with address 0x4
46+
47+
48+
@dataclass
49+
class HeaderMetaData:
50+
"""4 bytes of data that can be stored at the hdmi-cec handler in the vector-table (0x01B8)"""
51+
52+
external_flash_size: int # Actual size in bytes (will be divided by 4096 for storage)
53+
54+
# 4 LSb are used for MAGIC
55+
is_mario: bool # 1 bit
56+
is_zelda: bool # 1 bit
57+
# 2 MSb are free
58+
59+
def pack(self) -> bytes:
60+
# Convert size to 4K blocks (right shift by 12)
61+
blocks_4k = round_up_page(self.external_flash_size) >> 12
62+
63+
# Ensure the shifted value fits in 3 bytes
64+
if not (0 <= blocks_4k < (1 << 24)):
65+
raise ValueError(
66+
"external_flash_size must fit in 3 bytes when divided by 4096"
67+
)
68+
69+
# Pack the flags into a single byte
70+
flags = METADATA_MAGIC | (int(self.is_mario) << 4) | (int(self.is_zelda) << 5)
71+
72+
# Pack as little-endian:
73+
# - First 3 bytes: external_flash_size
74+
# - Last byte: flags
75+
return struct.pack("<I", (blocks_4k & 0xFFFFFF) | (flags << 24))
76+
77+
@classmethod
78+
def unpack(cls, data: bytes) -> "HeaderMetaData":
79+
# Unpack the 32-bit value
80+
[value] = struct.unpack("<I", data)
81+
82+
# Extract fields
83+
# Convert from 4K blocks back to bytes (left shift by 12)
84+
external_flash_size = (value & 0xFFFFFF) << 12
85+
flags = (value >> 24) & 0xFF
86+
if (flags & 0x0F) != METADATA_MAGIC:
87+
raise ValueError("Invalid Magic.")
88+
is_mario = bool(flags & (1 << 4))
89+
is_zelda = bool(flags & (1 << 5))
90+
91+
return cls(external_flash_size, is_mario, is_zelda)
92+
93+
4394
class Firmware(FirmwarePatchMixin, bytearray):
4495

4596
RAM_BASE = 0x02000000
@@ -675,8 +726,23 @@ def move_to_compressed_memory(self, ext, size, reference):
675726
return new_loc
676727

677728
def __call__(self):
729+
from . import MarioGnW, ZeldaGnW
730+
678731
self.int_pos = self.internal.empty_offset
679-
return self.patch()
732+
out = self.patch()
733+
is_mario, is_zelda = False, False
734+
if isinstance(self, MarioGnW):
735+
is_mario = True
736+
elif isinstance(self, ZeldaGnW):
737+
is_zelda = True
738+
metadata = HeaderMetaData(
739+
external_flash_size=len(self.external),
740+
is_mario=is_mario,
741+
is_zelda=is_zelda,
742+
)
743+
# hdmi-cec = 0x01B8; not used in the gnw hardware.
744+
self.internal.replace(0x01B8, metadata.pack())
745+
return out
680746

681747
def patch(self):
682748
"""Device specific argument parsing and patching routine.

0 commit comments

Comments
 (0)