PowerPC Tutorial

Previous Chapter

Chapter 25: Callee Register Application

Section 1: Intro

We can use the callee-saved registers to keep data intact thru various function calls. A good way to showcase this is with some File Management. We will do the following..

  1. Open a File
  2. Get its Size
  3. Allocate Some Memory
  4. Read the File
  5. Close File
  6. Print a Message

We'll look at the whole Program then dissect it.

.section .text
    .globl main

main:
#Prologue; save r3 registers
    stwu sp, -0x0020 (sp)
    mflr r0
    stmw r29, 0x8 (sp)
    stw r0, 0x0024 (sp)
    
#Open the file
    lis r3, filename_ptr@ha
    la r3, filename_ptr@l (r3)
    lis r4, perms_ptr@ha
    la r4, perms_ptr@l (r4)
    bl fopen
    
#Save File Stream Pointer in a non-vol register while checking against null pointer
    mr. r31, r3
    beq- print_error
    
#Seek to end of file
    li r4, 0    
    li r5, 2
    bl fseek
    
#Get size of file
    mr r3, r31
    bl ftell

#Save size in a non-vol register
    mr r30, r3

#Rewind back to start of file
    mr r3, r31
    bl rewind
    
#Allocate Memory
    mr r3, r30
    bl malloc

#Verify Malloc's return value while saving Allocated Memory Ptr
    mr. r29, r3
    beq- print_error
    
#Read file into the Allocated Memory; r3 arg already set
    li r4, 1
    mr r5, r30
    mr r6, r31
    bl fread

#Verify that fread worked
    cmpw r3, r30
    bne- print_error
    
#Close file
    mr r3, r31
    bl fclose
    
#Everything worked, print Success
    lis r3, success_msg@ha
    la r3, success_msg@l (r3)
call_puts:
    bl puts

#Exit Program (0). Epilogue; restore 3 registers
    lwz r0, 0x0024 (sp)
    lmw r29, 0x8 (sp)
    mtlr r0
    addi sp, sp, 0x0020
    blr

#Portion of Program to handle errors; print Error
print_error:
    lis r3, error_msg@ha
    la r3, error_msg@l (r3)
    b call_puts

#Directives
filename_ptr:
    .asciz "random_file.txt"
perms_ptr:
    .asciz "rb"
success_msg:
    .asciz "Success!"
error_msg:
    .asciz "Error!"

Callee-Saved Register Overview and Record Trick

Alright a lot to unpack. First, take a look of how we use r29, r30, r31 to keep data for later use by other functions. You CANNOT do this with the caller-saved registers (r3 thru 10) or r11/12. Due to the design of the PowerPC calling convention, those registers are not guaranteed to remain intact after a function call.

We ended up having to use 3 callee-saved registers. This is why we created a Stack Frame that had the size to contain 3 registers.

Take a look at the code that checks the return values of fopen, and malloc. With these two functions, it's proper procedure to check their return values. We also needed to place the return values in callee-saved registers. So by using the Record feature that comes with the mr instruction, we can do those two tasks at the same time. Recall back in Chapter 13 that the Record feature is a free use of "cmpwi rX, 0". Example....

Bad Code:

cmpwi r3, 0
beq- error
mr r31, r3

Good Code:

mr. r31, r3
beq- error

mr. r31, r3 = mr r31, r3 then do cmpwi r3, 0


More C Function Discussion

Fseek:
By default when you read/write files, the very beginning of the file is the starting point. With fseek, you can change this. In our program we seek to the last byte of file + 1. fseek has two args..

Most programs do not check the return value of fseek as it never fails natively.

Ftell:
This will give you the byte amount of where the fseek's "cursor" is located in the file.

Start of the file would be byte 0. So a 0 would be returned. When calling ftell after seeking to the end+1 of a file, this will give you back the file's exact byte size.

Rewind:
Since we were at the end+1 of the file, we need to go back to the start. Yeah you could use fseek with r4 being 0, but its much faster to use Rewind.

Most programs don't check the return value for this function as it never fails natively.

Malloc:
This will allocated memory for us. There will be times when allocated buffer/memory space in a Stack Frame won't be large enough to get the job done. For such cases, the C function of malloc needs to be used.

Question: Vega, are what exact size should I not use a Stack Frame for allocating memory and start using Malloc?

