Eroxl's Notes
07 - Procedure Calls and the Stack (CPSC 213 Practice)

Problem 1

Assume the following code in SM213 Assembly:

_1:  deca r5
_2:  deca r5
_3:  ld $22, r0
_4:  st r0, (r5)
_5:  ld $25, r0
_6:  st r0, 4(r5)
_7:  gpc $6, r6
_8:  j foo
_9:  inca r5
_10: inca r5
_11: halt

_12: foo: deca r5
_13:      deca r5
_14:      st r6, 4(r5)
_15:      ld 8(r5), r2
_16:      ld $0x3000, r0
_17:      ld (r0), r1
_18:      ld 12(r1), r2
_19:      ld 12(r5), r3
_20:      st r3, 0(r5)
_21:      ld 4(r1), r2
_22:      st r2, 8(r1)
_23:      ld 4(r5), r6
_24:      inca r5
_25:      inca r5
_26:      j(r6)

Which statements below best describes the following lines of the assembly code above?

Options:

  • It reads from or writes to an argument.
  • It reads from or writes to a local variable.
  • It saves or restores the return address to or from the stack.
  • It reads from or writes to a member of a dynamic struct.
  • It reads from or writes to a global variable.
  • None of them.
  1. _20: st r3, 0(r5): It reads from or writes to a local variable.
  2. _14: st r6, 4(r5): It saves or restores the return address to or from the stack.
  3. _19: ld 12(r5), r3: It reads from or writes to an argument.
  4. _17: ld (r0), r1: It reads from or writes to a global variable.

Problem 2

Match each line with the most accurate description of its role

foo:  ld   $-8, r1
      add  r1, r5        # A
      st   r6, 4(r5)     
      ld   8(r5), r2
      ld   12(r5), r3
      ld   $-8, r1
      add  r1, r5        # B
      st   r2, (r5)
      st   r3, 4(r5)     
      gpc  $6, r6
      j    p             # C
      ld   $8, r1
      add  r1, r5        # D
      st   r0, (r5)      
      ld   4(r5), r6     
      ld   $8, r1
      add  r1, r5        # E
      j    (r6)

Options:

  • Call
  • Callee Epilogue
  • Callee Prologue
  • Caller Epilogue
  • Caller Prologue
  • None of the Above
  1. A: Callee Prologue
  2. B: Caller Prologue
  3. C: Call
  4. D: Caller Epilogue
  5. E: Callee Epilogue

Problem 3

Consider the following C code.

int w;

void func(int d) {
  int n;
  
  // some other code
  w = d + n; // <== Translate this line only
  bar();
  // some more code
}

Convert the highlighted line above to an appropriate SM213 equivalent code by dragging appropriate lines of code from the left panel to the right panel. You should convert only the highlighted line. Do not assume that any values have been loaded into registers, other than the stack pointer (r5) and the return address (r6). Assume that the order of values in the stack is the usual order used in class, i.e., that local variables are found above the return address, which is found above arguments.

ld $w, r0
ld (r5), r1 # n
ld 8(r5), r2 # d
add r1, r2
st r2, (r0)

Problem 4

Consider the following C code.

int d;

int bar(int);

int bat(void) {
  // some other code
  d = bar(56); // <== Translate this line only
  // some more code
}

Convert the highlighted line above to an appropriate SM213 equivalent code by dragging appropriate lines of code from the left panel to the right panel. You should convert only the highlighted line. Do not assume that any values have been loaded into registers, other than the stack pointer (r5) and the return address (r6).

deca r5
ld $56, r0
st r0, (r5)
gpc $6, r6
j bar
inca r5
ld $d, r1
st r0, (r1)

Problem 5

Assume the following C code is given to you. Assume that both dynamic memory allocation and stack initialization have already been done. Start your code with the label list_sum.

Keep in mind that pointers are 4 bytes long in SM213.

// Function Definition
int list_sum(int* list) {
  if (*list) {
    return *list + list_sum(list + 1);
  }

  return 0;
}

Translate the function definition into SM213 assembly.

list_sum:
	# -=- callee prologue -=-
	deca r5
    st r6, (r5) # store return address after args
    # -=- callee prologue -=-
    
	ld 4(r5), r1 # list
	ld (r1), r2 # *list
	
	beq r2, list_null # *list == nullptr
	br list_not_null
	
	list_null:
		ld $0, r0
		br return

	list_not_null:
		inca r1 # list + 1
		
		# -=- prologue epilogue -=-
		deca r5
		st r1, (r5)
		
		gpc $6, r6
		# -=- prologue epilogue -=-
		
		# -=- call -=-
		j list_sum
		# -=- call -=-
		
		# -=- prologue epilogue -=-
		inca r5
		# -=- prologue epilogue -=-

		ld 4(r5), r1
		ld (r1), r2
		
		add r2, r0
		
		br return

	return:
		# -=- callee epilogue -=-
		ld (r5), r6
		inca r5
		j (r6)
		# -=- callee epilogue -=-

Problem 6

Assume the following C code is given to you. Assume that both dynamic memory allocation and stack initialization have already been done.

Keep in mind that pointers are 4 bytes long in SM213.

// Declarations
struct Node {
    int val;
    struct Node* left;
    struct Node* right;
};
int a;
struct Node* root;

// Function Definition
int tree_sum(struct Node* node) {
    if (node == NULL) {
        return 0;
    }

    return ((node->val & 1) ? 0 : node->val) + tree_sum(node->left) + tree_sum(node->right);
}

// Function Call
a = tree_sum(root);

Translate the function definition and the call to it into SM213 assembly.

ld $root, r1               # r1 = address of root
ld (r1), r2                # r2 = value of root (the pointer)
deca r5                    # allocate space for argument
st r2, (r5)                # push root as argument

