10 Development for L3/L4 (under construction)

10.1 Programming Style

10.1.1 Names and Identifier

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:

C
for a constant, for example const FamDridC=234;

T
for a type, for example typedef unsigned int DataSpaceT;

T
for a class, for example class DisplayT;

Ptr
for a pointer, for example typedef int *NumberPtrT;

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:

P
for a parameter of a function, for example void Fkt(DummyT TestP);

V
for a local variable, for example DummyT CounterV;

G
for a global variable, for example DummyT SemaG;

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:

  1. Avoiding problems with multiple inclusision of header files

    Use the folling construct:

    #if !defined __headerfilename without extension_H
    #define __headerfilename without extension_H
    /* body of header */
    #endif

  2. Force the user to use the appropriate compiler

    Use the folling construct:

    #if !defined __cplusplus
    #error C++ compiler required
    #endif

  3. Cope with name mangeling of C++ compilers

    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

10.1.4 Encapsulation of debug code

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

10.2 Tools (under construction)

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:

-o
specify the name of the output file

-c
compile only, don't call ld

-S
compile to assembly language (useful to check code generated for asm statement)

-E
stop after preprocessing (useful to check macro expansion)

-Wall
enable all (nearly all) warnings

-Wstrict-prototyps
warn if a function is declared or defined without a prototype

-Wmissing-prototype
warn if a global function is defined without a prototype

-L
specify where to look for libraries

-l
specify library to link with

-I
specify where to look for include files

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:

-o
specify the name of the output file

-L
specify where to look for libraries

-l
specify library to link with

-e
specify the entry point of the executable programm (usually you have to preceed a C function with an underscore)

-static
generate a statically linked binary

10.2.3 ar

10.2.4 make

10.2.5 objdump

10.2.6 nm

10.2.7 cvs

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:

checkout
The very first thing you have to do is creating of your own working copy of the repository. For this, you use the checkout command:

cvs checkout l3

This will create a directory named l3 containing your copy of the source files.

add
If you create a new file belonging to the project you have to add it to the repository. You use the add command of cvs to do so.

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.

update
If other developers have made changes to files in the repository you have to incorporate these changes into your own working copy. Cvs tries to detect differences between the repository and your working copy and to merge in the changes into your file to provide an up to date version of the project. You can do this using update.

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.

commit
To add your changes to the repository you have to commit your changes using

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.

diff
To check out differences between a locally changed file and the repository or two versions in the repository you can use the diff command:

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.

log
To get information about the history of a repository file you can use the command log. It provides the history of changes including the commit messages.
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

...

status
To find out the current state of a file you can use status. It figures out the current state and displays one of the folling states:
$ 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)

10.2.8 emacs

10.2.9 elan scripts

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.

runx(file name[, task name [,param string]])
Runx creates a task using an a.out binary. If you provide a task name, the new task will be named task name otherwise the name of the binary is taken to name the new task. If a task with the same name already exists runx will fail. If you provide a param string runx will supply this string for the newly created task. In conjunction with the startup code supplied by libl3serv.a you can use the normal C declaration main(int argc, char **argv) for the main function, otherwise you don't have any parameters for the main function.

runxp(file name, param string)
This function is does the same like runx. It is only provided to allow a file name and a param string as parameter which wouldn't be possible with runx.

dumpx(file name)
Dumpx provides some useful information about a.out binaries, like segment sizes, start addresses and other useful things.

xtods(file name [, dataspace name])
Xtods creates a special dataspace used by L3 to create driver tasks. It contains all necessary information (stack pointer, program entry point) allowing th hardware configurator to run the driver at L3 startup. All you have to do is to create a subdirectory ,,/driver/<driver name>'' within the hardware configurator, copy the dataspace to this directory using the name stdds and reboot L3. If the system comes up again there will be a new task named ,,<driver name>''. This will be your new driver.

sethw([file name,] io base, io extend, irq)
Drivers need access to special resources like interrupt lines and io space. You have to assign these parameters to the dataspace of your driver before copying it to the hardware configurator. L3 will check for conflicts and deny the operation if there is one.

core dump facility

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:

Writing a Makefile

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");
	    }
	}
    }
}

Compiling and linking your program

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:

the network
If your development plattform is connected to a L3 station via network you can use ftp to transfer your program to L3. Normally every L3 station configured for network use has a task named anonymous which is accessible through ftp. You connect to the L3 station and use the task name (in this case 'anonymous') as user name. There is normally no station password set so you kann simply enter anything you want. After the login procedure you can use the normal ftp command to transfer your images (don't forget to switch to binary transfer mode befor transferring binary images).

the floppy disks
You can simply copy the image to a floppy disk formatted for MSDOS. After that you put the floppy into the L3 station, open ARCHIVE A and copy the image into the destination task.

a available MSDOS partition on your hard disk
You can copy the image to your DOS partition, boot L3, open ARCHIVE C and copy it to your destination task.

Creating and preparing a L3 task

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

Creating a core dump

Analyse a core dump

10.3 Inline Assembler

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

Opcode Naming

Register Naming

Opcode Prefixes

Memory References

Handling of Jump Instructions

Floating Point

Notes

So the assembly sequence would look like:

        pushl   %eax
        movl    _page_address, %eax
        addb    $0,            (%eax)
        popl    %eax

10.3.2 Assembler Instructions with C Expression Operands

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:

10.3.3 Constraints

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

10.3.4 A more complex example

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

Marion Schalm, Jean Wolter, Michael Hohmuth
26.12.1995 (unfinished)