Friday, January 13, 2017

MIPS: Millions of Instructions Per Second (Part 4)

In the last post I talked about functions and register conventions in MIPS.  I will continue the topic of functions in the next and final part, but we'll take a break from that and discuss bitwise operations and files.

 

Bitwise Operations

 

In MIPS and in most processor architectures, memory is byte-addressed.  Every byte is given its own number as a reference for accessing it.  But a byte consists of 8 bits.  This means that none of those bits have an explicit address, even though we may sometimes want to look at one bit individually.  To get around this problem, we can use bitwise operations.

MIPS provides the instructions and, or, xor, and nor, and their immediate counterparts.  You should be familiar with how these operations work on single-bit inputs.  On 32-bit registers, these instructions run 32 operations in parallel, one on each of the bits in the register.  So with two inputs, the far right bit of the first input is matched with the far right bit of the second input and put through the gate to get the result for the far right bit of the output.

For example, if we were to AND two 8-bit inputs:


All bitwise operations work on this simple principle.  There are uses for bitwise operations that come up a lot - let's go over those uses.

 

Isolating Bits With AND

 

This one is derived directly from the picture above.  There may be times when you want to observe a specific bit in a register, or a collection of bits - the simplest way to do this is to use andi with ones in the bit positions that you're looking at.  We will be using hex in our code - if you're not able to do the conversions in your head, I recommend practice but you may also use a calculator such as this one.

For example, if I want the lower 16 bits of a register, I can do this:


Anding with zeros sets the higher 16 bits to zero, while anding with ones ensures the lower 16 bits are kept the same.  This is equivalent to the following Java code:


Usually this sort of thing is done when dealing with bitfields, where each bit is a distinct boolean representing some condition.  Getting a nonzero result means the bit you're looking for is set, which means the condition is true.

Setting Bits With OR

On the other hand, you might want to write certain bits instead of reading them.  You can use the ori instruction to do this:


This will set the second bit from the right to one, and keep all other bits the same.  Usually this tactic is used in regards to bitfields as well, but in code that is setting conditions rather than checking for them.  This is equivalent to the following Java code:

Canceling Bits With AND

This is the opposite of the above - instead of setting a bit to one, you might want to set it to zero and keep all other bits the same.  You use andi for this too:


This sets the second bit from the right to zero, and is equivalent to:

Flipping Bits With XOR

Sometimes you just want to set certain bits to the opposite of what they are currently, regardless of what value they have.  For this, you can use xori with ones in the positions you want to flip:


This will flip the 13th bit from the right, while keeping all others the same, and is equivalent to:


And that's really it for bitwise operations.  They're pretty simple as long as you're able to do the binary to hex conversion.  Now let's move on to files.

 

Files

Java has abstractions for file operations that MIPS does not.  Instead of an object, each open file is given a number that we call its file descriptor.  All operations on that file will refer to this number so that we know we're dealing with it and not some other file.  If you've dealt with files in C, it is the same system just without the high-level code to simplify things.

All operations are done with syscalls.  Below is a table of the syscalls we'll be using:


Obviously the file has to be opened before we do anything with it.  Syscall 13 takes three arguments - the filename, a flag argument, and a mode.  Counter-intuitively, the "mode" in the traditional sense (read-only, write-only, etc) is sent through the flag argument, and mode is ignored entirely.  This usage is defined by the MARS environment, and may be different for other systems.  If we just want to read an existing file, we pass 0 for the flag.  For write-only mode, pass 1.  For write-only mode with automatic creation, pass 9.

The code below opens a file and prints its file descriptor:


MARS will assign file descriptors starting from 3, because 0, 1, and 2 are reserved for stdin, stdout, and stderr.  If your file is found, then 3 should be printed to the console.  If it isn't found, -1 will be printed instead.  The working directory for MARS is different depending on the operating system you're running it on - on Windows and Mac OS X, the working directory is the directory that the MARS jar is located.  On linux, the working directory is your home directory.

You can now use this file descriptor on the other three syscalls.  We can't write to the file because we've given 0 (read-only) for the flag argument, but we can read.  Syscall 14 accepts the file descriptor as its first argument, then the address of an input buffer to store the read data, then the number of bytes to read.  We need to set aside some space for the buffer in the .data section of our code in order to read:


$v0 will contain the number of characters read from the file.  If there are more than 16 left, it will read all 16 in this case.  If there are less, it will read however many bytes are left.  If it is already at the end of file, it will set $v0 to 0.  If there is an error, it will set $v0 to -1 or some other negative value.

There is no distinction between binary mode and text mode here.  If the data in the file is stored in ASCII, then it will load the ASCII data as is.  If it's not, it will load whatever else is in there in binary form and store it as is.  If we are dealing with an ASCII file and we print out the buffer as if it were a string...


...we will see the first 16 characters of its contents (also note that I forgot to use syscall 10 - don't do that).

Syscall 15 for writing is the same thing, just in the reverse direction.  It will read your buffer and write it to the file.  You can use flag 1 to write/replace, or you can use flag 9 to create a new file and write to it:


Since I use linux, the file showed up in my home directory and contained the string "this_buffer."

Finally, close the file using syscall 16:


This code just opens and immediately closes the file, but closing the file always works the same way.  Just put the file descriptor in $a0 and run the syscall.

To Be Continued

There's one last part!  Next time I'll be talking about recursion and the frame pointer.

No comments:

Post a Comment