The debugger, while a useful tool for exploration, is obviously not ideal for general input and output, especially for ASCII text strings. Textbook presentations of assembly language programming often imply that routines written in assembly language can be called from high-level languages, but perhaps more by silence than by statement routines written in a high-level language cannot or should not be called from assembly language programs. In actuality, mixed-language programming at the assembly language level is possible, but does require strict adherence to published calling standards. 6.7.1 GETPUT: Encapsulating C FunctionsOur minimal illustration (Figure 6-6) involves a pair of C external routines, chrput (encapsulating the C library putchar function) to print a character on the screen, and chrget (encapsulating getchar) to obtain a character from the keyboard. Even though these routines process individual characters, we need to be aware that the operating system handles terminal input and output a line at a time. Figure 6-6 Encapsulating the putchar and getchar routines of C// getput.c // Encapsulate putchar and getchar #include <stdio.h> long long int chrput(long long int ch) { return (long long int) putchar(ch); } long long int chrget() { return (long long int) getchar(); } We encapsulated the putchar and getchar functions because the routines they represent may be implemented as "macros" (instructions inserted inline by the compiler) rather than as called functions. Encapsulation allows us to specify explicitly that we want to work with 64-bit integer values (long long int), rather than rely on a system default. Encapsulation also permits us to hide system-dependent or distracting details irrelevant to our algorithms. 6.7.2 IO_C: A Simple Test ProgramThe Linux and HP-UX programming environments for Itanium systems specify similar conventions for the use of registers and passing of arguments. The conventions include: what a caller must do, what a called procedure must do, and what resources are not to be overwritten. Since our encapsulated routines specify long long int as the data type, the incoming and outgoing values correspond to quad word (64-bit) quantities. Figure 6-7 shows a minimal test program that calls the encapsulated C routines for single-character input and output in order to validate their operation. We preview here another Itanium instruction, alloc, which allocates general registers Gr32 Gr127 on a module-by-module basis, and some of its associated assembler directives. Simple procedures interact with their callers using the general registers numbered Gr32 and up for their inputs, and register Gr8 for the value returned. This program uses additional forms of the Itanium branch instructions that perform call and return operations. Focus first on the central portion between the labels first and done. The external routines are called using the br.call instruction, which sets the instruction pointer (IP) to the address of the routine being called and stores a return address in branch register Br0. The completers sptk and many have the same meaning and purpose as for ordinary branch instructions. We have seen the function return instruction br.ret at the bottom of previous programs. Routines pass values, or addresses to values, as arguments. A function like chrget returns its value (the ASCII code for one character as a 64-bit value) in general register Gr8. A function like chrput will look for its input value in a specific region of the Itanium register stack, general registers Gr32 Gr127. The IO_C program passes a character code to chrput in register r35; choosing the correct register is explained more fully in Chapter 7. The files containing the C routines and the assembly language program can be processed with a single command line. Linking and production of an executable file is automatic, unless errors occur. Suitable system commands are as follows: L> gcc O0 -o bin/io_c io_c.s getput.c L> bin/io_c L> ecc O0 -o bin/io_c io_c.s getput.c L> bin/io_c H> cc +DD64 -o bin/io_c io_c.s getput.c H> bin/io_c HP-UX compiler tools may produce separate object modules, io_c.o and getput.o, in addition to an executable output file. Those files may be reused by the linker when their corresponding source files have not been modified. Figure 6-7 IO_C: A test of input and output// IO_C Test character I/O using C NL = 0xa // Newline character .global chrget,chrput // External references .type chrget,@function // and their .type chrput,@function // types .text // Section for code .align 32 // Desired alignment .global main // These three lines .proc main // mark the mandatory main: // 'main' program entry .prologue 12,r33 // rp, ar.pfs @r33+ .save ar.pfs,r34 // Previous function state alloc r34 = ar.pfs,0,3,1,0 // 3 locals, 1 out .save rp,r33 // Say where we store mov r33 = b0 // the return address .body // End of prologue first: // Now we really begin... mov r32 = r1;; // Save gp across calls br.call.sptk.many b0=chrget;; // Get one character mov r1 = r32 // Restore gp mov r35 = r8 // r35 = character to send br.call.sptk.many b0=chrput;; // Put one character mov r1 = r32 // Restore gp mov r35 = NL // r35 = newline to send br.call.sptk.many b0=chrput;; // Put one character mov r1 = r32 // Restore gp done: mov r8 = 0 // Signal all is normal mov ar.pfs = r34 // Restore previous state mov b0 = r33 // Restore exit address br.ret.sptk.many b0;; // Back to command line The linker collects the data and executable portions from the IO_C program and the other two modules into a unified address space and assures that all necessary intermodule pointers are accessible at runtime. By default, the executable file will be named a.out unless a name is specified with the -o option. It is important to ensure that only one module contains code designated as the main entry point; all the rest should be callable functions. If, at the keyboard, we type some characters like abc, say and then push the return (or enter) key, this program should output the first character that we typed on a line by itself. As the system input is actually line-oriented, the program waits until we have indeed pushed return (or enter) before proceeding; chrget (getchar) receives a line, no matter how small, rather than single characters. Similarly, no output will become visible until a program has sent a "newline" character through chrput (putchar). 6.7.3 Additional ConceptsRegister-level programming involves adherence to calling conventions in order to prevent the loss or corruption of the contents of the shared processor registers. Some Itanium assemblers, compilers, and linkers need to have global symbols explicitly indicated. The .global and .type directives describe the two encapsulated functions that we named chrget and chrput. Proper operation of the debugger, and recovery from errors detected by the system software, both require unwind information that the programming environment tools prepare as part of an executable program. Mosberger and Eranian describe the specific unwinding arrangements for Linux, while the Itanium Software Conventions and Runtime Architecture Guide sets forth general unwinding expectations that any operating system must provide. By passing assembly language programs through the C/C++ compiler front-ends (gcc, ecc, cc) instead of directly to the assemblers, some of the unwind information is built for us. We still must write appropriate prologue and epilogue segments. The prologue, extending from the .prologue directive to the .body directive, involves an additional directive (.save), a previously unseen Itanium instruction (alloc), and another application register (ar.pfs). The epilogue does not have a special directive to mark its top, but in IO_C it extends from the line after the label done to the program exit. Since every calling level might need a different address in the global pointer (gp, or register r1), it is proper to save register r1 before the first br.call and restore it after every br.call statement. |