gpc $6, r6                 # save return address
j tree_sum                 # call tree_sum(root)

inca r5                    # pop argument

ld $a, r1                  # r1 = address of a
st r0, (r1)                # a = return value

halt                       # stop program

# Function Definition
tree_sum:
    # -=- callee prologue -=-
    deca r5                    # allocate space for saved r6
    st r6, (r5)                # save return address
    deca r5                    # allocate space for local variables
    # -=- callee prologue -=-
    
    ld 8(r5), r1               # r1 = node (parameter at offset 8)
    
    beq r1, node_is_null       # if (node == NULL)
    br node_is_not_null
    
node_is_null:
    ld $0, r0                  # return 0
    br end_if
    
node_is_not_null:
    ld 8(r5), r1               # r1 = node
    ld (r1), r2                # r2 = node->val
    
    ld $1, r3                  # r3 = 1
    and r2, r3                 # r3 = node->val & 1
    
    beq r3, val_is_even        # if ((node->val & 1) == 0)
    br val_is_odd
    
val_is_even:
    ld 8(r5), r1               # r1 = node
    ld (r1), r2                # r2 = node->val
    st r2, (r5)                # store node->val in local variable
    br end_ternary
    
val_is_odd:
    ld $0, r2                  # r2 = 0
    st r2, (r5)                # store 0 in local variable
    
end_ternary:
    # Call tree_sum(node->left)
    ld 8(r5), r1               # r1 = node
    ld 4(r1), r2               # r2 = node->left (offset 4 for pointer)
    deca r5                    # allocate space for argument
    st r2, (r5)                # push node->left
    
    gpc $6, r6                 # save return address
    j tree_sum                 # call tree_sum
    
    inca r5                    # pop argument
    
    ld (r5), r2                # r2 = accumulated value
    add r0, r2                 # r2 += return value from tree_sum(node->left)
    st r2, (r5)                # store back
    
    # Call tree_sum(node->right)
    ld 8(r5), r1               # r1 = node
    ld 8(r1), r2               # r2 = node->right (offset 8 for pointer)
    deca r5                    # allocate space for argument
    st r2, (r5)                # push node->right
    
    gpc $6, r6                 # save return address
    j tree_sum                 # call tree_sum
    
    inca r5                    # pop argument
    
    ld (r5), r2                # r2 = accumulated value
    add r0, r2                 # r2 += return value from tree_sum(node->right)
    mov r2, r0                 # move final result to r0
    
end_if:
    # -=- callee epilogue -=-
    inca r5                    # deallocate local variables
    ld (r5), r6                # restore return address
    inca r5                    # deallocate saved r6
    j (r6)                     # return
    # -=- callee epilogue -=-

Problem 7

When converting C code into Assembly, the compiler often has the option to optimize the code and use registers instead of stack memory locations for some local variables and values. It also has the ability to avoid storing the value of register r6 into the stack in some situations.

Based on the conventions using in class for the value of r6, when would a compiler be compelled to store the value of r6 into memory (i.e., when is the optimization of using registers only not possible for register r6)?

  • [ ] (a) When the value of r6 represents the stack pointer.
  • [ ] (b) When the function includes a conditional statement (if/then/else).
  • [ ] (c) When the value of r6 represents the base address of an array.
  • [ ] (d) When the function includes a loop.
  • [x] (e) When the function includes a call to another function (or the same function in case of recursion).

Problem 8

Let us assume that a new architecture is deployed in which the stack grows down instead of up. This means that allocating an activation frame is accomplished by ADDING to the stack pointer. Let us also assume that in each activation frame, the return address is located before arguments and local variables (i.e., the stack address where the return address is stored is lower than the stack address for arguments and local variables).

Does this new architecture change the circumstances under which the stack-smash, buffer-overflow attack is possible? If so, how?

  • [ ] (a) It makes the attack impossible
  • [x] (b) The attack is still possible, but only for code with an array underflow error (i.e., depending on input it may access elements before the beginning of the array).
  • [ ] (c) The attack is still possible, but only for code with an array overflow error (i.e., depending on input it may access elements after the end of the array).
  • [ ] (d) The attack is still possible, but it requires a longer nop sled.
  • [ ] (e) It has no affect on the attack

Problem 9

Consider the copy() routine below, which is vulnerable to stack-smash attacks, and assume you have a suitable attack string for this function.

void copy() {
    int i = 0;
    int dst[100];

    while (src[i] != 0) {
        dst[i] = src[i];
        i++;
    }

    dst[i] = 0;
}

Suppose that the function is now changed, as shown below, to add an extra local variable j to the copy routine.

void copy() {

	int i = 0;
	
	int dst[100];
	
	while (src[i] != 0) {
		dst[i] = src[i];
		i++;
	}
	
	int j = 0xF0F0F0F0;
	dst[i] = 0;
}

Suppose that local variables are stored as discussed in class: in the order in which they are declared, with the first local variable stored at the start of the function's activation frame (i.e., 0(r5)), with following variables in increasing offset order; and that the return address is stored after the local variables, but before the arguments. Assume there is no local variable optimization, i.e., all local variables are stored in memory in the stack, as is the return address.

Which of the following statements is true in regards to your existing attack string? Assume that local variables are stored in the stack in the same order as they are declared in the C code, with the first variable stored on top of (with a lower numeric address than) the second variable, and so on.

  • [x] (a) The buffer overflow string will need to be longer to compensate for the size of j.
  • [ ] (b) No changes are necessary to the attack string.
  • [ ] (c) The position of the shell code will need to be shifted in the attack string.
  • [ ] (d) The attack now becomes impossible.
  • [ ] (e) The value of the jump location that needs to be overwritten in r6 will need to change.