CS 208 s21 — Implementing Procedure Calls
Table of Contents
1 Video
Here is a video lecture for the material outlined below. It covers CSPP sections 3.7.3 through 3.7.6 (p. 245–254). It contains sections on
- local variables on the stack (0:22)
- example of a pointer to a local variable (5:33)
- example of passing arguments on the stack (14:18)
- local variables in registers (22:56)
- example of callee-saved registers (25:11)
- recursive procedures (29:24)
The Panopto viewer has table of contents entries for these sections. Link to the Panopto viewer: https://carleton.hosted.panopto.com/Panopto/Pages/Viewer.aspx?id=cc1f6a57-c4f1-4e91-95db-ac50011403ba
2 Procedures Continued
2.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
2.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(long*, long*): movq (%rdi), %rax movq (%rsi), %rdx movq %rdx, (%rdi) movq %rax, (%rsi) ret f(long, long): 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(long*, long*) movq 8(%rsp), %rax subq (%rsp), %rax addq $16, %rsp // free stack space by moving the stack pointer back up ret
void parse_five_numbers(char *input, int numbers[]) { int numScanned = sscanf(input, "%d %d %d %d %d", &(numbers[0]), &(numbers[1]), &(numbers[2]), &(numbers[3]), &(numbers[4])); if (numScanned < 5) printf("KABLOOEY!!!"); }
.LC0: .string "%d %d %d %d %d" .LC1: .string "KABLOOEY!!!" parse_five_numbers: subq $16, %rsp // allocate stack frame movq %rsi, %rdx // address of numbers[0] as third arg leaq 16(%rsi), %rax // load address of numbers[4] into rax leaq 4(%rsi), %rcx // address of numbers[1] as fourth arg pushq %rax // push address of numbers[4] onto the stack as seventh arg leaq 12(%rsi), %r9 // address of numbers[3] as sixth arg leaq 8(%rsi), %r8 // address of numbers[2] as fifth arg movl $.LC0, %esi // address of format string as second arg movl $0, %eax call __isoc99_sscanf // never loaded something into %rdi (first arg), why? // it's the same as the first arg to parse_five_numbers, so we just keep it addq $16, %rsp // deallocate stack frame cmpl $4, %eax jg .L1 movl $.LC1, %edi movl $0, %eax call printf .L1: addq $8, %rsp // cleaning up the pushq %rax from before ret
2.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 (whenP
callsQ
,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 (whenP
callsQ
,Q
is free to modify these registers, soP
must save any that it needs)
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
2.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
2.5 Example in gdb
- Not in today's video, included here for completeness
- using
-tui
layout asm
thenlayout regs
to get assembly and register viewsfocus cmd
to have arrows go through command historywinheight regs -LINES
to have registers take up less of the screen- note
%rip
changes each step %rsp
changes when we callcall_inrc
even beforesub $0x10,%rsp
—why?Print
vseXamine
- 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 stackx $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