====================================== Lec-LC3-project-2.html ====================================== Linking LC3 C code and assembly code ====================================== This assignment has to do with writing C code that can call code written in assembly language, and writing that assembly code so that it can be called by C code. We will use lcc to both compile our C code and to link our assembly code with our C code. The result will be a .asm file that lc3as assembles into an LC3 load object, a .obj file. We will implement our OS in C and assembly this way. What to turn in: 1. A cover sheet w/ the usual info (name, course, date). Also include notes about what you worked on, and where the files are in your branch. Attached to the coversheet or on it, include your answers to any questions. ====================================== ------------------------ -- Get source code ------------------------ In src/ you will find a directory, src/C-linking/. It contains some program skeletons you can use to get started on your OS: os.c (base C code for the OS) asm-utils.asm (assembly utilities) asm-utils.h (C prototypes for utilities) Makefile (commands for assembling, compiling, and linking) Copy that directory to your src2/, and add it to your branch. Note that C function prototypes are needed for any externally defined routines called by C code. These are included in asm-utils.h for functions in asm-utils.asm. NB--Make sure you have an up-to-date src/. Just to be sure, do, cd src/ svn info and check that your src/ is checked out from projects/LC3trunk/src/. Then do, svn up in src/ to make sure it is up to date. If this is a problem, just use a browser and go to projects/LC3trunk/src/ and get the source code mentioned above. ------------------------------------------------ -- Build os.obj, load it to PennSim, -- and trace its execution. ------------------------------------------------ Look at os.c. It is a skeleton of a potential OS. Do this to build it, make os.obj Now, start PennSim.jar, and load os.obj. Use "step" to execute the code one instruction at a time. Q. Because lcc sets ".Orig x3000" by default, how is that PennSim.jar loaded os.obj to x0200? Hint, see the Makefile, and look at a.asm and os.asm. Those are the outputs from 1) lcc and 2) the processing done by Makefile. In PennSim.jar, step through the program up to the first JSSR. Q. What is the stack pointer, SP aka R6, initially set to by the PREAMBLE? What is the base pointer, BP aka R5, set to? What is the Global Data Pointer, GDP aka R4, set to? Now, step through the first JSSR. Q. What function did it jump to? Where is the source code for that function? At what address does this function begin? The next 6 instructions are lcc's C protocol for entering a function and setting up its call frame. R7 is pushed, and BP/R5 is pushed. Next, space is allocated for local variables on the stack. The BP is set to the first local variable and SP/R6 is set to the last local variable (which might be the same location if there are none or only one local variable, one word is always allocated). Q. Draw a picture of the stack. Show the current words pointed to by SP and BP. What addresses to they contain? How many local variables were allocated? Immediately after that, a value is fetched from the Global Data table (GD) addressed via R4 (the Global Data Pointer, GDP). There are two instructions associated with that access of the GD. The first just gets something into R0. Q. What is it? Look at the content of R0 and explain what that value is. Is it a memory address? Q. What is the label associated with that address? You can figure this out by looking at the address in R4, and the offset added to it. Find that address, and look at what label is found there. NOTE: in a real machine, no symbolic information is availabe, only machine code. PennSim uses a.sym to figure out the labels associated with addresses. The next instruction actually does the data fetch, using R0's content as an address and storing what is fetched into R0. Q. What label is associated with the content of R0 after the data fetch? Look at the memory location now in R0 and find what label is associated with that address. What instruction is at that address? The next instruction is JSSR R0. This is a C call. Q. Where in the source code is this call (which file, which line)? Where is the source code for the function jumped to? Is that a C routine or an ASM routine? Continue stepping through the program until you come to the next JSSR. Look at the content of R0 just before the jump is executed. Q. What function is being jumped to? Where in the source code is the jump? Is this a C call? In what file is the source code for the function being jumped to? You will see some unfamiliar syntax in that source code. These are lcc linking directives, similar to assembler directives such as ".END" and the like. They declare labels that will be associated with GD. Look in the assembler's output, os.asm, and find the GD (it is at the bottom). Q. What label is associated with the GD entry for this function? Q. What content is in that location in the GD? Explain what effect execution of this code would have at this point, ldr r3, r4, 5 ldr r0, r3, 0 jssr r0 That is, what gets loaded into R3? What gets loaded into R0? And what function is jumped to? Continuing stepping through the function you are in until you see R0 loaded with data from the stack. This is an argument. (R1 is also loaded with an argument later.) Q. What is loaded into R0? What is at that address? If you look at the comments in asm-utils.asm for this function, you will see a C prototype declaration. The first argument is a pointer to a function whose type is "void f( void )". The function pointer, in an argument list defined in the definition of a C routine, would look like this, int foo ( void (*f_ptr)( void ) ) { ... } and from that we would assume the formal argument, f_ptr, is a pointer variable that when deferenced provides the address of a function. We would invoke or jump to that function using this syntax in the body of foo: (*f_ptr)(); which says, find the memory location associated with the variable f_ptr, get its content, and use that as a jump address. Step through the next instruction in which R0 gets loaded again using R0's content as an address. Q. What is that value fetched into R0? What does that value refer to? If you look at the C source that is made the call to the function we are currently stepping through, you will see the current function's argument list. Q. What is being passed in to this function as its first argument? By C convention, the name of a function refers to a pointer variable that contains the address of the function. Look as os.asm, find the GD. Q. What is the name of the pointer variable that was passed as an argument? It contains the address of a function. Looking in os.asm, and the DG, what function's address is in that pointer variable? Q. Looking at PennSim, what is the address of the pointer variable? Stepping execution a bit more, the content of R0 is written to memory. Q. What memory location is written into? What value is written to that location? What have we just accomplished? Continuing stepping through two RETs to get out of the two nested function calls we have made. Execution is back in main. Look at os.c to see what function will be called next. Step execution in PennSim until you enter this function via JSSR. Q. What is the address of the function you have just entered? The function you have entered has its source code in asm-utils.asm. It is a wrapper for the putc trap routine. Trap routines are entered via a TRAP instruction. How can you make a TRAP in C code? You can't. So, this do_putc allows us to call it from C code, and it does the TRAP in assembly language. This is the simplest routine in asm-utils.asm. The C code pushes one argument, which by convention is pointed to by SP. We just put the argument in R0 and TRAP x21 to putc. Note that registers are saved on the stack, but this do_putc does not set up its call frame. Any C function it might call would assume the SP was set before the call, so the callee can push and pop as it desires. That is not the case here. Our do_putc is counting on putc to not mess up the stack so that its stored register values will still be there after putc returns. In fact, we wrote putc with that in mind, so it does not use the stack at all. To be safe, we could have simply set SP above where we stored our register values, and moved it back after putc returned. ----------------------------- -- Project development ----------------------------- Typically, do_putc and any C routines that call it would be part of a system library that users would link into their programs. The user would call the C routines as system utilities, which would make the actual system calls via things like do_putc. Printf is a good example. The C conventions we have to abide by are listed in asm-utils.asm. Actually, lcc's version of printf is written in assembly and calls PUTS to display a string. lcc's library includes (see bin/lcc-1.3/lc3lib), printf.asm, stdio.asm, getchar.asm, putchar.asm, and scanf.asm. These use the following traps: GETC (trap x20), OUT (trap x21), and PUTS (trap x22). (Note, we call OUT's code, putc.) These are needed for C code compiled by lcc for the LC3. You now have template code for building other OS routines. Start with PUTS. This is a TRAP routine: it is entered by TRAP x22; it is not jumped to from C code. It writes a string to the display. Its argument is in R0, the same as putc's, but R0 contains the address of a memory location, which contains an ASCII character code in its low byte and zero in its high byte, the same data that putc expects. The subsequent memory locations also contain ASCII, and PUTS writes these to the display until it finds a word containing ASCII NUL in the low byte, i.e., x0000. It calls, i.e., via TRAP x21, PUTS/putc to display the characters. We can initialize its VT slot the same as we did putc's. It's trap number is x22. Since we can't use the word "puts" or "PUTS" in our assembly language, let's name our TRAP x22 routine, "puts_trap". We could have a companion routine, do_puts, that C code can use, just as we did for putc. However, since the lc3lib already implements printf() and scanf(), we don't really need to. The C convention for strings is the same as TRAP x22 expects. So, this, char message[] = "Hello World!\n" generates a succession of memory locations with a NUL at the end, each location holding a single ASCII character. Printf would pass the first address to PUTS in R0, then TRAP x22. Using the existing asm-utils.asm as a template, write puts_trap. Also, as was done w/ putc in os.c, call setVTentry( puts_trap, 0x0022) to initialize. C code the executes after that call can then use printf(). You already have puts.asm as a starting point. It needs just a little fixing up to work. You can drop its code into asm-utils.asm, and add the ".global" stuff around it. Don't use our lc3pre macro extensions though: lcc won't give them a chance to be pre-processed. Also, don't use the stack: you can't be certain how it's being used when the trap occurs. Usually, you can use the stack, but it's less of a headache not to.