It is hard to start learning advance programming techniques without decent programming knowledge before hand. Add to this the fact you are approaching things from the "wrong" end, observing and retrieving data from memory into some sort of user generated "holder" (variable or struct to hold data you retrieve from memory).
I will try to show first, how Pw data is organized from programmers stand point, before moving on to how it's structured in memory.
I would provide examples in AutoIt, but being I've never used it and am not familiar with how it works (when and how pointers are used and if), that wouldn't be of much help not to mention probably wrong.
Instead examples will be based on Delphi (Pascal).
Let us imagine we want to build a game, or more precisely, part that will handle players. Focusing on holding data only and ignoring any routines for manipulating it, we can start with a simplified rule set:
1. players have lots of data associated with them
2. there will be cases where we will encounter more than one.
We can try and define player as
(Cardinal = in Delphi, 4 byte unsigned type)
but that wouldn't hold a lot of data.
So instead, what we'll do, is define a structure. Structure is known as record in Delphi and struct in C++ and in both, it describes the same thing.
Code:
TPlayer = record
Id: Cardinal;
HPMax: Cardinal;
HPCurr: Cardinal;
MPMax: Cardinal;
MPCurr: Cardinal;
Name: Array[0..15] of WideChar;
PosX: Single;
PosZ: Single;
PosY: Single;
end;
(Cardinal = in Delphi, 4 byte unsigned type)
(Single = in Delphi, 4 byte floating point type, 7-8 significant digits)
(Array[0..15] of WideChar = let's leave this one for later)
That's better, now our player has both HP values, both MP values, it has Id we can use to track and address it, it has position so we know where it is and it's has a name.
Real player would have much more, but our game is simple and that's really all we need.
If we use this type in code now, it would look like this:
Code:
Player: TPlayer; // we declare variable Player to be of type TPlayer defined above
Player.Id := 1; // First player, yey.
Player.HPMax := 400;
Player.HPCurr := 350;
Player.MPMax := 200;
Player.MPCurr := 150;
Player.Name := 'My player';
Player.PosX := 5.152;
Player.PosZ := 22.346;
Player.PosY := 17.454;
Player variable now holds player data. Name might be boring, but in his defence, he is the first character on server, so that's ok :).
Please note that "Player.Name := 'My player';" wouldn't compile, but it was defined array on purpose and will be explained later.
Assuming it would compile, Player variable would be written to memory and it would look like this:
01 00 00 00 90 01 00 00 5E 01 00 00 C8 00 00 00 96 00 00 00 00 00 00 00 2F DD A4 40 9C C4 B2 41 CB A1 8B 41
Bloody mess at first glance, let us break it down:
01 00 00 00 <- Id (if read as 4 byte unsigned type, Cardinal)
90 01 00 00 <- HpMax (ditto above)
5E 01 00 00 <- HpCurr (ditto above)
C8 00 00 00 <- MPMax (ditto above)
96 00 00 00 <- MPCurr (ditto above)
00 00 00 00 <- Name (pointer, explanation incoming soon and it wouldn't be 00 00 00 00)
2F DD A4 40 <- PosX (if read as 4 byte floating point type, Single)
9C C4 B2 41 <- PosZ (ditto above)
CB A1 8B 41 <- PosY (ditto above)
First thing to note is that endian is reversed.
01 00 00 00 = reverse endian = 00 00 00 01 hex = 1 dec = Player.Id
90 01 00 00 = reverse endian = 00 00 01 90 hex = 400 dec = Player.HpMax
and so on...
Second, that player data is written in exact order as defined in TPlayer, starting with Id, HPMax, HPCurr, MPMax, MPCurr, etc,...
Another way to put it, Id is offset by 0 bytes from start of Player. MpMax is offset by 4 bytes from start of Player.
But what is "start of player"?
It turns out, that when we declared Player: TPlayer, what happened was a pointer was made for us. It's not shown as one to programmer and yet, it is one.
In fact, a whole lot of other things were done "behind our backs" so to speak.
First, size of TPlayer was calculated:
Size of TPlayer = size of type(Id) + size of type(HPMax) + size of type(HPCurr) + size of type(MPMax) + size of type(MPCurr) + size of type(Name) + size of type(PosX) + size of type(PosZ) + size of type(PosY).
Comes out as 36 bytes, all of types used in creating TPlayer occupy 4 bytes and there are 9 of them. Yes, Name as well.
Then, memory for it is allocated, 36 bytes in all.
Data is written into allocated memory space.
Address to start of that data block is written into Player, or to put it another way, POINTER pointing to start of block is written into Player.
Now it gets interesting.
In order for assembler to read/write to any value (field) in Player, it needs to:
- get address Player points to, in order to know where data starts. It does this by dereferencing pointer, which means it gets value of pointer.
- read/write from/to field requested
Code:
mov eax, [Player] // Dereferences Player pointer, eax will now contain address of start of data block
mov [eax+00], 1 // Writes value of 1 into ecx+00, in other words, writes 1 into Player.Id, remember Id is address Player points to + 00.
mov [eax+04], 400 // Writes value of 400 into ecx+04, in other words, writes 400 into Player.HpMax, HpMax is address Player points to + 04.
Note that in assembly, 1, 400,... will be written in hex. I wrote it in decimal so it's clear what get's written, without the need for you to convert it.
Starting to look familiar?
Imagine it's running and we wish to make offset.ini for some bot to use.
It would end up looking like this:
player_Id = 0
player_HPMax = 4
player_HPCurr = 8
player_MPMax = C (dec. 12)
player_MPCurr = 10 (dec. 16)
player_NameAddress = 14 (dec. 20)
player_PosX = 18 (dec. 24)
player_PosZ = 1C (dec. 28)
player_PosY = 20 (dec. 32)
See the pattern, it's in the same order as TPlayer was defined in:
Code:
TPlayer = record
Id: Cardinal; // from 00 to 03, it occupies 4 bytes, 00 01 02 03
HPMax: Cardinal; // 04-07, again, occupies 4 bytes
HPCurr: Cardinal; // 08-0B
MPMax: Cardinal; // 0C-0F
MPCurr: Cardinal; // 10-13
Name: Array[0..15] of WideChar; // 14-17
PosX: Single; // 18-1B
PosZ: Single; // 1C-1F
PosY: Single; // 20-23
end;
We understand structures now, we can finally get to that Name.
If you recall, it was defined as:
Code:
Name: Array[0..15] of WideChar;
You saw, that trough out the entire explanation, we treated Name as a 4 byte type. It is in fact a pointer and pointers are always 4 bytes long. Yes, again with pointers.
So name is a pointer, and what it points to is start of data block that holds player name. Like with Player pointer, this one is no different.
What "Array[0..15] of WideChar" means is, allocated me a space in memory where I can put 16 characters of type WideChar. WideChar in Delphi is a type that occupies 2 bytes, that is 2 bytes per every character. So if it needs to allocate space to store 16 of characters, it needs to allocate 16*2 bytes = 32 bytes.
And when it's done allocating memory, it stores address to start of it into Name, effectively turning Name into a pointer.
If we read Name directly (offset 14, we will not get 'My player', we will get address that points to 'My player'. What we do then is take that address and read it.
Player + 14 -> points to Name address, address holding the actual value
(Player + 14) + 00 -> gives us 'My player'
Another offset was added, you see, because Name is also a pointer. It points to first letter of player name and since we don't want to read 5th letter of player name, offset 00 will work fine for us, bringing us to start of data block holding 'My player'. Characters in array are also placed one after another in memory.
In fact, any elements in array, be it characters or other data types, are placed one after another in memory. If they are 4 bytes or less in length, they hold their values. However, if they are longer than 4 bytes, they hold address that points to the value. Like the case with Name.
Right, so the first rule is meet, player can have lots of data associated with it.
What about the second one, "there will be cases where we will encounter more than one."?
Since all players in our game will have same types of data associated with them, TPlayer is ok.
But declaration:
can only hold data for one player and our goal is to potentially hold many more.
We will thus define another type, one that is capable of holding data of many players. We will define array of TPlayer.
Code:
TPlayers: array of TPlayer;
and to use it, we declare:
Another way of doing it, would be to skip defining array all together and declare variable as such:
Code:
Players: array of TPlayer;
Either way, it will be written to memory in the same way.
It can now be used, like so:
Code:
SetLength(Players, 5);
Players[index].Id
Players[index].HpMax
....
Where Index is dynamic and changes depending on which player we are referring to. It can range from 0 to 4, since we made array 5 elements big with a call to SeLength.
And why SetLength?
"array of TPlayer;" is so called dynamic array, it's called dynamic because it has no fixed length set when it's declared, unlike "Array[0..15] of WideChar" which is a static one (length set to 16 WideChars in declaration).
What compiler knows is that it's an array of types TPlayer. It thus knows length of TPlayer, but it doesn't know how many there will be. We tell it that by a call to SetLength, passing array and number of records.
Compiler now knows all it needs to, to allocate memory and write data.
But how does it do it...
As already mentioned before, array is collection of elements. If elements are 4 bytes or less in length, their value is written, if longer pointers to data.
In our case, we have array of TPlayer, and since TPlayer occupies 36 bytes, what we've created is esentially array of pointers. Omg no, again with the pointers. I'm afraid so.
Every record in array:
Players[index] = pointer to Player (of type TPlayer).
and like already established every Player = pointer to values of player (id, HpMax, etc,...).
In the end, what snuck in, was another offset.
While "Player + 04" still gives us HpMax, we now also need to know which Player is in question.
Players (array of Player) is a pointer to start of array. By using index, we can traverse this array and access different players in it. Essentially, by using index, we can get the address of Player we want.
Does this look familiar:
What it means is, write into eax address of record in array, located at offset 4*edx. Edx in this case is the same as Index when using array in Delphi:
Index gets incremented (or decremented) by steps of 1. So does edx.
But edx, in our example above, is multiplied by 4.
Back to theory.. array of pointers.. pointers occupy 4 bytes. That means "4*edx" is moving in steps of 4 bytes trough a list of pointers.
ecx = start address of array
4*edx = offset
What it also means, is that pointers are written one after another in memory, else simply taking address doing "[Address+4*Index]" wouldn't work.
How very nice of them, putting them selves in line like this.
And it turns out it's not only pointers that are written like that.
If we take a look at how Players are written to memory, starting at address pointed to by Players, it would look somewhat like this:
00 00 00 00 24 00 00 00 48 00 00 00 6C 00 00 00 90 00 00 00
Broken down (ecx hold address of start of array, known also as Players pointer):
00 00 00 00 <- pointer to address of Player at Index 0 ([ecx+4*edx], edx = 0)
24 00 00 00 <- pointer to address of Player at Index 1 ([ecx+4*edx], edx = 1)
48 00 00 00 <- pointer to address of Player at Index 2 ([ecx+4*edx], edx = 2)
6C 00 00 00 <- pointer to address of Player at Index 3 ([ecx+4*edx], edx = 3)
90 00 00 00 <- pointer to address of Player at Index 4 ([ecx+4*edx], edx = 4)
Each line represents pointer to Player, address at which Player[Index] starts.
After first one "00 00 00 00", each is 36 bytes bigger than the one before, because 36 bytes is the size of TPlayer.
And assuming all players have the same values as Player in the first exmple, looking at these addresses, memory view would be:
01 00 00 00 90 01 00 00 5E 01 00 00 C8 00 00 00 96 00 00 00 00 00 00 00 2F DD A4 40 9C C4 B2 41 CB A1 8B 41 01 00 00 00 90 01 00 00 5E 01 00 00 C8 00 00 00 96 00 00 00 00 00 00 00 2F DD A4 40 9C C4 B2 41 CB A1 8B 41 01 00 00 00 90 01 00 00 5E 01 00 00 C8 00 00 00 96 00 00 00 00 00 00 00 2F DD A4 40 9C C4 B2 41 CB A1 8B 41 01 00 00 00 90 01 00 00 5E 01 00 00 C8 00 00 00 96 00 00 00 00 00 00 00 2F DD A4 40 9C C4 B2 41 CB A1 8B 41 01 00 00 00 90 01 00 00 5E 01 00 00 C8 00 00 00 96 00 00 00 00 00 00 00 2F DD A4 40 9C C4 B2 41 CB A1 8B 41
Or broken down (4 bytes before - is memory address, it is for clarification purposes and isn't written in memory):
00 00 00 00 - 01 00 00 00 90 01 00 00 5E 01 00 00 C8 00 00 00 96 00 00 00 00 00 00 00 2F DD A4 40 9C C4 B2 41 CB A1 8B 41 <- data for player at index 0
24 00 00 00 - 01 00 00 00 90 01 00 00 5E 01 00 00 C8 00 00 00 96 00 00 00 00 00 00 00 2F DD A4 40 9C C4 B2 41 CB A1 8B 41 <- data for player at index 1
48 00 00 00 - 01 00 00 00 90 01 00 00 5E 01 00 00 C8 00 00 00 96 00 00 00 00 00 00 00 2F DD A4 40 9C C4 B2 41 CB A1 8B 41 <- data for player at index 2
6C 00 00 00 - 01 00 00 00 90 01 00 00 5E 01 00 00 C8 00 00 00 96 00 00 00 00 00 00 00 2F DD A4 40 9C C4 B2 41 CB A1 8B 41 <- data for player at index 3
90 00 00 00 - 01 00 00 00 90 01 00 00 5E 01 00 00 C8 00 00 00 96 00 00 00 00 00 00 00 2F DD A4 40 9C C4 B2 41 CB A1 8B 41 <- data for player at index 4
Note that addresses used in example above, while being correct in sense of increment, they are not correct in the sense of it's initial value.
Windows do not allocate address of 00 00 00 00 to any application, so when searching for and looking at addresses in real world, they will be higher.
I've used 00 00 00 00 as a starting address to avoid additional confusion.
Back to offsets now, before we had Player that pointed to various fields like Id, HpMax,...
Player + 00 -> Id
Player + 04 -> HpMax
That didn't change.
What did change is value of Player, start address of Player.
It can now be different depending on which player we wish to access.
What exactly it's address, is given to use by array
(Players + 4*Index) -> gives us Player start address
So if we wished to access HpMax of first player in array, offsets would be:
(Players + 4*0) + 04
Players being start address of array of Players
4*0 = 4 bytes per every record, since they are pointers, and 0 for first player. Arrays are zero indexed thus first record is located at index 0.
04 = offset in Player to retrieve HpMax
We have now satisfied both rules for our game.
1. players have lots of data associated with them
They can thanks to use of structures, like TPlayer
2. there will be cases where we will encounter more than one.
They can thanks to use of array, like array of TPlayer, or TPlayers.
But even arrays can't exist on their own, they are always owned by some other type (or object) and/or used inside other objects.
Understanding, that deep down all is handled by pointers, it isn't hard to imagine why, for example, players offsets in PW are usually nested deeper than 2 offsets.