SCO Developers Network












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":

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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:

  • LD_TRACE

    Without LD_TRACE in the environment, no reporting will occur. If it exists but has an empty string value, lines of the form

    sym(arg1,arg2,arg3) from hex_addr
    are displayed. For unknown functions, three hexadecimal words are printed as arguments. Otherwise, the value of LD_TRACE is expected to be one or more comma-separated keywords from the following list:
    • sym -- display name+offset instead of hex_addr. The +offset part is only shown when nonzero.
    • lib -- add @lib after a hex_addr or name+offset to give the containing library (or executable).
    • ret -- also include tracing function returns.
    • tim -- add a at sec.usec.
    • hitim -- add either a at ticks or, when LD_TRACE_SCALE is set, at sec.ticks. Note that the high-resolution timer requires the PentiumTM rdtsc instruction. Moreover, using this instruction is a privileged operation by default. To enable this feature in UnixWare 7, you must set USER_RDTSC to nonzero using idtune and rebuild and reboot. Finally, since we do not synchronize the processor timers on multiple CPU boxes, the timings are likely to be inaccurate for such systems.
  • LD_TRACE_FILENO -- provides the file descriptor to which to write the reports. It is 2 (standard error) by default.
  • LD_TRACE_ARGNO -- provides the number of hexadecimal words to display for unknown functions. It is 3 by default.
  • LD_TRACE_SCALE -- provides the clock speed in MHZ to be used with hitim.
  • LD_TRACE_STACK -- provides a simple stack trace feature. Its value is expected to be a comma-separated list of function names for which to display a stack trace at each call. All other reporting is disabled when LD_TRACE_STACK is set. The stack trace algorithm uses a fairly straightforward scheme which, unfortunately, fails for functions that return structures.
  • LD_TRACE_FRAMES -- provides a maximum stack depth to trace when LD_TRACE_STACK is set. The default value is zero which is taken to mean the entire stack.
  • LD_TRACE_ROUTINE and LD_TRACE_LIBRARY -- provide comma-separated lists of specific routine names and library names, respectively, to report. Both of these interpret a leading "!" as inverting the list (all but ...). Note that LD_TRACE_STACK wins over LD_TRACE_ROUTINE. These two variables can be used to greatly trim down the large amount of noise generated by default with LD_TRACE.

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/.