Using Ghidra to patch my keyboard's firmware

The Epomaker Galaxy100 is a very nice, “creamy” sounding keyboard. Unfortunately, in contrast to Keychron’s excellent web-based config UI, it uses the flaky and restricted VIA for configuration. VIA does not let you make some even relatively simple changes to key mappings.

I’m on macOS. A nice feature most keyboard manufacturers have is default support for things like Launchpad and Mission Control through the QMK key codes KC_MISSION_CONTROL and KC_LAUNCHPAD.1 However, I want the function keys to be mapped to F1..=F12 by default, since this makes it easier to use debuggers that rely on the function keys to step through during a session.2 Then I’d have the convenience functionality of Launchpad, Mission Control, and media control gated behind the Fn key.

This is not possible to do in VIA. While you can remap the function keys to F1 and so on just fine, it’s not possible to assign KC_MISSION_CONTROL and KC_LAUNCHPAD. In fact, simply saving the “default” configuration in VIA breaks the existing Mission Control and Launchpad key mapping; restoring the out-of-the-box functionality requires a keyboard reset.

My first thought was just recompiling the firmware with the key mapping between the layers swapped. Unfortunately, the official Epomaker Galaxy100 firmware repo (1) does not compile, (2) provides incorrect instructions for getting into the bootloader, and (3) doesn’t support any wireless connection even if you do get it working. It’s worth noting that this is in violation of the QMK license if that’s what they’re basing the firmware on, a common complaint of the QMK devs.

I find this irksome and have been meaning to play with Ghidra anyway, so I resolved to take a look at the built-in firmware and just swap it myself. My expectation at the start—proved to be correct—was that this would be a simple matter of shuffling some constants around in the flash. So using everyone's favorite NSA-released RE tool is overkill for this, but it’s a learning exercise.

Identifying the microcontroller

A preliminary step in all this is figuring out what we’re dealing with as far as the microcontroller and chipset goes. This is my first nice-sounding keyboard, so I really don’t want to take the chance of messing with the typing noise by opening up the case and potentially rearranging things or whatever (this is mostly superstition). Thankfully, Apple’s systemprofiler utility reports the vendor ID 0x342D, corresponding to Westberry Tech. The product ID does not end up being helpful, but the Epomaker repo discussed earlier includes a JSON file with the bootloader (wb32-dfu) and the microcontroller product ID (WB32FQ95). Searching for this yields this datasheet, which provides the chip (ARM Cortex-M3 using the ARMv7m Thumb instruction set) and the memory map.3

Extracting firmware

