CS 208 s22 — Implementing Procedure Calls

Table of Contents

mem-layoutv2.png

/**
 * @file mem-layout.c
 * @author Aaron Bauer (awb@carleton.edu)
 * @brief A demonstration of memory layout
 * @version 0.1
 * @date 2022-04-27
 *
 * @copyright Copyright (c) 2022
 *
 * Compile with:
 * gcc -Og -g -o mem-layout mem-layout.c
 * To disable dynamic linking:
 * gcc -Og -g -static -o mem-layout mem-layout.c
 * To disable position-independent execution:
 * gcc -Og -g -no-pie -static -o mem-layout mem-layout.c
 *
 * Run with
 * setarch x86_64 -R ./mem-layout
 * to get consistent memory addresses on Linux 
 * (by disabling address space randomization)
 */

#include <stdlib.h>
#include <stdio.h>

long global_var = 208;

int main() {
    double local_array[10];
    double *heap_array = (double*) malloc(10 * sizeof(double));

    char *string_literal = "a happy little string";
    // string_literal[0] = 'x'; // <-- causes a segmentation fault because this memory is read-only

    // print out the addresses of everything we can
    printf("local_array: %p\n", local_array);
    printf("heap_array: %p\n", heap_array);
    printf("address of heap_array: %p\n", &heap_array);
    printf("string_literal: %p\n", string_literal);
    printf("address of string_literal: %p\n", &string_literal);
    printf("global_var: %p\n", global_var);
    printf("address of global_var: %p\n", &global_var);
    printf("address of main: %p\n", &main);
    printf("address of malloc: %p\n", &malloc);
    printf("address of printf: %p\n", &printf);
}

1 Warmup

        call next
next:
        popq %rax
  1. What gets stored in %rax by these instructions?1
  2. Write down the different uses for the stack (i.e., what kinds of data gets stored there?).2

2 Stack Frame Exercise

call-graph.png

Each node in the above tree represents a function call. How many stack frames will be created? How many will be present on the stack at one time?3

3 Practice

CSPP practice problems 3.34 (p. 252) and 3.35 (p. 255)

rfun:
        testq   %rdi, %rdi
        jne     .L8
        movl    $0, %eax
        ret
.L8:
        pushq   %rbx
        movq    %rdi, %rbx
        shrq    $2, %rdi
        call    rfun
        addq    %rbx, %rax
        popq    %rbx
        ret
long rfun(unsigned long x) {
    if (_______)
        return _______;
    unsigned long nx = ______;
    long rv = rfun(nx);
    return ______;
}

4 Procedures Continued

4.1 Local Storage on the Stack

  • not all local variables are necessarily stored on the stack
    • accessing registers is much faster than accessing memory, so the compiler will use a register for a local variable whenever possible
  • sometimes we have to store things on the stack:
    • when we run out of registers (we only have a fixed number of them, after all), we'll have to start using the stack
    • if code generates a pointer to a local variable (via & operator), we have to store that variable on the stack
      • why? Because a register doesn't have an address, only a name.
      • In order to have a pointer to something (i.e., a memory address for it), the value has to be in memory somewhere
    • if our local variables are data structures like arrays or structs that don't make sense to store in a register

4.2 Examples

void swap(long *xp, long *yp) {
    long t = *xp;
    *xp = *yp;
    *yp = t;
}

long f(long x, long y) {
    swap(&x, &y);
    return x - y;
}
swap:
        movq    (%rdi), %rax
        movq    (%rsi), %rdx
        movq    %rdx, (%rdi)
        movq    %rax, (%rsi)
        ret
f:
        subq    $16, %rsp            // allocate space on the stack by moving the stack pointer (grows DOWN)
        movq    %rdi, 8(%rsp)        // move x onto the stack
        movq    %rsi, (%rsp)         // move y onto the stack
        movq    %rsp, %rsi           // copy the stack address for y to rsi
        leaq    8(%rsp), %rdi        // copy the stack address for x in rdi
        call    swap
        movq    8(%rsp), %rax
        subq    (%rsp), %rax
        addq    $16, %rsp            // free stack space by moving the stack pointer back up
        ret

