Decompiling AMX Plugins, Part 2

It’s time to get our hands dirty. Unfortunately, AMX Mod X 1.60 plugins cannot be disassembled yet, so I used AMX Mod ’2005.1 beta’ for this portion. AMX Mod uses Small 2.7.3. For this demonstration, however, you will only need:

Wraith’s AMXReader/Disassembler
Sample .AMX File (we will be decompiling this)

Start up AMXReader. If you don’t feel like compiling it yourself, run AmxReader/Bin/Debug/AmxReader.exe. (As a side note, please keep in mind that ‘AMX’ does not mean ‘AMX Mod’ – AMX is the virtual machine which powers the Small/Pawn scripting language). Proceed to open the file. You will be presented with this screen:

This is the default ‘Winforms’ view type to AMXReader. The other type, ScAsm, is not as useful for disassembling. It’s meant to be fed back into the assembler scasm which I wrote while Wraith worked on his program. Winforms view shows extra information about DAT pieces which will be useful. Anyway, let’s start!

The first thing you should notice is this rather out of place looking line at the top:

 0x0         HALT                    0x0

This is a specific instruction that always exists at COD offset 0 in every plugin. The VM starts the stack by pushing a fake return value – 0 – onto it. When the script finishes executing, the final return call will pop the 0 off the stack as a return-to-cip (code instruction pointer) and jump to the HALT instruction. In this manner, scripts abort without any extra logic once they are done.

Below that, we take the next chunk of code:

 0x8         PROC                        ; plugin_init

Now, we get to actual plugin source code. The first line begins a new procedure (function). The comment to the right tells us it’s named public_init — public functions always have their names listed because the VM has to do name lookups. So, the first line gives us:

public plugin_init()
{
}

This is called the PROLOGUE. Every procedure begins with a PROLOGUE and ends with an EPILOGUE. In between is the procedure logic. This specific function seems to nicely divide into two statements. Let’s take the first one.

 0x18        PUSH.C                 0x28
 0x20        PUSH.C                 0x18
 0x28        PUSH.C                  0x0
 0x30        PUSH.C                  0xC
 0x38        SYSREQ.C      register_plugin
 0x40        STACK                  0x10

This is the basic, classic structure of an AMX native call. They will almost always look the same – PUSH.S, PUSH.C, or PUSHADDR, followed by SYSREQ.C, and then a STACK. We know from amxmod.inc that register_plugin has this prototype:

register_plugin(const plugin_name[], const version[], const author[])

Strings, like all arrays in Small, are passed by reference. This means the native will always receive an offset into the data (DAT) section — also called a DAT relative offset. Luckily, AMXReader attempts to enumerate DAT offsets for you.

The

PUSH.C
opcode pushes a constant onto the stack. Therefore the stack will contain (0×28, 0×18, 0×0, 0xC) right before the call is made.
SYSREQ.C
is always preceded by an instruction pushing the width of the parameters onto the stack, so the final
PUSH.C
is for the number of parameters in bytes. That’s 0xC, which is 12; the size of a 32bit cell is 4, and 12/4 equals 3. So there should be 3 other PUSH instructions, which indeed both matches the disassembly and the requirement of 3 parameters for
register_plugin
.

The DAT section begins with a hardcoded, precompiled chunk of memory. All strings and global arrays are held there. Following precompiled data is the heap and stack, but those don’t concern us here. These three offsets are at the very beginning of DAT, so I expanded the first three DAT string entries in AMXReader:

Bingo! Where AMXReader says ‘Start’ is the offset of that chunk in the DAT section. The first three strings are at 0×0, 0×18, and 0×28. The plugin is pushing these three offsets onto the stack, then using

SYSREQ.C
to call ‘register_plugin’. Because native calls do not correct the stack, the
STACK
opcode is used to remove, here 0×10 (or 16 bytes, or 4 cells) from the stack pointer.

Our reconstructed pseudo-assembly looks like:

push ["BAILOPAN"]
push ["1.0"]
push ["Hello"]
native register_plugin

From these, we have our first line of real code:

public plugin_init()
{
    register_plugin("Hello", "1.0", "BAILOPAN")
}

Tomorrow: Continuing the example, going into floats, branches, and local procedure calls.

Bonus question: Why are parameters pushed in reverse order? (hint: the stack, like all stacks, is top-down, meaning that it starts at the highest possible address. A push grows downward and a pop grows upward).

90 Responses to “Decompiling AMX Plugins, Part 2”

  1. PM says:

    Why are parameters pushed in reverse order? So that the first parameter (=last parameter pushed) is on the lowest memory address on the stack (the stack grows downward). This way the native can access each parameter using params[i] where params is a pointer into the stack and i is the parameter number.

Leave a Reply

You must be logged in to post a comment.