Overview of Function Lifecycle in Assembly Programming

Overview of Function Lifecycle in Assembly Programming
Slide Note
Embed
Share

Delve into the function lifecycle in assembly programming, covering the steps involved in both caller and callee preparations, execution, and termination. Learn about the x86-64 System V Calling Convention and how arguments and return values are managed. Explore a sample function implementation and caller information. Discover the intricacies of managing stack frames and register usage.

  • Assembly Programming
  • Function Lifecycle
  • x86-64
  • System V Calling Convention
  • Stack Frames

Uploaded on Feb 23, 2025 | 0 Views


Download Presentation

Please find below an Image/Link to download the presentation.

The content on the website is provided AS IS for your information and personal use only. It may not be sold, licensed, or shared on other websites without obtaining consent from the author.If you encounter any issues during the download, it is possible that the publisher has removed the file from their server.

You are allowed to download the files provided on this website for personal or commercial use, subject to the condition that they are used lawfully. All files are the property of their respective owners.

The content on the website is provided AS IS for your information and personal use only. It may not be sold, licensed, or shared on other websites without obtaining consent from the author.

E N D

Presentation Transcript


  1. Assembly: Part II CS 61: Lecture 8 10/2/2023

  2. The Function Lifecycle: Overview Step 1: Caller preparation Caller sets up the arguments arguments for the callee If the caller has values in caller caller- -saved registers saved registers, the caller saves their values on the stack (so that the callee can use those registers without fear of overwriting the caller s values!) Caller tells the callee where to return to where to return to (i.e., which caller instruction the callee should jump to upon returning) Caller invokes invokes the callee Step 2: Callee preparation Callee creates the callee s new stack frame new stack frame If the callee wants to manipulate values in callee the callee must save those registers in the stack Step 3: Callee execution Step 4: Callee termination Callee sets up the return value return value Callee deallocates the stack frame deallocates the stack frame Callee returns returns to the appropriate instruction in the caller Calling convention call call instruction Function prologue callee- -saved registers saved registers, Calling convention Calling convention Function epilogue ret ret instruction

  3. x86-64: System V Calling Convention Simplest case: callee arguments+retval are integers or pointers Caller stores first six arguments in %rdi, %rsi, %rdx, %rcx, %r8, and %r9 Remaining arguments are passed via the stack Callee places return value in %rax int64_t foo(int64_t a, int64_t b, int64_t c, int64_t d, int64_t e, int64_t f, int64_t g, int64_t h){ int64_t local0 = a*b*c*d; int64_t local1 = e*f*g*h; return local0 - local1; } Caller info h g return addr saved %rbp local0 local1 %rbp %rsp a b c d e f Compiler may store locals in registers if there are enough spare ones! %rdi %rsi %rdx %rcx %r8 %r9

  4. main: #...initial code... #Call foo()! movq %rbx, (%rsp) movq %rax, 8(%rsp) callq foo(long, long, long, long, long, long, long, long) #main resumes here... post_call_instrs foo(long, long, long, long, long, long, long, long): movq %rdx, %rax imulq %rsi, %rdi imulq %rcx, %rax imulq %rdi, %rax imulq %r9, %r8 imulq 8(%rsp), %r8 imulq 16(%rsp), %r8 subq %r8, %rax retq Other stuff in main() s stack frame h %rbp g %rsp return addr

  5. main: #...initial code... #Call foo()! movq %rbx, (%rsp) movq %rax, 8(%rsp) callq foo(long, long, long, long, long, long, long, long) #main resumes here... post_call_instrs foo(long, long, long, long, long, long, long, long): movq %rdx, %rax imulq %rsi, %rdi imulq %rcx, %rax imulq %rdi, %rax imulq %r9, %r8 imulq 8(%rsp), %r8 imulq 16(%rsp), %r8 subq %r8, %rax retq Math: % %rax is local0 local0 and %r8 %r8 is local1 local1; no need to store locals on the stack! the calling convention! rax Other stuff in main() s stack frame h Put foo() foo() s return value in % %rax rax, as required by %rbp g %rsp return addr

  6. Functions: Red Zones A leaf function leaf function is a function that does not invoke other functions For such a function, the compiler may emit code that: Does not decrement %rsp to make space for local variables, but instead . . . Just assumes that the memory in the 128 bytes below %rsp can be used by the function to store local variables The 128 byte region beneath %rsp is called the red zone zone Red zones allow a function to avoid the overheads incurred by: Decrementing %rsp on function entry Incrementing %rsp on function exit Call graph main() f() q() g() r() s() Leaf function red Caller s stack frame %rsp Callee s red zone

  7. Callee-saved vs. Caller-saved Registers A calling convention distinguishes between two kinds of registers: Caller Caller- -saved registers saved registers can be overwritten by the callee, so if the caller wants to preserve their value, the caller must push the register values on the stack before invoking the callee Callee Callee- -saved registers saved registers are assumed by the caller to not be modified by callee, so the callee must push the register values on the stack before updating these registers The distinction is useful because it prevents the unnecessary saving and restoring of registers If the caller doesn t use a caller-saved register, the caller doesn t have to save it before invoking another function If the callee doesn t use a callee-saved register, the callee doesn t have to save it before executing callee code

  8. //Code at start of callee! Save //the old breakpointer. push %rbp mov %rsp, %rbp Callee-saved vs. Caller-saved Registers Callee-saved: %rbp, %rbx, %r12, %r13, %r14, %r15 %rbp (the breakpointer) is callee-saved! This is why the callee is responsible for saving the caller s old breakpointer before making %rbp point to the callee s stack frame Caller-saved: %rdi%, %rsi, %rdx, %rcx, %r8, %r9, %r10, %r11, %rax Note that the first six caller-saved registers are the same ones used to pass the first six arguments to a function! This is why those registers are caller-saved: the callee needs to be free to overwrite those registers //Assume that callee will later //need to overwrite callee-saved //registers %rbx and %r12. push %rbx push %r12 //Allocate space for local vars. subq $0x10, %rsp //[Main body: use %rbx and %r12] //Deallocate stack frame, restore //callee-saved registers. add $0x10, %rsp pop %r12 pop %rbx pop %rbp ret

  9. Compilation Lifecycle Up to this point in the class, we ve treated a compiler like gcc or clang as a monolithic program that takes source code as input and generates an executable file However, compilation is actually a multi-step process! The compiler compiler converts source code to assembly The assembler assembler converts assembly to an object file The linker linker combines object files into a final executable file What developers colloquially call a compiler is actually a compiler driver which orchestrates communication between the real compiler, the assembler, and the linker

  10. Assembly Files An assembly file contains: Human Human- -readable machine instructions readable machine instructions (as represented by compiler mnemonics like jmp and %rsp) Labels Labels that represent jump targets (e.g., the beginning of a function, the start of a loop body) Directives Directives that convey helpful information to the assembler (i.e., the next actor in the compiler toolchain) g++ -S file.cc tells the compiler to stop after compiling a file, with the generated assembly going into file.s //Return the sum of the entries //in an array. int sum (int* a, int n) { int s = 0; for (int i = 0; i < n; i++) { s += a[i]; } return s; }

  11. Name of source code file The kind of file data (code for the text segment) The symbol sum() global one (i.e., visible to other object files) The symbol represents a function sum() is a Loop body The assembly code for the sum() sum() function Loop test Note that labels like .L3 associated with addresses because we don t know where this code will live in the address space! .L3aren t

  12. .file "main.cc" .text .globl array .data .align 8 .type array, @object .size array, 8 array: .long 1 .long 2 .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $16, %rsp movl $2, %esi movl $array, %edi call _Z3sumPii movl %eax, -4(%rbp) movl -4(%rbp), %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc The globally-visible symbol is data (not code) of type array, has an alignment of 8 bytes, and has two elements having values 1 and 2 (note that the assembler directive long corresponds to a 32- bit (i.e., 4 byte) number //A driver program that //contains a reference to //an external function //called sum(). int sum (int* a, int n); //^^^^^^^ //This declaration should //really be in a header //file :-D. int array[2] = {1, 2}; int main () { int val = sum(array, 2); return val; } Note that this symbol isn t locally-defined in this file!

  13. Object Files The compiler consumes a source code file and generates a human-readable assembly file assembly file The assembly file contains assembler mnemonics, but with some parts missing The assembly file doesn t associate locally-defined code or static data with memory addresses (because we don t know where code or data will live yet!) The assembly file also doesn t know the addresses for externally-defined symbols The assembler consumes an assembly file and generates an object file An object file is a binary file, not a human-readable text file An object file contains binary-level representations of machine instructions and static data like strings and numbers An object file also contains other stuff (e.g., a symbol table and debugging information) g++ -c file.cc o file.o: Generate an assembly file and then generate an object file, placing the result in file.o object file

  14. //Return the sum of the entries //in an array. int sum (int* a, int n) { int s = 0; for (int i = 0; i < n; i++) { s += a[i]; } return s; } g++ -c sum.cc -o sum.o objdump d C t sum.o sum.o: file format elf64-x86-64 SYMBOL TABLE: 0000000000000000 l df *ABS* 0000000000000000 sum.cc 0000000000000000 l d .text 0000000000000000 .text 0000000000000000 l d .data 0000000000000000 .data 0000000000000000 l d .bss 0000000000000000 .bss 0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack 0000000000000000 l d .eh_frame 0000000000000000 .eh_frame 0000000000000000 l d .comment 0000000000000000 .comment 0000000000000000 g F .text 0000000000000045 sum(int*, int) The sum() sum() symbol is a globally-visible ( g ) function ( F ) The assembler can calculate address offsets because the assembler knows how big each instruction is . . . but to be useful, sum.o sum.o must be linked with another object file which invokes sum() Disassembly of section .text: 0000000000000000 <sum(int*, int)>: 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: 48 89 7d e8 mov %rdi,-0x18(%rbp) 8: 89 75 e4 mov %esi,-0x1c(%rbp) b: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) sum()!

  15. int sum (int* a, int n); g++ -c main.cc -o main.o objdump d C t main.o int array[2] = {1, 2}; int main () { int val = sum(array, 2); return val; } main.o: file format elf64-x86-64 SYMBOL TABLE: 0000000000000000 l df *ABS* 0000000000000000 main.cc 0000000000000000 l d .text 0000000000000000 .text 0000000000000000 l d .data 0000000000000000 .data 0000000000000000 l d .bss 0000000000000000 .bss 0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack 0000000000000000 l d .eh_frame 0000000000000000 .eh_frame 0000000000000000 l d .comment 0000000000000000 .comment 0000000000000000 g O .data 0000000000000008 array 0000000000000000 g F .text 000000000000001f main 0000000000000000 *UND* 0000000000000000 sum(int*, int) The array array symbol is the name of an object ( O ), i.e., a program variable that is globally ( g ) visible to other objects The main main symbol is a globally-visible function Disassembly of section .text: 0000000000000000 <main>: 0: 55 push %rbp 1: 48 89 e5 4: 48 83 ec 10 sub $0x10,%rsp OH NO IT S UNDEFINED LOCALLY DOES SOMEONE ELSE DEFINE IT? mov %rsp,%rbp

  16. Linking The compiler compiler consumes a source code file and generates a human-readable assembly file The assembly file contains assembler mnemonics, but with some parts missing The assembly file doesn t associate locally-defined code or static data with virtual addresses (because we don t know where code or data will live yet!) The assembly file also doesn t know the addresses for externally-defined symbols The assembler assembler consumes an assembly file and generates a binary-formatted object file that contains: Machine instructions and their relative offsets (i.e., addresses) A symbol table which indicates: The global symbols that are defined by the object and visible to other objects The global symbols defined by the object but only visible within the object: C++ variables defined with the static attribute (e.g., static int bar(){. . .}) The external symbols that the object wants to import from other objects The linker linker combines object files to produce an executable file! The linker creates a new object that merges all of the code and data from the individual objects The linker then patches unresolved symbol references, using the symbol table from each individual object file to locate everything

  17. sum.cc main.cc int sum (int* a, int n) { int s = 0; for (int i = 0; i < n; i++) { s += a[i]; } return s; } int sum (int* a, int n); int array[2] = {1, 2}; int main () { int val = sum(array, 2); return val; } COMPILATION g++ -S sum.cc g++ -S main.cc ASSEMBLY g++ -c sum.s o sum.o g++ -c main.s o main.o LINKING g++ sum.o main.o o prog FINAL EXECUTABLE

  18. In the final executable, the symbols are all resolved (i.e., they all have concrete locations)! (Abridged) output of objdump objdump - -d d - -C C - -t prog t prog prog: file format elf64-x86-64 SYMBOL TABLE: 00000000004004eb g F .text 000000000000001f main 00000000004004a6 g F .text 0000000000000045 sum(int*, int) 00000000004004eb <main>: 4004eb: 55 push %rbp 4004ec: 48 89 e5 mov %rsp,%rbp 4004ef: 48 83 ec 10 sub $0x10,%rsp 4004f3: be 02 00 00 00 mov $0x2,%esi 4004f8: bf 28 10 60 00 mov $0x601028,%edi 4004fd: e8 a4 ff ff ff callq 4004a6 <sum(int*, int)> 400502: 89 45 fc mov %eax,-0x4(%rbp) 400505: 8b 45 fc mov -0x4(%rbp),%eax 400508: c9 leaveq 400509: c3 retq 40050a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)

  19. Dynamic Linking The previous example showed static linking (i.e., linking at compilation time) Dynamic linking Dynamic linking (i.e., bringing in new object files while a program is executing) is also possible! We already saw a cheaty example of this in Lecture 6 The less morally corrupt approach is to use dlopen(const char *filename, ) and friends, but they essentially do the same cheaty things Map an object file into memory as readable and executable Parse the symbol table of the object file to find the locations of exported symbols Allow the process to query the table and get pointers to symbols (e.g., exported functions)! static linking int add(int a, int b) { // Open the image file const char* file = "cs61hello.jpg"; int fd = open(file, O_RDONLY); assert(fd >= 0); // Look up the file s size struct stat s; int r = fstat(fd, &s); assert(r >= 0 && S_ISREG(s.st_mode) && s.st_size > 0); // Load it into memory starting at // address `data`; treat it as executable! void* data = mmap(nullptr, s.st_size, PROT_READ | PROT_EXEC, MAP_SHARED, fd, 0); assert(data != MAP_FAILED); // Obtain address of bytes in the image // that represent 8d 04 37 c3 . . . uintptr_t function_address = (uintptr_t) data + 0x9efc; // Add a and b using that code! int (*function_pointer)(int, int) = (int (*)(int, int)) function_address; // Call add function! return function_pointer(a, b); }

Related


More Related Content