CS 208 s22 — Implementing Procedure Calls
Table of Contents
/** * @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
2 Stack Frame Exercise
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 (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
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
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
Footnotes:
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
.
- Arguments for a function call (beyond the first six)
- Saved registers (callee-saved)
- Local variables
- Return address
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.