4.3 Local Storage in Registers

  • The compiler has to make sure callee does not overwrite register values the caller plans to use later
  • x86-64 has uniform conventions that all procedures must adhere to
    • %rbx, %rbp, and %r12%r15 are callee-saved registers (when P calls Q, Q must preserve these values)
      • preserving means not writing to that register or pushing its value onto the stack and popping it off to restore before returning (saved registers portion of the stack frame)
    • all other registers besides the stack pointer %rsp are caller-saved (when P calls Q, Q is free to modify these registers, so P must save any that it needs)

reg-conventions.png

long P(long x, long y) {
    long u = Q(y);
    long v = Q(x);
    return u + v;
}
P:
        pushq   %rbp        // save value of callee-saved register this procedure will use
        pushq   %rbx        // save value of callee-saved register this procedure will use
        movq    %rdi, %rbp  // %rdi is a caller-saved register, we'll need its value later so we
                            // save it in a callee-saved register that Q will have to preserve
        movq    %rsi, %rdi
        call    Q
        movq    %rax, %rbx  // %rax is also caller-saved, so put in it callee-saved %rbx for 
                            // later use
        movq    %rbp, %rdi
        call    Q
        addq    %rbx, %rax
        popq    %rbx        // restore value of callee-saved register this procedure used
        popq    %rbp        // restore value of callee-saved register this procedure used
        ret

4.4 Recursive Procedures

  • the stack and register conventions facilitate recursion without needing any extra apparatus
long fact_rec(long n) {
    long result;
    if (n <= 1)
        result = 1;
    else 
        result = n * fact_rec(n - 1);
    return result;
}
fact_rec:
        cmpq    $1, %rdi
        jg      .L8
        movl    $1, %eax
        ret
.L8:
        pushq   %rbx
        movq    %rdi, %rbx
        leaq    -1(%rdi), %rdi
        call    fact_rec
        imulq   %rbx, %rax
        popq    %rbx
        ret

4.5 Example in gdb

  • using -tui
  • layout asm then layout regs to get assembly and register views
  • focus cmd to have arrows go through command history
  • winheight regs -LINES to have registers take up less of the screen
  • note %rip changes each step
  • %rsp changes when we call call_inrc even before sub $0x10,%rsp—why?
  • Print vs eXamine
    • display the value of an expression vs use the value of an expression as a memory address and display the value stored in memory there
    • p $rsp displays the address of the top of the stack
    • x $rsp displays the value stored at the top of the stack
long increment(long* p, long val) {
    long x = *p;
    long y = x + val;
    *p = y;
    return x;
}

long call_incr() {
    long v1 = 208;
    long v2 = increment(&v1, 124);
    return v1+v2;
}

int main() {
    call_incr();
}
increment:
        movq    (%rdi), %rax
        addq    %rax, %rsi
        movq    %rsi, (%rdi)
        ret
call_incr:
        subq    $16, %rsp
        movq    $208, 8(%rsp)
        movl    $124, %esi
        leaq    8(%rsp), %rdi
        call    increment
        addq    8(%rsp), %rax
        addq    $16, %rsp
        ret
main:
        movl    $0, %eax
        call    call_incr
        movl    $0, %eax
        ret

Footnotes:

1

Those instructions store the address of the popq %rax instruction in %rax. call pushes the address of the next instruction (the return address) onto the stack and then jumps to the specified target. popq %rax is the next instruction, so call next pushes its address onto the stack, and then jumps to the next label, which happens to be at popq %rax. popq %rax then pops the value from the top of the stack into %rax.

2
  1. Arguments for a function call (beyond the first six)
  2. Saved registers (callee-saved)
  3. Local variables
  4. Return address
3

Six stack frames will be created, one for each call. Four will be on the stack at one time, (e.g., slorp, squish, splat, printf), since once squish calls slop, splat and the first printf will have returned and been popped off the stack.