Tutorial 6 (Nov 2): Structs, nested structs, passing pointer into subroutine
Downloads (examples from class)
[structs.s][2764B] Example 1: Structs, nested structs
[pointer.s][1772B] Example 2: Passing a pointer into a subroutine
Structs
Coming from CPSC 233 (Java), you will find that structs share some similarities with classes. A struct is like a blueprint or a directory, for multiple values grouped together as one object. In assembly, a struct is defined as a group of offsets, relative to the base address for an instance of the struct.
For example, we could use a Course struct to define information about a particular course:
// Equates for a Course struct
course_number = 0 // the first variable has offset 0
course_classroom = 4 // the first variable took 4 bytes (int), so the second offset starts at 4
course_section = 8 // the second variable took another 4 bytes (int), so the third offset starts at 8
course_struct_size = 12 // NOTE: this is NOT an offset, this is the total size of this struct
This is just the definition or blueprint for our struct, but we haven't actually made an instance of this struct yet. We can use this struct to define several instances:
// Set the size of each struct
courseA = course_struct_size // "Creating" an instance of a struct just means allocating space for it.courseB = course_struct_size // We refer to the "blueprint" above and use the size of the struct.
courseC = course_struct_size // To access the values in each instance, we use its base address (below) and add the appropriate offset (refer to blueprint)
// Allocate memory for all our structs
alloc = -(16 + courseA + courseB + courseC) & -16
dealloc = -alloc
// Calculate the offset to each instance (same as how you calculated offsets for variables in A3)courseA_s = 16 // $fp+16 is the start of our variables, so 16 is the offset for the first local variable
courseB_s = courseA_s + course_struct_size // we store courseB after courseA, so the offset is courseA's offset (16) + the size of courseA (12) = 28
courseC_s = courseB_s + course_struct_size // we store courseC after courseB, so the offset is courseB's offset (28) + the size of courseB (12) = 40
.balign 4
.global main
main: stp x29, x30, [sp, alloc]! // memory is allocated on the stack for all our structs
mov x29, sp
To store/load from these structs, we use their base address plus an offset:
// Initialize courseA
str wzr, [x29, courseA_s + course_number] // $fp + offset for courseA + offset to course_number
str wzr, [x29, courseA_s + course_classroom] // $fp + offset for courseA + offset to classroom
str wzr, [x29, courseA_s + course_section] // $fp + offset for courseA + offset to section
ldr w19, [x29, courseA_s + course_number] // same offsets as we used for storing
ldr w20, [x29, courseA_s + course_classroom]
ldr w21, [x29, courseA_s + course_section]
Nested Structs
Just like nested loops or nested conditionals, nested structs means we have a struct inside a struct (STRUCTCEPTION?!). If you understoor structs from above, nested structs will be very simple for you.
Assume we have the Course struct defined above. Now we want to define an Assignment struct:
// Equates for an Assignment struct
assignment_course = 0 // first variable in the struct has offset 0
// This is a nested struct, but we treat it no differently from a regular variable.
// We just have to know the size of the struct so we can allocate memory for it.assignment_number = 12 // We know course_struct_size = 12, so the variable after assignment_course starts at offset 12.
assignment_studentID = 16 // 4 bytes after assignment_number (int)
assignment_grade = 20 // 4 bytes after assignment_grade (int)
assignment_struct_size = 24 // Again, this is not an offset, but rather the total size of the Assignment struct
We allocate memory and calculate offsets for instances of the nested struct Assignment, the same way we did for the Course instances above.
// Set the size of each struct
assignment_one = assignment_struct_size // assign the size of "assignment_one" variable
assignment_two = assignment_struct_size // assign the size of "assignment_two" variable
// Allocate memory for all our structsalloc = -(16 + assignment_one + assignment_two) & -16
dealloc = -alloc
// Calculate the offset to each instance (same as any other local variable)assignment_one_s = 16 // $fp+16 is the start of our variables, so 16 is the offset for the first local variable
assignment_two_s = assignment_one_s + assignment_struct_size // we store assignment_two after assignment_one, so the offset is assignment_one's offset (16) + the size of assignment_one (24) = 40
.balign 4
.global main
main: stp x29, x30, [sp, alloc]! // memory is allocated on the stack for all our structs
mov x29, sp
To store/load from nested structs, we just add an extra offset:
// Initialize assignment_one
str wzr, [x29, assignment_one_s + assignment_course + course_number] // $fp + offset for assignment_one + offset to assignment_course + offset to course_number
// $fp + 16 + 0 + 0str wzr, [x29, assignment_one_s + assignment_course + course_classroom] // $fp + offset for assignment_one + offset to assignment_course + offset to course_classroom
// $fp + 16 + 0 + 4str wzr, [x29, assignment_one_s + assignment_course + course_section] // $fp + offset for assignment_one + offset to assignment_course + offset to course_section
str wzr, [x29, assignment_one_s + assignment_number] // $fp + offset for assignment_one + offset to assignment_number
// $fp + 16 + 12
str wzr, [x29, assignment_one_s + assignment_studentID] // $fp + offset for assignment_one + offset to assignment_studentID
str wzr, [x29, assignment_one_s + assignment_grade] // $fp + offset for assignment_one + offset to assignment_grade
// Loading is the same
ldr w19, [x29, assignment_one_s + assignment_course + course_classroom] // $fp + offset for assignment_one + offset to assignment_course + offset to course_classroom
ldr w20, [x29, assignment_one_s + assignment_number] // $fp + offset for assignment_one + offset to assignment_number
Passing a pointer into a subroutine
Previously, I posted examples of simple subroutines which took parameters through the w0-w7 registers. The subroutines would then return a single value through w0. However, sometimes you might have more than 8 arguments for a subroutine (albeit rarely). More often, you might want to pass a large array or data structure into the subroutine. Of course, a register can only store 32 or 64 bits of information, which isn't very much. To get around this, we can pass the pointer to the array/struct/variable as an argument instead. A pointer is simply a variable which holds a memory location.
Registers are global, and can be accessed if you know the register number. Similarly, local variables stored on the stack can be accessed if you know the address of the variable.
Take this swap() example from Prof. Manzara's slides:
swap: stp x29, x30, [sp, -16]!
mov x29, sp
// x0 and x1 are pointers, each containing the an address
// 1. load values from the two addresses
// 2. store the swapped values back to the to addresses
ldr w9, [x0] // w9 = *x
ldr w10, [x1] // w10 = *y
str w10, [x0] // *x = w10
str w9, [x1] // *y = w9
// We don't need a return value, since we swapped the variables directly in memory
// not all subroutines have return values, just like void() functions in Java have no return value ldp x29, x30, [sp], 16
ret
The swap() function/subroutine takes two arguments from x0 and x1. We must use x registers (64-bit) because the addresses are 64-bits. Recall that the STR and LDR instructions store/load to a memory location on the stack. The addresses are enclosed in square brackets [], which dereference the memory location (similar to * in C). Once we pass the addresses (and not the values) of two variables into swap(), we can access these variables the same way we did previously in main(). Afterall, main() is just another subroutine.
a_size = 4 // size of var a
b_size = 4 // size of var b
alloc = -(16 + a_size + b_size) & -16 // memory to allocate on stack
dealloc = -alloc // memory to deallocate from stack
a_s = 16 // offset for var a from fp
b_s = 20 // offset for var b from fp
main: stp x29, x30, [sp, alloc]!
mov x29, sp
// Initialize a and b mov w19, 5 // init a to 5
str w19, [x29, a_s]
mov w20, 7 // init b to 5
str w20, [x29, b_s]
// Swap a and b by passing their ADDRESSES to the swap(x,y) subroutine add x0, x29, a_s // x0 becomes a POINTER which contains the ADDRESS ($fp+a_s) of "a"
add x1, x29, b_s // x1 becomes a POINTER which contains the address ($fp+b_s) of "b"
bl swap
mov w0, 0 ldp x29, x30, [sp], dealloc
ret
Now that you know how to pass addresses into subroutines using pointers, it will be very simple to learn how to pass structs into subroutines. Afterall, a struct is stored as a local variable on the stack, and you just have to pass the base address of the struct into the subroutine. Then you combine the base address with an offset (from the "blueprint" or definition of the struct) to access the struct, the same way you would normally access structs. I'll post an example of this in the next section.