Assembly tricks - Printable Version + open.mp forum (https://forum.open.mp) -- Forum: SA-MP (https://forum.open.mp/forumdisplay.php?fid=3) --- Forum: Tutorials (https://forum.open.mp/forumdisplay.php?fid=37) --- Thread: Assembly tricks (/showthread.php?tid=2321) |
Assembly tricks - Y_Less - 2022-09-18 Over the years of writing a lot of assembly in pawn I?ve developed a few tricks for things.? These are just some of them. Calling a function by variable.
Normally to call a function you use CALL, but that only takes a constant, not a variable.? If you want to load the target address from a variable you instead need to use SCTRL to set the target.? But CALL does some other bits as well like setting the return address, which must be done manually with SCTRL using LCTRL.? LCTRL gets the current instruction pointer, but the return address should be the instruction after the SCTRL.? With this snippet it turns out that that return address is exactly nine cells later, so the code to call a function stored in a local variable called func is: Pawn Wrote:#emit LCTRL? ? ? 6 Control register 8
If you?re using the JIT plugin the return address needs to be modified to a new address space.? This is what LCTRL 8 does.? It takes the AMX address in pri and converts it to a JIT address.? However if you?re not using the JIT plugin there is no register eight and pri remains the same.? So always using it is perfectly safe - either the address will be correctly updated or it won?t change at all. Named registers.
That code is already a bit unreadable.? I said the return address is nine cells later, yet 9 doesn?t appear anywhere in the code.? Also what are 6 and 8 doing?? Fortunately const values work in #emit, so we can name some of these constants and make them more readable.? The previous snippet thus becomes: Pawn Wrote:#emit LCTRL? ? ? __cip [See this file]() for the full list of constant offsets defined in YSI, there are too many to list here.? But a few highlighted ones are:
So for example to load the number of parameters passed to the current function use: Pawn Wrote:#emit LOAD.S.pri __args_offset // In bytes. Or using another YSI define: Pawn Wrote:#emit LOAD.S.pri __args_offset // In bytes. Current stack address.
This code will get the current address of the stack pointer (stk): Pawn Wrote:#emit LCTRL __stk But it will get the value in to the pri register while sometimes you want it in the alt register instead.? You could load it and move it: Pawn Wrote:#emit LCTRL __stk But that takes two instructions and clobbers pri.? You could swap the registers about to preserve pri: Pawn Wrote:#emit MOVE.alt But that takes even more instructions.? Or you could just load the value straight in to alt: Pawn Wrote:#emit STACK 0 This adjusts the stack size by naught cells.? It doesn?t get any bigger or smaller.? At first that seems pointless, but pawn-impl.pdf says this about STACK: Quote:ALT = STK, STK = STK value So the instruction first saves the current value of stk in alt, then adjusts the size.? We only want the first part so we NOP the second part by making the size adjustment naught, yet still get the register being saved. Masking with shifts.
There is no AND.C so to do: Pawn Wrote:a = b & 0xFFFF0000; In assembly is: Pawn Wrote:#emit LOAD.S.pri b That?s usually fine, but what if there is some data we want to keep in alt?? It could be saved to the stack: Pawn Wrote:#emit LOAD.S.pri b But that takes many extra instructions.? But this code is the same as: Pawn Wrote:a = (b >>> 16) << 16; >>> and << lose data, and there are SHR.C and SHL.C instructions: Pawn Wrote:#emit LOAD.S.pri b This is one cell longer than the first method, but two instructions shorter than the second and doesn?t clobber alt.? However, it doesn?t work for masks like 0x00FF0000 without yet another shift, at which point it probably isn?t worth the effort compared to AND, nor at all for any mask with naughts in like 0xF0F0F0F0: Pawn Wrote:#emit LOAD.S.pri b Escaping DAT
The data segment is called dat, and is distinct from the code segment cod.? The former is where all data is stored, including the stack and heap, the latter is where all the code is stored.? But to write self-modifying code of the sort found in @emit we must write to cod instead of dat, so how is that done?? The instruction STOR.pri writes the value of the register pri to the given variable, but the VM checks that these variables are valid, i.e. that they are in some active part of global memory the stack.? But there is an oversight in the indirection instructions - SREF.pri, LREF.S.alt, etc.? These take a variable and check that this variable is within the memory segment, then use the value in that variable as a pointer to the location to write to.? However, crucially this second access is NOT checked for validity, so we can write anywhere.? The code section comes immediately before the data section so we can write there using negative addresses.? The offset of dat is found in LCTRL 1, the offset of cod is in LCTRL 0, so the two together give the relative offset of cod from dat.? For example to read the parameter of HALT at address 0 in cod: Pawn Wrote:new ptr; This trick is basically the core of amx_assembly, indirection, YSI, and more.? Without it there would be no code re-writing at all of the sort needed for advanced techniques like hook and inline. You can invert strings.
For some reason this code compiles: Pawn Wrote:Func(const str[]) This is likely an oversight in the compiler because the inversion of a string is gibberish, the code will probably just crash.? But inverting an inversion gives the original value back so this is fine: Pawn Wrote:Func(const str[]) It might seem pointless but in assembly you end up with: asm Wrote:ADDR.pri str Still pointless, except for the fact that it is a very interesting sequence of instructions that can be scanned for.? This INVERT/INVERT pair is the core of how y_inline actually locates inline functions and their names in memory (searching through the dat segment with LREF as detailed above). Labels reset the stack.
Pawn Wrote:Func() Compiles as: asm Wrote:PROC So we push something to the stack, then reset the stack.? This is exploited in the decl keyword which declares a large variable without initialising it: Pawn Wrote:decl a[128]; Becomes: Pawn Wrote:goto after_a; Which compiles as: asm Wrote:JUMP after_a Most importantly the FILL opcode is skipped, but because labels don?t modify scope a still exists. There is at least one bit of assembly in YSI where the LCTRL/SCTRL pair generated by a label are important to functionality (Inline_NumArgs to cancel out a PUSH.pri elsewhere in the function), but they?re extremely rare for one reason - you can only jump backwards in assembly, future labels can?t be used. Starting a function just to end it.
Consider the following function: Pawn Wrote:sprintf(const fmat[], {Float, _}:...) There are four main ways to implement this function and forward all the parameters - 1) a macro, 2) y_va, 3) copy all the parameters as in the very common bit of assembly everyone copies, 4) the clever way.? We already have most of the parameters for format on the stack, we just need to add two more.? To call a function the number of parameters in bytes is pushed, then the function is called with CALL, which adds the return address to the stack and jumps to the address specified.? The first thing in a function is then PROC, which saves the current frame pointer to the stack and sets up a new frame.? So at the moment that format is called in this function the stack looks like:
target is declared static in this example to make the stack much simpler.? So really to call format we need:
Most of that data is already there if we can just modify the rest: Pawn Wrote:// Remove the frame pointer from the stack and use it. We have now modified the stack to look how we want, put the return address in alt, and set the frame pointer back to what it used to be.? So call format: Pawn Wrote:#emit SYSREQ.C format After format returns we need to restore the stack to how is was without clobbering alt, which still holds the return address we need, since SYSREQ.C doesn?t touch that register: Pawn Wrote:// Remove and reset the count again. Now the part this entire section has been building to - we need to get the frame pointer back out and back on to the stack.? We used SCTRL to save it (we could have saved it to a global variable, but why waste one when the control register is right there, plus a global variable might not work if the native calls a callback), so the obvious code is: Pawn Wrote:#emit LCTRL 5 But there?s an instruction that does all of this in one go: Pawn Wrote:#emit PROC So you may sometimes see PROC randomly in the middle of a function, and this is what it is doing.? In fact it isn?t unusual to see: Pawn Wrote:#emit PROC Which is the name of the section - we call PROC just to set up the stack correctly for calling RETN. The stack doesn?t even need restoring.
Normally after calling a native you need to remove all the parameters pushed for it, but you don?t after calling a normal function - RETN does that for you.? In the above example we needed to restore the stack to how it was before format was called, but sometimes you don?t need to because the native is the last thing done in the function.? In that case we can just exploit RETN to remove all the extra parameters we added too: Pawn Wrote:// As before. |