Mario Kart Wii Gecko Codes, Cheats, & Hacks
Using Assembler Directives - Printable Version

+- Mario Kart Wii Gecko Codes, Cheats, & Hacks (https://mariokartwii.com)
+-- Forum: Guides/Tutorials/How-To's (https://mariokartwii.com/forumdisplay.php?fid=45)
+--- Forum: PowerPC Assembly (https://mariokartwii.com/forumdisplay.php?fid=50)
+--- Thread: Using Assembler Directives (/showthread.php?tid=1083)



Using Assembler Directives - Vega - 03-03-2019

Using Assembler Directives in your Source



Chapter 1: Intro

Gecko Code Assemblers all have a feature called Assembler Directives. Assembler Directives are various instructions (these are NOT actual PPC Instructions!!!) that tell the Assembler about any symbols, macros, statements, etc that are in your Source and how to apply them to help assemble your Code. If you have been following my "Go From Noobs to ASM Coder" index, you will have already learned about some Assembler Directives known as "Pseudo Ops" in the "BL Trick" thread.

When your ASM Codes are becoming too large/complex, it may be time to start using Directives. You may have a chunk of code that is present multiple times in your source. Why write it out multiple times when you can just write it out once and have it applied to multiple locations? Not only that, are you running into situations of having to memorize a specific numerical value for something that is commonly known by name? Directives allow you to use Names to represent any numerical value.



Chapter 2: Setting Symbols

You can direct specific names to any numerical value you want. This is done via the SET directive. Anything that uses the SET directive is known as a Symbol.

Format~
.set name, expression

name = The name ofc
expression = Value/Number, but this can also be a name (more on names within names in Chapter 5)

Let's say you want to set the name True for the numerical value of 1. You would write the following line anywhere in your source~

.set True, 1

You can write it out in Hexadecimal as well

.set True, 0x1

With the name True set to the value of 1, you can now write True in your source wherever you need to value of 1 applied. Like this....

Code:
.set True, 1 #This line only needs to present once in your source and can be located anywhere as long as the symbol is above the instruction that uses said symbol

lwz r3, 0 (r30)
cmpwi r3, True #Use 'True' in place of numerical value 1
beq- 0xC
addi r4, r4, 4
stw r10, 0x8 (sp)

Some more Symbol examples~

.set Offset, 0x1340
.set Function_Address, 0x8045ECC4
.set Error, -1




Chapter 3: Using @h, @ha, and @l with your Symbols for Memory Addresses

Having Symbols set for commonly used Memory Addresses can come in handy. Like so...

.set Function_Address, 0x8045ECC4

Therefore, if you need to set Function_Address into a register (let's say r12), you can do so something like this...

Code:
.set Function_Address, 0x8045ECC4

lis r12, Function_Address@h
ori r12, r12, Function_Address@l
  • @h = Use the upper 16 bits of the Symbol
  • @l = Use the lower 16 bits of the Symbol

Let's say we use that same address value but instead we have it called Loading_Spot, and we need to load a word value from the address. We will use r5 for the example. You would write it out like so..

Code:
.set Loading_Spot, 0x8045ECC4

lis r5, Loading_Spot@ha
lwz r5, Loading_Spot@l (r5)

@ha = Use the upper 16 bits, but add 0x10000 to it. And makes next use of @l afterwards be sign-extended.
@l = Use the lower 16 bits.

@ha stands for High Algebraic. You need to use @ha if you are ever storing/loading to/from an Address. This is just in case the lower 16 bits of your store/load Address exceed 0x7FFF. Even if the use of @ha isn't necessary for a particular store/load, you should always be applying it as a good force of habit.



Chapter 4: Using Math, Binary, and Logical Operations within Symbols

Instead of writing just plane jane values for your Symbols. You can incorporate basic calculations. Like this..

.set add_values, 0x1000 + 0xB00
.set subtract_values, 100 - 50
.set divide_values, 33 / 11
.set multiply_values, 0xA0 * 0x14

You can beyond basic math and utilize basic binary logical operations

.set OR_values, 0x1008 | 0x400
.set AND_values, 0x800 & 0x000
.set XOR_values, 0xF8001CFD ^ 0x01000AAB

You can do non-Binary Logical Statements like this...

.set logical_and_result, 1 && 2

Please note that these type of Logical Statements (non-Binary) only output a bool value (0 or 1). The above Symbol will do a logical AND of 1 and 2. If the result is non-zero, then the Symbol 'logical_and_result' will have the value of 1 tied to it. If the result is zero, then the Symbol will have the value of 0 tied to it.

Other Logical Statement Examples...

.set logical_or_result, 278 || 000
.set logical_not_result, !(2 && 1) #This will do a Logical AND Statement, but the Result of the AND'ing is flipped.



Chapter 5: Names within Names, and Psuedo-Ops

As mentioned in chapter 2, you can use names within names for Symbols. Like this...

.set bowser, 0x1E00
.set auto_drift, 1
.set character_config, bowser | auto_drift

Another example~

.set table_address, 0x81500078
.set index_one, 4
.set index_two, index_one*2
.set index_three, index_two*2
.set table_special_data_address, table_address + index_three

As you can see, you can establish symbols that can be used directly in calculations of other symbols.

You may be in a situation where you need to create a Lookup Table (via a BL Trick), and you need plain jane numerical values written in said table, but you don't wanna go thru the hassle of setting up Symbols for every numerical value.

You can use what are referred to as "Pseudo-Ops". Here's a list of all of them.
  • .zero X #X = Amount of null bytes to write
  • .space X #Same as .zero
  • .byte X #X = byte amount to write (0x00 thru 0xFF Hex range; 0 thru 255 decimal range)
  • .short X #X = halfword amount to write (hex range 0x0000 thru 0xFFFF, decimal range 0 thru 65535)
  • .long 0xXXXXXXXX = #X = Word value to write, really no point writing these in decimal
  • .llong 0xXXXXXXXXXXXXXXXX #X = Double-word value to write, no point writing these in decimal
  • .float X #X = decimal value for single precision float, takes up one Word of space
  • .string "write string here" #Writes out a typical 8-bit ASCII string that is auto appended with a Null byte
  • .asciz "write string here" #Same as .string, but you should make a habit of using this over .string because other Assemblers (non-PPC related) may have .string not append the Null byte. So this may help you from headaches in the far future in non-PPC work stuff.
  • .ascii "write string here" Writes out a 8-bit ASCII string !!!WITHOUT!!! the null byte appended.
  • .string16 "write string here" Writes out a 16-bit ASCII (wide) string and auto appends a null halfword. This Pseudo-Op only works in the PyiiASMH family of Assemblers.

There is also one more important Pseudo-Op and that is ".align X". It aligns your block of Pseudo-Op data. This is needed if your block of data (as a total) has a size that isn't divisible by 4. You can choose how to align the block of data.

.align X Guide
  • .align 1 = Align data to make data block size have a total that is divisible by 2
  • .align 2 = Divisible by 4; WHAT YOU WILL USE 99.9% OF THE TIME!!!
  • .align 3 = Divisible by 8
  • .align 4 = Divisible by 16 aka 0x10
  • .align 5 = Divisible by 32 aka 0x20
  • .align 6 = Divisible by 64 aka 0x40

etc etc...

You SHOULD ALWAYS add in a ".align 2" after any use of any Psuedo-Op that incorporates an ASCII string. '.align 2' may also have be used if you have incorporated byte and halfword Pseudo-Ops.

The great thing about .align is that if no alignment is required, your Assembler will ignore it. Thus, no extra unnecessary null bytes are added to your code.

Confused? A picture is worth 1000 words. Take a look at the following picture (RAW option in PyiiASMH used so no Gecko related stuff is included)..

[Image: directive01.png]

As you can see the "Mario Kart Wii" string (with a null byte auto appended at the end) creates a block of data that isn't divisible by 4. Another term is that it's not "word-divisible".

Now take a look at the next picture below...

[Image: directive02.png]

As you can see we now have a .align 2 placed below the .asciz. The block of data is now aligned and divisible by 4.

A good use of Pseudo-Ops is for creating Lookup Tables for your Code. A Lookup Table is a great way to allocate a block of memory withing the code itself, and have valuable data that will be referenced/used by your Code multiple times.

Example of creating a basic Lookup Table~


Code:
bl lookup_table
.long 0x80001500
.float 3.5
.asciz "/shared2/sys/SYSCONF"
.align 2 #Make the block of data be word-divisible
lookup_table:
mflr r12 #r12 now points to start of the Lookup Table


Since the length of ASCII strings can vary, it's best to place any such string(s) at the end of your lookup table and then finish it off with a '.align 2' to enforce address alignment. If you don't place all your string(s) at the end, then extra unnecessary null bytes (auto appended by .asciz) will take up space in your Lookup Table.

You can also place label names (like how you would write branch destination spots) within a Lookup Table to 'point' to various items without any manual calculation needed. Example~

Code:
bl lookup_table

Table_Start: #label pointing to start of the Lookup Table itself which also points to the .long
.long 0x80001500

Float_Constant: #label pointing to .float
.float 3.5

File_Path: #label pointing to .asciz
.asciz "/shared2/sys/SYSCONF"
.align 2

lookup_table: #Table is complete, continue with regular PPC instructions
mflr r12 #r12 now points to start of the Lookup Table
addi r4, r12, File_Path - Table_Start #r4 now points to where the .asciz String is at within the Lookup Table.

Take a good look at the addi instruction at the very end of the above source. Notice how I implemented a basic Symbol Calculation to auto-calculate the numerical value that the addi instruction needs to use, so the addition of that value with r12 will be placed in r4. Thus, r4 will contain the Memory Address that directly points to "/shared2/sys/SYSCONF".



Chapter 6: Macros

While symbols can be handy, they are very limited. If you want PPC instruction(s) to be tied to a particular Name, you will need to use what are called Macros.

Format of a Macro~

.macro Name Optional-Arg**
contents of Macro located HERE
.endm

**Optional-Arg is optional ofc, and will be discussed in the next Chapter.

Let's say we have the following PPC instructions...

Code:
lis r12, 0x8000
lwz r12, 0x1500 (r12)
lwz r12, 0 (r12)

...And these instructions occur multiple times in your source. Well instead of 'manually' writing these batch of instructions for every single occurrence, you can write out the macro for it just once, with a name tied to it, and then literally just write out the name.

Like this..

Code:
.macro Load_Pointer #Write out the Macro, can be anywhere in your Source
lis r12, 0x8000
lwz r12, 0x1500 (r12)
lwz r12, 0 (r12)
.endm

stwu sp, 0x80 (sp)
stmw r3, 0x8 (sp)
Load_Pointer #Insert batch of instructions (aka Macro) here
mtlr r12
blrl



Chapter 7: Macros w/ Args

Now we will discuss the Optional Arguments in Macros. With the above macro example in Chapter 6, you were 'forced' to use r12. If you wanted to use a different register and keep the original macro as well, you would have to write out a whole new 2nd macro. Well, with Argument options, you wouldn't need to resort to that.

A Macro can be given arg(s) with custom names. Look at the following example..

.macro Load_Pointer register
lis \register, 0x8000
lwz \register, 0x1500 (\register)
lwz \register, 0 (\register)
.endm

As you can see, we have have the Macro equipped with 'register' as an Arg. To use the Arg within the Marco you need to prepend "\" before its name (i.e. \register). With this macro, we can now select with register to use with it. Like so...

Code:
.macro Load_Pointer register #Create Macro with 1 argument. Argument's name is register.
lis \register, 0x8000
lwz \register, 0x1500 (\register)
lwz \register, 0 (\register)
.endm

stwu sp, 0x80 (sp)
stmw r3, 0x8 (sp)
Load_Pointer r12 #Insert batch of instructions here, use r12 as the argument for the macro
mtlr r12
blrl

The Macro Argument named as 'register' allows us to choose whatever register we want to use when referencing our Macro within the Source.

Here's an another example of a Marco Arg (using custom name 'address' for the use of writing in Memory Address's)

Code:
.macro Load_Pointer address #Create Macro w/ 1 arg, titled 'address'
lis r12, \address@ha #REMEMBER WE ALWAYS USE @ha for store/loads!!!!
lwz r12, \address@l (r12)
lwz r12, 0 (r12)
.endm

stwu sp, 0x80 (sp)
stmw r3, 0x8 (sp)
Load_Pointer 0x80001500 #Use 0x80001500 for the Macro Arg
mtlr r12
blrl

Notice the comment I placed in the macro regarding @ha. Remember, that for any store/load, use @ha for establishing the upper bits of the Address! If it's not a store/load, you can simply use @h.

Fyi, you are allowed to have more than just 1 Macro Arg. Like so...

Code:
.macro Load_Pointer register, address #NOTICE the comma separating the two Args. This is needed
lis \register, \address@ha
lwz \register, \address@l (\register)
lwz \register, 0 (\register)
.endm

stwu sp, 0x80 (sp)
stmw r3, 0x8 (sp)
Load_Pointer r12, 0x80001500 #NOTICE the comma separating r12 & 0x80001500
mtlr r12
blrl




Chapter 8: Making Sources Region Friendly via If-Statements

If-Statements are really useful for having one source be able to be quickly changed on the fly before compilation in regards to differences due to Region issues. You can have one main source with If-Statements to handle Region differences instead of having multiple slightly different Sources to accommodate every Region.

MKWii has 4 regions. So writing the same source slightly different a total of 4 times would be quite annoying. Anyway, you have the following directives at your disposal.
  • .if #Starts the If-Statement
  • .elseif #If above statement is not true, a new If-Statement started
  • .else #Do task(s) below since above If-Statement(s) are not true
  • .err #Forces a Assembler halt/error
  • .abort #Does the same thing as .err. However try to stick using .err only. If you happen to do non-PPC-related assembler work, .abort on the non-PPC assembler may be deprecated.
  • .endif #End If-Statement(s)

Here is a handy template that you can use to easily handle Region-specific versions within one source.

Code:
.set region, '' #Fill in E, P, J, or K within the quotes for your region when Compiling! Lowercase letters can also be used.

.if     (region == 'E' || region == 'e') #NTSC-U (Americas)
        #PLACE NTSC-U region dependent symbols and macros HERE
.elseif (region == 'P' || region == 'p') #PAL
        #PLACE PAL region dependent symbols and macros HERE
.elseif (region == 'J' || region == 'j') #NTSC-J (Japan)
        #PLACE NTSC-J region dependent symbols and macros HERE
.elseif (region == 'K' || region == 'k') #RMCK (Korea)
        #PLACE NTSC-K region dependent symbols and macros HERE
.else #Invalid Region
        .err #Region match not detected. Tell the Assembler to halt and throw an error.
.endif

Anyway, regarding the template, it requires you fill in the Region "letter" for the source to assemble. The letter can be upper or lower case. The Assembler will assemble its contents differently depending on what Region "letter" is filled in more the ".region" Symbol.

If no Region "Letter" is used or an invalid one is supplied, the Assembler will output an Error and not even attempt to compile your source.

Here's an example of having region-specific Macros using the above template~

Code:
.set region, '' #Fill in E, P, J, or K within the quotes for your region when Compiling! Lowercase letters can also be used.

.if     (region == 'E' || region == 'e') #NTSC-U
        .macro Function_Addr register
        lis \register, 0x8000
        ori \register, \register, 0x1500
        .endm
.elseif (region == 'P' || region == 'p') #PAL
        .macro Function_Addr register
        lis \register, 0x8000
        ori \register, \register, 0x1504
        .endm
.elseif (region == 'J' || region == 'j') #NTSC-J
        .macro Function_Addr register
        lis \register, 0x8000
        ori \register, \register, 0x1508
        .endm
.elseif (region == 'K' || region == 'k') #NTSC-K
        .macro Function_Addr register
        lis \register, 0x8000
        ori \register, \register, 0x150C
        .endm
.else #Invalid Region
        .err #Region match not detected. Tell the Assembler to halt and throw an error.
.endif

add r3, r3, r14
Function_Addr r11 #Use r11 for our region-specific Macro
stw r11, 0 (r3)



Chapter 9: Conclusion + Handy Reference Page

Here's a site that covers most Assembler Directives. Keep in mind this isn't 100% accurate as the content is based on the GNU Assembler - https://sourceware.org/binutils/docs/as/Pseudo-Ops.html

For an actual real world code that uses some good macros plus symbols, check out Star's Screenshot code - https://mkwii.com/showthread.php?tid=1080