If you choose names for your modules, identifiers, etc, try to use meaningful names. To achive a consistency in use of names, we encourage you to follow the following rules.
The name of a module or compilation unit is created like this:
If you have to create name for an identifier, you should use meaningful names. To seperate words in a name, start every single word with a capital letter, for instance SetAccessRight.
To distinguish between different types of names, use the following suffixes:
Variables of minor importance may be short and are written in lower cases.
If there are conflicts of names, add an additional suffix to your name to resolve the conflict. Use:
Use L3 specific prefixes to avoid pullution of name space, for instance: L3_SV_ACK (l3, sv-protokoll, acknowledge). Maybe we should focus on L3_.
Avoid the use of the following pre/suffixes. They are reserved for
libc. (If we use L3_ as our prefix, there should be no problem.)pre_/_suffix header file using name pre_/_suffix(is, to ctype.h)d_ dirent.hE errno.hL_, F_, O_, S_ fcnstl.h_MAX limits.hLC_ locale.hpw_ pwd.hsa_, SA_ signal.hmem, str, wcs string.hst_, S_ stat.htms_ times.hc_, V, I, O, T, C termios.h_t type definitions
10.1.2 Conventions for function declaration
If you declare a function, write a short description of your function
above your declaration. Use the following structure for this
description:
/*
* Description of parameter
* (optional) pre- and postcondition
* (optional) Errorcodes
*/
If you want to declare a function, that is used to assert a structure, use the following conventions for nameing assert functions:
There should be no side effect in your assert function.
10.1.3 Headers
In header files you have to cope with two problem:
Use the following constructs to cope with these problems:
Use the folling construct:
#if !defined __headerfilename without extension_H
#define __headerfilename without extension_H
/* body of header */
#endif
Use the folling construct:
#if !defined __cplusplus
#error C++ compiler required
#endif
If you write a library, that shall be used with C and C++, you have to cope with name mangeling. A C++ compiler appends type information to function names to allow type safe linking. You have to switch name mangeling off using the folling link directive:
#if defined __cplusplus
extern ,,C'' {
#endif
/* body of header */
#if defined __cplusplus
extern ,,C'' {
#endif
We enforce you to use
#ifndef NDEBUG
/* debug code */
#endif /* NDEBUG */
Debug code must be read only!
to encapsulate debug code and to allow generation of debug free code.
10.1.5 CVS related things
Every Module should contain a line describing the version and date of last change, user etc. You can achive this by inserting the folling lines into your file:
The information is then automagically written to your file every time
it changes.
10.1.6 Library depencies
There should be no circular depency between libraries. For instance if
you use only functions of libl3sys.a, you need only libl3util.a and
libl3sys.a to generate you programm. There should be no linker sequence
like:
-L l3/l3/lib -ll3util -ll3prot -ll3sys -ll3util
10.1.7 Miscellaneous Conventions
The development for our project is done on various unix plattforms, for instance ultrix, freebsd and linux. We are using tools which are available for free, so that everyone wanting to take part in our project can do so. Another advantage is the possibility to set up a cross compile environment for these tools. Your os assignement will be done on the same base.
The development for our project is done in a combination of C, C++ and assemby language. We have choosen this languages because of they are highly available and they are the languages typically used for os projects.
This chapter contains a short description of the tools we use. If you
want to know more about it, you will need to have a look at the
mentioned references for each tool. 10.2.1 gcc, g++
The C/C++ compiler we use is gcc. It is used to compile C/C++ code to assembly language or objectcode or to generate complete programms using several other tools like ld. The main options used to invoke gcc are:
For a complete documentation of
gcc
and especially the
options of gcc
look at the appropriate info page. Aditionally there is a chapter
coping with the asm statement in the
l3-documentation.
10.2.2 ld
The GNU linker ld is used to combine a number of object and archive files to create an executable file. It is normaly invoked as the last step in program generation. The main options are:
To allow more then one person to work on our project we use a revision control system named cvs. In contrast to other systems it doesn't use file locking. The first developer committing a change in his file doesn't anything about changes other developers have made locally. If one of these other developers tries to check in his changes he will get an error message and has to use cvs commands to bring in his changes.
For a short walk through have a look at a sample session.
The most interesting cvs commands are:
cvs checkout l3
This will create a directory named l3 containing your copy of the source files.
cvs add foo.c
This will add the file foo.c to the repository. But you have to commit the file to allow other programmers to work with the newly added file.
cvs update
If you have changed the file locally it is possible, that there are conflicts between your version and the repository version. Cvs will detect this and generate a file containing the conflicting parts of both versions. You have to clean up the conficts before checking your new version.
cvs commit
Cvs tries to integrate your changes and will abort commiting if there are conflicts. If there are conflicts you have to resolve it before trying to commit again.
cvs diff foo.c
to get a diff between the last and you current version or
cvs diff -r1.11 -r1.12 foo.c
to get a diff between two versions.
cvs log svprot.c RCS file: Working file: svprot.c head: 1.17 branch: locks: strict access list: symbolic names: description: revision 1.17 date: 1996/01/25 14:08:35; author: mh1; state: Exp; lines: +8 -9 SendSVMsg(): specify all scratch registers to the asm() statement instead of trying to safe some of them ourselves; also, use a local symbol ("0:") instead of a named symbol in order to avoid clashes with the compiler's symbol name space ...
The file is identical with the latest revision in the repository.
You have edited the file, and not yet committed your changes.
Someone else has committed a newer revision to the repository.
Someone else have committed a newer revision to the repository, and you have also made modifications to the file.
$ cvs status svprot.c File: svprot.c Status: Needs Merge Working revision: 1.14 Repository revision: 1.17 Sticky Tag: (none) Sticky Date: (none) Sticky Options: (none)
There are several (two at the moment) useful scripts written in ELAN,
the built in language of L3. They provide some useful facilities for
programmers allowing an easier development for L3. runx
Runx is a collection of routines providing methods for handling a.out binaries. You can run binaries, create special dataspace used to create driver tasks at startup an similar things. Before using the scripts you have to insert them in the environment of your task using the L3 command insert(,,<file name>''). The following part presents a list of available routines, their parameters and an example for their usage.
To allow a minimal amount of debugging there is a core dump facility
consisting of an ELAN script and some procedures within libl3serv. If
an exception occurs the library catches it and tries to create a core
dump. To do this it waits for a request from a distinct task. You
can specify this task at initialization time of the library
(InitCore(task name)). After receiving the request the library
replies with information necessary to create the core dump, for
instance register and memory contents. The request is sent after
starting a script called create core dump(task name) and if the
reply comes in the script generates a core dump named <task
name>.core. Any existing core file with the same name will be
overwritten.
10.2.10 How to build an run a program using these tools
There are several steps involved in making and running a L3 programs. This section tries to explain these step using a simple example and a stepwise walk through. The steps are:
If you write a program consisting of more than one source and header file, it seems to be a good idea to use make to build your libraries and executable image. Make automates the process of building your library or executable and tries to find the minimal set of operations necessary to do so. A short introduction (and a lot of more information) can be found in the make info file.
A make file in general consists of rules and targets. ,,A 'rule' appears in the makefile and says when and how to remake certain files, called the rule's 'targets' (most often only one per rule). It lists the other files that are the 'dependencies' of the target, and''commands,, to use to create or update the target'' [make info] .
Assuming our program consists of a main program and several L3 libraries our Makefile could look like follows.
# -*-makefile-*- CPPFLAGS= -I../include \ -I../../../../l3/lib/libl3prot/include \ -I../../../../l3/lib/libl3sys/include \ -I../../../../l3/lib/libl3util/include \ include ../../../../Makeconf HELLO_SRCS= hello.c HELLO_OBJS= $(HELLO_SRCS:.c=.o) HELLO_LDFLAGS= -static -g -L ../.. -L ../../../../l3/lib ../../servcrt0.o HELLO_LIBS= -lserv -ll3prot -ll3sys -ll3util hello: $(HELLO_OBJS) $(LD) -o hello $(HELLO_LDFLAGS) $(HELLO_OBJS) $(HELLO_LIBS) clean:: $(RM) -rf $(HELLO_OBJS) *~ include $(HELLO_SRCS:.c=.d)
The very first line is a comment telling emacs that this file is a make file. Emacs switches to a special mode in this case allowing a more comfortable editing.
The second line defines a variable .
Variables are used to simplify make files and are referenced using the character '$'. This variable is a special variable and contains flags for the C compiler. In this case it is set to the include directory allowing the compiler to find specified include files. The next line will add some more switches to this variable. It includes a file named Makeconf containing some general rules like how to create object files, dependancies and some other things.
The third line specifies a set of C files which are necessary for our executable. In this case it is only one file, but normally you will have several files. The next line takes these names and replaces the .c at the end of the name with a '.o' resulting in a variable which contains all necessary object files.
The next lines contain some more definitions. The only option worth to diskuss is -static. You have to specify static to tell the linker that it should generate a static linked image rather then a dynamic linked one. This is necessary because of our current ELAN scripts can't handle shared libraries.
And finally there is our first goal named hello. It is the deault goal if there is no parameter givven to make. The target has one depency, $(HELLO_OBJS) meaning that it has to be rebuild if the target doesn't exists or one of the object files is changed. In that case the next line is executed calling ld to generate the executable image. If there is an object file missing it is build using de fault rules specified by special built in rules or rules from Makeconf.
The include statement at the end of our make file includes some '.d' files. There is a rule telling make how to create these files. It will call the compiler with a special option causing the compiler to generate a list of all files the current file depends on. The output is redirected to the '.d' file and included. This allows us to rebuild our programm if some include files have changed.
Last but not least an example for building a library. Assuming there is a variable called LIBOBJS containing all necessary objects to build the library. Then the following target produces a library with the same name like the target.
libserv.a: $(LIBOBJS) $(AR) rv $@ $^ $(RANLIB) $@
In this example the variables '@' and '^' are a variables defined by make. '@' contains the name of the target and '^' contains the list of dependencies. So make would call ar and ranlib to generate the library.
If you need more informations there are a lot of make files in the
source tree of our L3 project and there is the
gnu info file
on gnu make.
Writing your program
I think, there is not much to say about this. You have to have a problem, some ideas about a solution and some skills in programming C/C++. Just to show, how a short L3 program could look like, I will add a (very) small and simple program. The only thing it will do is to create a new task, send a request and wait for an answer. The newly created task in turn will wait for a request and send an answer.
#include <l3/util.h> #include <l3/ipc.h> #include <l3/syscalls.h> #include <l3/l3term.h> #include <l3/herclog.h> #include <l3/l3term.h> #include <l3/svprot.h> #include <l3/servutil.h> #include <l3/definthandler.h> extern int _astack; struct { MsgDopeT Size, Msg; int d1, d2, d3; } Msg; int TermPrint(char *text); int TermPrint(char *text) { return TERMwrite(text, _strlen(text)); } void TestLoop(void) { int ret; ThreadT Client; DefaultInit(); HercInit(); for(;;) { Msg.Size.DWord = MSGDOPE(1,0,0,0); LogStr(MySelfID(), "Waiting for Order"); ret = Wait(&Client, &Msg, NEVER); if (IPCError(ret)) { LogRet(MySelfID(), "IPCError ", ret); } while (!IPCError(ret)) { LogStr(MySelfID(), "Order received"); Msg.Size.DWord = Msg.Msg.DWord = MSGDOPE(1,0,0,0); LogStr(MySelfID(), "Sending Reply"); ret = ReplyAndWait(Client, &Msg, &Client, &Msg, 0, NEVER); if (IPCError(ret)) LogRet(MySelfID(), "IPCError in ReplyAndWait", ret); } } } void main(int argc, char **argv) { char buffer[180]; int result, i; DataSpaceT stdds; ThreadT Son; DefaultInit(); HercInit(); CopyStdds(&stdds); result = SVBeginTask("SonTask", 0, (int)TestLoop, _astack, stdds, &Son); if (result == L3_SV_ACK) { Msg.Size.DWord = Msg.Msg.DWord = MSGDOPE(1,0,0,0); Msg.d1 = Msg.d2 = Msg.d3 = 0xaffe; for (i=0; i<5; i++) { LogStr(MySelfID(), "Calling Son"); result = Call(Son, &Msg, &Msg, NEVER,NEVER); if (IPCError(result)) { LogRet(MySelfID(), "IPCError in Call", result); } else { LogStr(MySelfID(), "IPC call ok"); } } } }
The easiest way to compile and link your program is to write a good Makefile as explained earlier. If you have one the only thing you have to do is to call make -k. Make will execute the necessary things to build the requested target and will either finish with a successfully build target or with a bunch of error messages.
If you decide to compile you program without using make have a look at
the description of the necessary tools. (to be continued)
Transferring your program to L3
After building your image you have to transfer it to L3 to be able to run and test it. There are several posibilities to do that depending on the configuration you are using. You can transfer your program using:
In L3 every task manages its own file system, so you have to create a task, copy the necessary files to this task and test your program.
To create a task you activate the session manager (????) using <ALT> <PRTSCR> go to OBJ ROOT (or any other task) and press <ALT> <E>. L3 will request a task name, create the new task and activate it.
Now you have a new task with nothing in it (except some system files
necessary for L3DOS). You have to transfer your image and two other
files to this task using ftp or disk and the L3 copy command. The
'other' files are runx and core.elan. These two files
provide the functionality described earlier. To be able to use the
functionality you have to insert it in your environment using the L3
command insert (insert(,,runx'');insert(,,core.elan'')).
Running your program
In some situations we need a possibilty to generate a distinct sequence of assembler code, for instance to provide an interface for L3 system calls or to access registers or memory in a direct way.
It is obvious that you have to use assembler to provide kernel interfaces. But why should a normal application use assembler directly?
Consider a pager which need to page in a distinct page provided by another pager. The most obvious way to do this is to access the page with the appropriate operation (read operation for read access and write operation for write access). But it has to do in without changing the contents of that page, therefore it has to do something like:
/* * execute a write access to a page <page> to ensure the page is * available for write */ *page = *page + 0;
But nearly every compiler will throw away this statement
because it does nothing. So the only way is to use assembly
language. The following provides an introduction to inline assembler
in gcc (excerpt from info). For a more detailed documentation look at
gcc.info and gas.info. 10.3.1 Syntax
Currently we develop for the intel platform, so you probably expect an assembler which uses intel syntax. The code to access the page would look like:
push eax mov eax, _page_address add [eax], 0
But for several reasons that's not true. The following sections provide the intel specific information based on the gas info files.
AT&T Syntax versus Intel Syntax
So the assembly sequence would look like:
pushl %eax movl _page_address, %eax addb $0, (%eax) popl %eax
To implement this sequence within a C program you have to use the asm statement. The the code would look like:
void *page_address; /* some C code, initializing the global variable page_address */ asm("pushl %%eax \n\t" "movl _page_address, %%eax \n\t" "addb $0, (%%eax) \n\t" "popl %%eax \n\t" );
The main differences are:
To specify the operands for an asm statement you have to use constraints. There are different types of constraints, simple constraints and machine specific constraints. These constraints are modified by some special modifiers. The most import modifiers are ,,='' and ,,+''.
Using simple constraints our example would look like:
void *page_address; /* some C code, initializing the variable page_address */ asm("pushl %%eax \n\t" "movl %0, %%eax \n\t" "addb $0, (%%eax) \n\t" "popl %%eax \n\t" : /* no output */ : "g" (page_address) );
If you specify that the register eax is clobbered by the assembler statements you can leave out push and pop.
void *page_address; /* some C code, initializing the variable page_address */ asm("movl %0, %%eax \n\t" "addb $0, (%%eax) \n\t" : /* no output */ : "g" (page_address) : "eax" );
And last but not least using machine specific constraints and creating a function the code would look like:
void foo(void *page_address) { asm("addb $0, (%%eax) \n\t" : /* no output */ : "a" (page_address) );
The result would look like (assuming optimization):
.globl _foo _foo: movl 4(%esp),%eax #APP addb $0, (%eax) #NO_APP ret
Consider you have to implement a L3 system call for instance GetIDT(), a system call which provides the current interrupt descriptor table for the calling thread.
The system call is selected through a number in register al, invoked through int 7 and expects its parameter in ebx and ecx. The results are returned in ebx, ecx and edx.
The code looks like the following:
IDTEntryT *GetIdt(int *SizeP) { int type; IDTEntryT *idt; asm ("int $7\n " /* return values: */ : "=b" (type), /* type of idt <- ebx */ "=c" (idt), /* address of idt <- ecx */ "=d" (*SizeP) /* size of idt <- edx */ /* input values: */ : "a" ((ByteT)SPECIAL_OP), /* syscall number -> a */ "0" (SO_SET_IDT),/* opcode set/getidt -> ebx */ "1" (-1) /* -1 (no idt = getidt) -> ecx */ ); return idt; }
The resulting code looks like this:
_GetIdt: pushl %ebx xorl %ebx,%ebx movl $-1,%ecx movb $52,%al #APP int $7 #NO_APP movl 8(%esp),%eax movl %edx,(%eax) movl %ecx,%eax popl %ebx ret