Answer: It really depends on the System's overall design. There's no clear cut answer to this. Rough shot, I would say 500 bytes. However whats really key here is that if you are dealing with an item that has a dynamic size (like a file), you should use Malloc. Files can vary in size, what if you thought a file was going to be small and end up being 10,000 bytes? For items that use a static size (predictable size that is always the same, and it's small), use the Stack Frame.

Fread:
Alrighty, this is what you use to copy-paste (dump) a file's contents to the Program's memory.

Args & Return Values:

When verifying fread, you always want to check r3 against what you used for the r5 arg. If not equal, a boo boo happened.

Puts:
If you are printing a string that you know will never have format descriptors and you also want to enter into a newline, then use Puts instead. It can only print basic strings and automatically does "/n" without needing to have it in the String. It's much faster than printf.

Same deal as printf, don't need to check the return value on puts.

Section 4: Running the Program

Save the source as Our_Program.S and assemble it to Our_Program.

powerpc-linux-gnu-gcc -mregnames -ggdb3 -o Our_Program -static Our_Program.S

Now here is a random_file.txt that contains a basic message.

LINK

Place random_file.txt in the program's directory. Be sure it can be read by a regular User within your Linux OS. Run the program.

chmod +x ./Our_Program
./Our_Program

You should see "Success!"

Now rename the random_file.txt to norandom_file.txt. Rerun the program. You should see "Error!". Obviously one the functions (fopen specifically) failed and therefore, the custom Error Message was printed. Now rename the file BACK to random_file.txt. We will need it again for the next Section.


Section 5: Another Example Program

Here's a similar Program as above but it will print the file's contents as 8-bit ASCII.
.section .text
    .globl main

main:
#Prologue; save r3 registers
    stwu sp, -0x0020 (sp)
    mflr r0
    stmw r29, 0x8 (sp)
    stw r0, 0x0024 (sp)
    
#Open the file
    lis r3, filename_ptr@ha
    la r3, filename_ptr@l (r3)
    lis r4, perms_ptr@ha
    la r4, perms_ptr@l (r4)
    bl fopen
    
#Save File Stream Pointer in a non-vol register while checking against null pointer
    mr. r31, r3
    beq- print_error
    
#Seek to end of file
    li r4, 0    
    li r5, 2
    bl fseek
    
#Get size of file
    mr r3, r31
    bl ftell

#Save size in a non-vol register
    mr r30, r3

#Rewind back to start of file
    mr r3, r31
    bl rewind
    
#Allocate Memory + 1 extra byte
    addi r3, r30, 1
    bl malloc

#Verify Malloc's return value while saving Allocated Memory Ptr
    mr. r29, r3
    beq- print_error
    
#Read file into the Allocated Memory; r3 arg already set
    li r4, 1
    mr r5, r30
    mr r6, r31
    bl fread

#Verify that fread worked
    cmpw r3, r30
    bne- print_error
    
#Close file
    mr r3, r31
    bl fclose
    
#Append the file contents with a null byte because we will be printing it as a string and ALL strings need to end in null or else we may print extra junk.
    li r3, 0
    stbx r3, r30, r29
    
#Everything worked, print file contents
    mr r3, r29
call_puts:
    bl puts

#Exit Program (0). Epilogue; restore 3 registers
    lwz r0, 0x0024 (sp)
    lmw r29, 0x8 (sp)
    mtlr r0
    addi sp, sp, 0x0020
    blr

#Portion of Program to handle errors; print Error
print_error:
    lis r3, error_msg@ha
    la r3, error_msg@l (r3)
    b call_puts

#Directives
filename_ptr:
    .asciz "random_file.txt"
perms_ptr:
    .asciz "rb"
error_msg:
    .asciz "Error!"

In this portion of the program...

#Allocate Memory + 1 extra byte
    addi r3, r30, 1
    bl malloc

...we allocate one extra byte. Why? Because when printf/puts outputs a string to the console, it knows when to stop printing once it reads a null byte. Txt files aren't required to end in a null byte. Most don't. Therefore, after we read/dump random.txt to memory, we want to make sure a null byte is after the last byte of the file. So obviously, we need one more byte for our allocated memory. Next we need to write the null byte. This is why we have the following in the program..

#Append the file contents with a null byte because we will be printing it as a string, and ALL strings need to end in null or else we may print extra junk.
    li r3, 0
    stbx r3, r30, r29

By using the file size with the pointer to the files contents, a stbx instruction will store a byte to EndofFile+1.

Assemble and run the program (as Our_Program2.S).

powerpc-linux-gnu-gcc -mregnames -ggdb3 -o Our_Program2 -static Our_Program2.S
chmod +x ./Our_Program2
./Our_Program2

You will see the contents of the file printed on the screen! Hooray. If you're familiar with C/C++ you may know that the %s format descriptor for printf could have been used to limit the output just in case a string doesn't end in a null byte. It wasn't used in the above Program because puts is faster, and you wouldn't be able to print accurately if the file is dynamic/changing size. Onto the next chapter!


Next Chapter

Tutorial Index