140 likes | 313 Views
The compilation process. To produce assembly code: gcc –S test.c produces test.s To produce object code: gcc –c test.c produces test.o To produce executable code: gcc test.c produces a.out. The purpose of a linker.
E N D
The compilation process • To produce assembly code: gcc –S test.c • produces test.s • To produce object code: gcc –c test.c • produces test.o • To produce executable code: gcc test.c • produces a.out
The purpose of a linker • The linker is a program that takes one or more object files and assembles them into a single executable program. • The linker resolves references to undefined symbols by finding out which other object defines the symbol in question, and replaces placeholders with the symbol's address.
Loader • Before we can start executing a program, the O/S must load it: • Loading involves 5 steps: • Allocates memory for the program's execution • Copies the text and data segments from the executable into memory • Copies program arguments (command line arguments) onto the stack • Initializes registers: sets $sp to point to top of stack, clears the rest • Jumps to start routine, which: 1) copies main's arguments off of the stack, and 2) jumps to main.
Compiler • Purpose: convert high-level code into low-level assembly • Four key steps: lexing parsing code optimizations code generation • Code generation: • instruction selection (depends on ISA; easier for RISC or CISC?) • instruction scheduling (later in the course, CS 433) • register allocation (today’s topic) CS 421, 426
Register allocation • The compiler initially produces “intermediate” code that assumes an infinite number of registers $t0, $t1, … and maps each variable to a unique register • To get actual code, variables must share registers • Suppose there are only 3 real registers $t1, $t2, $t3 • An easy case: every scope defines at most three variables • But scope is an over-estimate, as in this example: // a, b, c defined in this scope for(int i = 0; i < a; i += b) // stuff c = 0; c is not live here live in scope, converse not true
Live variable analysis • A variable x is live at a point p in the code if: • x is defined at point d “” p • x is read at a point r “” p • x is not redefined between p and r • Intuitively, x holds a value that may be needed in the future • Liveness computed at compile time • may have to over-estimate liveness (for correctness) • If at some point p, number_of_live_variables(p) number_of_registers, we obviously have to spill some variables to memory • Is the converse true?
Example • Consider the following code snippet: a = 0; b = a + 1; c = b + 1; a = c + 1; • We define a graph G where • vertices are variables • edge between two variables if their live regions overlap • We want to assign variables to registers so that two variables that share an edge are not assigned the same register a a $t1 loop: b c b $t2 c $t1 Graph coloring j loop At most two live variables Suppose we have two registers $t1, $t2
Graph Coloring • Color the vertices of a graph with k colors so that no two neighboring vertices get the same color • A tree is always 2-colorable • A map is always 4-colorable • There isn’t an efficient way to decide if a graph is 3-colorable • unless “P = NP” (the biggest open problem in CS!) • Fortunately, there are some efficient heuristics that can produce a near-optimal coloring
Which variables should be spilled? • Register Allocation Problem asks two key questions: • Are we forced to spill registers? • Yes iff min_colors(graph) number_of_registers • If so, which registers should we spill? • Also hard to compute optimum • Before good heuristics for graph coloring, register allocation was hard for RISC architectures with many registers • Now we have good graph coloring heuristics, so the focus shifts to the second problem • much more critical with CISC architectures with few registers
Excess-k notation • A method for representing signed integers • add excess amount k to every number to obtain a positive integer (smallest number that can be represented is k) • most significant bit represents sign (1 = positive, 0 = negative) • Examples (excess-128): 1 0 1 0 0 0 0 1 = +33 0 1 0 1 1 1 1 1 = 33 1 0 0 0 0 0 0 0 = 0 unique representation of 0! • If bias k = 2n-1, then excess-k is almost identical to two’s complement • sign-bit reversed (1 means positive) • comparing integers identical to the unsigned case • Big endian (MSB first) vs. little endian (LSB first) 128 64 32 16 8 4 2 1
Floating Point Numbers • How can we represent 3.14 ? • What’s wrong with: (int_part, frac_part) • 3.14 and 3.014 have the same representation! • The leading-zeroes problem can be solved if numbers are normalized • write the number in the form d.f 10e , d is a single non-zero digit • normalized(3.14) = 3.14 100, normalized(0.314) = 3.14 101 • In binary, the “d” part will always be 1 (zero is a special case) • this implicit 1 can be ignored • Ideal representation scheme has these features: • can represent positive and negative, low and high magnitude • it is easy to compare two numbers • it is easy to do basic math
single precision float 1 sign bit 1 = negative 0 = positive 8-bit exponent e excess-127 notation 23-bit fraction f double precision double 1 sign bit 1 = negative 0 = positive 11-bit exponent e excess-1023 notation 52-bit fraction f IEEE 754 standard • Format for single-precision (32-bit) and double-precision (64-bit) reals • The normalized (non-zero) binary number 1.f 2e is stored as • Comparison of floats almost identical to comparison of ints! • MIPS has separate floating point registers and instructions