Lecture notes for CSC 252, Thurs. Jan. 30, 2014ff Read chapter 3. Be sure to read 3.11 (using gdb) carefully; it will be essential for the binary bomb assignment. Assignment 2 will be on the web by the end of the weekend trivia due before class on Tues Feb. 4 main assignment due midnight, W. Feb. 12 ======================================== ASSEMBLY-LEVEL COMPUTER ARCHITECTURE ---------------------------------- Fetch-execute cycle (logical behavior of processor -- not necessarily how it works under the hood) repeatedly fetch instruction decode instruction fetch operands execute store result(s) ------------------------------ Basic Issues In Instruction Set Design Lots of variety in instruction set design [ What operations (and how many) should be provided? [ How (and how many) operands should be specified? [ What data types and sizes should be supported? [ How should we encode these into consistent instruction formats? We'll be discussing these issues in more detail in chapter 4. For now, note that the x86 is a "two-address, register-memory" architecture. The typical instruction specifies two operands. One must be in a register; the other may be in a register or in memory. The result overwrites the second operand (with gnu[gas]/Linux/AT&T syntax -- Intel/Microsoft syntax is different). By contrast, most modern ISAs (ARM, for example) are "three-address, register-register" architectures. The typical instruction specifies three operands (two sources and a destination), all of which must be registers. The source operands are never overwritten (unless you so specify). Memory is accessed via separate load and store instructions that move data to/from registers. There are several tradeoffs between these approaches. Among them: Machine code tends to be denser on the x86, but it's somewhat easier to build a high performance implementation of a 3-address register-register ISA. Most machines have separate integer and floating-point registers. Several reasons for this: - since instruction opcode (int ADD v. fp ADD) specifies type, you save bits in the instruction encoding by referring to the Nth register of the appropriate kinds, rather than the Mth register overall. - FP ops are more expensive than integer ops. Handled in different pipelines. Separate register sets reduce interferences between the pipelines (increased register bandwidth). - registers have different lengths on some machines (including the x86) ------------------------------ x86 ISA has a long and interesting history (see p. 157 of the text) [ 8008 (1972), 8080 (1974) [ The first single-chip microprocessors. 8-bit word size. [ 8008 had a 16-pin package, could use only an 8-bit bus, and was [ limited to 16K of RAM (with the help of significant external logic). [ 8080 has a 40-pin package, could use a 16-bit bus, and could easily [ access 64K of RAM. [ [ 8086 (1978) [ 16-bit wordsize, 20-bit addresses. The original MS-DOS machine. [ Only 29K transistors. 8087 FP co-processor. [ [ 80286 (1982) [ Additional addressing modes, now deprecated. The original Windows [ machine. Added segmentation for multiple address spaces (more on [ this later in the semester). [ [ i386 (1985) [ 32-bit extension of the architecture. Added support for demand-paged [ virtual memory. The first x86 capable of running Unix. [ [ i486 (1989) [ Integrated FP on-chip. Performance enhancements. No changes to [ instruction set. [ [ Pentium (1993) [ Performance enhancements. Only minor changes to the instruction set. [ [ PentiumPro (1996) [ Complete re-design of the internal implementation. Basically [ figured out how to maintain ISA backward compatibility while [ working like a RISC machine internally. This is the point at which [ you start to get markedly better performance by sticking to a [ carefully chosen subset of the instruction set. [ [ Pentium II (1997) [ Merged MMX media instructions into the ISA. [ [ Pentium III (1999) [ Added yet another set of vector instructions to the ISA. [ [ Pentium 4 (2001) [ Additional extensions to the vector ISA. Now sporting 42M [ transistors on chip. [ [ Pentium M [mobile] (2003) [ A heavily modified version of the Pentium III, optimized for energy [ efficiency. Rebranded as "Intel Core"; now sold in single, dual, [ and quad configurations. [ [ x86-64 (aka x64, AMD64, EMT64T, Intel 64) [ 64-bit extension of the architecture, designed by AMD and licensed by [ Intel. Widens the registers, adds 8 more integer regs (for a total of [ 16), and 8 more SSE registers. Adds PC-relative data access, for [ position-independent code (useful for for DLLs). IEEE FP on the SSE [ regs. Allows data to be marked no-execute. Eliminates legacy [ features when not running in "legacy mode". ---------------------------------- The x86-32 ISA Registers %eip program counter integer registers %ax ----------------,--------,-------- %eax | | %ah | %al | ----------------'--------'-------- %ebx, %ecx, and %edx similar %esi and %edi low halves also named %si and %di low bytes not separately addressable %esp stack pointer %ebp frame pointer like %esi and %edi, but used for special purposes low halves also named %sp and %bp low bytes not separately addressable %esp and %ebp should not be used except according to standard conventions. %eip shouldn't be used by the programmer at all. The other six "extended" integer registers can be used more-or-less interchangably in modern code, except that (1) a few (mostly deprecated) instructions do special things to certain registers (2) Two groups of three ([%eax, %ecx, %edx]; [%ebx, %edi, %esi]) have different (software-only) conventions for saving and restoring at subroutine calls. More on this later. condition code registers flags that track outcome of recent operations; used to control branching FP registers -- 8 of them, overloaded by MMX SSE registers -- 8 of them in x86-32 (additional control registers of interest primarily to kernel hackers; take CSC 256 to learn more) -------------- Simple example subroutine (from the book, p. 162) int accum = 0; int sum(int x, int y) { int t = x + y; accum += t; return t; } > gcc -O2 -S code.c sum: pushl %ebp # save frame pointer (dec, then st) movl %esp, %ebp # set new frame pointer movl 12(%ebp), %eax # load x into register addl 8(%ebp), %eax # add y into same register addl %eax, accum # add sum (t) into accum; # leave return value in %eax popl %ebp # pop frame pointer ret # return to caller; result in %eax The stack and frame pointer manipulations manage space for local variables according to certain "calling conventions". More on this later. Three ways to see assembly language: run gcc -S, then view file, as above run "objdump -d filename" use the "x/##i addr" or "dissassemble subr" commands in gdb Sadly, the instruction nmemonics vary slightly from one tool to the other, and between the various tools and the Intel manuals (the latter are available on the course web site). You can usually tell what's going on, though. Doubly sadly, the Intel and Microsoft manuals reverse the source/destination conventions. For gcc and AT&T, the source comes first: movl %eax, %ecx # %ecx := %eax "move eax into ecx" In the Intel/Microsoft manuals: movl %ecx, %eax # %ecx := %eax "ecx gets eax" In most cases instruction nmemonics have a suffix to indicate operand size. For example, addl add long (4 bytes) addw add word (2 bytes) addb add byte When there's no ambiguity (e.g. addl %eax, %ebx), some tools leave off the suffix. For floating point instructions the suffixes are s (single precision, 4 bytes) and l (double precision, 8 bytes). Notes - x86 instructions can vary from 1 to 15 bytes in length. [In most modern ISAs, instructions are exactly 4 bytes in length. Some very simple instructions (e.g. trap) thus take many more bits than necessary. Also, some simple operations (load into register from arbitrary constant address) take more than one instruction. On the flip side, the regularity and simplicity of the encoding makes it much easier to produce fast implementations.] - Starting at any given address, the decoding into instructions is unique. HOWEVER, there's also a unique decoding starting at the second byte of the first intended instruction, and that decoding probably isn't what you want! Cf. UTF-8, where you can always tell where you are. - If you disassemble an executable you'll see a lot of initialization and finalization code that isn't in the plain object (.o) files. That stuff is glued in by the linker. Also, you'll see absolute virtual addresses for global variables, which are just annotated zeros in the .o (and .s) files. That stuff is "resolved" by the linker. - If you look at the output of gcc -S you'll see a bunch of "directives", in addition to instructions. These all begin with a dot (.) They create initialized data, and/or instruct the assembler and linker regarding memory protection, debugging information, etc. -------------- A closer look at an individual instruction C int t = x + y; assembly addl 8(%ebp), %eax # addl instruction is similar to x += y # same instruction, whether operands are signed or unsigned (if # you want to know about overflow, you need to check different # condition codes for the two cases) # x is in register %eax # y is in memory, 8 bytes away from the location whose address # is in register %ebp # t is in %eax after the instruction. Note that this is very # common: which registers are used for which C variables can # change on an instruction-by-instruction basis. The compiler # has to keep it all straight. object code 0x03 0x45 0x08 The bytes 0x03 0x45 encode the addl operation and the two register names. The third byte is the offset used in the first operand. I won't be covering encodings in any detail. They're pretty baroque. See the Intel manuals if you're curious. For the bomb assignment, let the debugger do the disassembling. -------------- Operand specifiers Operands can be constants (embedded in instruction) values from registers values from memory In gas notation, constants are indicated with a leading dollar sign. In a typical instruction at least one operand must be in a register; you can't operate on two constants or two memory locations. Addressing modes used to encode the "effective address" of an operand in memory. [NB: some authors use "addressing mode" the way B&O'H use "operand specifier"; i.e. they include addressing modes to specify constant or register values.] Many modern ISAs have only one addressing mode, called "displacement", and use it only for ld and st instructions. The "8(%ebp)" above is an example. You add a small (signed) constant to the value in a register to get the address of the operand. There's often a special, extra adder in the processor to allow this to happen independent of other arithmetic operations. To use the address in a register, you let the displacement be zero. To use a small constant address, you specify register 0, which on many machines is hardwired to contain 0. To use a large constant address, you need two instructions. The first moves the high half-word of the address into a register; the second does the load, using that register and a displacement equal to the low half-word of the address. Older ISAs tend to have many more addressing modes, usable in many instructions. The x86 has 9 of them. The most general is called "scaled indexed" mode. The others are all restricted forms of scaled indexed mode, so as an assembly-language programmer you really only have to learn the one, and then leave out the parts you don't need. Example: 8(%eax,%ecx,4) # address is 8 + %eax + (%ecx * 4) Intuition: you have an array in memory whose starting address is 8 bytes away from the address in %eax and whose elements are each 4 bytes long, and you have a corresponding array index in %ecx. The scaling factor has to be a small power of two: 1 (default), 2, 4, 8. ---------------------------------- Instructions you need to know READ THE BOOK. Chapter 3 has a really nice, accessible introduction to the x86. There are several hundred instructions in the x86-32 instruction set, and even more in x86-64. Fortunately, gcc only uses a relatively small subset of them. You don't need to know the deprecated (8086, 80286) stuff, nor the MMX and SSE stuff (unless of course you want to do fancier things than we're going to give out as assignments). Data movement mov[lwb] S, D D = S movsbl S, D D = sign-extend(S) movzbl S,D D = zero-extend(S) pushl S %esp -= 4; (%esp) = S popl D D = (%esp); %esp += 4 Notes: - There are MANY more move operations, involving different operand sizes and types (including vectors) and conversions -- even direct memory-to-memory copy instructions. No way can we cover them all in class. - register operands of movw and movb instructions must be among the individually nameable register halves and quarters - by convention stacks grow "downward" on most machines, toward smaller addresses. This convention is embedded in the push and pop instructions, used (among other things) for passing arguments to subroutines. - On the x86, the mov instruction serves multiple purposes. In more recent ISAs, there is a register-register mov instruction, a memory-register ld instruction, and a register-memory st instruction. Arithmetic leal S, D D = &S "load effective address" incl D D += 1 decl D D -= 1 negl D D = -D notl D D = ~D addl S, D D += S subl S, D D -= S note order of operands! imull S, D D *= S 32-bit product (but see below) xorl S, D D ^= S orl S, D D |= S andl S, D D &= S shll k, D D <<= k \ sall k, D D <<= k \these are equivalent shrl k, D D >>= k unsigned sarl k, D D >>= k sign-extend Notes: - There are variants of most of these for smaller integers - There are other operations, too (e.g. divide), but they don't appear in the examples in the text. - As with most instructions, at least one operand has to be in a register. - The motivation for leal is to compute pointer values, as suggested by the C equivalent above. It's also useful, however (and generated by the compiler) for general calculation of linear combinations: leal 7(A, C, 4), D D = 4C + A + 7 - Value k in the various shift instructions can be either an (unsigned) constant or the (unsigned) value in register %cl (no other registers can be used for this purpose). [ Full precision arithmetic: [ [ imull S %edx:%eax = S * %eax signed [ mull S %edx:%eax = S * %eax unsigned [ cltd %edx:%eax = sign-extend(%eax) [ idivl S %edx = %edx:%eax % S; signed [ %eax = %edx:%eax / S [ divl S %edx = %edx:%eax % S; unsigned [ %eax = %edx:%eax / S [ [ Notes: [ - The nmemonic 'imull' is used for both two-operand limited-precision [ multiply and for single-operand (other implied) full-precision multiply. [ The assembler can tell them apart by the number of operands. Note [ that the limited-precision case does not need to distinquish between [ the signed and unsigned case (that's why there's no two-operand mul [ instruction), but the full-precision case does. (Multiplication of [ n-bit operands may produce a 2n-bit product. The 2n-bit patterns [ may be different, depending on whether the operands are signed or [ unsigned, but the low n bits are always the same. See pp. 75-76 in [ the text.) [ [ - There are no limited-precision divide instructions. [ [ - The Intel docs say 'cdq' instead of 'cltd' [ (convert double [to] quad v. convert long to double). [ Sorry about that. ================================== Control flow PC (%eip) incremented automatically by size of current instruction --> sequential execution unless explicitly changed Changes are effected with jump (branch), and subroutine call and return instructions. Some ISAs distinguish between "branch" and "jump", e.g., based on whether the target is absolute or relative to the PC. The x86 ISA doesn't use the term "branch". ------------------------------ Methods of Testing Conditions in the ISA Suppose we want to implement if (A < B) { ... } If A is in register r1 and B is in r2, this is equivalent (at a lower level) to if r1 >= r2 goto foo ... foo: There are at least 4 rather different ways such conditional branches are supported in modern processors. Condition Codes (x86, Sparc, PowerPC): Processor status bits are set as a side effect of certain arithmetic or move instructions, or explicitly by compare or test instructions. Let A be in %eax and B be in %ebx: cmpl %eax, %ebx ja label # jumps if carry and zero CCs are both # clear, i.e. if (B - A) > 0 Condition Register (MIPS): slt r3, r1, r2 # r3 := (r1 < r2) [Boolean] beq r3, label # branch on r3 == 0 (false) Compare and Branch (MIPS, PA-RISC): combine the above two instructions. bge r1, r2, label # fall through if (r1 < r2) Predicated execution (ia64, ARM): like condition code or condition register, but without branch cmp.lt p1,p2=r1,r2 # p1,p2 = r1=r2 (p1) \ # (p1) if_part # execute if A < B (p1) / # (p2) else_part # execute if A >= B The advantage of condition codes is short instruction sequences. Any sort of test and branch takes just two instructions -- often only 1, if the codes were set as a side effect of something else you had to do. A possible disadvantage of condition codes is that they induce extra work on any instruction that sets them, but this is minor enough that they probably don't force a slower clock speed. More significantly, they induce an additional sort of dependence between instructions, which the compiler and processor must track when considering which instructions can safely be executed simultaneously or out of order. Condition registers are a simple idea, but they require that a register be available (not in use for anything else). Compare-and-branch avoids the need for a register and combines two instructions into one, but may be too much work for a single instruction. Predicated execution avoids the need to predict branch outcome (and to recover if wrong) in a pipelined machine. NB: x86 has conditional move, which is sort of a restricted form of predication. ------------------------------ The x86 condition codes: CF: carry. Most recent operation generated a carry out of the MSB. Used to detect overflow of unsigned operations. ZF: zero. Most recent operation yielded zero. SF: sign. Most recent operation yielded negative result. OF: overflow. Carries into and out of MSB differed on most recent operation. Used to detect overflow of 2's complement operations. Almost all instructions, including moves (but not lea) set the condition codes. For logic operations (e.g. xor) CF and OF are set to zero. For shifts, the carry flag is set to the last bit shifted out (of either end of the word); the overflow flag is set to zero. You can set the condition codes without changing the value in any register: cmp[bwl] S2, S1 test[bwl] S2, S1 Cmp (compare) calculates S1-S2 and sets the condition codes accordingly. It doesn't modify any of the integer registers. (Recall that "subl S, D" computes D = D-S, *not* D = S-D. Don't be confused by the intuitively backward order of the operands.) Test calculates S1 & S2 and sets the ZF and SF codes accordingly. "test %eax, %eax" can be used to tell if the value in the register is negative or zero. "test $0x00f0 %eax" can be used to tell if any bits are set in the second-lowest byte of the register. ------------------------------ Set instruction To calculate a boolean value (e.g. c = a < b), use the "set" instruction: cmpl %ebx, %eax # compare a and b (i.e., compute a - b) setl %al # set %al (low byte of %eax) if a < b movzbl %al, %eax # zero-extend to other three bytes Note that setl is "set less than", not "set long". There are lots of possible tests, many of which have two names: equality Z sete, setz equal, zero ~Z setne, setnz not equal, not zero sign S sets negative ("signed") ~S setns non-negative unsigned comparisons ~C & ~Z seta, setnbe above, not below or equal ~C setae, setnb above or equal, not below C setb, setnae below, not above or equal C | Z setbe, setna below or equal, not above signed comparisons ~(S^O) & ~Z setg, setnle greater than, not less than or equal ~(S^O) setge, setnl greater than or equal, not less than (S^O) setl, setnge less than, not greater than or equal (S^O) | Z setle, setng less than or equal, not greater than The signed comparisons are the tricky ones to understand. Consider "setl" (set when less), in the wake of "cmpl b, a". If a-b does not overflow, then SF is the value we want: a-b will be negative when a is less than b. If a-b *does* overflow, then SF is the opposite of the value we want, but OF indicates that the overflow happened. In either case, S^O is the value we want. ------------------------------ Jump instructions There are a bunch of conditional jump instructions. Most have names analogous to the set instructions above: je, jz jne, jnz js jns jg, jnle jge, jnl jl, jnge jle, jng ja, jnbe jae, jnb jb, jnae jbe, jna There is also an unconditional jump: jmp label jmp *EA In the first of these forms the assembler calculates an address and embeds it in the instruction. In the second form it generates an instruction that calculates an effective address, as in the leal instruction, and jumps there. jmp *%eax # jump to location in %eax jmp *(%eax) # jump to location specified in memory location # whose address is in %eax Note that all the conditional branches have only the label form; the EA form is not available. (You can of course branch to an unconditional jump.) If a conditional branch is not taken, execution continues at the next instruction. ------------------------------ Control constructs (Take 254 to learn much more about this stuff.) Basically assembly language has to do everything with gotos. before; if A < B { foo; } else { bar; } after; becomes cmpl B, A jnl .L1 # jump if not less jmp .L2 .L1: .L2: -------- The simplest loop is a post-test: before; do { body; } while (A < B); after; becomes .L1: cmpl B, A jl .L1 -------- Slightly fancier is a pre-test: before; while (A < B) { body; } This has several possible translations. One possibility is .L1: cmpl B, A jnl .L2 jmp .L1 .L2: This has two jumps in every iteration, which is unfortunate. Many C complers generate two copies of the test instead: cmpl B, A jnl .L2 .L1: cmpl B, A jl .L1 .L2: If you take 254 you'll learn that compilers can optimize (improve) loops in several important ways. The textbook talks about some of these. Two of the most common: - Expressions whose values don't change within the loop may be pre-computed and kept in registers, esp. on machines with lots of registers. Compilers call these "loop invariants". - Expressions (e.g. array indices) whose values are always some multiple of the loop index may be updated with adds or subtracts, rather than by multiplying by the updated loop index on each iteration. Compilers call this "stength reduction". The bottom line: what you see for the body of a loop may not have an obvious correspondence to what you wrote in your C code, especially when the compiler was told to use higher levels of optimization. Be patient, study what it created, and you'll usually be able to figure out what's going on. -------- Most complicated is a for loop. They're especially tricky in Fortran and Algol-family languages, such as Ada and Pascal. They're not too bad in C. before; for (initial; test; update) { body; } after; is defined by the C manual to be equivalent to before; initial; while (test) { body; update; } after; which ends up being translated into jX .L2 .L1: jnX .L1 .L2: -------- The final C control construct (other than subroutine) is the switch statement: switch (expr) { case val1: body1; break; case val2: body2: break; ... case valN: bodyN; break; default: bodyD; break; } Sometimes, especially for small switch statements, the C compiler turns this into an equivalent straightforward sequence of if-then-else's. Other times (and this is why the construct is in the language) it generates a *jump table*. The jump table is basically an array of addresses, one for each of the "body" blocks above. The compiled code uses the switch expression to index into this table and find the address to which to jump. (Particularly clever compilers may use fancier lookup schemes -- a hash table or binary search -- when the set of case values isn't dense.) The address of the default block appears in the jump table for every value that doesn't have its own block. An initial test covers the case where the value is out of range. Recall that in C if no default is given the implicit default is to do nothing. Example: before; switch (E) { case 10: body_10; case 11: body_11; case 13: body_13; case 20: body_20; default: body_default; } after; becomes .section .rodata .align 4 .L1: .long .L10 # 10 .long .L11 # 11 .long .L2 # 12 .long .L13 # 13 .long .L2 # 14 .long .L2 # 15 .long .L2 # 16 .long .L2 # 17 .long .L2 # 18 .long .L2 # 19 .long .L20 # 20 .section .code # suppose E is in %eax subl $10, %eax # %eax -= 10 jl .L2 # too small; use default (jump if signed) cmpl $10, %eax jg .L2 # too big; use default (jump if greater) jmp *.L1(,%eax,4) # jump through table .L10: jmp .L99 .L11: jmp .L99 .L13: jmp .L99 .L20: jmp .L99 .L2: .L99: NB: the jump table for a switch statement ends up in the read-only (initialized) data segment, at an address far removed from the code. You can find it in gdb by pulling the address out of the code (the messy jump statement above) and using the x/11xw command. If you know the address of the table you can also use objdump -s --section=.rodata but be warned that the output will be byte-reversed, because the x86 is little-endian. Also be warned that you may see nop's between the bodies of the arms of the switch statement. These may be inserted to improve the performance of the instruction cache, by aligning each body on a new cache line. Nop's aren't always easy to recognize; there isn't a special op-code. You may see things like movl %esi, %esi or leal 0x0(%esi), %esi neither of which has any noticable effect. ================================== Subroutines Overview (specific to x86, but reminiscent of other machines; take 254 for more detail) Stack grows from high end of address space toward lower addresses. Some authors draw it growing up the board, others down. 252 and 254 texts are backward from each other. Sorry. I'll try to stick to B&O'H conventions here. Each subroutine allocates a new frame. Whether bookkeeping stuff at the border (arguments, return address, saved regs, etc.) is part of the caller or callee frame is simply a matter of definition. B&O'H say the return address is the last thing in the caller's frame. high | | addresses |===============| --- | | ^ | | | stack | | caller frame growth | | | | | | | | | | | | |---------------| | v | return addr. | v |===============| --- | | | | | callee frame | | | | | v -------------------- Relevant instructions pushl R subl 4, %esp movl R, (%esp) popl R movl (%esp), R addl 4, %esp call L # or call *EA pushl %eip # after updating to refer to next instruction jmp L ret popl jmp * enter C # not used much by modern compilers pushl %ebp movl %esp, %ebp sub C, %esp leave # no arguments movl %ebp, %esp popl %ebp -------------------- More detail on frame layout high addresses | | | | |---------------| stack | last arg. | growth | . | | | . | ^ | | . | | v | first arg. | caller frame |---------------| | | return addr. | v |===============| --- | saved %ebp | | <--- %ebp |---------------| callee frame | other | | | saved regs | | |---------------| v | | | local vars | | and temps | | | |---------------| | arg | | build area | <--- %esp |---------------| | | | | Stack pointer %esp points at last used location on the stack. (On some architectures it points at the first _unused_ location; be warned.) Frame pointer %ebp points at or near beginning of current frame. Arguments have positive offset from %ebp; local variables have negative offset from %ebp. Offsets are known at compile time. Arguments are passed by pushing them onto the stack at the top (lowest end) of the frame, or by mov-ing them into pre-allocated locations near the lowest end of the frame (typically using offsets from %esp, not %ebp). Conventions for machines with more registers (including the x86-64) pass arguments in registers whenever possible. Return value is passed in %eax if it fits, or written to address passed as extra hidden parameter. -------------------- swap_add example from the book int swap_add(int *xp, int *yp) { int x = *xp; int y = *yp; *xp = y; *yp = x; return x + y } calling sequence for S = swap_add(A, B), where A, B, and S are all globals: leal B, %eax movl %eax, 4(%esp) # assuming build area already allocated leal A, %eax movl %eax, (%esp) call swap_add # callee executes here movl %eax, S # or else use it directly, if S is "dead" code for swap_add itself (no significant optimization): # prologue: pushl %ebp # save frame pointer movl %esp, %ebp # set up new frame pointer pushl %ebx # so we can safely trash it # body: movl 8(%ebp), %edx # xp movl 12(%ebp), %ecx # yp movl (%edx), %ebx # *xp movl (%ecx), %eax # *yp movl %eax, (%edx) # *xp = y movl %ebx, (%ecx) # *yp = x addl %ebx, %eax # x + y # remember that %eax is the return value # epilogue: popl %ebx # restore saved reg # * movl %ebp, %esp # restore stack pointer * popl %ebp # restore frame pointer ret # pop return address and goto Notes: (1) #-ed out line not needed for this routine, since no locals (%esp and #ebp are already the same) (2) Two *'ed lines could have been replaced with leave instruction. (3) Less trivial subroutines generally need space for local variables. Prologue subtracts constant from %esp to reserve space for these. (Or uses 'enter' instruction in archaic code.) Epilogue doesn't have to do anything special to deallocate: move of %ebp into %esp takes care of it. (4) Most modern compilers pre-reserve space for longest list of args passed to any callee. Why not just push them? Main reason: args can be computed and stored to memory in arbitrary order (not necessarily last-first), which creates new opportunities for instruction-level parallelism (5) If frame is small (so displacement addressing covers the whole thing) and doesn't change (no calls to alloca), compiler may omit use of %ebp, freeing it up for other purposes. (6) Optimization may change/rearrange things - use different (equivalent) instruction sequences - interleave prologue with start of body, or end of body with epilogue -------------------- Register saving conventions You'll notice that the prologue and epilogue saved and restored %ebx. Why not %eax, %ecx, and %edx, which are also overwritten? Answer: register saving conventions. Suppose g is a global variable. Consider: int a = g = a + foo(a) Assembly code probably looks something like < compute into %ebx > movl %ebx, (%esp) call foo addl %ebx, %eax movl %eax, g Now suppose foo trashes %ebx. Who should save it, foo or caller? Foo doesn't know whether callers, in general, need %ebx. No point saving it if old value isn't needed. But caller, likewise, doesn't know whether foo will trash it; no point in saving it if not. Convention: %ebx, %ebi, and %esi are "callee saves": callee (foo) saves them if it trashes them. %eax, %ecx, and %edx are "caller saves": caller saves them if it needs them after the call. Callers preferentially leave values they need in callee saves registers; callees preferentially trash caller saves registers. -------------------- Recursive routines Factorial example in book. Key point: given stack discipline, there is NOTHING EXTRA needed to implement recursion. ================================== Structured data Structs (records) usually laid out contiguously possible holes for alignment reasons permits copying but NOT comparison with simple block operations smart compilers may re-arrange fields to minimize holes C compilers promise not to Unions (variant records) overlay space cause problems for type checking C allows structured data to appear in the stack frame. Java insists it be new-ed from the heap. -------------------- Arrays Two layout strategies for arrays: contiguous elements column major -- basically used only in Fortran; due to historical accident row major -- used by everybody else; makes array [a..b, c..d] the same as array [a..b] of array [c..d]. row pointers an option in C allows rows to be put anywhere -- nice for big arrays on machines with segmentation problems. avoids multiplication, which used to be more expensive than dereferencing (now it's the other way around) nice for matrices whose rows are of different lengths e.g. an array of strings requires extra space for the pointers Strings are really just arrays of characters in C, terminated by a NULL byte. .ascii .asciiz ---------------------- Pointers serve two purposes: efficient (and sometimes intuitive) access to variables (as in C) dynamic creation of linked data structures, in conjunction with a heap storage manager Several languages (e.g. Pascal) restrict pointers to accessing things in the heap. C allows pointers to functions. Other languages with similar functionality often express it at a higher level. Examples: first-class functions in Scheme, delegates in C#. Some C programs (e.g. the Linux kernel) use explicit arrays of function pointers to imitate object-oriented programming. Some languages (Java, Lisp, Perl, ML) automatically reclaim heap objects that aren't in use anymore (this is called "garbage collection"). Others (C, C++, Pascal, Fortran) don't. In those you have to figure out for yourself when something isn't in use anymore, and reclaim it. too soon --> dangling pointers too late --> leak storage ---------------------- C pointers and arrays Variable definitions: int *a[n] n-element array of row pointers int a[n][m] 2-d array Beware the difference between definitions, which allocate space, and declarations, which merely introduce names. Since function prototypes (headers) are just declarations, and since arrays are passed as pointers in C, and since C doesn't try to do array bounds checking, the following parameter declarations are equivalent: int *a == int a[] pointer to int int **a == int *a[] pointer to pointer to int Note that these equivalences do NOT hold for definitions. Compiler has to be able to tell the size of the things to which you point. So the following aren't valid, even as parameter declarations: int a[][] bad int (*a)[] bad C declaration rule: read right as far as you can (subject to parentheses), then left, then out a level and repeat. int *a[n] n-element array of pointers to integers int (*a)[n] pointer to n-element array of integers int (*f) (int *) pointer to function taking pointer to integer as argument, and returning integer When is an array not a pointer? (a) in a declaration, where the array allocates space (b) in a sizeof, where the array represents the whole thing double A[10]; double *p = A; sizeof(A) == 80 // the whole array sizeof(A[0]) == 8 // one element sizeof(p) == 4 // a pointer Pointer arithmetic You can add or subtract integers to/from pointers: foo A[10]; p = &A[3]; p += 3; // p == &A[6] (regardless of size of foo) You can also subtract pointers from one another: p = &A[3]; q = &A[6]; (q-p) == 3 // regardless of size of foo Results are undefined if p and q point into different arrays. Once upon a time, pointer arithmetic was more efficient than array subscripting. Nowadays the opposite is generally the case, so use it only if it makes your code clearer. Formal definition A bare array name (without index) is equivalent to a const pointer to its first element (element 0). That's what we mean by "arrays are passed as pointers". A[i] == *(A + i) Amazingly, that's the same as *(i + A), which means that A[i] == i[A] This actually works in C, believe it or not. ---------------------- Buffer overflow bugs overwrite saved FP saved regs arguments data in calling routines * return address particularly dangerous is targeted program is running as a privileged user (e.g. root) usefulness of relative addresses in branches (v. jumps) great in shared libraries also great for hackers :-( BTW, this is serious stuff; don't take it lightly. Write safe code! don't use gets, use fgets The danger of these bugs is significantly reduced (though not eliminated) if your machine distinguishes between read and execute permission on memory, and your OS exploits the distinction. The x86 didn't used to distinguish (and in fact some x86 code *depends on* the ability to write instructions into data space and then execute them). Recent models provide the option. OSes turn on protection for new code; off for legacy code. We'll turn it off for the buffer bomb assignment. The danger of overflow bugs is also reduced (but not eliminated!) by "randomizing" the stack. Recent versions of Linux do this by default; again, we'll turn it off for the buffer bomb assignment. (Be aware, though, that later phases of the assignment turn it on again at the source level, at which point you have to take extra steps to defeat it.) BTW, the book mentions the 1988 Internet Worm. It notes that the creator of the worm, then an UG at Cornell, was convicted, put on probation (which prohibited him from using computers for several years), and had to pay a fine. It doesn't mention that he's now a CS prof at MIT. A word to the wise: there are safer ways to get famous.