-
Notifications
You must be signed in to change notification settings - Fork 4
Getting started
In higher level languages, you're used to declaring your own variables on a single line and passing them to functions like(this, and, this)
and storing function return values
like = this()
. Assembly doesn't have functions or variables in the way you might think of them. It's all an elaborate ruse. Given some tools, you have to problem-solve until you can simulate variables and functions.
In assembly, you have these things called registers. You can think of them as global variables, except they're the only variables you can use. There is no language-level concept of null
in assembly like there is in Swift, so these variables always have some value that can be interpreted however you see fit (like in C). You also have to remember what you're doing with each register, since you can't name them.
Since you have a limited number of these registers, you sometimes run out of space. Thankfully you have another place to store things called the stack. (If you're not familiar with the stack data structure, it's basically an array you can only ever access the last element of. You push things on and pop them off as needed.) When you run out of registers to use, you can push their values onto the stack to save them for later, and pop them back off when you're doing using some other registers.
TBA. At the time of writing, there is no way to dynamically allocate memory in this project.
Assembly also doesn't have a language-level concept of function calling. To simulate calling a function, the caller and callee (callee = function) have to agree on where parameters will be and where the return value will go. This is known as the calling convention.
Most of the time, parameters can be put into registers. But if there are more parameters than can fit in registers, parameters can be put on the stack, too, and the callee can retrieve them as needed. You should document where the callee expects parameters to be.
Usually you will be able to return a value by putting it in a register. But if you want to return more than one value, you'll have to split them up into multiple registers, or put them on the stack, or dynamically allocate some memory for them and put a pointer to this memory in a register. Problem solve! Just be sure to document where it will be so the caller knows where to look.
This project provides some functions that closely represent actual assembly instructions (and so I will refer to them as instructions). For example, if you wanted to add two numbers together, here's the quickest way to do it:
mov(.rb, 5) // Store 5 in the rb register
add(.rb, 3) // Add 3 to the value in rb and put it in the ra register
Once this code has executed, ra
holds the sum of 3 and 5 (and rb
still has a 5
in it). If you couldn't tell, mov
stands for "move," and add
means addition. Other instructions, as I will refer to them, are included for all kinds of arithmetic (sub
, idiv
, imul
, etc). Make note, most of these instructions only operate on integers.
How about something a little more complex? To print a string to the console, you first declare the string globally, then move it into a register inside the main
function:
let message = "Hello, World!"
func main() {
mov(.ra, message) // Store the string in .ra
print() // print(message)
}
main()
Quick note: main
is only here to enforce a separation of concerns. Global variables like message
should be declared outside any and all functions. There should be no top-level code executing except the main()
statement below itself (so, put all of your code in main
or in another function).
Anyway, the print
function here only expects one argument, a String
, which should be put into ra
prior to calling print()
. This is how you pass arguments to functions in assembly, by putting them where the function knows where to look for them. There is also a formattable print function, printf
, which takes two arguments in registers and variadic arguments on the stack. In a higher level langauge, you might call it like this:
printf("Hello %@! My name is %@", 2, "bob", "lisa")
where the 2
represents the number of arguments to be interpolated (aka VA args) into the string, and where the first argument is the string itself. In this playground, you would call it like this:
let msg = "Hello %@! My name is %@"
let name = "bob"
let myName = "lisa"
func main() {
mov(.rb, name) // Load strings into registers
mov(.rc, myname) // ...
Stack.push([.rc, .rb]) // Store variadic args on stack
mov(.ra, msg) // First param, msg, goes in ra
mov(.rb, 2) // Second param, # of VA args, goes in rb
printf() // printf(msg, 2)
}
printf
expects the first VA arg to be on top of the Stack
, so you must push them onto the Stack
in reverse order, as if you were stacking some books alphabetically.
--
That's all for now!