Writing a library
For example, given that you have an M68000 compiler running on an IBM PC
and that the target is an MC68040 VMEbus system, how do you modify or replace
the runtime libraries? There are two generic solutions: the first is to change
the hardware design so that it looks like the hardware design that is supported
by the run-time libraries. This can be quite simple and involve configuring the
memory map so that memory is located at the same addresses. If I/O is used,
then the peripherals must be the same — so too must their address locations to
allow the software to be used without modifica-tion. The second technique is to
modify the libraries so that they work with the new hardware configuration.
This will involve changing the memory map, adding or changing driv-ers for new
or different peripherals and in some cases even porting the software to a new
processor or variant.
The techniques used depend on how the run-time librar-ies have been
supplied. In some cases, they are supplied as assembler source modules and
these can simply be modified. The module will have three sections: an entry and
exit section to allow data to be passed to and from the routine and, sandwiched
between them, the actual code that performs the requested command. It is this
middle section that is modified.
Other compilers supply object libraries where the rou-tines have already
been assembled into object files. These can be very difficult to patch or
modify, and in such cases the best approach is to create an alternative
library.
Creating a library
The first step is to establish how the compiler passes data to routines
and how it expects information to be returned. This information is normally
available from the documentation or can be established by generating an assembler
listing during the compilation process. In extreme cases, it may be necessary
to reverse engineer the procedure using a debugger. A break point is set at the
start of the routine and the code examined by hand.
The next problem is concerned with how to tell the compiler that the
routine is external and needs to be specially handled. If the routine is an
addition and not a replacement for a standard function, this is normally done
by declaring the routines to be external when they are defined. To complement
this, the routines must each have an external declaration to allow the linker
to correctly match the references.
With replacements for standard library functions, the external
declaration from within the program source is not needed, but the one within
the replacement library routine is. The alternative library is accessed first
to supply the new version by setting the library search list used by the
linker.
To illustrate these procedures, consider the following PASCAL example.
The first piece of source code is written in PASCAL and controls a semaphore in
a typical real-time operating system, which is used to synchronise other tasks.
The standard PASCAL did not have any run-time support for operating system
calls and therefore a library needed to be created to supply these. The data
passing mechanism is typical of most high level languages, including C and
FORTRAN, and the trap mechanism, using directive numbers and parameter blocks,
is also common to most operating systems.
The PASCAL program declares the operating system calls as external
procedures by defining them as procedures and marking them as FORWARD. This
tells the compiler and linker that they are external references that need to be
resolved at the linker stage. As part of the procedure declaration, the data
types that are passed to the procedure have also been defined. This is
essential to force the compiler to pass the data to the routine — without it,
the information will either not be accepted or the routine will misinterpret
the information. In the example, four external procedures are declared: delay, wtsem,
sgsem and atsem. The procedure delay takes an
integer value while the others pass over a four
character string
— described as a packed array of char. Their operation is as follows:
delay delays the task by a number of milliseconds. atsem creates and attaches the task to a semaphore. wtsem causes the task to wait for a
semaphore. sgsem signals the
semaphore.
program
timer(input,output);
type
datatype
= packed array[1..4] of char;
var
msecs:integer;
name
:datatype; i :integer;
procedure delay( msecs:integer); FORWARD;
procedure wtsem( var name:datatype); FORWARD;
procedure sgsem( var name:datatype); FORWARD;
procedure atsem( var name:datatype); FORWARD;
begin
name:= ‘1sec’;
atsem(name);
delay(10000);
sgsem(name);
for i := 1 to 10 do begin;
wtsem(name);
delay(10000);
sgsem(name);
end;
end.
The program TIMER works in this way. When it starts, it assigns the
identity 1sec to the variable name. This is then used to create a semaphore
called 1sec using the atsem
proce-dure. The task now delays itself for 10000 milliseconds to allow a second
task to load itself, attach itself to the semaphore 1sec and wait for its
signal. The signal comes from the sgsem procedure on the next line. The other task receives the signal, TIMER
goes into a loop where it waits for the 1sec semaphore, delays itself for 10000
milliseconds and then signals with the 1sec semaphore. The other task
complements this operation by signalling and then waiting, using the 1sec
semaphore.
The end result is that the program TIMER effectively controls and
synchronises the other task through the use of the semaphore.
The run-time library routines for these procedures were written in
MC68000 assembler. Two of the routines have been listed to illustrate how
integers and arrays are passed across the stack from a high level language —
PASCAL in this case — to the routine. In C, the assembler routines would be
declared as functions and the assembler modules added at link time. Again, it
should be remembered that this technique is common to most compilers.
DELAY IDNT 1,0
*
++++++++++++++++++++++++++++++++++++++++++++++++++
*
++++++++++++++++++++++++++++++++++++++++++++++++++
*
++++ ++++
*
++++ Runtime procedure call for PASCAL ++++
*
++++ ++++
*
++++ Version 1.0 ++++
*
++++ ++++
*
++++ Steve Heath - Motorola Aylesbury ++++
*
++++ ++++
*
++++++++++++++++++++++++++++++++++++++++++++++++++
*
++++++++++++++++++++++++++++++++++++++++++++++++++
*
PASCAL call structure:
*
*
procedure delay(msecs:integer);FORWARD
*
This routine calls the delay directive of the OS
*
and delays the task for a number of ms.
*
The number is passed directly on the stack
XDEF DELAY
SECTION 9
DELAY EQU *
MOVE.L (A7)+,A4 Save
return address
MOVE.L (A7)+,A0 Load
time delay into A0
MOVE.L A3,-(A7) Save
A3 for PASCAL
MOVE.L A5,-(A7) Save
A5 for PASCAL
MOVE.L A6,-(A7) Save
A6 for PASCAL
EXEC MOVE.L #21,D0 Load
directive number 21
TRAP #1 Execute
OS command
BNE ERROR Error
handler if problem
POP MOVE.L (A7)+,A6 Restore saved
values
MOVE.L (A7)+,A5 Restore
saved values
MOVE.L (A7)+,A3 Restore
saved values
JMP (A4) Jump
back to PASCAL
ERROR MOVE.L #14,D0 Load
abort directive no.
TRAP #1 Abort
task
END
The code is divided into four parts: the first three corre-spond with
the entry, execution and exit stages previously mentioned. A fourth part that
handles any error conditions has been added.
The routine is identified to the linker as the delay proce-dure by the
XDEF delay statement. The section 9 command instructs the linker to insert this
code in the program part of the file. Note how there are no absolute addresses
or address references in the source. The actual values are calculated and
inserted by the linker during the linking stage.
The next few instructions transfer the data from PAS-CAL to the
assembler routine. The return address is taken from the stack followed by the
time delay. These values are stored in registers A4 and A0, respectively. Note
that the stack pointer A7 is incremented after the last transfer to effectively
remove the passed parameters. These are not left on the stack. The next three
instructions save the address registers A3, A5 and A6 onto the stack so that
they are preserved. This is necessary to successfully return to PASCAL. If they
are corrupted, then the return to PASCAL will either not work or will cause the
program to crash at a later point. With some compilers, more registers may need
saving and it is a good idea to save all registers if it is not clear which ones
must be preserved. With this example, only these three are essential.
The next part of the code loads the directive number into the right
register and executes the system call using the TRAP #1 instruction. The
directive needs the delay value in A0 and this is loaded earlier from the
stack.
If the system call fails, the condition code register is returned with a
non-zero setting. This is tested by the BNE ERROR instruction. The error
routine simply executes a termi-nation or abort system call to halt the task
execution.
The final part of the code restores the three address registers and uses
the return address in A4 to return to the PASCAL program. If the procedure was
expecting a returned value, this would be placed on the stack using the same
technique used to place the data on the stack. A common fault is to use the
wrong method or fail to clear the stack of old data.
The next example routine executes the atsem directive which creates the semaphore. The assembler code is a little
more complex because the name is passed across the stack using a pointer rather
than the actual value and, secondly, a special parameter block has to be built
to support the system call to the operating system.
ATSEM IDNT 1,0
*
++++++++++++++++++++++++++++++++++++++++++++++++++
*
++++++++++++++++++++++++++++++++++++++++++++++++++
*
++++ ++++
*
++++ Runtime procedure call for PASCAL ++++
*
++++ ++++
*
++++ Version 1.0 ++++
*
++++ ++++
*
++++ Steve Heath - Motorola Aylesbury ++++
*
++++ ++++
*
++++++++++++++++++++++++++++++++++++++++++++++++++
*
++++++++++++++++++++++++++++++++++++++++++++++++++
*
*
PASCAL call structure:
*
*
type
* datatype
= packed array[1..4] of char
*
*
procedure atsem(var name:datatype);FORWARD
*
*
This routine calls the OS and creates a
*
semaphore. Its name is passed across on the
* stack using an address
pointer. *
XDEF ATSEM
SECTION 9
DELAY EQU *
MOVE.L (A7)+,A4 Save
return address
MOVE.L (A7)+,A0 Get
pointer to the name
LEA PBL(PC),A1 Load
the PBL address
MOVE.L (A0),(A1) Move
the name into PBL
MOVE.L A3,-(A7) Save
A3 for PASCAL
MOVE.L A5,-(A7) Save
A5 for PASCAL
MOVE.L A6,-(A7) Save
A6 for PASCAL
EXEC MOVE.L #21,D0 Load
directive number 21
LEA PBL(PC),A0 Load
the PBL address
TRAP #1 Execute
OS command
BNE ERROR Error
handler if problem
POP MOVE.L (A7)+,A6 Restore saved
values
MOVE.L (A7)+,A5 Restore
saved values
MOVE.L (A7)+,A3 Restore
saved values
JMP (A4) Jump
back to PASCAL
ERROR MOVE.L #14,D0 Load
abort directive no.
TRAP #1 Abort
task
SECTION 15
PBL EQU *
DC.L ‘ ‘ Create
space for
name
DC.L 0 Semaphore
key
DC.B 0 Initial
count
DC.B 1 Semaphore
type
END
Assembler listing for the atsem call
The name is passed via a pointer on the stack. The pointer is fetched
and then used to point to the packed array that contains the semaphore name.
Normally, each byte is taken in turn by using the pointer and moving it on to
the next location until it points to a null character, i.e. hexadecimal 00.
Instead of writing a loop to perform this task, a short cut was taken by
assuming that the name is always 4 bytes and by transferring the four
characters as a single 32 bit long word.
The address of the parameter block PBL is obtained using the PC relative
addressing mode. Again, the reason for this is to allow the linker freedom to
locate the parameter block wherever it wants to, without the need to specify an
absolute address. The address is calculated and transferred to register A1
using the load effective address instruction, LEA.
The parameter block is interesting because it has been put into section
15 as opposed to the code which is located in section 9. Both of these
operations are carried out by the appropriate SECTION command. The reason for
this is to ensure that the routines work in all target types, irrespective of
whether there is a memory management unit present or the code is in ROM. With
this compiler and linker, two sections are used for any program: section 9 is
used to hold the code while section 15 is used for data. Without the section 15
command, the linker would put the parameter block immediately after the code
routine somewhere in section 9. With a target with no memory management, or
with it disabled, this would not cause a problem — provided the code was
running in RAM. If the memory management declares the program area as read only
— standard default for virtually all operating systems — or the code is
in ROM, the transfer of the semaphore name would fail as the parameter block
was located in read only memory. By forcing it into section 15, the block is
located correctly in RAM and will work correctly, whatever the system
configuration.
These routines are extremely simple and quick to create. By using a
template, it is easy to modify them to create new procedure calls. More
sophisticated versions could transfer all the data to build the parameter block
rather than just the name, as in these examples. The procedure could even
return a completion code back to the PASCAL program, if needed. In addition,
register usage in these examples is not very efficient and again could be
improved. However, the important point is that the amount of sophistication is
dependent on what the software engineer requires.
Device drivers
This technique is not just restricted to creating run-time libraries for
operating systems and replacement I/O functions. The same technique can even be
used to drive peripherals or access special registers. This method creates a
pseudo device driver which allows the high level language access to the lower
levels of the hardware, while not going to the extreme of hard coding or
in-lining assembler. If the application is moved to a different target, the
pseudo device driver is changed and the application relinked with the new
version.
Debugger supplied I/O routines
I/O routines which read and write data to serial ports or even sectors
to and from disk can be quite time consuming to write. However, such routines
already exist in the onboard debugger which is either shipped with a ready
built CPU board or can be obtained for them.
*
Output a character to console
*
*
The character to be output is passed to
*
this routine on the stack as byte 5 with
*
reference to A7.
*
*
A TRAP #14 call to the debugger does the actual
work
*
Tabs are handled separately
putch
move.b
5(A7),D0 Get char from stack cmp #09,D0 Is it tab character? beq _tabput Yes,go
to tab routine trap #14 Call debugger I/O dc.w 1 Output char in D0.B rts
An example putchar routine for C using debugger I/O
Many suppliers provide a list of basic I/O commands which can be
accessed by the appropriate trap calls. The mechanism is very similar to that
described in the previous examples: parameter block addresses are loaded into
registers, the command number loaded into a data register and a trap
instruction executed. The same basic technique template can be used to create
replacement I/O libraries which use the debugger rather than an operating
system.
Run-time libraries
The example assembler routines simply use the predefined stack
mechanisms to transfer data to and from PASCAL. At no point does the routine
actually know that the data is coming from a high level language as opposed to
an assembler routine — let alone differentiate between C and PASCAL. If a group
of high level languages have common transfer mechanisms, it should be possible
to share libraries and modules between them, without having to modify them or
know how they were generated. Unfortunately, this utopia has not quite been
realised, although some standards have been put forward to implement it.
Related Topics
Privacy Policy, Terms and Conditions, DMCA Policy and Compliant
Copyright © 2018-2023 BrainKart.com; All Rights Reserved. Developed by Therithal info, Chennai.