Porting to UnixWare ® 7
Whitepaper
Authors
David F. Prosser |
|
Jonathan Schilling |
Development Systems Architect |
|
Senior Software Engineer |
Introduction
Porting applications to UnixWare 7 is not hard!
Whose Tools Do I Use?
How do I get started?
Makefile commands and options
Commands
Command options
API issues
C language dialect issues
C++ language dialect issues
Java platform-specific issues
Binary debugging
truss
Dynamic Library Tracing
Memory debugging
memtool
MALLOC_CHECKS
Source debugging
Documentation
Introduction
This technical whitepaper describes some of the issues that are encountered
in porting applications to UnixWare® 7, and some of the techniques that can
be used to make such a port successful.
By its very nature such a whitepaper cannot be comprehensive; there
are as many different porting challenges as there are applications to
be ported. Instead we try to illustrate a few of the problems and solutions
that we've seen come up most often.
The techniques presented here should be useful no matter where the existing
application being ported came from; but the case most frequently assumed
here is that it comes from some other variant of UNIX, possibly
SCO OpenServerTM, possibly a non-SCO platform such as
Solaris, AIX, etc.
For a brief time the UnixWare 7 product was known as
Open UNIX® 8 (which was equivalent to UnixWare 7
Release 7.1.2); everything in this document applies
to porting to Open UNIX 8 as well.
First Things: Porting applications to UnixWare 7 is not hard!
This is an important point to make. Papers such as this tend to point out
a lot of the problems you might hit and how to deal with them. But don't
let that appearance be deceiving: any given port won't hit all of these
problems, and many ports may involve no more than making a few changes to
a makefile, compiling and linking, with the application pretty much working
after that.
Another important point to make is that you may not have to port
at all! UnixWare 7 can run three kinds of binaries built for other
platforms:
- Many application binaries built for SCO OpenServer will run
unchanged on UnixWare 7.
- Many "generic Intel SVR4 ABI" binaries (as they are sometimes
labelled by ISVs) will run unchanged on UnixWare 7.
(However most Intel Solaris binaries do not run on
UnixWare 7, due to ABI differences.)
Whose Tools Do I Use?
So now we're back to porting.
Assuming the application is written in C or C++, the first question you
have to confront is, which set of development tools should I be using? Your
choices are two:
- the SCO UnixWare OpenServer Development Kit (UDK)
[in Open UNIX 8, this was known as the Open UNIX
Development Kit (OUDK)]
- the GNU GCC toolchain
Both of these products are found
on the UnixWare OpenServer Development Kit CD-ROM in the
UnixWare 7 media kit; the GNU GCC tools are part of the "Open Source
Tool Kit" (which contains pre-built, fully packaged versions of these
free or GPL software) (there is no need to build GCC yourself!).
Both are officially supported by SCO.
So which should you choose?
The UDK gives you
- the best integration with UnixWare 7
- generally the best performing generated code (in terms of size and
speed)
- a stronger debugger
- the best ability to have the resulting binaries run not just on
UnixWare 7 but also on SCO OpenServer
as well.
On the other hand the GNU GCC toolchain gives you
- common language dialects with GNU compilers on other platforms
- tools that you may be more familiar with from using on other platforms.
The choice is yours. If a lot of nonstandard GNU extensions are used
in the source code, the path of least resistance is definitely to use
GNU tools. Otherwise, using the UDK will probably produce
the best final results.
This white paper will assume that the UDK is being used, although
some of the material also applies to the case where GNU tools are being
used.
Finally, if your application is in Java, then the Java 2 Standard
Edition for SCO UNIX Operating Systems is the vehicle to use
to build (if necessary) and run the application. This is installed
by default on every UnixWare 7 system.
How do I get started?
When you start porting an existing application you typically have a bunch
of source files and a makefile (or some kind of script that tells how to
compile and build the application).
If there is a makefile, take a look at it. If it is named GNUmakefile ,
or has conditional rules in it such as
ifeq ($(VARIANT), DBG)
CFLAGS_COMMON += -DJCOV
endif
then it's a GNU makefile, and may well have a whole bunch of extensions
that the standard UNIX (including UnixWare 7) make command
does not support. If you're not sure, trying running it through the
UnixWare 7 make ; if it's a GNU makefile,
the flurry of error messages you get
will tell you soon enough.
If this is the case, get GNU make (sometimes referred to
as gmake) from the UnixWare 7 Open Source Tool Kit
(see above) and use it. This is true even if
you are using the SCO UDK tools for compiling and linking!
It is most definitely not worth your time to be doing
wholesale makefile rewriting....
If you are porting an open source project, you may find that the
makefile itself is generated by a configure script, which
itself is generated by tools such as autoconf and
automake. These tools are provided in the
UnixWare 7 Open Source Tool Kit. You can use the UDK in the
presence of configure scripts (typically you override the
CC and CXX settings when doing the
initial configure), but in practice the presence of this kind
of scheme is often a hint that using the GNU GCC compilers is the
easier way to go.
The next thing to look at are the compile and link commands and options
used within the makefile.
Makefile commands and options
Commands
First some basics on commands. The names of the UDK C and C++ compilers
are cc and CC , with both living in /usr/ccs/bin/ .
You can usually set these by modifying or overriding some makefile macro:
$ make CC=/usr/ccs/bin/cc
The C compiler wants C files to have the .c extension.
The C++ compiler will accept C++ source files with every extension
we know of that's out there: .C .c .cpp .cxx .cc and so forth.
However note that CC something.c will do a C++ compile
of the source file; there are some other compilers for which the driver will
dispatch a .c file to the C compiler. This can create problems
in makefiles that hold the latter assumption.
Another command that you'll sometimes see used in makefiles from other
platforms is ranlib . This doesn't exist, and isn't necessary,
on UnixWare 7. You can work around this with minimal makefile changes
simply by changing or overriding ranlib usages to touch .
In a similar vein, the lorder and tsort commands
are present on UnixWare 7 but are not needed when building an archive
library. (Although using them may produce a faster-to-link-with archive.)
Command options
Now for compiler command options. The -D option is universally
used to indicate preprocessor macro definitions to pass into the compile.
Often applications will have been written using #ifdef 's
to control what code gets incorporated for what platform flavours. There
may be a general UNIX macro that needs to be set, and then there may be
individual macros corresponding to different UNIX variants.
If you see a macro SVR4 , that's a good one to turn on,
since UnixWare 7 is SVR4 (now SVR5) based. For example, you might set
CFLAGS = -DSVR4
inside the makefile. If you don't see that macro, UnixWare 7 is generally
closest to Solaris among all other major UNIX variants. So if you
see a -DSOLARIS or -DSOLARIS2 , go for it.
If as part of your port, you want to add a new #ifdef to
mark off any source changes you make for UnixWare 7, you can either make
up your own, say -DUNIXWARE , or you can simply reference
the predefined preprocessor symbol supplied by the UDK C and C++ compilers,
__USLC__ .
Now to other compiler options. Basic ones are defined by POSIX and tend
to be the same across platforms, such as -c and -P .
But others are trickier and can lead to big porting problems. Here are
some of the frequently hit options "gotchas":
- Link with the right command. Some existing makefiles
from other platforms will link C or C++ programs with
ld
or link C++ programs with cc . Neither of these will work
on UnixWare 7! Always use cc to link C programs and CC
to link C++ programs (if the program is a mixture, use CC ).
The same is true if you are linking dynamic libraries (.so
files). In this latter case, also use the -G -KPIC options
to indicate you want a dynamic library with relocatable code. Using
-s that some other systems use for this will only get
you a stripped library that has no debug information!
The reason using the right command is so important is that for both
C and C++, more is done during the link step than just the link. For
C, which now uses DWARF II for debug information, debug abbreviation
tables are brought in. For C++, template instantiation prelinking
is done, static constructor and destructor sequences are anchored,
and so forth. Pretty much the only time that using ld
directly is appropriate is when doing ld -r to create
a conglomeration of object files.
- Don't add
-lc or -lC to the link
command line. You see this in existing makefiles sometimes.
But in UnixWare 7, the order that key system libraries are linked in
is critical. The cc and CC commands know how
to set up this order; mentioning these libraries explicitly can easily
mess that up. So the compiler will warn you if you do this.
- Use
-Kthread or -Kpthread
instead of -lthread .
If you are building a multithreaded application, you'll frequently see
makefiles that pass -D_REENTRANT , -D__THREADSAFE ,
or some other such symbol in to compiles, and then link with -lthread .
On UnixWare 7, both C and C++ use a -Kthread (for
UI/SVR4 threads) or -Kpthread (for POSIX threads)
option at
preprocessing, compile, and link time to indicate that multithreading
is active. Because of the system library ordering problem discussed
above, using -lthread can lead to disaster, and again
the compiler will warn if you try to do it.
If you are using GCC on UnixWare 7, a similar consideration exists;
the option to use is
-pthread rather than -lthread .
- Do dynamic links, not static. Applications work best
on UnixWare 7 when they are dynamically linked against system libraries,
not statically linked. Sometimes people think static links are safer
because "you know what version of the library you are using no matter
what version of the system you are on". In fact the reverse is true:
dynamic links are safer because you'll be using the version of a
library that's right for that revision of the operating system,
rather than a possible mismatch coming from a statically-bound executable.
Indeed, there are no static versions of some system libraries, such
as
libsocket and libnsl , for just this reason.
If you do end up linking statically, the option to use is -dn ,
not -Bstatic . The latter might have the desired effect
on some other platforms, but its meaning in UnixWare 7 is different
(it toggles the way the linker looks for libraries) and using it improperly
will usually cause a runtime failure.
- Watch out for assumptions about symbol exporting.
On some other UNIX platforms, notably Solaris, the linker exports
all global symbols defined in an executable. That means that shared
libraries will see those symbols, even if the libraries are
dlopen 'd
during runtime. In UnixWare 7, the linker only exports those symbols
defined in an executable that are referenced in the shared libraries
seen during the link. That means that if a library is dlopen 'd
during runtime, it won't see any other symbols that may have been defined
in the executable. If these symbols are needed, they should be explicitly
exported by passing in the -Wl,-Bexport=name option
to the linker. Simply passing -Wl,-Bexport will cause the
linker to match the Solaris default behavior.
- Libraries for graphical apps. For UnixWare 7, there
is a fairly long list of libraries that must be named to link a Motif
application:
-lXm -lXt -lXext -lX11 -lSM -lICE -lsocket -lnsl
Most makefiles coming from other platforms do not have all these libraries
named, and without them you'll usually get unresolved symbols.
- Libraries for networking apps. For UnixWare 7, the
order that the networking libraries are specified in matters; it should
be done like this:
[ -lresolv ] -lsocket -lnsl
That is, libresolv need not be present, but if it is it
should go first; the other two always need to be there, in the order
shown.
API issues
In general, UnixWare 7 is POSIX, XPG4, and UNIX 95 compliant, with most
but not quite all of UNIX 98 as well. If the application being ported is
compliant to these standards as well, a simple recompile and relink should
be all that is needed.
Of course, in reality things are rarely that easy.
The same UNIX function can sometimes be in different headers and/or
libraries from one platform to another. The quickest way to find out where
a function is on UnixWare 7 is usually to use the man command:
$ man recv
recv(3sock)
recv -- receive a message from a socket
Synopsis
cc [options] file -lsocket -lnsl
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int socket, void *buf, size_t len, int flags);
...
The top of the man page tells you the headers to use in the source code,
and the libraries to link against in the makefile.
In UnixWare 7, both UI/SVR4 threads and POSIX threads are supported,
so multithreading support should not be a problem in porting. Even if
the application comes from Windows NT, there is a fairly straightforward
mapping of the NT threads calls onto UI/SVR4 or POSIX ones.
Another problem that sometimes comes up is applications that need use
of libucb . This library is present on UnixWare 7, but the
best advice is first to try building without it -- many of the functions
that people think are in there, are now in the regular UnixWare 7 libraries as well.
Graphical programs sometime rely on Motif 2.x. This version of Motif
is normally compatible with Motif 1.2, which is what is shipped with
UnixWare 7.
In case problems do occur, you can try using Lesstif (available from
http://www.lesstif.org).
A lot of graphical GNU programs use the xpm package, support for which
is not provided by UnixWare 7. The best thing to do is download
the xpm source and build it.
C language dialect issues
Generally the UDK C compiler can handle any ISO-standard C code that it
is given. In default mode, this also includes accepting K&R C dialect but
giving it ISO semantics where there is a difference.
By using the cc -Xt transition compilation option,
you can give those cases K&R semantics; the most frequent motivation for
this is preserving "unsigned-preserving" integral promotion rules versus
the standard's "value-preserving" integral promotion rules. But don't
use cc -Xt unless you have to; it's better to bring code
up to date, and using it can sometimes result in a significant performance
loss (due to the lack of the volatile attribute).
Another porting problem occurs for code that assumes a particular signedness
of "plain" char . By default, the UDK compiler takes them
as signed, but the -Kuchar option switches them to unsigned.
Note also that the UDK C compiler fully supports 64-bit integers via
the long long type.
The most frequent source of C dialect problems comes from gcc-built
applications. The GNU C compiler has a number of extensions
in it that aren't supported on other platforms. The most well-known of
these is support for variable-sized arrays:
int length(void);
int foo(void)
{
int x=length();
char f[x];
...
}
This feature is present in a somewhat different form in
the ANSI/ISO 1999 C standard, but is not supported in the
UDK C compiler. So instead
you just have to rewrite the code using heap allocation. An
existing program that uses this feature a lot is a good example of where
you are probably better off porting with the GNU compilation tools
rather than the UDK.
Another change brought about by the C 99 standard is that
inline and restrict are now C keywords.
If this conflicts with existing code and it is difficult or impossible
to modify the code, the UDK C compiler -Xb
option may be used to
suppress the recognition of these two new keywords.
C++ language dialect issues
The ANSI/ISO 1998 C++ standard invalidated a lot of old,
existing C++ code. It is beyond the scope of this paper to try to list
all such areas, but here are a few of the better-known ones:
- new keywords
- for loop variable scope change
- template specialization syntax, guiding declarations
- template friends syntax change
-
new failure throws exception
- implicit
int gone
A more detailed discussion of some of these incompatibilities can be found
in newer C++ textbooks, such as Bjarne Stroustrup's The C++ Programming
Language (Third Edition), or in the Internet McCluskey
C++ Newsletter.
So the question arises, how do you get existing C++ code bases to compile
using the UDK compiler?
For many years the original AT&T cfront was the most heavily
used C++ compiler. For existing cfront-built code, there is an UDK
CC -Xo compatibility option
that provides cfront source compatibility.
But this also enables cfront bug tolerance, ancient anachronisms, and
the like. It also isn't perfect -- there are some cfront-isms that it won't
detect or accept. So it's only intended as a transitional tool
to help you in converting your code in stages to the modern dialect of
C++ that will be accepted by the UDK C++ compiler. It should not be used
on a permanent basis; the only realistic way to deal with the evolving
C++ language is to change your code!
Here's an example. Look at the following code:
src.C:
template <class T>
class A {
int m, n;
int explicit;
public:
void f();
T g();
};
template <class T>
void A<T>::f() {
for (int i = 0; i < 10; i++)
n += i;
for (i = 0; i < 10; i++)
m += i;
}
char A<char>::g() { return 'a'; }
int main() {
A<int> a;
a.f();
}
It compiles under a cfront-era C++ compiler:
cfront> CC src.C
cfront>
But it gets all sorts of diagnostic messages under the UDK C++ compiler:
$ CC src.C
"src.C", line 4: error: "explicit" is not allowed
int explicit;
^
"src.C", line 4: error: declaration does not declare anything
int explicit;
^
"src.C", line 4: error: "explicit" is not allowed
int explicit;
^
detected during instantiation of class "A<char>" at line 18
"src.C", line 4: error: declaration does not declare anything
int explicit;
^
detected during instantiation of class "A<char>" at line 18
"src.C", line 18: error: specializing function "A<char>::g" requires
"template<> " syntax char A<char>::g() { return 'a'; }
^
"src.C", line 4: error: "explicit" is not allowed
int explicit;
^
detected during instantiation of class "A<int>" at line 21
"src.C", line 4: error: declaration does not declare anything
int explicit;
^
detected during instantiation of class "A<int>" at line 21
"src.C", line 14: error: identifier "i" is undefined
for (i = 0; i < 10; i++)
^
detected during instantiation of "void A<int>::f()"
This volume of errors might cause a porting engineer's heart to sink!
But the source can quickly be compiled under the UDK C++ compiler by
using the -Xo option described previously:
$ CC -Xo src.C
$
so that it doesn't become a blocking factor in the port.
More importantly, most ANSI/ISO source language incompatibilities are
not that complicated and can be straightforwardly resolved. This means
that it's generally best to just change the source and get the code up
to date. For instance, in this case it's simply a matter of avoiding the
new keyword, using the new template specialization syntax, and adjusting
to the new for loop variable scope rule. Here is the new version with
three simple edits:
src2.C:
template <class T>
class A {
int m, n;
int is_explicit; // changed
public:
void f();
T g();
};
template <class T>
void A<T>::f() {
for (int i = 0; i < 10; i++)
n += i;
for (int i = 0; i < 10; i++) // changed
m += i;
}
template<> char A<char>::g() { return 'a'; } // changed
int main() {
A<int> a;
a.f();
}
compiles cleanly on the UDK C++ compiler without using any compatibility
option:
$ CC src2.C
$
Java platform-specific issues
Most people naturally think of Java programs as needing little or no porting
effort, and indeed this is true. However there are a few areas that are
platform-dependent and worth mentioning.
Threads model. The Java language and platform supports
multi-threaded execution of programs as an essential and built-in part
of the Java language and core library specifications. However, every Java
implementation must decide how to implement Java threads internally. SCO's
Java implementation supports two alternate internal threads models:
- Green threads refers to a model where the Java virtual machine
itself creates, manages, and context switches all Java threads in user
space and within one operating system process. No operating system threads
library is used.
- Native threads refers to a model where the Java virtual
machine creates and manages Java threads using the operating system
threads library -- named
libthread.so on UnixWare 7 -- and
each Java thread is mapped to one threads library thread. These libthread
threads are then multiplexed onto UnixWare 7 kernel light-weight processes.
Native threads give much better performance on multi-processor machines,
and avoid a lot of problems green threads have when JNI native code is
being used.
The model is controlled by setting the THREADS_FLAG environment
variable to either green or native (on
UnixWare 7, native threads is the default).
Switching the threads model may change the behavior of the Java application.
The Java language specification does not give a lot of precise details
about how Java threads are scheduled, so there is some room for implementation
dependencies in this area (unlike the rest of the Java specification).
Java applications that (incorrectly) make assumptions about how threads
will be scheduled may work under one threads model but not under the other.
Most early Java applications were written under green
threads (that was the first model available on most platforms, including
SCO); since then, native threads has been used more.
Popup trigger. The definition of what mouse action
constitutes a trigger for a popup menu is platform-defined. Many platforms,
including Windows NT, define it as a mouse release, while most UNIX platforms,
including Java on UnixWare 7, define it as a mouse press.
The following simple program illustrates this:
import java.awt.*;
import java.applet.Applet;
import java.awt.event.*;
public class Popup extends Applet {
private java.awt.List list = null;
private java.awt.PopupMenu popup = null;
public Popup() {
list = new java.awt.List();
list.addMouseListener( new MouseAdapter()
{
/* commented out!
public void mousePressed(MouseEvent e)
{
System.out.println("Mouse pressed..." + e);
if(e.isPopupTrigger())
{
System.out.println("Popup now a!");
popup.show(e.getComponent(), e.getX(), e.getY());
}
}
*/
public void mouseReleased(MouseEvent e)
{
System.out.println("Mouse released..." + e);
if(e.isPopupTrigger())
{
System.out.println("Popup now b!");
popup.show(e.getComponent(), e.getX(), e.getY());
}
}
}
);
list.add(createPopup());
add(list);
this.setSize(388,388);
this.validate();
this.setVisible(true);
this.repaint();
}
public PopupMenu createPopup() {
String[] faces = new String[] {"Save","Load","Print"};
String[] actions = new String[] {"save","load","print"};
popup = new PopupMenu();
for (int i=0; i < faces.length; i++)
{
MenuItem pmi = new MenuItem(faces[i]);
pmi.setActionCommand(actions[i]);
pmi.addActionListener ( new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
System.out.println(e.getActionCommand());
}
}
);
popup.add(pmi);
}
return popup;
}
public static void main(java.lang.String[] args) {
Popup p = new Popup();
Frame base = new Frame("Simon's Console");
p.init();
base.add(p);
base.setSize(388,388);
base.setVisible(true);
p.start();
FileDialog fd = new FileDialog(base);
fd.show();
}
}
To use the program, when the window comes up, close the initial popup
and then right click on the square (blank) in the remaining (main) window
to bring up the popup.
On Windows NT this works, but on UnixWare 7 you do not get a popup after
the right click. If however you uncomment the code for mousePressed ,
it will work for both platforms.
Shortcut key. A similar kind of problem exists for
what the "shortcut key" is that allows menu manipulation from the keyboard.
On UnixWare 7 is it the ctrl key, possibly modified by shift.
On other platforms (e.g. Windows) it may be different.
For any platform, you can find out what key is being used by calling
the Toolkit.getMenuShortcutKeyMask() method. That way, your
application doesn't have to depend on magic knowledge of which platform
uses what. An example code snippet doing this might be:
int mask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
...
JMenuItem aboutItem = new JMenuItem( "About..." );
aboutItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, mask));
...
Binary debugging
truss
UnixWare 7 has an extremely useful command named truss that
will trace all of the system calls that an application makes.
Using truss does not require any recompilation or relinking
or visibility to the source code. The executable doesn't even need to
have a symbol table. An example that motivates its use is:
$ cat fstab.c
#include <stdio.h>
int main() {
FILE* fp = fopen("/etc/fstab", "r");
if (!fp)
fprintf(stderr, "*** cannot open file\n");
fclose(fp);
}
$ cc fstab.c
$ ./a.out
*** cannot open file
(The program fails because UnixWare 7 uses /etc/vfstab
not /etc/fstab .) The error message doesn't tell us which
file it can't open. But running the program through truss will:
$ truss ./a.out
execve("./a.out", 0x08047570, 0x08047578) argc = 1
open("/etc/fstab", O_RDONLY, 0666) Err#2 ENOENT
*** cannot open file
write(2, " * * * c a n n o t o".., 21) = 21
_exit(-1)
Every system call is shown, along with its arguments and the return code
it generates.
Obviously this is a trivial example, but in a large program (especially
one whose logic you are not that familiar with), truss can
often quickly narrow down the point of failure.
The truss tool can follow child processes as well as the
parent process by using truss -f . Moreover you can use it
to grab and release existing processes on the system.
Advanced Usage. Along with knowing symbolic names for
many manifest constants (the O_RDONLY above, for example),
truss also provides options to present further information
and others to further focus the trace. An explicit list of system calls
to trace (or not to trace) can be given to truss
with the -t option.
The -a and -e options respectively cause truss
to display all the passed arguments and the entire environment at an exec .
Another useful truss feature is that it knows structures
that cross the system call boundary. Using the -v option,
you can specify those system calls for which you want to see complete
structures displayed.
Finally, all of the data read and/or written on a file descriptor (instead
of the default first few bytes) can be displayed using the -r
and/or -w options.
$ truss -w2 ./a.out
execve("./a.out", 0x08047570, 0x08047578) argc = 1
open("/etc/fstab", O_RDONLY, 0666) Err#2 ENOENT
*** cannot open file
write(2, 0x08047C14, 21) = 21
* * * c a n n o t o p e n f i l e\n
_exit(-1)
If it wasn't already clear, this feature (as well as others) demonstrates
why you cannot run truss on privileged programs unless you
already have the necessary privileges.
Dynamic Library Tracing
A feature very similar to truss is provided for dynamic executables
by the runtime linker in UnixWare 7. But, first, a little background.
When a dynamic executable calls a function that (potentially) is defined
elsewhere -- outside of the calling shared library or executable -- a
call is actually made to a bit of code in the "procedure linkage table"
(PLT). This code then does an indirect branch to the target function.
But, at process startup (by default), all of the PLT entries will actually
end up branching to the runtime linker which will search the executable
and the current set of shared libraries for the actual definition of the
target function. Once found, the runtime linker modifies the destination
of the indirect branch for that PLT entry so that from then on it jumps
to the actual function.
Dynamic library tracing takes advantage of this lookup process and
interposes a reporting function between the caller and the callee.
It can also insert a bit of code to return to that comes between the callee
and the caller that similarly reports the function's return value.
Just like with truss , dynamic library tracing is disabled
for processes that gain privilege for obvious security reasons.
This tracing differs from truss in a number of ways:
- Since each function called can call further functions, the call and
return are displayed separately.
- The runtime linker only knows a name to search for and, when found,
its address. It just assumes that it's a function, for example. It does
not inherently understand anything else about a function in contrast
to
truss 's knowledge of each system call.
However, if the defining library provides a special table of encoded
bits with an entry for this function, the reporting code will be able
to do something other than just print a few hexadecimal words that might
be actual arguments. Only the C and threads libraries provide this
table.
Note that if there's an entry found, the reporting code can cause the
process to die if it attempts to dereference a bad string pointer.
- The runtime linker can only report functions that go through its lookup
processing. Thus all internal function calls are not reported.
- The reporting code also displays the caller of each function.
Dynamic library tracing is enabled by running a process with certain
environment variables set:
Tracing the same fstab.c example from above:
$ LD_TRACE=sym ./a.out
atexit(0x80483b4) from 0x8048459
atexit(0xbff9b09c) from 0x8048465
atexit(0x8048514) from 0x804846f
__fpstart() from 0x804847b
fopen("/etc/fstab","r") from 0x80484b1
_findiop() from _realfopen+7
_open("/etc/fstab",0x0,0x1b6) from endopen+141
__thr_errno() from _cerror+29
fprintf(0x80495ec,"*** cannot open file"...,0x0,0x8047cb8,0x8048485) from 0x80484cc
_findbuf(0x80495ec) from fprintf+116
_xflsbuf(0x8047c44) from _idoprnt+358
_write(2,0x8047c04,21) from _xflsbuf+89
*** cannot open file
fclose(0x0) from 0x80484d7
exit(-1) from 0x804848e
_exithandle() from exit+18
_setjmp(0xbfffe628) from _exithandle+100
_cleanup() from _exithandle+151
fflush(0x0) from _cleanup+9
Note that even with symbols enabled, the reporting of some addresses
(like the caller of fopen ) is still just a hexadecimal value.
This is because the only names available to the reporting code are those
exported from the executable and shared libraries, and by default only
the necessary few global names are exported by the linker in the executable.
If you use pass the -Wl,-Bexport option to the linker (see
above regarding exported names) all global functions will instead be exported.
Adding return value tracing:
$ LD_TRACE=sym,ret ./a.out
atexit(0x80483b4) from 0x8048459
=> atexit returned 0
atexit(0xbff9b09c) from 0x8048465
=> atexit returned 0
atexit(0x8048514) from 0x804846f
=> atexit returned 0
__fpstart() from 0x804847b
=> __fpstart returned
fopen("/etc/fstab","r") from 0x80484b1
_findiop() from _realfopen+7
=> _findiop returned 0x80496d0
_open("/etc/fstab",0x0,0x1b6) from endopen+141
__thr_errno() from _cerror+29
=> __thr_errno returned 0xbfffedd0
=> _open returned -1
=> fopen returned 0x0
fprintf(0x80495ec,"*** cannot open file"...,0x0,0x8047cb8,0x8048485) from 0x80484cc
_findbuf(0x80495ec) from fprintf+116
=> _findbuf returned 0x80495ec
_xflsbuf(0x8047c44) from _idoprnt+358
_write(2,0x8047c04,21) from _xflsbuf+89
*** cannot open file
=> _write returned 21
=> _xflsbuf returned 0
=> fprintf returned 21
fclose(0x0) from 0x80484d7
=> fclose returned -1
exit(-1) from 0x804848e
_exithandle() from exit+18
_setjmp(0xbfffe628) from _exithandle+100
_cleanup() from _exithandle+151
fflush(0x0) from _cleanup+9
=> fflush returned 0
=> _cleanup returned
=> _exithandle returned
Note in the above example that the return from setjmp is
not displayed. Certain special functions like setjmp are
sensitive to their return address.
Finally, a stack trace for write gets:
$ LD_TRACE=sym LD_TRACE_STACK=_write ./a.out
[0] _write(2,0x8047bec,21)
[1] _xflsbuf+89(0x8047c2c) [0xbffa7b05]
[2] _idoprnt+358(0x8047c2c,0x80484f8,0x8047c88) [0xbffa1eb6]
[3] fprintf+201(0x80495ec,"*** cannot open file"...,0x0,0x8047ca0,0x8048485) [0xbffa8195]
[4] 0x80484cc(0x1,0x8047cac,0x8047cb4)
[5] 0x80484cc(0x8047cac,0x8047cb4,0x0)
*** cannot open file
Memory debugging
C and to some degree C++ are infamous for presenting difficult-to-debug
memory allocation/corruption problems. These can sometimes show up during
ports of "working" code because you may happen to get away with a malloc/free
mistake on one platform but get caught on another one. Furthermore such
problems can go undetected in code for some time before turning up.
Commercial tools such as Purify are a great help in finding and tracking
down such problems. The UnixWare 7 Release 7.1.3 UDK has a similar tool
created by SCO, called memtool. Previous releases
of UnixWare 7 had an earlier facility known as
MALLOC_CHECKS.
memtool
Take this simple error-ridden program:
1 #include <stdlib.h>
2 #include <stdio.h>
3 #include <string.h>
4
5 int main() {
6 char *p = malloc(7), *q = "yes, too long\n";
7 strcpy(p, q);
8 free(p);
9 q = 0;
10 *p = 'Y';
11 realloc(p, 3);
12 fputs(p, stderr);
13 return *q;
14 }
Most of the time when you run it, the program will "work" just fine:
$ cc error.c
$ ./a.out
Yes, too long
But in fact bugs are there lurking. So how would we find out about them?
Take the same program, optionally recompile it with -g for maximum
symbolic information, and then run the executable using memtool(1).
The output that memtool will produce is shown here:
$ cc -g error.c
$ ./memtool ./a.out
==============================================================================
Some abuses or potential misuses of the dynamic memory allocation subsystem
have been detected. The following multiline diagnostics are descriptions
of what exactly has been seen. They may include use of terms and concepts
with which you may be unfamiliar. Use the "-v" option to get the complete
version of this introduction.
==============================================================================
A block's spare bytes have been modified. This usually occurs due to
writing beyond the block's regular bytes, either because an insufficient
number of bytes were requested when the block was allocated or simply
due to a programming logic error.
History for block at address 0x8049b70:
*Stack trace when detected:
[0] free(ptr=0x8049b70)
[1] main(0x1,0x8047a3c,0x8047a44) [error.c@8] in ./a.out
[2] _start() [0x80485f4] in ./a.out
*Stack trace when block at 0x8049b70 was allocated:
[0] malloc(sz=7)
[1] main() [error.c@6] in ./a.out
[2] _start(presumed:0x1,0x8047a3c,0x8047a44) [0x80485f9] in ./a.out
Annotated snapshot surrounding the live allocation at address 0x8049b70
when the 5 bytes [0x8049b77,0x8049b7b] were found to have been modified.
This allocation holds 7 bytes followed by 5 extra (or spare) bytes,
and, in this case, spare bytes were found to have been modified.
0x8049b60: 0x00000000 0x00000000 0x00000000 0x00000011 ................
: ******** ****
0x8049b70: 0x2c736579 0x6f6f7420 0x6e6f6c20 0x00000a67 yes, too long...
: -------- ^^------ ^^^^^^^^ -------^^^^^
==============================================================================
A block's header has been modified. Often this occurs due to mistakenly
writing past the end of the preceding block. You might also try using the
"-x" option to add spare bytes to the end of each block, and see whether
your application behaves differently.
History for block at address 0x8049b80:
*Stack trace when detected:
[0] free(ptr=0x8049b70)
[1] main(0x1,0x8047a3c,0x8047a44) [error.c@8] in ./a.out
[2] _start() [0x80485f4] in ./a.out
Annotated snapshot surrounding the live allocation at address 0x8049b80
when the 4 bytes [0x8049b7c,0x8049b7f] were found to have been modified.
This allocation holds 4 bytes, but, in this case, the allocation's
header was found to have been modified.
0x8049b70: 0x2c736579 0xca6f7420 0xcacacaca 0x00000a67 yes, to.....g...
: ^^^^^^^^ ^^^^
0x8049b80: 0x00000000 0x00000015 0xcacacaca 0xcacacaca ................
: -------- ----
==============================================================================
A recently free()d block was passed as the first argument to realloc().
Only null pointers or live block addresses are permitted to be passed to
realloc(), although, in this implementation, were dynamic memory checking
not enabled, this block's contents would have been preserved between its
being freed and this call to realloc(), but this is a nonportable feature
of this implementation which should not be relied on.
History for block at address 0x8049b70:
*Stack trace when detected:
[0] realloc(ptr=0x8049b70,sz=3)
[1] main(0x1,0x8047a3c,0x8047a44) [error.c@11] in ./a.out
[2] _start() [0x80485f4] in ./a.out
*Stack trace when block at 0x8049b70 was released:
[0] free(ptr=0x8049b70)
[1] main() [error.c@8] in ./a.out
[2] _start(presumed:0x1,0x8047a3c,0x8047a44) [0x80485f9] in ./a.out
*Stack trace when block at 0x8049b70 was allocated:
[0] malloc(sz=7)
[1] main() [error.c@6] in ./a.out
[2] _start(presumed:0x1,0x8047a3c,0x8047a44) [0x80485f9] in ./a.out
ÅÅÅÅÅÅÅÅÅÅÅÅy==============================================================================
A free()d block has been modified. This usually means that the block was
passed to free() or realloc(), but the block continued to be used by your
application, possibly far removed from the deallocating code.
History for block at address 0x8049b70:
*Stack trace when detected:
[0] realloc(ptr=0x8049b70,sz=3)
[1] main(0x1,0x8047a3c,0x8047a44) [error.c@11] in ./a.out
[2] _start() [0x80485f4] in ./a.out
*Stack trace when block at 0x8049b70 was released:
[0] free(ptr=0x8049b70)
[1] main() [error.c@8] in ./a.out
[2] _start(presumed:0x1,0x8047a3c,0x8047a44) [0x80485f9] in ./a.out
*Stack trace when block at 0x8049b70 was allocated:
[0] malloc(sz=7)
[1] main() [error.c@6] in ./a.out
[2] _start(presumed:0x1,0x8047a3c,0x8047a44) [0x80485f9] in ./a.out
Annotated snapshot surrounding the free allocation at address 0x8049b70
when the byte at 0x8049b70 was found to have been modified. This
allocation holds 12 bytes, one of which was found to have been modified.
0x8049b60: 0x00000000 0x00000000 0x00000000 0x00000011 ................
: ******** ****
0x8049b70: 0xcacaca59 0xcacacaca 0xcacacaca 0x00000179 Y...........y...
: ------^^ -------- -------- ^-----------
==============================================================================
LIVE ALLOCATIONS AT PROCESS EXIT
==============================================================================
Memory allocations that have not been released by the time your process is
finished are in no way an error, but they are potentially ``leaks'' --
allocations that inadvertently have not been released once they were no
longer needed or in use. If your application has more than a few live
allocations displayed below with the same size and/or were allocated at the
same location, you may well have a leak; or if your application, when run
with more data or for longer periods, displays more live allocations here,
you also may have a leak. A leak also need not be repaired: A short-lived
process can easily survive having many leaks, as long as they are not too
large and there are not so many that available memory resources could become
exhausted (or even strained). Moreover, it may well be that ``the leaks''
are allocations that were all in use up to just before the process exits,
and the effort and the expense to release them all is not warranted.
Stack trace for 4 byte block at 0x8049cf8:
[0] realloc(ptr=0x8049b70,sz=3)
[1] main() [error.c@11] in ./a.out
[2] _start(presumed:0x1,0x8047a3c,0x8047a44) [0x80485f9] in ./a.out
*Stack trace for block previously at 0x8049b70 before realloc() to 0x8049cf8:
[0] free(ptr=0x8049b70)
[1] main() [error.c@8] in ./a.out
[2] _start(presumed:0x1,0x8047a3c,0x8047a44) [0x80485f9] in ./a.out
As you can see, memtool will flag the spots in your application where
programming errors created memory-allocation-related bugs. This will
save you a lot of time and effort!
MALLOC_CHECKS
Versions of UnixWare 7 before UnixWare 7 Release 7.1.3 do not have
memtool. But they (and UnixWare 7 Release 7.1.3)
do have an predecessor facility that
works by defining the environment variable MALLOC_CHECKS
to a given level of checking,
then running the program. Full details are in the malloc(3C) man page,
but an example of using it on the example program in the previous section
would be:
$ MALLOC_CHECKS=3 ./a.out
alloc error (0x8049ad0/owner @0x80485b9):
spare block space was modified [for ./a.out]
alloc error (0x8049ae0):
block header was modified [for ./a.out]
alloc error (0x8049ad0/owner @0x80485b9):
free()d block was modified [for ./a.out]
alloc error (0x8049ad0/owner @0x80485b9):
realloc() of just free()d block [for ./a.out]
alloc error (0x8049ad0/owner @0x80485b9):
free()d block was modified [for ./a.out]
ÊÊÊÊÊÊÊÊÊÊÊÊy$
Not only has the bad memory usage been discovered, but when used in
conjunction with the debugger, you can pinpoint the source of the
error.
In the above diagnostics, the first is detected at the free(p)
(line 8) and is complaining about what the strcpy(p, q) call
did. The arena is then checked and the next two diagnostic lines are written
based on what is found during that sweep. The block allocated on line
6 has 7 bytes allocated and 5 spare bytes (for alignment purposes). The
strcpy() call spilled over into the spare bytes and then
into the header of the next block. The last two complaints occur at the
realloc(p, 3) (line 11).
The ÊÊÊÊÊÊÊÊÊÊÊÊy$ printed by the program is the
result of the checking code having filled the deallocated block's bytes
with 0xCA (or '\312' ). The character Ê
is what this byte value turns into.
There is also a MALLOC_STATS environment variable which
lets you get a trace of all malloc/free/etc. calls and allocation amounts.
A limitation on UnixWare 7 Release 7.1.1 and before, is that
if you combine LD_TRACE=ret with MALLOC_CHECKS
or MALLOC_STATS , the block owner (return) addresses are bogus
since they are the values constructed by the dynamic library tracing code.
In UnixWare 7, read accesses to page zero (null pointer reads) can
be disabled through MALLOC_CHECKS . This differs from nullptr(1)
which affects all processes on a per-user basis in that it only affects
those processes that have an exported MALLOC_CHECKS and that
do some memory allocation. This feature is enabled
by specifying "null " in the MALLOC_CHECKS
argument string (in some earlier releases, it was done by setting
the MALLOC_CHECKS
string to a start with a space;
see the relevant malloc(3C) man page
for details).
Note that the return from main() in our
example involves a null pointer dereference.
$ MALLOC_CHECKS=' ' ./a.out
Yes, too long
Memory fault(coredump)
MALLOC_CHECKS also provides access to
a "memory fault" style allocation model through negative values. Unlike
the "regular" implementation which attempts to use space efficiently,
this approach allocates at least one page per allocation and arranges
the address returned so that one end of the space is on the edge of the
page. The intent is that a typical misuse of allocated memory will result
in an immediate SIGSEGV (memory fault).
Again, see malloc(3C) for more details, but here's what happens with
our broken code using this feature. Note that we will run the program
under the control of the debugger so that we can catch the SIGSEGV right
away.
$ debug -ic
debug> set $MALLOC_CHECKS="-1"
debug> export $MALLOC_CHECKS
debug> create ./a.out
New program t000 (process p1) created
HALTED p1 [main in error.c]
6: char *p = malloc(7), *q = "yes, too long\n";
debug> r
SIGNALED 11 (segv) in p1 [_rtstrcpy]
0xbffa1d9f (_rtstrcpy+31:) repz movsl (%esi), (%edi)
debug> t
Stack Trace for p1, Program a.out
*[0] _rtstrcpy(0xbff7fff9, 0x80496c0) [0xbffa1d9f]
[1] main(0x1, 0x8047c34, 0x8047c3c) [error.c@7]
[2] _start() [0x8048558]
debug> q
Source debugging
The UDK debugger, command name debug , is a powerful and effective
tool for source and assembly-level debugging.
The debugger supports both C and C++ language debugging. (For Java debugging,
use the command-line jdb
debugger.)
The debugger has two basic user interfaces: command line and graphical.
The UDK debugger is not related to either dbx or gdb,
two debuggers many people are familiar with, and uses a different command
syntax. However there is a "Guide to debug for dbx users" that is part
of SCOhelp, that can help in gaining familiarity with the debugger.
Debugging done doing porting of applications can be different in nature
from that done during program development. Some debugger features that
may be useful in this regard are:
- The
map command is excellent at showing the layout of
memory, and where any arbitrary address might be.
- Multithreaded debugging and multiprocess debugging support is strong
in the debugger. Many commands take a
-p process-nbr.thread-nbr
argument that indicates which process or thread they apply to, with
-p all meaning the action applies to all processes or threads.
- Another strong point is that you can follow program forks into both
the parent and child processes; some other debuggers only allow you
to follow into one or the other.
- Finally, the debugger allows you to grab existing processes (for
example, that are hung in loops) or to debug after-the-fact against
core dumps.
Documentation
UnixWare 7 documentation is always easy to get. It's bundled into the SCOhelp
(Netscape) browser on the desktop; just click on the SCOhelp button. Traditional
command-line man pages are also available.
Furthermore, the latest and greatest versions of the UnixWare 7 documentation
set are always available on our web site at
http://www.sco.com/support/docs/.
|