AMX Assembly (#emit) -
Yashas - 15.10.2015
Moved to GitHub: https://github.com/YashasSamaga/AMX-Assembly-Docs
DEPRECATED
#emit - Learn from the basics
Last Update:15/7/2016
If you had ever asked someone how to use #emit, they would say "its very advanced and you needn't learn it". Yes, indeed it is advanced but that doesn't mean that one cannot learn it. This series of tutorials is intended to teach how to use #emit from the ground up. This tutorial will cover the very basics, i.e: from basic concepts like that of the stack, registers to the level of writing working assembly snippets. Before you begin to read this tutorial, it is important that you have a very good understanding of the PAWN Language. The aim of this tutorial is to provide you the necessary foundation which you'll need to become an independent learner.
Every computer provides a set of simple instructions and each instruction has a code. In principle, you can write a program with numbers but it is practically not achievable. To make writing a program easier, we have low level languages such as assembly where the instructions are given understandable names instead of numbers but writing a program in assembly is also difficult as it still requires you to know a lot of technical details about the computer. To make programming even easier, we have high level languages like PAWN. However, all your PAWN code at the end, ends up as machine code (assembly code is machine code with instructions written in letters instead of numbers).
PAWN is a high level language and does not have the complications which assembly would have posed, however, it allows you to add assembly code with PAWN code. #emit is the PAWN directive which allows you to put assembly instructions to your PAWN script.
The assembly language is a very low-level language and it requires a you to be fully aware of how everything works internally to write programs with it. If you want to be successful in mastering the assembly language, you need to be dedicated and hard-working. You might find it hard initially but I am sure with a little bit of practice you will be able to master the feature. You'll need to try and play with opcodes after learning it. I mean it! You got to try every example and also play with it a bit to understand better. Remember that just reading won't help you at all.
Basic Concepts & Terminology
Learning how to use #emit is as good as learning the assembly language. As said in the introduction, programming in assembly language requires you to know how the computer works in dept. Though most of the concepts mentioned here are well-documented on the internet, I have explain them here to an extent which is required for this tutorial.
AMX Machine
AMX stands for Abstract Machine eXecutor.
Abstract Machine, as the name says, is an abstract model of a computer which when implemented in the form of a software is called a
virtual machine. It is technically a virtual computer with its own way of functioning. Just like your programs run on your computer, AMX programs(.amx scripts) run in an AMX Computer (its virtual of course and runs on top of your real computer). This is something similar to the virtual machines or emulators you get on the internet like Bochs, VMWare, etc
(to be honest, even PS, NDS emulators are virtual computers and AMX Machine is just another one).
Windows is an operating system for PCs. It provides you with libraries to make a Windows executable (.exe file). Similarly, you can think of SAMP as an operating system for the AMX Machine. SAMP provides you lot of libraries (a_samp, a_player, etc) to interact with SAMP using which you make an AMX executable (.amx file).
When you click on a Windows executable (.exe), you create a Windows process. Similarly, when you execute an .amx program, you create an AMX Instance. For every AMX File you load, you create an AMX Instance. AMX Instance is basically analogous to a process in Windows. Your game mode is an AMX instance and each of your filterscript is also an AMX instance of its own.
AMX Assembly Language
A computer is only capable of understanding machine code.
For example, these numbers (machine code),
actually tell the computer to do the following
Код:
mov eax, ebx
cli
hlt
The above code was actually assembly (Intel Assembly, not the one we will be learning here) which the computer cannot understand.
Machine Code is made up of a sequence of bits where ordered combinations of bits represent instructions and data, in other words, machine code is just binary code which can be executed directly by the computer.
To make our life easier, we have assembly language which is just plain text containing instructions/data written in human readable form. Except for the numbers to words change, Assembly language is identical to machine language, in fact, they are one-to-one analogs of each other. Assembly language literally names each instruction (which is actually just a number) and provides a syntax to write understandable code.
In the earlier Intel assembly example which you saw, the 'mov' is an instruction to move data between registers, memory, etc. The 'cli' is an instruction to disable interrupts and the 'hlt' is an instruction to halt the computer. Registers? Interrupts? These are some technical words related to the Intel CPUs. Assembly language does make coding easier by avoiding numbers but it still isn't easy.
To make coding even easier, we have high level languages such as PAWN. A computer cannot understand PAWN or even assembly language. Therefore, we have programs called compilers which convert your high level code into machine executable code.
Like every computer, this AMX Machine has its own machine code (the machine code is know as pseudo-code for abstract machines). As said earlier, writing code in machine language will drive you insane, therefore we have AMX Assembly.
Binary Number System
Every information that is stored in a computer is stored in the form of numbers. Just like we use the decimal system (Base 10: there are 10 different digits that can be used to form a number) the computer uses Binary System (Base 2: there are just 2 digits using which numbers are formed) to store information. What we call a digit in decimal system is known as a bit in the Binary System. Every bit can either be 1 or 0. A sequence of 8 bits make a byte. 1024 bytes make a kilo byte, 1024 kilobytes makes a megabyte and then comes gigabyte, terabyte, ....
I would recommend you to go through
this wiki page if you are totally new to number systems.
There are lot of tutorials on the internet which explain how to interpret and perform basic operations on binary numbers so I am not going to explain any more about it here.
You also need to learn about the bit operations such as AND, OR, XOR, SHIFTS,etc because we will be using them a lot when writing 'useful' code in assembly.
You'll also need to know about
Two's Complement method to understand how negative numbers are stored.
There is a
SA-MP Wiki page which talks about bit operations.
Here is a another
good tutorial on bitwise operators. The tutorial will teach what each bitwise operation means and how to use them in PAWN. There are instructions which AMX assembly provides to carry out the same operations in assembly.
Hexadecimal Number System
The hexadecimal number system uses the base 16 to represent numbers. That means that there are 16 unique symbols which make up numbers. It is possible to use 0-9 to represent hexadecimal numbers but what about the remaining 6 symbols?
Hexadecimal numbers are written as a set of symbols which can consist of the digits 0 to 9 and the alphabets A to F.
In the hexadecimal system,
0 stands for 0
1 stands for 1
2 stands for 2
.
.
.
9 stands for 9
A stands for 10
B stands for 11
C stands for 12
D stands for 13
E stands for 14
F stands for 15
We use a convention/notation that helps us distinguish between decimals and hexadecimals. Every hexadecimal number is prefixed with 0x or 0X. And this is how we as well as the compiler would distinguish between decimals and hexadecimals.
0xF = 15
0xFF = 255
How is 0xFF equal to 255 in decimal notation? I won't be explaining it here. There are lot of tutorials on the internet on hexadecimal numbers.
If you were wondering why would you need to know this, it is because PAWN compiler for mysterious reasons loves to write numbers in hexadecimal notation in the assembly output (PAWN compiler can produce assembly code of your PAWN code if you ask it to do so). If you are impatient, you can use a calculator to convert between numbers from one system to another but it is always good to know how read and write numbers in hexadecimal notation.
The Cell Concept
PAWN is a typeless language which means there are no data types. Every data is stored in a cell or collection of cells. Though PAWN supports 64-bit and 16-bit cells, we will assume throughout the tutorial that a cell takes up 4 bytes of space since SA-MP is configured to make use of 32-bit cells.
The above code creates a cell called "a" with the value 5.
The above code creates a cell called "b" with the value 65, the ASCII equivalent of the character 'A'. Even though ASCII characters can be uniquely represented with just one byte, PAWN stores the characters in 4 byte cells.
Addresses & Offsets
In this generation of computers, the fundamental memory unit is a byte. That means everything you store takes integral multiples of bytes of space.
Just like every place on Earth has a unique address or co-ordinates, every byte in memory has a unique address associated with it. In Computer Science, an address is just a number which a computer uses to identify a byte in the memory. You can treat the computer memory to be a linear ordered sequence of bytes where a byte would have its address one more than the address of the preceding byte.
A segment is just a section of memory. Base address is an address which points to the start of a segment and an offset is an address relative to it. Or put in different words, the memory is divided into small chunks or sections which we call a segment, just like we have cities and house no, base addresses are like cities and offsets are addresses relative to the base address, just like the house no. To know the exact location of a house, we need to specify the city as well as the house no. Similarly, to get the absolute address of a byte in the memory, we need to specify the segment address as well as the offset from the segment.
We will look into an example of a one dimensional array to understand better. Though this does not exactly strictly demonstrate the addressing scheme, it will give an idea how it works.
Suppose we have an array of 10 integers. Every element in the array has its own unique address. But we programmers instead of using these addresses directly, we make use of indexes.
The address of the first element of the array is the
base address of the array. The index which you use is like an offset which is added to the base address to retrieve the data that is stored in that segment.
As said earlier every data stored in memory has a unique address using which you fetch data from the memory. So the first step before retrieving something from memory is to obtain the address of the data. The computer uses a simple algorithm to find the address of an array element. Let's say the computer needs to find the address of array[5], the computer will do the following calculations to do that.
- Obtain the base address of the segment (array[0] is the base address here)
- Multiply the index by the size of the element to obtain the offset (Here it is 5 * 4 since every array element is a cell and takes 4 bytes)
- Add the offset to the base address to obtain the address of array[5].
If the base address of the array was 1000, array[5] would have an address of 1000 + 5*4 = 1020.
Stack & Heap
Stack is a data structure which is used to store temporary information. You can think of it as a list of items where new items can be added to the list and old items can be removed. A stack is linear data structure which means all the data is arranged in a linear fashion. A stack uses a concept known as LIFO (last-in-first-out) according to which the item which was last put in to the stack will be removed first. A stack comes with two principal operations know as PUSH and POP. A push operation adds an item onto the stack and a POP operation to retrieves/removes the last pushed element from the stack. As always, we will see some examples to understand the concept better.
Here is a list of operations which I wish to perform on an empty stack.Try guessing how the final stack would look like.
Код:
PUSH 5
PUSH 10
PUSH 20
POP
PUSH 15
PUSH 20
POP
POP
The final stack will just have two items at the end.
I first add the number 5 to the stack then add number 10 then 20. Then I execute a POP instruction to remove the most recent item from the stack, i.e: the number 20. After that I push 15 then 20 and then use POP instruction twice which removes number 15 and number 20 from the stack. So my final stack would have just 5 and 10. Note that, the 10 is at the top. The last item which was pushed stays at the top and hence is poped out first.
The stack occupies some memory which essentially tells you that every item which is pushed onto the stack has an address. If you were to PUSH something onto the stack, where would you store the item? At which location/address in the memory? How do you/AMX machine know where the last pushed item is stored to pop the item? To solve this issue, a program implementing the stack data structure keeps few special variables to hold the addresses of the first element and last inserted element which are updated when a relavent change is done to the stack. This is how the computer knows where the last pushed item is stored.
What happens if you try to POP an item from an empty stack? Your program will most probably crash. This error is known as 'stack underflow'.
Most programs (the case with AMX scripts) implementing a stack allocate memory in advance for the stack whose size cannot be changed during run-time. So what happnens if you try to push an item when you have already reached the end of the stack memory, in other words, you ran out of space? Your program will most probably crash. This situation is called stack overflow.
AMX Machine has a stack which is usually used to store arguments of functions, local variables and other information. It is also used as a temporary storage. Instead of the special variables which are required as mentioned earlier, the AMX Machine has a set of registers which do the same work. More on registers soon. For the time being, you may consider registers to be speical variables.
Heap, as the name says, is just a data heap. It is a region of memory where most of the data is stored. In AMX scripts, the default values of global variables, global arrays, etc are all stored in the heap. It is also used to store local arrays. A program implementing a heap requires special variables for the very same reasons why special variables were needed in case of a stack. In case of AMX machine, we use registers in place of special variables.
In the AMX Machine, the heap and stack share the same memory block. The heap grows upwards and the stack grows downwards. Given below is an example how the heap-stack region of memory looks in an AMX machine.
Код:
Heap>> local array1, local array2 .............. [EMPTY UNUSED SPACE] ............... last pushed item, previous item, ....., first item <<Stack
There lies lot of unused space between the heap and the stack most of the times but sometimes you may actually run out of space. Therefore, it is possible for the heap and the stack to overwrite each other which will result in a Heap-Stack Collusion Error at run-time. More about it later.
Registers
Every CPU (microprocessor) comes with a set of registers. These registers are like temporary storage locations. Every register is capable of holding a data whose size depends on the machine. 32-bit computers have 32-bit registers and 64bit computers have 64bit registers. The CPU can perform operations ONLY on the data which are stored in registers. If you wanted to add the contents of two variables, the values of the variables are retrieved from the memory and are stored in registers where the addition operation is performed. After the operation, the contents of the registers are transferred to memory. Therefore, technically all the calculations such as adding, multiplying are carried out on the data stored in the registers.
Here is an example which will make you understand the concept better.
To add two numbers and then save the number at some memory location, the computer does something similar to the following:
1. Get one of the numbers that is to be added from the memory and store it in one of the CPU's Register (Lets call this REGISTER 1)
2. Get the other number and store it another register (Lets call this REGISTER 2)
3. Execute the ADD Instruction which adds the contents of the two registers and stores it in one of them, let it be REGISTER 1 in our example.
4. Get the address of the memory where the result has to be stored and store the address in REGISTER 2
5. Then execute a MOVE instruction which copies the data in REGISTER 1 and stores it in the memory location pointed by the address in REGISTER 2
This is how a computer basically works. There are few registers where all operations are carried out. The registers which we have used in our example belong to a class of registers known as general purpose registers. As the name suggests these registers can be used for any purpose. Apart from these registers, computers have special registers which are meant to perform a specific task. For example, the current instruction pointer register holds the address (if you did not know, the instructions or the code that is to be executed is stored in memory - code segment) of the current instruction that is being executed.
Just like a computer, the AMX Virtual Machine has its set of registers. It mimics a duel-register processor. It means that there are two registers that carry out almost all the operations. These registers are known as the primary and the alternate register. These are general purpose registers. There are few more registers which have a specific purpose and cannot be used as general purpose registers. Below is a list with the names and description of all the registers of the AMX Machine.
Primary Register (PRI) : All arithmetic operations are carried out in this register (Sometimes with the help of the alternate register). It is a general purpose register and you can use if for any purpose.
Alternate Register (ALT) : This is another general purpose register which is used to carry out many operations along with the primary register.
Frame Pointer (FRM) : This register points to the bottom of the current function's header. Local variables of a function are stored in the stack and the compiler does not know the absolute address of these variables rather it keeps track of the local variables using offsets which are relative to the frame pointer. Don't worry if you didn't get that part, we will discuss more about this in detail later.
Current Instruction Pointer (CIP) : All the code is stored in the code segment in the memory and every instruction has an address. The CIP register stores the offset relative to the code segment of the current instruction being executed. The CIP register is incremented after execution of every instruction using which absolute address of the next instruction is obtained from the code segment by adding the offset stored in CIP to the base address of the code segment. The instruction is executed and the cycle repeats until the program ends. The increment and fetching of the next instruction is done automatically by the AMX machine and the programmer needn't write the assembly code to do it. In fact, it is impossible since the instruction to increment CIP would itself be an instruction. Nevermind, if you did not understand the previous line. All you need to know is that the incrementation of the CIP register is done automatically by the machine and you needn't worry about it.
Data Segment Register (DAT) : This gives address to the start of the data segment relative to start of prefix.
Code Segment Register (COD) : This gives base address (actually an offset relative to the start of the program) to the start of code segment.
Stack Top Pointer (STP) : This points to the top of the stack.
Stack Pointer (STK) : This register indicates current position in the stack.
Heap Register (HEA) : This register indicates the current position in the heap.
Language Syntax & Terminology
Mnemonic: Every operation/command can be represented as a series of letters and symbols in assembly language which are known as mnemonics.
OpCode: The part of the instruction that specifies what operation to performed is called the OpCode. Every instruction is assigned a unique id using which the machine identifies what instruction it is. This unique id is known as OpCode.
Operand: The object of the operation/instruction is called the operand. In other words, the arguments or parameters that are passed to the instruction are called operands. You can use the terms operand, parameters and arguments inter-changeably.
Every instruction in the AMX Assembly Language follows a particular format. Every instruction is written by first writing the mnemonic and then the operands/parameter.
Код:
[mnemonic] [operands]
Код:
PUSH 10 ; Here PUSH is the mnemonic and 10 is the parameter
POP.pri ; Here POP.pri is the mnemonic and it doesn't take any parameter
To write assembly instructions in PAWN, you need to use the #emit directive. You first write "#emit" and then write the OpCode and then its operands. We will look at few examples.
Код:
#emit opcode operand/parameter
#emit PUSH.C 123
#emit POP.pri
Unlike other pre-processor directives, #emit must be used within a function. (Obviously an instruction lying in some non-executable part of the script wouldn't make any sense)
Код:
myFunc()
{
#emit PUSH.C 10
#emit POP.pri
return 0;
}
As said earlier, every instruction is stored in memory and hence they take some space. Every
instruction takes 4 bytes to store the OpCode and if it has an
operand then it takes 4 more bytes (4 bytes per operand). It is important that you know the above fact since you can abuse the knowledge to do stunts in your code. You can directly screw the CIP register and make fake function calls and do a lot more including crashing the script (by using setting the CIP register to incorrect values).
Код:
#emit PUSH.C 10 //Takes 8 bytes, 4 for the instruction opcode and 4 for the operand
#emit POP.pri //Takes just 4 bytes since the instruction doesn't have any operand
AMX Assembly also allows the use of comments. Just like '//' in PAWN, AMX Assembly uses ';' as a prefix for comments. So any text that follows after a ';' is a comment. You will find such comments in the .asm file which the PAWN compiler generates from the .pwn file when you ask it to do so. However, the '\\' comments work when you are using emit (#emit is a PAWN pre processor directive and is not a part of the assembly language).
AMX File & Memory Structure
Every program loaded into memory is not simply a randomly arranged chunk of memory. It is organized into different sections. A typical Windows Program has different sections for storing initialized data (data segment), uninitialized data (BSS Segment), code (Code Segment) , stack, heap, etc. Similarly, every AMX program (files with .amx extension) follows a definite structure in memory.
The segments in memory are broadly classified into 5 types
- Prefix - Present in the AMX File - Contains Start-Up Information, definitions of native,public functions,etc
- Code - Present in the AMX File - Contains Code
- Data - Present in the AMX File - Contains Data
- Heap - NOT Present in the AMX File - Will be built from the information in prefix
- Stack - NOT Present in the AMX File - Will be built from the information in prefix
The AMX program file contains only the prefix, code and data sections. The AMX Machine loads the program file into the memory and then allocates space for the heap and stack. Therefore, the program file has the following structure:
Код:
| ---------------------- |
PREFIX
| ---------------------- |
CODE
| ---------------------- |
DATA
| ---------------------- |
The actual memory structure will contain the heap-stack section in a separate memory block. However, an implementation can choose to keep the heap-stack memory block adjacent to the prefix-code-data memory block. In SA-MP, the heap-stack memory block follows the data section. Therefore, the actual memory layout of the AMX program loaded by SA-MP will be:
Код:
| ---------------------- |
PREFIX
| ---------------------- |
CODE
| ---------------------- |
DATA
| ---------------------- |
HEAP
| |
| |
FREE SPACE
| |
| |
STACK
| ---------------------- |
For the AMX Program, the absolute address 0 is assigned to the first byte of the prefix. Every absolute address in the AMX program is relative to the start of prefix.
Here is a more detailed version of the AMX File/Memory Structure
Type | Size in bytes | Description |
size | 4 | size of the memory image, excluding the stack/heap |
magic | 2 | indicates the format and cell size |
file version | 1 | the format version, currently 8 |
amx version | 1 | required minimal version of the abstract machine |
flags | 2 | More information can be found in PAWN Implementor's Guide |
defsize | 2 | size of a structure/entry in the native functions table and the public functions tables |
cod | 4 | offset to the start of the code section |
dat | 4 | offset to the start of the data section |
hea | 4 | initial value of the heap, end of the data section |
stp | 4 | stack top value (the total memory requirements) |
cip | 4 | starting address (main() function), -1 if none |
publics | 4 | offset to the “public functions” table |
natives | 4 | offset to the “native functions” table |
libraries | 4 | offset to the table of libraries |
pubvars | 4 | offset to the “public variables” table |
tags | 4 | offset to the “public tags” table |
nametable | 4 | offset to the symbol name table |
overlays | 4 | offset to the overlay table |
publics table | variable | publics function table |
natives table | variable | native functions table |
library table | variable | library table |
pubvars table | variable | public variables table |
tags table | variable | public tags table |
overlay table | variable | the overlay table |
name table | variable | the symbol name table |
Structure of an entry in the tables look like
Address | 4 bytes | If the entry is from the Publics Table, then this would be the offset (relative to the COD Register) to the start of the public function |
Pointer to the name | 4 bytes | If this was from the Publics Table, then this would give the offset (relative to the PREFIX) to the name of the public (which is stored like any other string). |
Every public function you write in your script is assigned a unique number by the compiler known as index according to the alphabetical order (the order in which the public function entries are stored in the public function table). You can obtain the index number of a public function with some emit tricks or by using the
funcidx native. Similarly, indexes are assigned for other entities of other tables including the native functions table. Play a bit with the funcidx function to get confortable with the index concept. Try changing the names of the publics and you might see a change in the index since the compiler stores the publics in alphabetical order. Do you know why it does so? Think for a while and you will get the answer. It has something to do with the working of CallLocalFunction and CallRemoteFunction. The spoiler hint I can give is that CallLocalFunction and CallRemoteFunction do a
Binary Search on the tables to find the public function's address.
The "native function table" and "library table" has a the same layout but the address field is meant to be used internally and should be zero in the AMX File. The native function table name field is filled by the compiler during compile time and the address field is left uninitilized (zero by default). The address field is left uninitialized because there is no way the PAWN compiler would be aware of address of the native function until the script is loaded (as a matter of fact, changing the order of the plugins they are loaded atually changes the offsets to the native function which clearly means there is no way the compiler would know the address of the natives beforehand). In fact, for those who have worked with plugins before, the RegisterNatives function basically fills the native functions table of the script with the correct addresses.
The “public variables” table, again, has a similar record lay out as the public functions table. The address field of a public variable contains the variable’s address relative to the DAT section.
The “tags” table uses the same format as well. This table only holds tags whose name or number might be used with the
tagof operator. The “address” field of a tag record contains the tag identifier.
This is how all the information is stored in your AMX File/Memory. Most of the entries in the table must be understandable for you except for few such as overlay, magic, size,etc which are of no use to us.
If you are still curious to know about the size, magic number, flags, etc. you can find the information regarding them in the PAWN Implementer Guide. If you are suprised by hearing terms such as 'public variables' (not many know that you can make public variables, "public myvariable = 100;") you need to go through the PAWN Language Guide.
It is important to know how the information is organized to make some good use of the inline assembly feature. By knowing how information is organized, you can now obtain meaningful information that is stored in the memory. The following example will show you how you can utilize the information that is given in this section of the tutorial to do something useful.
How to find number of commands in your script? (Works for ZCMD variants which prefix "cmd_" to every command public function's name)
Suppose you are using ZCMD, then every command 'COMMAND' you make is a public function with the function name being "cmd_COMMAND". Suppose you make a command called "slap", then ZCMD creates a public function for it with the name "cmd_slap".
Now if someone asks you to find the number of commands in your game mode, how will you do it? Here is the algorithm.
Note that this is just the algorithm, everything that I mention here is possible using #emit.
- Get the address of the start of prefix
- To find the address of start of the public function list, you first need to find the address of the public function list pointer and for that you need to find out how far away is the public function list pointer from the start of the prefix. Go check the table and start counting how many bytes you would need to go to reach the public function list pointer from the start. Let's start counting, "Size" takes 4 bytes, "magic" takes 2 bytes, "file version" takes 1 byte,.....,"CIP" takes 4 bytes, now by adding all those up you get 32 which means you need to go 32 bytes ahead to reach public function list pointer.
- Now obtain the address stored in the public function list pointer.
- Similarly obtain the address of the native function list.
- Since the native function list comes immediately after the public function list. By knowing the difference in the addresses of the start of the native and public function list, you can find out how many public functions have been declared in the script. Since each entry in the list takes 8 bytes (Refer "Structure of an entry in the table looks like"), by dividing the difference in the addresses by 8, we get the number of public functions.
- Now since we know the offset to the start of the public function list from the start of prefix, we need to add the address of the start of prefix
- Now weed to go through each and every entry. Since the first 4 bytes of every entry gives the pointer to the public function which we don't need, we need to skip the first 4 bytes of every entry. The next 4 bytes will give the address to the name string.
- Since we have the address to the name string (name of the public function), we need to check if the first 4 characters of the name string are "cmd_".
- If the first 4 characters are "cmd_", then increment a counter.
- Keep going through every entry until you have finished checking the names of all the public functions.
- The counter will now have number of functions whose names are prefixed with "cmd_".
Working example of the above can be found
here (Click)but you probably won't understand until you finish the tutorial. Visit the link after you complete reading the tutorial to understand better.
Instruction/OpCode Library
As explained earlier, every instruction consists of an OpCode follower by zero or one parameter.
Many opcodes have implied registers as operands. In several cases, the implied register is part of the name of the opcode. For example, PUSH.pri is the name of the opcode that pushes the contents of PRI register on to the stack. This instruction has no parameters: its parameter (pri) is implied in the opcode name. This reduces the number of operands that are needed to decode an instruction and, hence, it reduces the time needed to decode an instruction thereby enhancing the performance.
Core instructions set consists of fundamental instructions such as instructions that are used for loading data into the registers, accessing the special registers, etc.
To improve speed and to reduce the memory footprint of the compiled programs, the abstract machine includes several macro instructions. These macro instructions are a sequence of “plain” instructions, in a single opcode. This not only improves performance but also reduces the memory footprint since now a single opcode replaces two or more plain opcodes. Whatever can be done by a single macro instruction can be done by a set of many instructions. The sole purpose of having macro instructions is to reduce the memory footprint and improve performance. Therefore, you should make use of macro instructions whenever possible.
I have selected only few OpCodes from the PAWN Implementer Guide which I believe are important and compiled them into a neat table.
The “semantics” column gives a brief description of what the opcode does. It uses the C language syntax for operators, which are the same as those of the pawn language. An item between square brackets indicates a memory access (relative to the DAT register, except for jump and call instructions). So, PRI = [address] means that the value read from memory at location DAT + address is stored in pri.
I had once said about country code while discussing about segments, addresses and offsets. Here you can think of the DAT register to be the country code. You add the offset to the base address (say, offset to an item on the stack to the stack pointer) then you add the same to the address stored in the DAT register to obtain the absolute address. This however is not important since many instructions automatically add the address to the DAT before retrieving data.
Notation:
DISCLAIMER: The notation, format and description given below are my way of understanding.
Код:
mnemonic | prefix | suffix
Код:
SHL.C.pri (shift bits to the left)
SHL | C | pri
ADD.C (add integers)
ADD | C
ZERO.alt (set to zero)
ZERO | | alt
The mnemonic gives the idea of what the instruction does. For example, the ADD instruction tells that the instruction does some sort of addition operation.
The prefix provides more information about the instruction. For example, the ".C" prefix to the "ADD.C" instruction tells that a constant is to be added.
.C = constant
.S = stack
.I = indirection
.B = variant of the one without B
.ADR = address
The suffix provides information about which register the instruction primarily affects. For example, the ".alt" suffix to the "ZERO.alt" instruction tells that the alternate register will be set to zero.
.pri = primary register
.alt = alternate register
Instruction Set
# | mnemonic | operand | semantics |
1 | LOAD.pri | address | PRI = [address] |
2 | LOAD.alt | address | ALT = [address] |
3 | LOAD.S.pri | offset | PRI = [FRM + offset] |
4 | LOAD.S.alt | offset | ALT = [FRM + offset] |
5 | LREF.pri | address | PRI = [[ address ]] |
6 | LREF.alt | address | ALT = [[ address ]] |
7 | LREF.S.pri | offset | PRI = [[FRM + offset]] |
8 | LREF.S.alt | offset | ALT = [[FRM + offset]] |
9 | LOAD.I | | PRI = [PRI] |
10 | LODB.I | number | PRI = "number" of bytes from [PRI] (read 1/2/4 bytes) |
|
11 | CONST.pri | value | PRI = value |
12 | CONST.alt | value | ALT = value |
13 | ADDR.pri | offset | PRI = FRM + offset |
14 | ADDR.alt | offset | ALT = FRM + offset |
15 | STOR.pri | address | [address] = PRI |
|
16 | STOR.alt | address | [address] = ALT |
17 | STOR.S.pri | offset | [FRM + offset] = PRI |
18 | STOR.S.alt | offset | [FRM + offset] = ALT |
19 | SREF.pri | address | [[ address ]] = PRI |
20 | SREF.alt | address | [[address]] = ALT[\td] |
21 | SREF.S.pri | offset | [[FRM + offset]] = PRI |
22 | SREF.S.alt | offset | [[FRM + offset]] = ALT |
23 | STOR.I | | [ALT] = PRI (full cell) |
24 | STRB.I | number | "number" of bytes at [ALT] = PRI (store 1/2/4 bytes) |
25 | LIDX | | PRI = [ ALT + (PRI Ч cell size) ] (load what is there at calculated index) |
26 | LIDX.B | shift | PRI = [ ALT + (PRI << shift) ] (load what is there at calculated index) |
27 | IDXADDR | | PRI = ALT + (PRI Ч cell size) (calculate indexed address) |
28 | IDXADDR.B | shift | PRI = ALT + (PRI << shift) (calculate indexed address) |
29 | ALIGN.pri | number | Little Endian: PRI ^= cell size - number |
30 | ALIGN.alt | number | Little Endian: ALT ^= cell size - number |
31 | LCTRL | index | PRI is set to the current value of any of the special registers. The index parameter must be: 0=COD, 1=DAT, 2=HEA,3=STP, 4=STK, 5=FRM, 6=CIP (of the next instruction) |
32 | SCTRL | index | Set the indexed special registers to the value in PRI.The index parameter must be: 2=HEA, 4=STK, 5=FRM, 6=CIP |
33 | MOVE.pri | | PRI = ALT |
34 | MOVE.alt | | ALT = PRI |
35 | XCHG | | Exchange contents of PRI and ALT |
36 | PUSH.pri | | [STK] = PRI and STK = STK - cell size |
37 | PUSH.alt | | [STK] = ALT and STK = STK - cell size |
|
38 | PICK | offset | PRI = [STK + offset] |
39 | PUSH.C | value | [STK] = value,STK = STK - cell size | 40 | PUSH | address | [STK] = [address], STK = STK - cell size |
41 | PUSH.S | offset | [STK] = [FRM + offset], STK = STK - cell size |
42 | POP.pri | | PRI = [STK] and STK = STK + cell size |
43 | POP.alt | | ALT = [STK] and STK = STK + cell size |
44 | STACK | value | ALT = STK and STK = STK + value |
45 | HEAP | value | ALT = HEA and HEA = HEA + value |
46 | PROC | value | ALT = HEA and HEA = HEA + value |
47 | RET | | STK = STK + cell size, FRM = [STK],STK = STK + cell size,CIP = [STK], The RET instruction cleans up the stack frame and returns from the function to the instruction after the call |
48 | RETN | | STK = STK + cell size, FRM = [STK],STK = STK + cell size, CIP = [STK], STK = STK + [STK]The RETN instruction removes a specifed number of bytes from the stack. The value to adjust STK with must be pushed prior to the call. |
49 | CALL | offset | [STK] = CIP + 5 STK = STK − cell size CIP = CIP + offset The CALL instruction jumps to an address after storing the address of the next sequential instruction on the stack. The address jumped to is relative to the current CIP, but the address on the stack is an absolute address. |
50 | CALL.pri | | [STK] = CIP + 1STK = STK − cell size CIP = PRI Jumps to the address in PRI after storing the address of the next sequential instruction on the stack. |
65 | SHL | | PRI = PRI << ALT |
66 | SHR | | PRI = PRI >> ALT (without sign extension) |
67 | SSHR | | PRI = PRI >> ALT (with sign extension) |
68 | SHL.C.pri | value | PRI = PRI << value |
69 | SHL.C.alt | value | ALT = ALT << value |
70 | SHR.C.pri | value | PRI = PRI >> value |
71 | SHR.C.alt | value | ALT = ALT >> value |
72 | SMUL | | PRI = PRI * ALT (signed multiply) |
73 | SDIV | | PRI = PRI / ALT (signed divide),ALT = PRI mod ALT |
74 | SDIV.alt | | PRI = ALT / PRI (signed divide),ALT = ALT mod PRI |
75 | UMUL | | PRI = PRI * ALT (unsigned multiply) |
76 | UDIV | | PRI = PRI / ALT (unsigned divide),ALT = PRI mod ALT |
77 | UDIV.alt | | PRI = ALT / PRI (unsigned divide),ALT = ALT mod PRI |
78 | ADD | | PRI = PRI + ALT |
79 | SUB | | PRI = PRI - ALT |
80 | SUB.alt | | PRI = ALT - PRI |
81 | AND | | PRI = PRI & ALT |
82 | OR | | PRI = PRI | ALT |
83 | XOR | | PRI = PRI ^ ALT |
84 | NOT | | PRI = !PRI |
85 | NEG | | PRI = -PRI |
86 | INVERT | | PRI = ~PRI |
87 | ADD.C | value | PRI = PRI + value |
88 | SMUL.C | value | PRI = PRI * value |
89 | ZERO.pri | | PRI = 0 |
|
90 | ZERO.alt | | ALT = 0 |
|
91 | ZERO | address | [address] = 0 |
92 | ZERO.S | offset | [FRM + offset] = 0 |
93 | SIGN.pri | | sign extent the byte in PRI to a cell |
94 | SIGN.alt | | sign extent the byte in ALT to a cell |
95 | EQ | | PRI = PRI == ALT ? 1 : 0 |
96 | NEQ | | PRI = PRI != ALT ? 1 : 0 |
97 | LESS | | PRI = PRI < ALT ? 1 : 0 (unsigned) |
98 | LEQ | | PRI = PRI <= ALT ? 1 : 0 (unsigned) |
99 | GRTR | | PRI = PRI > ALT ? 1 : 0 (unsigned) |
100 | GEQ | | PRI = PRI >= ALT ? 1 : 0 (unsigned) |
101 | SLESS | | PRI < ALT ? 1 : 0 (signed) |
102 | SLEQ | | PRI = PRI <= ALT ? 1 : 0 (signed) |
103 | SGRTR | | PRI = PRI > ALT ? 1 : 0 (signed) |
104 | SGEQ | | PRI = PRI >= ALT ? 1 : 0 (signed) |
105 | EQ.C.pri | value | PRI = PRI == value ? 1 : 0 |
106 | EQ.C.alt | value | PRI = ALT == value ? 1 : 0 |
107 | INC.pri | | PRI = PRI + 1 |
108 | INC.alt | | ALT = ALT + 1 |
109 | INC | | [address] = [address] + 1 |
|
110 | INC.S | offset | [FRM + offset] = [FRM + offset] + 1 |
|
111 | INC.I | | [PRI] = [PRI] + 1 |
112 | DEC.pri | | PRI = PRI - 1 |
113 | DEC.alt | | ALT = ALT - 1 |
|
114 | DEC | address | [address] = [address] - 1 |
|
115 | DEC.S | | [FRM + offset] = [FRM + offset] - 1 |
116 | DEC.I | | [PRI] = [PRI] - 1 |
117 | MOVS | number | Copy memory from [PRI] to [ALT]. The parameter specifies the number of bytes. The blocks should not overlap |
118 | CMPS | number | Compare memory blocks at [PRI] and [ALT]. The parameter specifies the number of bytes. The blocks should notoverlap. |
119 | FILL | number | Fill memory at [ALT] with value in [PRI]. The parameter specifes the number of bytes, which must be a multiple of the cell size. |
120 | HALT | 0 | Abort execution (exit value in PRI), parameters other than 0 have a special meaning. |
121 | BOUNDS | value | Abort execution if PRI > value or if PRI < 0 |
122 | SYSREQ.pri | | call system service, service number in PRI |
123 | SYSREQ.C | address | call system service |
129 | SWITCH | offset | Compare PRI to the values in the case table (whose address is passed as an offset from CIP) and jump to the associated the address in the matching record. |
130 | CASETBL | ... | A variable number of case records follows this opcode, where each record takes two cells. |
131 | SWAP.pri | | [STK] = PRI and PRI = [STK] |
132 | SWAP.alt | | [STK] = ALT and ALT = [STK] |
133 | PUSH.ADR | offset | [STK] = FRM + ofset,STK = STK - cell size |
134 | NOP | | no operation; used for code alignment |
Switch-Case
The "SWITCH" instruction takes an address (relative to CIP) to the case table as a parameter. The case table formally starts with a casetbl instruction followed by two numbers and then a series of records. The first number which follows the casetbl instruction indicates the number of cases in the case table and the second number holds the address (relative to CIP) to the default case. Each record consists of two numbers: case value and jump address. The first number indicates the case value and the second number gives the address (relative to CIP of the current record).
A typical switch-case layout in PAWN would look like:
Код:
switch(expression)
{
case 2: {}
case 4: {}
case 3: {}
case 7: {}
case 5: {}
}
When the code is compiled the compiler adds instructions to evaluate "expression". The result of the evaluation is stored in the primary register. After that, the switch instruction and the case table is added. The "SWITCH" instruction compares the value stored in PRI to the case value of each record in the case table. If it finds a match, it jumps to the address pointed by the matching record. If it fails to find a match, it uses the default address which is stored in the second number that immediately follows the "CASETBL" instruction.
When the compiler is asked to give the assembly output for the PAWN code, the compiler gives the assembly code which won't be the actual assembly code. The compiler makes many changes so as to make it easier to interpret and understand the assembly code. One way by which the compiler helps is by changing the instruction addresses with labels. The labels are sort of markers in the assembly output. Therefore, the assembly output shown does not have a one-to-one correspondence with the AMX binary.
The assembly equivalent given by the compiler for the aforementioned example PAWN code would be:
Код:
; PRI has the value of the expression
switch 0
; the following are different code blocks represented by label (l.2, l.3, l.4, ...)
l.2 ; 2c
jump 1
l.3 ; 34
jump 1
l.4 ; 3c
jump 1
l.5 ; 44
jump 1
l.6 ; 4c
jump 1
l.0 ; 54
casetbl
case 5 1 ; the first two numbers indicate number of records and default jump address
case 2 2 ; the first record
case 3 4 ; case value: 3, jump label: 4
case 4 3 ; the compiler writes correct addresses in place of the labels while building the actual binary (.amx file)
case 5 6 ; the labels are there only to make things easier for us
case 7 5 ; the fifth & the last record
If you haven't noticed yet, look how the case values in the records are ordered. They are ordered in ascending order. This is done by the compiler intentionally so that the AMX machine can search the case table using binary search which of course is faster than a linear search.
Examples
Usage of basic Load/Store instructions
LOAD.pri/alt is used to load global variables
LOAD.S.pri/alt is used to load local variables
".S" stands for stack
Код:
new a = 10, b = 15;
func()
{
new local1, local2 = 10;
#emit LOAD.pri a //Will store the contents of a in PRI
#emit LOAD.alt b //Will store the contents of b in PRI
#emit XCHG //Exchange the contents of PRI and ALT
//Now PRI has the contents of b and ALT has the contents of a
#emit STOR.S.pri local1 //Value of PRI is now stored in local1
//Note that .S must be used while performing the operation on local variables (variables stored in the stack)
#emit LOAD.S.pri local1 //Will store the contents of local1 in PRI
#emit LOAD.S.alt local2 //Will store the contents of local2 in ALT
//Note that .S must be used while loading the contents of local variables
#emit ADD // PRI = PRI + ALT = local1 + local2 =
#emit STOR.pri a //Store the value of PRI in the global variable a
//Note that .S hasn't been used here
printf("a:%d b:%d local1:%d local2:%d", a, b, local1, local2);
}
Usage of stack as temporary storage
While pushing constants onto the stack, the ".C" suffix is used with PUSH
".C" and "CONST." stand for constant
".S" stands for stack
When variable names are loaded as constant, for example,
Код:
new variable_name;
func()
{
#emit CONST.pri variable_name
}
the address of the variable_name is stored in PRI. In general, the names of variables represent the address.
To load the contents of a global variable to the primary register, you must use "LOAD.pri variable_name". By the definition of the instruction, LOAD.pri stores the contents stored at the address given as a parameter. Therefore, variable_name acts as an address which is replaced by actual numbers by the compiler.
You can also push addresses of variables using "PUSH.C variable_name".
Код:
new global = 123;
func()
{
new local = 10;
new addr_global;
#emit CONST.pri 8 //PRI = 8
#emit LOAD.S.alt local //ALT = 2
#emit SUB // PRI = ALT - PRI = 10 - 8 = 2
#emit PUSH.pri //Push the value of PRI to the stack
#emit PUSH global //Push the value stored in global on to the stack
#emit POP.alt //Remove the value stored in global out of the stack
#emit PUSH.C global //Push the address of global onto the stack
#emit PUSH.S local //Push the value stored in local to the stack
local = 6;
global = local * local;
#emit POP.pri //Pop the last pushed value into PRI
//last pushed was local, which had a value of 10 when pushed and hence PRI = 10
#emit CONST.alt 5 //ALT = 5
#emit SDIV.alt //PRI = ALT / PRI (signed divide),ALT = ALT mod PRI
//Now ALT = 5 since 5 divided by 10 leaves a remainder of 5
//PRI = 0 since 10 does not divide 5
#emit POP.pri //Get the address of global which was pushed before local
#emit STOR.S addr_global //Store the address of global in addr_global
#emit POP.pri
#emit STOR.S.pri local
}
Finding the address of functions
Код:
new addr;
#emit CONST.pri func1
#emit STOR.S.pri addr
}
There is a bug in PAWN Compiler which crashes the compiler if you use a function name which isn't present in the file.
For example, if func1 was present in another file (it could be in an include) and you try to use "CONST.pri func1" in another script, it will crash the compiler even if you #include the other file.
Accessing elements of arrays that are arguments
To obtain the value stored at an address, we will use LOAD.I (PRI = [PRI]).
".I" means indirection.
Код:
public OnPlayerCommandText(playerid,cmdtext[])
{
new index = 2;
#emit LOAD.S.alt cmdtext
#emit LOAD.S.pri index
#emit IDXADDR //PRI = ALT + (PRI Ч cell size) (calculate indexed address)
//Now PRI contains the address of the element stored in index 2 of cmdtext (cmdtext[2])
#emit LOAD.I //PRI = [PRI] //Since PRI contains the address of cmdtext[2], [PRI] will give the value stored at index 2
//Instead of using IDXADDR and then LOAD.I, you can do the same with just one instruction using LIDX
//LIDX = PRI = [ALT + (PRI Ч cell size)]
}
Accessing the element of a local array
Since local arrays are stored in heap, we cannot load the array using LOAD.S. We need to directly load the address of the array into PRI using the ADDR.PRI instruction.
Код:
main()
{
new a[] = "AMX Assembly";
new character;
#emit ADDR.pri a
#emit ADD.C 8
#emit LOAD.I
#emit STOR.S.pri character
printf("%c", character);
}
Skipping instructions by directly manipulating the contents of CIP
LCTRL is used to obtain the value stored in the special registers and SCTRL is used to update the contents of the special registers. In the following example, we will obtain the value of CIP and modify it thus skipping instructions in between.
Код:
func()
{
#emit LCTRL 6 //Get the current value of CIP
#emit ADD.C 28 //Add 28 to PRI (This instruction takes 8 bytes)
#emit SCTRL 6 //Set CIP to the value stored in PRI (This instruction takes 8 bytes)
//Adding 28 will make the AMX Machine to jump skip the next 3 instructions and jump to the last NOT instruction
#emit PUSH.pri //Takes 4 bytes - THIS WONT BE EXECUTED
#emit CONST.alt 10 //Takes 8 bytes - THIS WONT BE EXECUTED
#emit NOT //Takes 4 bytes - THIS WILL BE EXECUTED
}
Multiplying/Dividing numbers by powers of 2
To understand the following code, you need to know bitwise operators.
Shifting the bits of a number by n to the left multiplies the number by 2^n and shifting the bits of a number by n to the right divides the number by 2^n.
Код:
MultiplyBy4(a)
{
#emit LOAD.S.pri a
#emit SHL.C.pri 2
#emit STOR.S.pri a
//Equivalent code
//#emit LOAD.S.pri a
//#emit CONST.alt 2
//#emit SHL
return a;
}
DivideBy4(a)
{
#emit LOAD.S.pri a
#emit SHR.C.pri 2
#emit STOR.S.pri a
//Equivalent code
//#emit LOAD.S.pri a
//#emit CONST.alt 2
//#emit SHR
return a;
}
We haven't tried using all the instructions yet but most of the remaining instructions are easy if you have understood these examples properly.
There are still some instructions such as STACK,RETN,SYSREQ,etc about which we will discuss in extreme detail later in this tutorial.
The examples in this section are too simple and were meant to give you a feel for #emit. You can find #emit being used for useful purposes at the snippets section.
Stack & Functions
This section will explain how stack really works in AMX and how to perform various tasks related to functions such as calling, returning, passing arguments, etc.
Parameters and Local Variables of a function
The frame pointer ("FRM") points to the address at the bottom of the running function's header (can be treated as the address to the start of the stack for the running function). If that scared you, here is a much nicer way to say the same. The FRM pointer basically stores an address which points to the start of the stack for the current function that is being executed. When you use an offset of a local variable (or that of an item on stack) say, for the PUSH.S instruction, you add that offset to the FRM to get the address which is then added to DAT to get the absolute address. Basically, you use offsets of the local variables and function arguments which are relative to the frame pointer when you write the code using variable names. The offsets are simple whole numbers such as 4, 8, 12! You will learn more about it soon! The stack pointer("STK") points to the last data put on to the stack whereas frame pointer ("FRM") points to a data somewhere in the middle of the stack. Where exactly in the middle? We will talk about in a later section.
Every local variable that you create are pushed on to the stack and every local array gets are stored in the heap. Note that it is possible for the heap and stack to overwrite each other since they share the same memory block. In such a case you should see a run-time error similar to the following:
Код:
[15:39:44] [debug] Stack pointer (STK) is 0xFFFFF3E0, heap pointer (HEA) is 0x244
[15:39:44] [debug] AMX backtrace:
[15:39:44] [debug] #0 000003fc in main () from test.amx
[15:39:44] Script[gamemodes/test.amx]: Run time error 3: "Stack/heap collision (insufficient stack size)"
I managed to force a collision error by creating a local array with 5000 elements (Stack/Heap Size was 16384 bytes when compiled but I needed at least 20108 bytes to store the contents of the stack and heap without colliding which is more than what I have hence the error).
You can solve the problem by asking the compiler to increase the stack size by using the dynamic pragma directive.
Код:
#pragma dynamic [required size]
The "new" keyword when used within a function is basically responsible for creating space for your variable in the stack.
For example, the following code
Код:
func()
{
new x = 101,y = 102 , z = 103;
}
would have an assembly equivalent to (as produced by the PAWN compiler)
Код:
;$lcl x fffffffc
push.c 65 //x on the stack, 65 = 101 in hexadecimal
;$exp
;$lcl y fffffff8
push.c 66 //y on the stack, 66 = 102 in hexadecimal
;$exp
;$lcl z fffffff4
push.c 65 //z on the stack, 67 = 103 in hexadecimal
;$exp
Ignore the ";$exp" lines. The "$lcl" lines are quite useful since they give the relative address of the variable. These lines are added by the compiler to improve readability of the assembly output.
If you observe the addresses carefully, you will realize that the addresses of the local variables are actually negative. Local variables are located below the function frame pointer and the function arguments are located above the function frame pointer. Therefore, the relative addresses of local variables are negative. If that did not make any sense, the following examples should make it clear.
Код:
myFunc(a,b,c)
{
new m, n;
for(new i = 0; i < 10; i++) //STK 12 bytes below the frame pointer here since there are 3 local variables (m, n, i)
{
}
}
If you remember, the stack pointer gives the current position in the stack. In the above code, at the instant when AMX is executing the first statement inside the loop; at the start of the loop, since 3 local variables have been created, the stack pointer will be 12 bytes below the frame pointer (don't get confused, just remember that the frame pointer points to a position somewhere in the stack - we will discuss where in detail soon) at "i". If you now declare a new variable, you would push the variable onto the stack which would move the stack pointer down by another 4 bytes.
Similarly, when a variable goes out of scope, the stack pointer moves up by 4 bytes.
The local variables "m", "n" and "i" have relative address of -4,-8 and -12 respectively. The argument "a" has a relative address of 12, "b" has a relative address of 16. Where did the address 0-12 go? There are important information stored in those addresses about which we will discuss in a few minutes. For now, assume that the addresses of the function arguments start from 12 and some top secret information is stored from 0-12.
I had earlier mentioned that names of functions, local variables act like addresses for the compiler.
Код:
new var_name;
#emit CONST.pri var_name //Loads the address of var_name relative to FRM into PRI
The compiler will automatically replace the "var_name" with the correct address. You can give the address in numbers too but finding the numerical address would be a really hectic job most of the times. We will study them anyway.
Finding the address of local variables or arguments is easy since there are not too many local variables/arguments. Many OpCodes accept the address relative to the frame pointer so it could be useful to know the relative addresses of the local variables.
Код:
func(a,b)
{
#emit LOAD.S.pri a
//is same as
#emit LOAD.S.pri 12 //The relative address of a is 12
}
The first argument in the above code has the address 12 and the second has the address 16.
If you are not sure of the address or lazy to find the address, you can use the compiler to find the address using the idea given below.
Код:
func(a,b)
{
new x;
new y;
new z;
#emit CONST.pri x
#emit STOR.S.pri y
#emit CONST.pri y
#emit STOR.S.pri z
#emit CONST.pri z
#emit STOR.S.pri x
printf("Address of x: %d",y);
printf("Address of y: %d",z);
printf("Address of z: %d",x);
}
The output will be
If you try to find the address of the function parameters you should get 12 and 16 for 'a' and 'b' respectively.
If you want to pass the negative address to the instructions, you will have to use hexadecimal notation because the PAWN Compiler doesn't allow negative decimals as operands (the compiler isn't smart enough to realize that '-' sign you add to the decimal number indicates that the number is negative - the compiler complains about the '-' sign because it expected a number not a sign - it is a n00b). The hexadecimal numbers should be in
two's complement since AMX makes use of the two's complement system to store negative numbers.
The compiler keeps track of the STK pointer itself when it is compiling. The compiler won't be aware of any changes you do to the STK pointer yourself using #emit (PUSH and POP instructions alter the STK pointer so any such instruction added by you using #emit won't inform the compiler about it). This has horrible consequences. More about the issue with examples will be discussed in the later.
Making function calls
Before calling a function, you need push the arguments, total size of arguments and the return address to the stack. Based on this information, the function knows how many arguments are there, what are the arguments and where to jump to after the function completes its execution.
The general procedure for calling a function and the timeline of the executing function is given below
Код:
; Push the arguments
; Push the total size of all arguments combined (number of arguments pushed * 4)
; Push Return Address
; Function Call
; Function executes
; Restores the previous state and returns to the caller function
To learn more about the structure of the stack after a function is called, we will pop the contents of the stack and print them on the screen and use the printed information to compare with the values we had set to the locals and arguments and try to decode the stack structure. The following code will print the contents of the stack on the screen.
Код:
new x;
func(a,b)
{
new z = 101;
new y = 102;
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
//Nothing more to POP but lets investigate what is in the FRM register
#emit LCTRL 5
#emit STOR.S.pri z
printf("FRM in func: %d", z);
for(;;) { } //If I allow the function to return, it will crash the server (run-time error) because the POP instructions change the STK Register and the return instruction expects STK to be having the correct address so that the next POP instruction would pop the return address. Did not get it? Anyway I am lying a bit here. The compiler sets the STK to the correct value before returning but it wont be aware of the changes we did so it wont set it to the correct value while returning. More about this later.
return x; //If this were to be executed, you would get a "Invalid memory access" run-time error.
}
main()
{
new zyx ;
#emit LCTRL 5
#emit STOR.S.pri zyx
printf("FRM in main: %d", zyx);
zyx = 1;
func(1001,1002);
new abc = 2;
}
Код:
FRM in main: 17988
102 ; y
101 ; x
17988 ; Frame of "main"
1900 ; Return address
8 ; we pushed 2 arguments, number of bytes they take is 8
1001 ; argument 1
1002 ; argument 2
1 ; zyx local variable from the main()
FRM in func: 17960
From the above information , you can guess the stack structure after a function call. Here is how it looks (descending order of relative addresses)
The number before the : indicates the address relative to the frame.
Код:
; Stack before initiating the calling process
; Argument n
; Argument n-1
; .
; .
; .
; 16:Argument 2
; 12:Argument 1
; 8:Number of arguments in terms of the size (4 * n)
; 4:Return Address
; 0:Previous Function's Frame Pointer (FRM)
; Local Variables created in the called function
Before getting into the details, let us first discuss what that "frame" thing really means. As said in an earlier section, every item in the stack has a relative address with respect to frame pointer (the negative relative addresses we talked about). To obtain the complete address we add the relative address/offset to the frame pointer.
Just a remainder, I said 'complete address' but it actually it isn't, you need to add address of DAT register to that so called complete address to obtain the actual address. Never mind if you don't understand it. The assembly OpCodes accept addresses relative to DAT everywhere so it shouldn't bother you. You will learn eventually as time passes.
I hope that you are gaining some intution about the frame pointer. At this stage, I can revial a new fact. The frame pointer is related to the current executing function and is changed when you make a function call or when you return. It is set to such a value that all local variables go below the position pointed by the frame pointer and all arguments and other information (return address, previous function's frame pointer) go above the frame pointer.
Go back to the output printed by our stack printing code and observe that the local variable 'abc' is not yet on the stack when func is called because func is called before the new instruction which puts abc onto the stack. Also note that there is no 'x' in the stack since 'x' is a global variable and resides in the heap.
If you observe the order in which the arguments are pushed, you will find that arguments are pushed in the reverse order. It is done so that when the function is called the arguments are popped in the correct order. Do you remember the LIFO rule? What you pushed last will be poped first. Of course, the arguments are not poped when the function alled but the order of arguments start from the top of the stack so the last argument you pushed must be the first argument of the function.
Lets verify the above statement again and investigate what that number 8 which was pushed in our previous example really means.
Код:
call_f(a,b)
{
}
func()
{
call_f(5,6);
}
The PAWN Compiler gives an assembly output as follows for the above code
Код:
push.c 6 //Argument 2
push.c 5 //Argument 1
push.c 8 //Sum total of the size of arguments
call Func
The arguments are pushed first in the reverse order after which the number/size of arguments is pushed. Since two arguments are being passed, the total size of the arguments will be 2*4, hence 8 is pushed.
The next instruction, "CALL", jumps to an address after storing the absolute address of the next sequential instruction of the calling function, i.e: the return address, on the stack. It is using this address which was pushed, the function which was called knows where to return after the called function ends.
To find the return address manually (address of the instruction which comes after CALL instruction of the calling function), we need to add 8 to the CIP when it is pointing to the CALL instruction. Since every instruction takes 4 bytes and every parameter takes 4 bytes, the total number of bytes to jump will be 8. By adding 8 to the CIP, we get the address of the next instruction, i.e: the return address in our case.
The return address is pushed artifically by directly assigning the 'return address' to the position pointed by the STK and then by decreasing the cell size by 4 (just updating the STK pointer so that it points to the new top of the stack position).
Код:
[STK] = CIP + 2*4
STK = STK - cell size
After pushing the return address, CIP is then set to the address of the function which has to be called.
The AMX machine now executes the instruction pointed by CIP, in other words, executing the code of the called function.
We actually needn't do the above calculation while using CALL instruction since the instruction does all those calculations automatically.
There is also an instruction CALL.pri which allows us to jump to the address stored in PRI. Unfortunately the CALL and CALL.pri instructions have been
removed for security reasons.
That sounds bad... after studying how CALL instruction works and we can't even use them. No need to get disappointed because by studying how the CALL instruction works, we know how function calls are made step by step and we can FAKE the call instruction.
Since we know what the CALL instruction does, we can mimic it by pushing the return address ourselves and directly modifying the CIP pointer ourselves to point to the start of the function that is to be called.
Код:
#emit PUSH argument
#emit PUSH argument
.
.
.
#emit PUSH.C total_size_of_arguments_pushed //number of arguments * 4
//Now we need to calculate and push the return address
#emit LCTRL 6 //Get the value of CIP and store it in PRI
#emit ADD.C 28 // Add 28 to PRI to get the address of the instruction which comes after SCTRL
#emit PUSH.pri // PRI will be having the return address and we will PUSH it onto the stack
#emit LOAD.pri pointer // Load the address of the function to be called from a variable into PRI
#emit SCTRL 6 // Set the CIP to the address in PRI
//The function will set the CIP to the return address which is here
The working of the above code has been explained in the comments and requires no further explanation. The only thing you might be confused with is about that '28' and where we get the 'pointer' variable. Remember that each OpCode takes 4 bytes and every operand takes 4 more bytes. Now count the number of OpCodes (number of instructions) and the number of operands and find out by what amount you should increment address stored in CIP from LCTRL to get the address of the instruction that comes after SCTRL. Regarding the pointer thingy, ignore it for now. There is a dedicated sub-section which explains how to obtain addresses of the function.
Calling native functions using SYSREQ
Every native that is used in the script is given a unique index in the native function table by the compiler (just like an index is assigned to public functions). The SYSREQ.C takes one parameter which is an index to the native function in the native function table.
Calling a native function is quite similar to the procedure we adapt to call any other function. The only difference lies in the final step. The SYSREQ instruction must be used to call a native function instead of manually updating the CIP pointer.
Код:
#emit PUSH Argument3
#emit PUSH Argument2
#emit PUSH Argument1
#emit PUSH.C total_size_of_arguments
#emit SYSREQ.C native_function
A function in pawn cleans up its arguments that were pushed on the stack, because it returns with the RETN instruction. But the function called using SYSREQ.C instruction does not remove items from the stack, so the AMX machine expects us to explicitly clean the stack using the STACK instruction after the SYSREQ.C instruction.
Код:
PUSH.S health
PUSH.S playerid
PUSH.C 8
SYSREQ.C SetPlayerHealth
STACK 12 //Restoring the stack to its original condition.
Since we pushed 12 bytes (inclusive of the PUSH.C 8) onto the stack before calling, we need to move the STACK pointer up by 12 bytes (which is equivalent to popping three items from the stack). If the stack is not restored to the original condition, the script might show undefined behaviour and will also crash the AMX Instance in most cases. It is important that the stack has been restored before returning from a function.
The PAWN Compiler adds an entry to the native function list for every native you use in your script. But as said earlier while we were discussing few examples the PAWN compiler isn't aware of any changes done using #emit. The PAWN compiler adds an entry to the native functions table if and only if the script makes a function call to the native. When you call a native the usual way (without using emit), the compiler checks if the native has already been added to the native functions table and if not added, it adds it after which it spits out the SYSREQ instruction with the native function's id. That means, if a native function has not been used anywhere in your script and you try to call it using SYSREQ.C, it will crash the compiler because the native you are trying to all does not exist in the native function's table and the compiler panics when it doesn't find it.
Here is one workaround for the bug.
Код:
public _SYSREQ_FIX()
{
CallTheNativeFunction();
}
You needn't really call the function "_SYSREQ_FIX", you just need to declare a public function so that the compiler adds the native function to the native function table.
Now your SYSREQ calls will no more cause the compiler to crash.
Methods of passing arguments
Call by value
When an variable is passed by call by value method, a copy of the variable is created. Any modifications done by the called function won't affect the original variable.
Код:
func(a,b)
{
a = 6;
}
func2()
{
new m = 10;
func(m,5);
//m will be 10
}
The argument "m" is pushed in this manner
The argument is modified using the following instructions
Код:
#emit CONST.pri 6
#emit STOR.S.pri a
Call by reference (Reference Variables)
When variables are passed as references, the address is pushed instead of creating a copy of the variable on the stack. Therefore, any changes made in the called function will affect the variable.
Код:
func(&a)
{
a = 5;
}
func2()
{
new m = 10;
func(m);
//m will be 5
}
The address of the variable "m" is pushed using the following instruction
The value at that address is modified using the following instructions
Код:
#emit CONST.pri 5
#emit SREF.S.pri a
Passing Arrays as arguments
Passing the array is equivalent to passing the address of the array. This explains why modifying the contents of an array which was passed as an argument affects the array which was originally passed.
Код:
new global_str[10];
public OnPlayerCommandText(playerid, cmdtext[]) //cmdtext is like a local array
{
//Pushing the address of the global string
#emit PUSH.C global_str
//Pushing a local string
#emit PUSH.S cmdtext
// Rest of the function call instructions
}
The array is modified in the called function as if it was passed by call by reference.
Код:
func(a[])
{
a[5] = 123;
}
The function which was called will use the following code to modify the contents of the array
Код:
#emit LOAD.S.pri c
#emit ADD.C 14
#emit MOVE.alt
#emit CONST.pri 7b
#emit STOR.I
We will be discussing about arrays in detail in "Arrays & Strings" section, so you needn't worry if you couldn't understand the above set of instructions.
You can pass part of the arrays in PAWN using the idea used in the following code
Код:
func(a[])
{
printf("String:%s",a);
}
main ()
{
new a[] = "AMX Assembly";
func(a[1]);
}
The printf will print just "MX Assembly" in the console since the address of "a[1]" was passed instead of "a[0]". It should be clear how strings work in PAWN once you see the assembly equivalent of the above PAWN code.
Код:
#emit ADDR.pri a //Load the address of a into PRI
#emit ADD.C 4 //Since we need 'a[1]', we need to add 4 bytes to the address of 'a' (each array element is a cell which takes 4 bytes)
#emit PUSH.pri //Push the calculated address
//Push 4 onto the stack and call the function
Variable Arguments
Arguments that are passed to function as a variable argument are passed as references which is why you can modify the variable argument using
setarg.
Код:
func(a,...)
{
setarg(1, 0, 8);
}
func2()
{
new a = 7;
new b = 12;
func(a,b);
//b = 8
}
The argument 'a' will be pushed as if it was passed by call by value and the argument 'b' will be pushed as if it was passed by call by reference.
Код:
#emit PUSH.ADR b //Variable Argument
#emit PUSH.S a //Call by Value
Return Instruction
The stack must be restored to its initial state (when the function was called) before returning. If not restored correctly, then it will give rise to a run-time error and will render you whole script unusable.
Код:
func()
{
new a,b;
}
main ()
{
func();
}
The PAWN Compiler keeps track of the STK and updates it every time you create a local variable or when a local variable goes out of scope. Using this information, the compiler restores the stack to its original condtition before returning. The assembly equivalent should show this step.
Код:
stack 8
zero.pri
retn
It moves the STK pointer up by 8 bytes since there are still two local variables on the stack after which it returns.
The PAWN Compiler is doing a good job but unfortunately the compiler isn't smart enough to update the STK pointer when we emit assembly instructions which modify the stack. The following example clearly shows the problem with the compiler's limited intelligence.
Код:
func()
{
#emit PUSH.C 123 //The compiler isn't aware of this
}
The above code will give rise to a run-time error. The compiler doesn't add a "STACK" instruction to move the STP pointer up by 4 bytes while returning as it is not aware of the PUSH instruction, therefore the return instruction (added by the compiler) will pop "123" and use it as the return address, hence leading to a crash.
If you have pushed values onto the stack, you need to clear them before using the return instructions. When you use "return value;" in a function, the compiler automatically clears the STACK as per its knowledge before returning.
The above buggy code can be fixed in two ways
- Simply POP the value from the stack before returning
- Use STACK 4 to move the stack pointer up by 4 bytes before returning
That is just one possibility. The compiler won't restore the stack to its original condition for us when we are placing the return instructions ourselves. It is our responsibility to ensure that the stack has been restored before we use the return instruction.
Код:
func()
{
new a,b;
#emit ZERO.pri
#emit RETN
return 0; //The compiler will complain about not returning even though we use the RETN instruction. The sole purpose of this line is to keep the compiler happy.
}
In the above code, the compiler does add a "STACK 8" instruction just before "return 0" but it doesn't add it before the "RETN" instruction which we placed manually. Hence, the stack won't been restored to its original condition before returning and hence it will give a run-time error.
In general , you must move the stack pointer up by 4 bytes for every local variable which is still in scope and 4 bytes for every mismatched PUSH instructions.
What ever the case maybe, you just ensure that the stack is at the same condition as it was at the start of the function before returning.
The reason for the crash when you don't restore the stack due to the RETN/RET instructions pop the wrong return address from the stack and jumping to it We know that before calling a function, the return address is pushed onto the stack. The return address will be the next item on the stack to be popped. If the STK pointer is not set properly or in other words the stack hasn't been restored to the initial state, the RET/RETN instruction will pop the wrong address from the stack and will jump to some unknown location which is usually invalid. Therefore, causing the server to crash.
Returning values
You already know about the "return" keyword but you do not know how it works.
When you use "return 0;" , this is what happens
Код:
#emit ZERO.pri //PRI = 0
#emit RETN
If you do "return variable; ,
Код:
#emit LOAD.S.pri variable //Assuming variable is a local variable
#emit STACK x //To fix the stack
#emit RETN
You by now might have guessed how return works.
Whatever has to be returned by a function is stored in PRI register before executing a RET/RETN instruction.
We will look at a useful example to make you more comfortable with it.
Код:
stock getAddress(arr[]) //Finds the address of the passed array and returns it
{
#emit LOAD.S.pri arr
#emit RETN //We don't need a STACK instruction because we never created any locals nor we pushed something on to the stack
//does the same as the following
new addr;
#emit LOAD.S.pri
#emit STOR.S.pri addr
return addr; //The compiler will automatically add the stack correct instruction before returning
}
Finding functions
There are two ways to obtain the address of a function.
Using CONST.pri
Код:
#emit CONST.pri func_name
#emit STOR.S.pri local_var
This is one and the only way to find the address of a non-public and non-native function.
Searching the public function list
The AMX machine
performs a binary search to find the address of a public function when you use CallLocalFunction/CallRemoteFunction for which the public function list needs to be sorted.
You can treat the public functions list as a sorted array of strings. Hence, every public function in that list has an index which
funcidx returns.
Using the index you can now jump to the entry in the public function list to find out the address of the function.
Код:
new
idx = funcidx("function"),
pointer;
#emit LCTRL 1 //Get the offset to DAT
#emit NEG //Invert to get the offset to start of prefix
#emit ADD.C 32 //Public function list comes 32 bytes after the start of prefix
#emit STOR.S.pri pointer //Store the address to the public function list
#emit LREF.S.alt pointer
// Get the pointer to the function at the given index.
#emit LCTRL 1
#emit NEG
#emit ADD
#emit LOAD.S.alt idx //Load the index to ALT
#emit SHL.C.alt 3 //Multiply the value stored in ALT by 8 since each entry in the list takes 8 bytes
#emit ADD
#emit STOR.S.pri pointer
#emit LREF.S.pri pointer //Obtain the address
#emit STOR.S.pri pointer
Arrays & Strings
Single Dimensional Arrays
The memory layout of a single dimension array in PAWN is similar to the memory layout of a single dimension array in C/C++. The contents of an array are stored in a linearly where the passing the array is equivalent to passing the address of the first element.
If you know the address of the first element of the array, then you can find the address of any element of the array using the following formula.
Since every item in the array is a cell, every item takes 4 bytes. To get the address of the second element in the array, we add 4 to the address of the first element. To get the address of the third element of the array, we add 8 to the first element of the array. We can generalize this to a formula.
Код:
Address of nth element of the array = Address of the first element + (n-1) *4
If you have understood how single dimension arrays are stored in memory, then you should be able to understand the following code which modifies the 4 element of the array.
Код:
new arr[100];
main ()
{
#emit CONST.pri arr //Load the address of the array (address of the first element)
#emit ADD.C 12 // (n-1)*4 = (4-1) *4 = 3 * 4 = 12
//PRI now has the address of the 4th element of the array
#emit PUSH.pri //Push the address of the 4th element of the array
#emit CONST.pri 10
#emit POP.alt //Pop the address into ALT
#emit STOR.I //Store PRI in [ALT]
}
There are two macro instructions "IDXADDR" and "LIDX" which can be used for calculating the address of an array element and loading the data stored in an array element respectively.
Код:
#emit CONST.alt array_address
#emit CONST.pri n
#emit IDXADDR //PRI now has the address of the (n + 1)th element
#emit CONST.alt array_address
#emit CONST.pri n
#emit LIDX //PRI now has the value stored in the (n + 1)th element
Two Dimensional Arrays
Two dimensional are not that nice and they are completely different from C/C++ Style memory layout of two dimensional arrays.
Two dimensional arrays consists a single dimensional array which holds the offsets to the sub-arrays.
For example, an array declared as "arr[5][6]" would have a single dimensional array with 5 elements where each element points to a six element sub-array. You can imagine a two dimensional array to be a array of pointers where each pointer points to a sub-array.
The total memory footprint of a two dimensional array is given by the following formula
Код:
number of cells needed = major_dimension + major_dimensionЧminor_dimension
As stated above, the “major dimension” of two-dimensional arrays holds the offsets to the sub-arrays. This offset is in bytes and it is relative to the address of the cell from which the offset was read.
If that sounds complicated, the example should clear all your doubts.
If you have to obtain the address of the first element of arr[2], then you will have to follow these steps:
*
Major Array:I call the array that holds the offsets to the sub-ararys as major array- Get the address of the major array
- Find the address of the 2nd element of the major array
- Now find the offset to the arr[2] by loading the value stored in the address found in the previous step
- Add the offset to the address of the 2nd element of the major array
You now have the address of the first element of arr[2].
Once you know the first element of arr[2], you can treat arr[2] as a single dimension array.
To find the value of arr[2][4] should now be extremely simple.
Anyway here is the snippet which does it
Код:
#emit CONST.alt arr //Load the address of the array
#emit CONST.pri 2 //We want to access the 2nd sub-array
#emit IDXADDR //Address of the 2nd element of the major array
#emit MOVE.alt //Keep a copy of that address since we need to add it to the offset to get the address of the sub array
//ALT = PRI = Address of the 2nd element of the major array
#emit LOAD.I
//ALT = Address of the 2nd element of the major array
//PRI = offset relative to the address stored in the ALT to the 2nd sub-array
#emit ADD
//PRI now has the address of the sub-array
#emit MOVE.alt //Move the address of the first element of the sub-array from PRI to ALT
#emit CONST.pri 4 //We want the 4th element of the sub-array
#emit LIDX//Load the value stored at arr[2][4]
#emit STOR.S.pri arr_4_2
Higher Dimensional Arrays
A three dimensional array holds a single dimensional array of pointers to sub-two-dimensional-arrays which in turn hold single-dimensional array of pointers to sub-sub-arrays. This isn't that hard once you have understood two dimensional arrays properly.
Here is an example which illustrates how to access the data stored in arr[3][3][3]
Код:
#emit CONST.pri arr //Load the address of the array
#emit ADD.C 12 //Get the address of the 3rd element of the major array
#emit MOVE.alt //Make a copy of the address since we need to add it to the offset
#emit LOAD.I //Get the offset
#emit ADD //ALT contains the address of the 3rd element of the major array and PRI contains the offset
//PRI now has the address of the 3rd sub-two-dimensional array
//Now everything is similar to what we've used in the previous section
#emit ADD.C 12 //Get the address of the 3rd element of the sub-2-dimensional array
#emit MOVE.alt //Make a copy of the address
#emit LOAD.I //Get the offset
#emit ADD //Get the address of the arr[3][3]
//Now its an operation on a single dimensional array
//PRI has the address of the sub-sub-array
#emit ADD.C 12 //We need the 3rd element of the sub-sub-array
//PRI now has the address of arr[3][3][3]
#emit STOR.S.pri arr_3_3_3
Packed Arrays
One thing most of you don't know is the characters are not stored in the same order in packed strings as they are stored in unpacked strings.
For example, "AMX Assembly" is stored in different ways would look like
Код:
AMX Assembly ; C String
A M X A s s e m b l y ; PAWN Strings
XMAessAylbm ; Packed Strings
Packed strings look messed up. The reason why PAWN stores packed strings in such order is because PAWN cells are stored in
little-endian system.
By little-endian, we mean that bytes of every cell are read backwards.
So how do you interpret a packed string? Let's take the same example, " XMAessAylbm".
We first divide the packed string to make groups of 4 characters.
Now we reverse the order in each cell to obtain a byte order string.
*note that I have ignored the null character in all the examples
I hope by now you are familar with packed strings. The purpose of adding a Packed Strings sub-section is to demonstrate how to obtain the individual bytes from a cell and to let you know that 4 characters are stored in one cell so that you push the correct character when passing packed strings. Explore and find out what the compiler does when you use the following code
Код:
packed_Array{5} = 123;
Код:
new str[4 char] = !"AMX"; 3 characters + 1 null character
To rip apart str[0] into 4 byte pieces, we need to make use of bitwise operators.
n = 0 for the first byte
n = 1 for the second byte
n = 2 for the third byte
n = 3 for the fourth byte
Snippets
I-ZCMD Variants
I-ZCMD has two special beta versions which heavily rely on AMX assembly to function properly.
I-ZCMD Beta 03 version
The principle is pretty simple. This version of IZCMD searches the public function list and finds every command function's address and its name and builds a sorted list of commands. When a command is called, it performs a binary search on the sorted list and finds the address of the command function if it exists which is then used to call the command function.
I-ZCMD Single Use Version
This makes a cache of commands with their function's addresses and stores them as properties. When a player uses a command it searches through the property list to find the correct address for the command function and executes it.
y_amx
Quote:
This library simply provides simple access to the various AMX file elements. Information on segment offsets (absolute and relative to "DAT"), various methods to list all public and native functions (and some other rarely used elements), and read and write arbitrary addresses within the AMX address space. See above for more details.
|
Every #emit beginneer needs to explore y_amx.
You can find y_amx.inc at
YSI Includes Respository at GitHub.
Sort multi-dimensional arrays
This include was written by Slice which uses a quite unique out-of-box algorithm to sort multi dimensional arrays.
The algorithim doesn't really modify the contents of the array while sorting rather it changes the addresses of the major array.
You can find the MD-Sort Include
here.
Variadic Functions
This include was written by Emmet which makes use of lot of #emit directives to allow a new way of handling functions that accept variable arguments.
You can find the Variadic Functions include
here
Compiler Bugs
There are lot of compiler bugs which you need to be aware of while using #emit.
You can find the list of known bugs in this thread,
"Known Runtime/Compiler PAWN Bugs "
Additional Information
#emit Tutorial by ******
here.
#emit Discussion Thread
There is a topic at the Disscusion Board which you can post your questions/queries regarding emit usage.
You can find the topic
here.
PAWN Documentation
The official documentation can be found here.
http://www.compuphase.com/pawn/Pawn_...nter_Guide.pdf
Note that the above documentation is for the latest version of PAWN. SA-MP isn't using the latest version of PAWN and hence there might be some discrepancies in the documentation and the version which the SAMP is using.
If you are using YSI, then there should be copies of the 2007 PAWN documentation in the pawno folder.
PAWN Implementer Guide Link #1
PAWN Implementer Guide Link #2
A copy of the 2007 PAWN Implementer Guide Document to the thread.
PAWN Generate Assembly File Option
You can tell the PAWN Compiler to give you the assembly generated for you PAWN Code by using the "-a" flag.
If you don't know how to feed the flags to the compiler then follow these steps:
- Create a new file in the pawno directory called "pawn.cfg"
- Add a line with "-a"
- Save!
Now the PAWN Compiler will create an .asm file instead of an AMX.
Credits
Thanks to everyone who have displayed their scripts to the public which use AMX Assembly from which I learnt most of the assembly. Especially ******'s includes from which I learnt a lot.
Special Thanks to ****** for his #emit tutorial.
Re: AMX Assembly (#emit) -
Yashas - 15.10.2015
Jump Instructions
There are assembly instructions which allow you to jump.
Instruction List & Usage
Here is a list of all of them:
# | mnemonic | operand | semantics |
51 | JUMP | offset | CIP = CIP + offset (jump to the address relative from the current position) |
53 | JZER | offset | if PRI == 0 then CIP = CIP + offset |
54 | JNZ | offset | if PRI != 0 then CIP = CIP + offset |
55 | JEQ | offset | if PRI == ALT then CIP = CIP + offset |
56 | JNEQ | offset | if PRI != ALT then CIP = CIP + offset |
57 | JLESS | offset | if PRI < ALT then CIP = CIP + offset (unsigned) |
58 | JLEQ | offset | if PRI <= ALT then CIP = CIP + offset (unsigned) |
59 | JGRTR | offset | if PRI > ALT then CIP = CIP + offset (unsigned) |
60 | JGEQ | offset | if PRI >= ALT then CIP = CIP + offset (unsigned) |
61 | JSLESS | offset | if PRI < ALT then CIP = CIP + offset (signed) |
62 | JSLEQ | offset | if PRI <= ALT then CIP = CIP + offset (signed) |
63 | JSGRTR | offset | if PRI > ALT then CIP = CIP + offset (signed) |
64 | JSGEQ | offset | if PRI >= ALT then CIP = CIP + offset (signed) |
128 | JUMP.pri | | CIP = PRI (indirect jump) |
The PAWN Implementer Guide says we can pass offsets to the jump instructions but unfortunately passing offsets as numbers crashes the compiler.
Код:
main()
{
new x,y = 10,z = 50;
#emit LOAD.S.pri y
#emit LOAD.S.alt z
#emit JEQ 8
#emit CONST.pri 100
#emit STOR.S.pri x
printf("%d",x);
}
The above code will crash the compiler.
The only way you can now pass offsets to the jump instructions is by using labels.
The following code illustrates how to use labels as offsets for jump instructions
Код:
#include <a_samp>
main()
{
new y = 10,z = 11;
print("Checking two numbers for equality");
goto check;
not_equal:
printf("The numbers are not equal");
return 0;
equal:
print("The numbers are equal!");
return 0;
check:
#emit LOAD.S.pri y
#emit LOAD.S.alt z
#emit JEQ equal
#emit JUMP not_equal
}
Using jump instructions in assembly hardly gives you any advantage. It will only improve performance by negligible amount when compared to using "goto label;".
Код:
if(k == 0) goto label;
will produce an assembly output
Код:
#emit LOAD.S.pri k
#emit JZER somewhere
somewhere:
JUMP label
The extra jump can be avoided by manually jumping using assembly
Код:
#emit LOAD.S.pri k
#emit JZER label
Compiler Bug
If you try to compile the following code you will get an error stating that symbol "abc" is undefined even though it is defined.
Код:
goto abc; //No error
#emit LOAD.pri a
#emit LOAD.alt b
#emit JSLESS abc //Error, undefined symbol abc
abc:
your code
The compiler allows you to use goto with lables that are defined later. But unfortunately the compiler does not recognize labels that are defined later when you use #emit.
Bug #13 in Known Runtime/Compiler Bugs topic.
Re: AMX Assembly (#emit) -
Yashas - 15.10.2015
Reserved
Re: AMX Assembly (#emit) -
Ahmad45123 - 15.10.2015
Wow, finally a proper tutorial, thanks allot matte.
+REP you have great effort here.
E: oops, cant rep cuz i am on mobile.. Will do it once i am on pc.
Re: AMX Assembly (#emit) -
SecretBoss - 15.10.2015
Amazing job, but it will take ages to read also it took more to write it, thanks its very useful!
Re: AMX Assembly (#emit) -
Beckett - 17.10.2015
Magnificent. Didn't really have time to read it all but there's plenty of useful information out there, well done.
Re: AMX Assembly (#emit) -
Stanford - 13.01.2016
Amazingly written thanks for this huge efforts I hope that I will know how to use #emit properly one day.. Who knows maybe this tutorial will save me
Re: AMX Assembly (#emit) -
darkdevil - 17.05.2016
Big essay btw nice tutorial
Re: AMX Assembly (#emit) -
Dutheil - 24.06.2016
Quote:
; 16:Argument 2
; 12:Argument 1
; 8:Number of arguments in terms of the size (4 * n)
; 4:Return Address
; 0:Frame
|
It is not exact. *(data + frm) stores the previous frame pointer and not the frame pointer.
Except if there is a difference between "frame" and "frame pointer".
Re: AMX Assembly (#emit) -
Nero_3D - 24.06.2016
Quote:
Originally Posted by Dutheil
It is not exact. *(data + frm) stores the previous frame pointer and not the frame pointer.
Except if there is a difference between "frame" and "frame pointer".
|
You can't store pointers, you store addresses (absolute or relative)
And in that case the 0 is a relative address, relative to the current frame which is (DAT + FRM), so its the same
And nearly all addresses are relative to DAT
Re: AMX Assembly (#emit) -
Yashas - 25.06.2016
Quote:
Originally Posted by Dutheil
It is not exact. *(data + frm) stores the previous frame pointer and not the frame pointer.
Except if there is a difference between "frame" and "frame pointer".
|
Yes, the way I have written is quite ambiguous and the context is so wrong too. I was talking about the stack printed from the func() and I should have cleary mentioned that it is the FRM of the main. My intention was to convey that 0 is the FRM pointer's start. I mean, everything above and below is with respect to the frame but yeah, I agree with you that is misleading.
Thanks for letting me know.
Corrected it & added new information about FRAMEs.
Quote:
Originally Posted by Nero_3D
You can't store pointers, you store addresses (absolute or relative)
And in that case the 0 is a relative address, relative to the current frame which is (DAT + FRM), so its the same
And nearly all addresses are relative to DAT
|
You are right but he was talking about something else. I said "frame" without mentioning of which function it was (caller or called function).
I was looking for suggestions to improve the tutorial. Let me know if you have any. I hope to make it unambiguous and complete by the end of this year. The ambiguity can be found only by a reader who is new to emit because most of us tend to implicitly assume the correct meanings of the ambiguous terms.
If you think there is any part which isn't covered in this tutorial, let me know that as well!
Changes to the tutorial:
Code:
The frame pointer ("FRM") points to the address at the bottom of the running function's header (can be treated as the address to the start of the stack for the running function). The stack pointer("STK") points to the last data put on to the stack.
Code:
The frame pointer ("FRM") points to the address at the bottom of the running function's header (can be treated as the address to the start of the stack for the running function). If that scared you, here is a much nicer way to say the same. The FRM pointer basically stores an address which points to the start of the stack. When you use an offset of a local variable (or that of an item on stack) , you add that offset to the FRM to get the address which is then added to DAT to get the absolute address. If you did not understand, no need to worry because you will get another chance at "Making function calls" section. The stack pointer("STK") points to the last data put on to the stack.
--------------------------------------------------------------------------------------------------
Code:
new x;
func(a,b)
{
new z = 101;
new y = 102;
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
//Nothing more to POP
for(;;) { } //If I allow the function to return, it will crash the server (run-time error) because the POP instructions change the STK Register and the return instruction expects STK to be having the correct address so that the next POP instruction would pop the return address. Did not get it? More about this later.
return x; //If this were to be executed, you would get a "Invalid memory access" run-time error.
}
main()
{
new zyx = 1;
func(1001,1002);
new abc = 2;
}
Code:
102 ; y
101 ; x
16468 ; Function Frame
596 ; Return Address
8 ; Since we pushed 2 arguments, we had pushed 8
1001 ; argument 1
1002 ; argument 2
1 ; xyz, Local Variable from the previous function
Code:
new x;
func(a,b)
{
new z = 101;
new y = 102;
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
printf("%d",x);
#emit POP.pri
#emit STOR.pri x
//Nothing more to POP but lets investigate what is in the FRM register
#emit LCTRL 5
#emit STOR.S.pri z
printf("FRM in func: %d", z);
for(;;) { } //If I allow the function to return, it will crash the server (run-time error) because the POP instructions change the STK Register and the return instruction expects STK to be having the correct address so that the next POP instruction would pop the return address. Did not get it? More about this later.
return x; //If this were to be executed, you would get a "Invalid memory access" run-time error.
}
main()
{
new zyx ;
#emit LCTRL 5
#emit STOR.S.pri zyx
printf("FRM in main: %d", zyx);
zyx = 1;
func(1001,1002);
new abc = 2;
}
Code:
FRM in main: 17988
102 ; y
101 ; x
17988 ; Frame of "main"
1900 ; Return address
8 ; we pushed 2 arguments, number of bytes they take is 8
1001 ; argument 1
1002 ; argument 2
1 ; zyx local variable from the main()
FRM in func: 17960
--------------------------------------------------------------------------------------------------
Added
Code:
Before getting into the details, let us first discuss what that "frame" thing really means. As said in an earlier section, every item in the stack has a relative address with respect to frame pointer (the negative relative addresses we talked about). To obtain the complete address we add the relative address/offset to the frame pointer.
Just a note, I said 'complete address' actually it isn't, you need to add DAT to that address to obtain the actual address. Never mind if you don't understand it. The assembly OpCodes accept addresses relative to DAT everywhere so it shouldn't bother you. You will learn eventually as time passes.
Also note that abc is not yet on the stack when func is called nor is x there since x is a global variable and resides in the heap.
--------------------------------------------------------------------------------------------------
Re: AMX Assembly (#emit) -
Yashas - 15.07.2016
Updated all sections with new content.
More detailed explainations for parts which people found hard to understand (Reported via PM).
Fixed all ambigious explainations which were reported via PMs.
If you have suggestions/improvements, please let me know via PM. Also let me know if you find any part of the tutorial hard to understand so that it can be improved.
Re: AMX Assembly (#emit) -
Luicy. - 05.08.2016
This is wonderful, if I sometime get interested in AMX Assemblies, I will read this for sure.
Enjoy your reputation and keep it up.
Re: AMX Assembly (#emit) -
Sanya4 - 26.08.2016
WTF?! Just wanted to ask: Y_Less did it on purpose?!!111
IN "Usage of stack as temporary storage":
we see:
PHP Code:
#emit POP.pri //Pop the last pushed value into PRI
//last pushed was local, which had a value of 10 when pushed and hence PRI = 10
//by me: PRI is 10, ALT is 5
#emit CONST.alt 5 //ALT = 5
#emit SDIV //PRI = ALT / PRI (signed divide),ALT = ALT mod PRI
//by me: let's count, 5/10=0
// 5 mod 10= 5
//Now ALT = 0 since 5 divides 10 completely and leaves no remainder
//PRI = 2 since 10 divided by 5 gives 2 <=== WTF?!
Did anyone get it? xD (Usually, in asm's it collects to PRI(As PRI is A, ALT is B, where a is accumulator register, b is base register))
IN "Finding the address of functions"
PHP Code:
new addr;
#emit CONST.pri func1
#emit STOR.S.pri addr
Doesn't work with public function, should it be or what?!(Compared with funcidx)
IN "Accessing elements of arrays that are arguments"
PHP Code:
public OnPlayerCommandText(playerid,cmdtext[])
{
new index = 2;
#emit LOAD.S.alt cmdtext
#emit LOAD.pri index //<==================== GOOD joke!
#emit LOAD.pri index FACEPALM x999
Also it's ok:
PHP Code:
#emit LOAD.S.alt cmdtext
You are loading a value, NOT address!
I think, it should be:
PHP Code:
#emit CONST.pri cmdtext
And btw, write right sematics for IDXADDR:
PHP Code:
PRI = ALT + (PRI Ч cell size) (calculate indexed address)
Everyone can get it.
Change it to understandable(not everyone can understand it):
PHP Code:
PRI = ALT + CELLSIZE * PRI
And where is wrong(who didn't get it):
PHP Code:
public OnPlayerCommandText(playerid,cmdtext[])
{
new index = 2;
#emit CONST.alt cmdtext
#emit LOAD.S.pri index
#emit IDXADDR //PRI = ALT + (PRI * cell size) (calculate indexed address)
//Now PRI contains the address of the element stored in index 2 of cmdtext (cmdtext[2])
#emit LOAD.I //PRI = [PRI] //Since PRI contains the address of cmdtext[2], [PRI] will give the value stored at index 2
//Instead of using IDXADDR and then LOAD.I, you can do the same with just one instruction using LIDX
//LIDX = PRI = [ALT + (PRI x cell size)]
}
I read until it, Gonna post more <s>bugs</s> easter eggs(maybe).
I see no the first time when Y_Less using trololo functions.
edit 1:
PHP Code:
main()
{
new a[] = "AMX Assembly";
new character;
#emit ADDR.pri a
#emit ADD.C 8
#emit LOAD.I
#emit STOR.S.pri character
printf("%c", character);
}
Just gonna wrote here. 8 is 2 symbols as cell is 4. u wouldn't understand maybe cos I wrote it.
PHP Code:
#emit ADD.C 4*n //where n is amount of symbols
Re: AMX Assembly (#emit) -
Nero_3D - 27.08.2016
Quote:
Originally Posted by Sanya4
IN "Usage of stack as temporary storage":
we see:
Did anyone get it? xD (Usually, in asm's it collects to PRI(As PRI is A, ALT is B, where a is accumulator register, b is base register))
|
it should be PRI / ALT, ALT / PRI would be SDIV.alt
Quote:
Originally Posted by Sanya4
IN "Finding the address of functions"
PHP Code:
new addr;
#emit CONST.pri func1
#emit STOR.S.pri addr
Doesn't work with public function, should it be or what?!(Compared with funcidx)
|
As far as I know it works flawless if the function is defined before this code
Quote:
Originally Posted by Sanya4
I think, it should be:
PHP Code:
#emit CONST.pri cmdtext
|
#emit LOAD.S.alt cmdtext is correct, use const.pri to get the relative address (cmdtext would be 16)
Re: AMX Assembly (#emit) -
Sanya4 - 27.08.2016
btw, can u give me link to all opcodes?(not found as well)
About getting address of function: I think funcidx returns id only for public(and count only publics)/
And why we are using LOAD.S.alt(it stores content, not address)? But CONST.pri loads addreess?!?!!?!?
edit 1:
semantic for sub:
WRONG:
PHP Code:
PRI = ALT - PRI
right:
semantic for sdiv:
WRONG:
PHP Code:
shown in example
right:
PHP Code:
PRI = PRI/ALT (quotient)
ALT = PRI%ALT (the second register consists remainder)
Re: AMX Assembly (#emit) -
Nero_3D - 27.08.2016
Quote:
Originally Posted by Sanya4
btw, can u give me link to all opcodes?(not found as well)
|
Look into the Pawn Implementer's Guide
http://y-less.com/uploads/pawn-imp-3.0.3367.pdf (2005 version)
(Or in the rar file from the first post - 2007 version)
and search for Instruction reference (should be either page 94ff or 99ff)
Quote:
Originally Posted by Sanya4
About getting address of function: I think funcidx returns id only for public(and count only publics)
|
funcidx returns the index in the public table, see the implementer's guide
Quote:
Originally Posted by Sanya4
And why we are using LOAD.S.alt(it stores content, not address)? But CONST.pri loads addreess?!?!!?!?
|
you needs to use load.s.alt because arrays are always passed by reference (only the address is passed)
const.pri/alt doesn't load anything, it just puts the given value into pri/alt
Doing load.s.alt cmdtext would load the address where content of cmdtext is saved
however doing const.alt cmdtext would only transfer the relative address into alt
const.pri/alt 5 // pri/alt = 5
const.pri/alt global_variable // gets address relative to DAT section
const.pri/alt local_variable // gets address relative to FRAME
const.pri/alt function // gets address relative to COD
also add.c cmdtext is the same as add.c 16
Re: AMX Assembly (#emit) -
JakeXxX - 28.08.2016
Nice tutorial!
Re: AMX Assembly (#emit) -
spookie - 31.01.2017
Apologies for bumping an old thread, but this tutorial is fantastic!.. Fair play to you, Yashas!
Re: AMX Assembly (#emit) -
HydraHumza - 01.02.2017
A pretty decent tutorial. Really appreciate it.