Armed with this information, it’s time to get past the first barrier: Dumping the firmware from the keyboard with the hope there are no shenanigans intended to prevent reverse-engineering. From the efforts of some Github users (see Issue #2 in the repo above), I have the correct key sequence for getting into bootloader mode (Fn+L+Esc ). The keyboard flashes the LEDs briefly and becomes unresponsive. Checking systemprofiler shows that there is a device connected by the name of Westberry Device in DFU Mode. Seems promising.

DFU mode means trying dfu-util , a nice GPL utility that implements DFU upload/download. Surprisingly, using dfu-util --list doesn’t work—no devices show up. After doing some more digging, I see that Westberry Tech uses its own DFU update tool called wb32-dfu-updater.4 I’m able to see the device after running wb32-dfu-updater_cli --list. Since the datasheet specifies that the microcontroller has 25KB RAM, we can dump the firmware with wb32-dfu-updater_cli --upload “firmware.bin” --upload-size 256000.5 The result of this is a nice bin file containing the firmware.

Poking around

Let’s see what we get with a hexdump. Since this is an ARM Cortex-based microcontroller, I expect to find an Interrupt Vector Table at the beginning that sets up the main stack pointer, the Reset handler, and so on. Note that Cortex M3 is little-endian.

> xxd -l 128 firmware.bin
    00000000: 0004 0020 b901 0008 bb01 0008 bb01 0008  ... ............
    00000010: bb01 0008 bb01 0008 bb01 0008 bb01 0008  ................
    00000020: bb01 0008 bb01 0008 bb01 0008 ddd0 0008  ................
    00000030: bb01 0008 bb01 0008 bb01 0008 17e3 0008  ................
    00000040: bb01 0008 bb01 0008 bb01 0008 bb01 0008  ................
    00000050: bb01 0008 bb01 0008 d936 0008 1937 0008  .........6...7..
    00000060: 5937 0008 9d37 0008 e137 0008 bb01 0008  Y7...7...7......
    00000070: bb01 0008 bb01 0008 e1e9 0008 bb01 0008  ................

This is as expected: The first two words set up the main stack pointer at 0x20004000 (the datasheet says that we have 36 KiB of SRAM from 0x20000000..=0x20008FFF) and to the reset handler at 0x0800b901 respectively. The address 0x080001bb appears quite frequently and appears to be the default interrupt handler.

At first, I’m confused that we’re seeing 0x08000000 addresses, as I had thought that the IVT would be at 0x0 and we only have 256KB of firmware. As it turns out, 0x0 is aliased to the main flash memory which starts at 0x08000000.

Firing up Ghidra

Okay, let’s load the bin into Ghidra. After setting up the project, we need to select appropriate language. This ends up being trickier than I expected: As far as I can tell, Ghidra has no ARMv7m option. The most appropriate choices seem to be one of ARMv7 (which would be wrong since we only have Thumb2 instructions), ARMv8m (which, while having only Thumb2 instructions, might have changes that break our disassembly), or the generic ARM Cortex. I end up choosing ARM:LE:32:v8m, but after looking into it more I think the correct choice was ARM:LE:32:Cortex, which might actually refer to the ARMv7m instruction set.

After importing the binary file, I take the advice of the Sticky Bits blog of skipping auto analyze until we set up the memory mapping. As discussed earlier, this is important because the “physical”6 address of the flash memory is 0x08000000 which is aliased to 0x00000000. If we don’t set up the correct memory mapping, references will not resolve correctly and Ghidra won’t be able to do its magic. Unlike Niall from Sticky Bits, I leave the base address at 0x0 and add a memory block 0x08000000 of the appropriate length and mark it as an overlay. While it’s true that the base address really is at 0x08000000, telling Ghidra the binary starts at 0x0 helps with auto-labelling the IVT.

With this in mind, the layout we want is like this: The default block is given as the “logical” range 0x00000000 to 0x0003FFFF (name flash). Then we add the range 0x08000000 to 0x0803FFFF (name alias) as an overlay with the same file source as the default block (the file offset stays at 0x0). Next add the SRAM sections, which range from 0x20000000 to 0x20008FFF (name sram). It’s also helpful to add the peripheral sections which range from 0x40000000 to 0x60000000 (name peripheral). It’s very important to annotate these correctly: flash and alias should be marked R^X while sram and peripheral are marked R^W.

Now we can auto-analyze with the default settings. I tried with both the “ARM Aggressive Instruction Finder” setting toggles but didn’t notice a difference.

Remember, our task here is relatively simple: I’m just trying to find the function row and swap the key mapping on the default layer and the fn-pressed layer. But since this is my first toe-dip into the reverse engineering world I wanted to poke around a little more and see what I could learn. I talk about that after I accomplish my immediate goal.

Finding the key codes

The basic strategy is to identify the data the keyboard sends to the OS to indicate keypresses. We should expect to find several tables of similar codes (“layers” in the nomenclature). I suppose they don’t need to be in row order, but that seems the most obvious way to organize it.

What is this data? In short, the key codes are just byte sequences called “Usage IDs” specified by the USB Implementer’s Forum in HID Usage Tables for USB. In practice, QMK has a list of keycodes based on the USB spec. They make some changes to reflect actual usage. For example, the KC_LAUNCHPAD code is 0x00C2 which is defined as Keypad XOR in the USB document.

The sequence I search for is 0x003A..=0x0045, the function key row. I couldn’t figure out how to do this in Ghidra so I just searched for the sequence with grep -abo '\x3a\x00\x3b\x00{so on...} firmware.bin. This prints two matches at 0x1049E and 0x1074A.7 Back in Ghidra, I can see the corresponding offset is definitely what we’re looking for—this row has the escape key (0x0029), not-a-key (0x0000) representing the gap between ESC and F1 on the keyboard, the function keys, then delete, home, end, print screen, and mute (this is what happens if you press the rotary knob).

The keyboard has 19 keys with six rows. I get the layer count by counting the ESC keycodes; there are five layers. Starting with the first instance of the ESC key, I define the datatype in Ghidra as uint16_t[5][6][19]. Since I want the keys labeled, I have Claude write up a script to extract the keycodes defined in the QMK JSON files and use PyGhidra to add the datatype. The data now looks nice and neat:


0801049a 29 00           keycode_t  KC_ESCAPE               [0]
0801049c 00 00           keycode_t  KC_NO                   [1]
0801049e 3a 00           keycode_t  KC_F1                   [2]
080104a0 3b 00           keycode_t  KC_F2                   [3]
080104a2 3c 00           keycode_t  KC_F3                   [4]
080104a4 3d 00           keycode_t  KC_F4                   [5]
080104a6 3e 00           keycode_t  KC_F5                   [6]
080104a8 3f 00           keycode_t  KC_F6                   [7]
080104aa 40 00           keycode_t  KC_F7                   [8]
080104ac 41 00           keycode_t  KC_F8                   [9]
080104ae 42 00           keycode_t  KC_F9                   [10]
080104b0 43 00           keycode_t  KC_F10                  [11]
080104b2 44 00           keycode_t  KC_F11                  [12]
080104b4 45 00           keycode_t  KC_F12                  [13]
080104b6 4c 00           keycode_t  KC_DELETE               [14]
080104b8 4a 00           keycode_t  KC_HOME                 [15]
080104ba 4d 00           keycode_t  KC_END                  [16]
080104bc 46 00           keycode_t  KC_PRINT_SCREEN         [17]
080104be a8 00           keycode_t  KC_AUDIO_MUTE           [18]

The Galaxy100 has a “Mac Mode” you enter by pressing Fn+S. This enables the convenience features like Launchpad and switches positions of the Command and Option keys (called the GUI key and Alt key by QMK). We can find the layers corresponding to “Mac Mode” by checking that the bottom row sequence is CTRL, ALT, GUI. This is the (zero-indexed) Layer 2. The first row is as expected:


08010662 29 00           keycode_t  KC_ESCAPE               [0]
08010664 00 00           keycode_t  KC_NO                   [1]
08010666 be 00           keycode_t  KC_BRIGHTNESS_DOWN      [2]
08010668 bd 00           keycode_t  KC_BRIGHTNESS_UP        [3]
0801066a 20 7e           keycode_t  7E20h                   [4]
0801066c c2 00           keycode_t  KC_LAUNCHPAD            [5]
0801066e 3e 00           keycode_t  KC_F5                   [6]
08010670 3f 00           keycode_t  KC_F6                   [7]
08010672 ac 00           keycode_t  KC_MEDIA_PREV_TRACK     [8]
08010674 ae 00           keycode_t  KC_MEDIA_PLAY_PAUSE     [9]
08010676 ab 00           keycode_t  KC_MEDIA_NEXT_TRACK     [10]
08010678 a8 00           keycode_t  KC_AUDIO_MUTE           [11]
0801067a aa 00           keycode_t  KC_AUDIO_VOL_DOWN       [12]
0801067c a9 00           keycode_t  KC_AUDIO_VOL_UP         [13]
0801067e 4c 00           keycode_t  KC_DELETE               [14]
08010680 4a 00           keycode_t  KC_HOME                 [15]
08010682 4d 00           keycode_t  KC_END                  [16]
08010684 46 00           keycode_t  KC_PRINT_SCREEN         [17]
08010686 a8 00           keycode_t  KC_AUDIO_MUTE           [18]

That mystery key code at position four is the Ctrl-Up sequence. I’m not sure why they didn’t just use KC_MISSION_CONTROL, but I’ll keep it as I’m sure they had a reason. The next layer has the normal function key row.

Before I get to swapping them, it piques my interest that there’s only one reference to this table. Since the primary function of the keyboard is, you know, to transmit keycodes, I would expect there to be a bunch of references to allow resetting key codes and so on.

So I take a look at the one referrer. It ends up being a pretty simple function that takes three values. Here’s the fixed-up decompilation:


keycode_t GetDefaultKeyAt(uint layer,uint row,uint col)

{
  if (((layer < 5) && (row < 6)) && (col < 19)) {
    return KeymapTable[layer][row][col];
  }
  return KC_TRANSPARENT;
}

Okay, simple enough: Check that we’re in bounds of a keypress and return the corresponding key code. Interestingly, this function also has just one caller with this fixed-up decompilation:


void ResetKeycodes(void)

{
  int five;
  undefined4 kc;
  uint masked_layer;
  uint i_layer;
  uint i_row;
  uint i_col;
  uint masked_col;

  i_layer = 0;
  do {
    i_row = 0;
    masked_layer = i_layer & 0xff;
    do {
      i_col = 0;
      do {
        five = return_5();
        masked_col = i_col & 0xff;
        if ((int)i_layer < five) {
          kc = GetDefaultKeyAt(masked_layer,i_row & 0xff,masked_col);
        }
        else {
          kc = 1;
        }
        i_col = i_col + 1;
                /* This writes the corresponding key code to SRAM somewhere */
        WriteKeycodeOffset(masked_layer,i_row & 0xff,masked_col,kc);
      } while (i_col != 0x13);
      i_row = i_row + 1;
    } while (i_row != 6);
    five = return_five_();
    if ((int)i_layer < five) {
            /* This is interesting. There's a table with a sequence of VOL_UP and
             VOL_DOWN key codes which I think is used by the rotary control. */
      kc = RotaryCodeStuff(masked_layer,0,1);
      WriteRotaryCode(masked_layer,0,1,kc);
      kc = RotaryCodeStuff(masked_layer,0);
    }
    else {
      WriteRotaryCode(masked_layer,0,1);
      kc = 1;
    }
    i_layer = i_layer + 1;
    WriteRotaryCode(masked_layer,0,0,kc);
  } while (i_layer != 5);
  return;
}

As you’ll know by the naming, it turns out the key code table in the flash is only read when resetting the key mapping. User-defined key mappings must live somewhere else. I have some brief commentary in the function body—I didn’t investigate the rotary knob functions that much since it was out of scope from what I was trying to do.

The outcome of this analysis is to confirm that overwriting the keycodes in the flash will indeed do what I want it to do (so long as I don’t use VIA to set a user-defined keymap).

Patching the binary

The two ranges of interest are the layer two top row from 0x00010662..=0x00010686 and the layer three top row from 0x00010746..= 0x0001076a (note that these are file offsets, which is why we don’t use the 0x080xxxxx range). To patch the binary, we just need to swap these two byte ranges.

I end up just doing this in Hex Fiend, which is simple enough: Navigate to the offset and copy/paste as needed 8. Save the binary, and voilà!

Getting the modified firmware back on the keyboard is also quite easy. We just specify the source file and the destination address. I wasn’t sure whether wb32-dfu-updater_cli wanted 0x0 or 0x080000000 as the destination address and the small --help text was not illuminating. Checking the source, I see that they will reject any address that’s out-of-bounds of 0x080000000 plus flash size, so I’m reassured. Updating the firmware is then just a matter of passing wb32-dfu-updater_cli --dfuse-address 0x08000000 —download firmware_modded.bin && wb32-dfu-updater_cli --reset. The keyboard reboots and my function keys are the way I want them!

Poking around in Ghidra

As I said, this was my first opportunity to use Ghidra, so I wanted to do some more investigating and looking around. So let’s take another look at the binary.

Vector table

At the start of the binary we’ll see the IVT, which we saw on the hexdump. Ghidra labels this for us, but it’s also documented on the ARM website.


                     MasterStackPointer
00000000 00 04 00 20     addr       DAT_20000400
                     Reset
00000004 b9 01 00 08     addr       DAT_080001b9
                     NMI
00000008 bb 01 00 08     addr       DAT_080001bb
                     HardFault
0000000c bb 01 00 08     addr       DAT_080001bb
                     MemManage
00000010 bb 01 00 08     addr       DAT_080001bb
                     BusFault
00000014 bb 01 00 08     addr       DAT_080001bb
                     UsageFault
00000018 bb 01 00 08     addr       DAT_080001bb
                     Reserved1
0000001c bb 01 00 08     addr       DAT_080001bb
                     Reserved2
00000020 bb 01 00 08     addr       DAT_080001bb
                     Reserved3
00000024 bb 01 00 08     addr       DAT_080001bb
                     Reserved4
00000028 bb 01 00 08     addr       DAT_080001bb
                     SVCall
0000002c dd d0 00 08     addr       DAT_0800d0dd
                     Reserved5
00000030 bb 01 00 08     addr       DAT_080001bb
                     Reserved6
00000034 bb 01 00 08     addr       DAT_080001bb
                     PendSV
00000038 bb 01 00 08     addr       DAT_080001bb
                     SysTick
0000003c 17 e3 00 08     addr       DAT_0800e317
        

The table mostly contains pointers to the address 0x080001bb, which must be the DefaultHandler. The exceptions are the initial stack pointer, which points into SRAM, and the following handlers:

Additionally, 40 IRQs are defined, most of which point to the default handler. 12 of them point to somewhere else:

IRQ Index Address
6 0x080036D9
7 0x08003719
8 0x08003759
9 0x0800379D
10 0x080037E1
15 0x0800E9E1
17 0x08003825
26 0x0800E5CD
27 0x0800E5E1
30 0x0800E335
31 0x080038C5
33 0x08003979

Inspecting the handlers

A good place to start seems to be to understand what the handlers are doing. Let’s look at the default handler first, which you’ll recall is at address 0x080001BB. Since ARM Cortex uses Thumb instructions, we mask the LSB and get the actual address 0x080001BA (from now on I’ll just use the actual addresses with the LSB masked), the contents of which are here:

080001ba 00 f0 00 f8     bl         Default_Handler
 080001be fe e7           b          Default_Handler

As you can see, this comprises two instructions: First, Branch with Link to next instruction (bl), where the next instruction is at offset 0 (so don’t move the program counter, which is already pointing to the next instruction); then unconditionally branch with an offset of -4 bytes (so the PC never moves). This produces an infinite loop, as we would expect for unimplemented handlers (I named this Default_Handler in Ghidra). 9

The Reset Handler

Next let’s look at the reset handler, which is at 0x080001b8. This address contains a branch instruction to 0x080000e0, which is the actual reset handler function. Here’s the naive decompilation:


void UndefinedFunction_080001b8(void)
{
  bool bVar1;
  char cVar2;
  undefined4 *puVar3;
  undefined4 *puVar4;
  disableIRQinterrupts();
  bVar1 = (bool)isCurrentModePrivileged();
  if (bVar1) {
    setMainStackPointer(&DAT_20000400);
  }
  bVar1 = (bool)isCurrentModePrivileged();
  if (bVar1) {
    setProcessStackPointer(&DAT_20000c00);
  }
  _DAT_e000ed08 = 0x8000000;
  bVar1 = (bool)isCurrentModePrivileged();
  if (bVar1) {
    setThreadModePrivileged(1);
    bVar1 = (bool)isThreadMode();
    if (bVar1) {
      cVar2 = isUsingMainStack();
      setStackMode(cVar2 == '\x01');
    }
  }
  InstructionSynchronizationBarrier(0xf);
  FUN_flash__0800cb10(2);
  FUN_flash__0800c6cc();
  for (puVar3 = &DAT_20000000; puVar3 < &DAT_20000400; puVar3 = puVar3 + 1) {
    *puVar3 = &DAT_55555555;
  }
  for (puVar3 = &DAT_20000400; puVar3 < &DAT_20000c00; puVar3 = puVar3 + 1) {
    *puVar3 = &DAT_55555555;
  }
  puVar3 = &DAT_flash__0801138c;
  for (puVar4 = &DAT_20000c00; puVar4 < &DAT_20001188; puVar4 = puVar4 + 1) {
    *puVar4 = *puVar3;
    puVar3 = puVar3 + 1;
  }
  for (puVar3 = &DAT_20001188; puVar3 < &DAT_20004228; puVar3 = puVar3 + 1) {
    *puVar3 = 0;
  }
  FUN_flash__0800cb18();
  FUN_flash__0800cb12();
  for (puVar3 = (undefined4 *)0x80000e0; (int)puVar3 < (int)(undefined *)0x80000e0;
      puVar3 = puVar3 + 1) {
    (*(code *)*puVar3)();
  }
  FUN_flash__08007714();
  for (puVar3 = (undefined4 *)(undefined *)0x80000e0; (int)puVar3 < (int)(undefined *)0x80000e0;
      puVar3 = puVar3 + 1) {
    (*(code *)*puVar3)();
  }
  do {
                    /* WARNING: Do nothing block with infinite loop */
  } while( true );
}
        

Ghidra very helpfully names some special functions for us, all related to low-level processor-y things. What’s more interesting to me is everything below line 28, which has the setup logic. Let’s go step-by-step. (Note that for brevity, I’m going to name everything in the flash block by the suffix of the address).

The function at cb10 is a noop, so we can skip that. The next one (at c6cc) is a wrapper around three functions at c12c, df20, and c6c8.

Hardware initialization

This function initializes the hardware by calling two functions:

c12c looks to be a bootloader entry check:


void BootloaderEntryCheck
                (undefined4 param_1,undefined4 param_2,undefined4 param_3,undefined4 param_4)

{
  bool bVar1;
  char cVar2;
                    /* Check if magic RAM location contains value DEADBEEF */
  if (DAT_20006ffc == 0xDEADBEEF) {
    DAT_20006ffc = 0;
    bVar1 = (bool)isCurrentModePrivileged();
    if (bVar1) {
      setThreadModePrivileged(1);
      bVar1 = (bool)isThreadMode();
      if (bVar1) {
        cVar2 = isUsingMainStack();
        setStackMode(cVar2 == '\x01');
      }
    }
    bVar1 = (bool)isCurrentModePrivileged();
    if (bVar1) {
      setMainStackPointer(_DAT_1fffe000);
    }
    enableIRQinterrupts();
    (*_DAT_1fffe004)(param_1,0xdeadbeef,_DAT_1fffe000,_DAT_1fffe004,param_4);
    do {
                    /* WARNING: Do nothing block with infinite loop */
    } while( true );
  }
  return;
}
        

My interpretation is that entering the bootloader sequence places 0xDEADBEEF into SRAM at a known address and then executes the reset handler. The reset handler checks if the sequence is there, and if so, initiates a move to supervisor mode and calls the system function at 0x1fffe004 (which is noted as “system memory” in the datasheet). The infinite loop indicates that this function is not expected to return from bootloader mode; the user just resets the device.

Also interesting is that Ghidra misidentifies the function as taking four parameters. I think this is because the registers r0 to r3 are used for executing the function. Here’s the relevant disassembly with annotations (note that I am very much not an assembly programmer so take this with a grain of salt, as its cribbed from the ARM docs), which show that the registers are all loaded by the function itself.


ldr    r2, [=0x20007000]         ; Load base address
push   {r3, lr}                  ; Save r3, lr to the stack
ldr.w  r1, [r2, #-4]             ; r1 = value at 0x20006ffc
ldr    r3, [=0xDEADBEEF]         ; r3 = magic value
cmp    r1, r3                    ; Compare
bne    /* exit */                ; Jumps to pop {r3, pc}
movs   r3, #0                    ; r3 = 0
str.w  r3, [r2, #-4]             ; Clear magic value at 0x20006ffc
msr    control, r3               ; CONTROL = 0 (privileged, MSP)
ldr    r3, [=0x1fffe000]         ; r3 = bootloader vector table address
ldr    r2, [r3, #0]              ; r2 = initial SP from vector table
msr    msp, r2                   ; Set stack pointer
cpsie  i                         ; Enable interrupts
ldr    r3, [r3, #4]              ; r3 = reset handler from vector table
blx    r3                        ; Jump to bootloader
        

df20 loads a bunch of data into the peripheral memory area, presumably for initialization, and c6c8 is a noop (there are a lot of noop functions… I don’t know why.)

Memory initialization loops

Next, the ResetHandler paints (1) the area beneath the stack with the value 0x55555555, then (2) the stack itself with the same value, probably to aid with debugging stack overflow issues.


            for (puVar3 = &DAT_20000000; puVar3 < &DAT_20000400; puVar3 = puVar3 + 1) {
    *puVar3 = &DAT_55555555;
}
for (puVar3 = &DAT_20000400; puVar3 < &DAT_20000c00; puVar3 = puVar3 + 1) {
    *puVar3 = &DAT_55555555;
}
        

The next loop copies data from the ROM to the RAM. The loop covers the entire .data section, including the default key mapping we saw earlier, and is mostly null bytes.

puVar3 = &DAT_flash__0801138c;
for (puVar4 = &DAT_20000c00; puVar4 < &DAT_20001188; puVar4 = puVar4 + 1) {
*puVar4 = *puVar3;
puVar3 = puVar3 + 1;
}
        

Other memory initialization

The function at cb18 looks like this:

void InitMemoryRegions(void)

{
  undefined4 *puVar1;
  int iVar2;
  undefined4 *puVar3;
  undefined4 *puVar4;
  undefined4 *puVar5;
  uint uVar6;
  int *piVar7;
  int iVar8;

  iVar8 = 8;
  piVar7 = &DAT_alias__08011144;
  do {
    puVar4 = (undefined4 *)piVar7[2];
    puVar5 = (undefined4 *)(*piVar7 + -4);
    puVar1 = (undefined4 *)piVar7[1];
    for (puVar3 = puVar1; puVar3 < puVar4; puVar3 = puVar3 + 1) {
      puVar5 = puVar5 + 1;
      *puVar3 = *puVar5;
    }
    uVar6 = (int)puVar4 + (3 - (int)puVar1) & 0xfffffffc;
    if (puVar4 < (undefined4 *)((int)puVar1 - 3U)) {
      uVar6 = 0;
    }
    iVar2 = (int)puVar1 + uVar6;
    uVar6 = (piVar7[3] + 3U) - iVar2 & 0xfffffffc;
    if ((uint)piVar7[3] < iVar2 - 3U) {
      uVar6 = 0;
    }
    __memset(iVar2,0,uVar6);
    iVar8 = iVar8 + -1;
    piVar7 = piVar7 + 4;
  } while (iVar8 != 0);
  return;
}
        

The data at 11144 is a set of memory addresses: One to a set of null bytes in the flash, a couple to RAM regions, and several 0x0. This one puzzles me; I think this is linker-generated .bss related stuff, but why it is loading from memory addresses with all zeroes is a mystery to me.

The final section

The decompilation obscures the true purpose here. The two loops before and after Main() just walk the pre-init and fini arrays. The Main() function is the real entry point. Diving into that will have to be its own post.

The final loop just indicates that the Reset_Handler never returns—it delegates to Main and that’s that.


  1. Curiously, the Galaxy100 maps Mission Control to an emulated Ctrl-Up keypress (the default keybinding on macOS). While KC_LAUNCHPAD works fine, KC_MISSION_CONTROL does not seem to. ↩︎

  2. Also, it’s easier to see what your teammates are doing on League of Legends. ↩︎

  3. Kudos to Westberry for well-written documentation. ↩︎

  4. This tool is MIT licensed. Given that CLI options are almost the same as those in dfu-util, I at first thought this was improperly relicensed fork of dfu-util. As it happens, the codebase is quite different. ↩︎

  5. I verified that trying any larger value results in an error. It will happily give you smaller values, though: If you pass no --upload-size option, you’ll get 0 bytes. ↩︎

  6. This is in scare quotes because I don’t know if this is reflects the physical layout of the flash or if its an abstraction over the actual layout or if this even matters anymore ↩︎

  7. Amusingly, the ASCII interpretation of this is the sequence :;<=>?@ABCDE ↩︎

  8. I use Maccy for clipboard management, which means that I don’t need to store the sequences in an intermediate file. I highly recommend Maccy! ↩︎

  9. I don’t know much about assembly, but I suppose this could be accomplished with just the second instruction? ↩︎