Tuesday, April 15, 2008

Openssh with AIX chroot

Sometimes you might want to restrict users to specific directories so that they are not able to look into the whole system. This can be achieved by creating the chroot users. This article describes how to set up an IBM® AIX® chroot environment and use it with ssh, sftp, and scp. You will also learn about the prerequisites for AIX and openssh, and how to configure and use a chroot environment. A downloadable sample shell script that automatically sets up this environment is also provided.

Introduction

IBM-supported versions of OpenSSH (openssh-4.5 onwards) has included the chroot feature. It looks for "." (dot) in the user's home directory and then the chroot () call changes the root directory of the user so that the directory before "." (dot) becomes the chrooted directory. This article helps you set up a chroot environment on AIX and use it with ssh, sftp, and scp.

We assume that the reader has basic AIX skills, so we do not explain general AIX tasks in detail like updating AIX, making logical volume, and the like. We concentrate on setting up the chroot environment and using it with IBM-supported openssh.

Prerequisites

The chroot feature is supported in the OpenSSH-4.5p1 version onwards. The filesets to be downloaded from sourceforge.net are openssh-4.5.0.5302(OpenSSH-4.5p2-r2) and later. AIX 5.3 (at minimum TL06 is required) and AIX 6.1 or higher are supported.

Openssh-4.5p1(openssh-4.5.0.5200) for AIX 5.2 also supports the chroot feature. The minimum AIX release is AIX 5200-10.

Download the latest level of openssh from SourceForge.net, and the latest openssl installp filesets from the IBM; see the Resources section for these downloads.

You need to register to this site for downloading the filesets.

Once you have all the filesets, you can install them with smitty install or using AIX NIM.

These are the filesets that get installed for openssh 4.5.0.5302 and openssl 0.9.8.4:

/home/chroot # lslpp -l | grep open
 openssh.base.client     4.5.0.5302  COMMITTED  Open Secure Shell Commands
 openssh.base.server     4.5.0.5302  COMMITTED  Open Secure Shell Server
 openssh.license         4.5.0.5302  COMMITTED  Open Secure Shell License
 openssh.man.en_US       4.5.0.5302  COMMITTED  Open Secure Shell
 openssh.msg.en_US       4.5.0.5302  COMMITTED  Open Secure Shell Messages -
 openssl.base               0.9.8.4  COMMITTED  Open Secure Socket Layer
 openssl.license            0.9.8.4  COMMITTED  Open Secure Socket License
 openssl.man.en_US          0.9.8.4  COMMITTED  Open Secure Socket Layer
  



 

Configuration of the chroot environment

To start with, you need to choose a chroot directory. We will choose /home/chroot as our chrooted directory.

#mkdir  -p /home/chroot

 

Inside /home/chroot, you need to create the necessary directories and subdirectories like dev, dev/pts, etc, usr, usr/bin, usr/sbin, usr/lib, and tmp.

#pwd
#/home/chroot
#mkdir –p dev/pts etc  usr/bin  usr/sbin  usr/lib/  tmp

 

So now we have following directories in the /home/chroot directory.

/home/chroot # ls -al
total 8
drwxr-xr-x   6 root     system          256 Feb 01 12:07 .
drwxr-xr-x  32 root     system         4096 Feb 01 12:06 ..
drwxr-xr-x   3 root     system          256 Feb 01 12:07 dev
drwxr-xr-x   2 root     system          256 Feb 01 12:07 etc
drwxr-xr-x   2 root     system          256 Feb 01 12:07 tmp
drwxr-xr-x   5 root     system          256 Feb 01 12:07 usr

/home/chroot # ls -al *
dev:
total 0
drwxr-xr-x   3 root     system          256 Feb 01 12:07 .
drwxr-xr-x   6 root     system          256 Feb 01 12:07 ..
drwxr-xr-x   2 root     system          256 Feb 01 12:07 pts

etc:
total 0
drwxr-xr-x   2 root     system          256 Feb 01 12:07 .
drwxr-xr-x   6 root     system          256 Feb 01 12:07 ..

tmp:
total 0
drwxr-xr-x   2 root     system          256 Feb 01 12:07 .
drwxr-xr-x   6 root     system          256 Feb 01 12:07 ..

usr:
total 0
drwxr-xr-x   5 root     system          256 Feb 01 12:07 .
drwxr-xr-x   6 root     system          256 Feb 01 12:07 ..
drwxr-xr-x   2 root     system          256 Feb 01 12:07 bin
drwxr-xr-x   3 root     system          256 Feb 01 12:07 lib
drwxr-xr-x   2 root     system          256 Feb 01 12:07 sbin

/home/chroot # ls -al usr/lib
total 0

drwxr-xr-x   3 root     system          256 Feb 01 12:07 .
drwxr-xr-x   5 root     system          256 Feb 01 12:07 ..

 

Copy binaries and libraries

Copy all of the binaries and the related libraries that are needed for the chroot environment. For ssh login, a shell is necessary (e.g., ksh); for scp the related binary and for sftp access the sftp-server binary are mandatory. In our example, we also chose the commands "cd," "pwd," "ls," "mkdir," "rmdir," "rm," and "cp" that should be allowed in our restricted shell.

The path information for the binaries can be seen with the "which" command and the related libraries can be seen with the "ldd" command. For example, to copy all the binaries and related libraries for the "ls" command, run the following:

# which ls | xargs ldd
/usr/bin/ls needs:
         /usr/lib/libc.a(shr.o)
         /unix
         /usr/lib/libcrypt.a(shr.o)

 

Copy these two libraries to the corresponding path of <chroot-dir> directories.

# cp /usr/lib/libc.a  /home/chroot/usr/lib/ 
# cp /usr/lib/libcrypt.a /home/chroot/usr/lib/

 

All binaries need /unix, as well. Check the /unix directory on the AIX system first:

/home/chroot # ls -al /unix
lrwxrwxrwx   1 root     system           21 Aug 10 2005  /unix -> /usr/lib/boot/unix_64

 

Then create the soft link for /unix as follows:

/home/chroot # ln -s /usr/lib/boot/unix_64 unix

 

Now we have these directories in the chroot directory:

/home/chroot # ls -al
total 8
drwxr-xr-x   6 root     system          256 Feb 01 13:11 .
drwxr-xr-x  32 root     system         4096 Feb 01 12:06 ..
drwxr-xr-x   3 root     system          256 Feb 01 12:07 dev
drwxr-xr-x   2 root     system          256 Feb 01 12:07 etc
drwxr-xr-x   2 root     system          256 Feb 01 12:07 tmp
lrwxrwxrwx   1 root     system           21 Feb 01 13:11 unix -> /usr/lib/boot/unix_64
drwxr-xr-x   5 root     system          256 Feb 01 12:07 usr

 

Similarly, copy all the desired binaries and the libraries needed into the corresponding chroot directory.

 

Create necessary devices

The next step is to create the necessary devices null, zero, tty, and pts/#. The devices in <chroot-dir>/dev should have the same “Major and Minor” and permissions as on the original AIX system. Check the values on the AIX system first, create the devices with "mknod," and assign proper permissions with "chmod" inside the chroot directory. For instance:

/home/chroot # ls –la /dev/tty
crw-rw-rw-   1 root     system        1,  0 Jan 30 13:54 /dev/tty

/home/chroot # ls –la /dev/null
crw-rw-rw-   1 root     system        2,  2 Feb 01 12:49 /dev/null

/home/chroot # ls –la /dev/zero
crw-rw-rw-   1 root     system        2,  3 Aug 10 2005  /dev/zero

 

Now create them in the chroot directory with the mknod command and assign the same permissions as on the original devices:

/home/chroot # mknod dev/tty c 1 0
/home/chroot # mknod dev/null c 2 2
/home/chroot # mknod dev/zero c 2 3

chmod 666 null tty zero

/home/chroot # ls -al dev
total 0
drwxr-xr-x   3 root     system          256 Feb 01 13:49 .
drwxr-xr-x   6 root     system          256 Feb 01 13:11 ..
crw-rw-rw-   1 root     system        2,  2 Feb 01 13:49 null
drwxr-xr-x   2 root     system          256 Feb 01 12:07 pts
crw-rw-rw-   1 root     system        1,  0 Feb 01 13:48 tty
crw-rw-rw-   1 root     system        2,  3 Feb 01 13:49 zero

 

Follow the same steps for pts devices. Normally, it is not necessary to have as many pts/# devices in the chroot as in the general AIX environment. On our test system we use 10 pts/# devices from 0 to 9. So based on the need, the pts devices can be created.

These are the pts devices that we have created for our chroot environment with same permissions as the original pts devices.

/home/chroot # chmod go+w /home/chroot/dev/pts/*

/home/chroot # ls -al /home/chroot/dev/pts/
total 16
drwxr-xr-x   2 root     system         4096 Feb 01 15:01 .
drwxr-xr-x   3 root     system         4096 Feb 01 15:00 ..
crw-rw-rw-   1 root     system       22,  0 Feb 01 15:01 0
crw-rw-rw-   1 root     system       22,  1 Feb 01 15:01 1
crw-rw-rw-   1 root     system       22,  2 Feb 01 15:01 2
crw-rw-rw-   1 root     system       22,  3 Feb 01 15:01 3
crw-rw-rw-   1 root     system       22,  4 Feb 01 15:01 4
crw-rw-rw-   1 root     system       22,  5 Feb 01 15:01 5
crw-rw-rw-   1 root     system       22,  6 Feb 01 15:01 6
crw-rw-rw-   1 root     system       22,  7 Feb 01 15:01 7
crw-rw-rw-   1 root     system       22,  8 Feb 01 15:01 8
crw-rw-rw-   1 root     system       22,  9 Feb 01 15:01 9

/home/chroot # chmod 620 /home/chroot/dev/pts/0
/home/chroot # chown root:security /home/chroot/dev/pts/0
/home/chroot # ls -al /home/chroot/dev/pts/0
crw--w----   1 root     security     22,  0 Feb 01 15:01 /home/chroot/dev/pts/0
/home/chroot # ls -al /dev/pts/0
crw--w----   1 root     security     22,  0 Feb 01 15:09 /dev/pts/0

 

Check chroot configuration

Now that the setup of the basic chroot environment has been finished, check the correct configuration with the chroot command:

/home/chroot # chroot /home/chroot /usr/bin/ksh
/ # ls
dev   etc   tmp   unix  usr
/ # scp -?
scp: illegal option -- ?
usage: scp [-1246BCpqrv] [-c cipher] [-F ssh_config] [-i identity_file]
           [-l limit] [-o ssh_option] [-P port] [-S program]
           [[user@]host1:]file1 [...] [[user@]host2:]file2
/ # cp -?
cp: illegal option -- ?
Usage: cp [-fhipHILPU] [-r|-R] [-E{force|ignore|warn}] [--] src target
   or: cp [-fhipHILPU] [-r|-R] [-E{force|ignore|warn}] [--] src1 ... srcN directory
/ # touch /tmp/test.out
/usr/bin/ksh: touch:  not found
/ # exit

 

Only those commands whose binaries and libraries have been copied can be executed (for example, "ls," "scp" and "cp"). To come out of chroot environment, use "exit."


 

Creating chroot user and finalizing installation

To access this chroot environment remotely using ssh, <chroot-user> has to be created. Normally, the user has a new home directory with the magic token, for example:

<chroot-dir>/./home/<chroot-user>
  

  

In our example, we create the user smile with home directory /home/chroot/./home/smile and /usr/bin/ksh as initial program:

/home/chroot # useradd -s /usr/bin/ksh -m -d
 /home/chroot/./home/smile/ -c "chroot test user" smile

/home/chroot # chown smile:staff /home/chroot/home/smile
/home/chroot # ls -al /home/chroot/home
total 0
drwxr-xr-x   3 root     system          256 Feb 01 18:15 .
drwxr-xr-x   7 root     system          256 Feb 01 18:15 ..
drwxr-xr-x   2 smile    staff           256 Feb 01 18:15 smile

 

Set the password for <chroot-user> and change it on the user shell:

/home/chroot # passwd smile
Changing password for "smile"
smile's New password:
Enter the new password again:
/home/chroot # su - smile
$ passwd
Changing password for "smile"
smile's Old password:
smile's New password:
Enter the new password again:
$ exit

 

Copy <chroot-user> entries from /etc/passwd and /etc/group to the related files in the chroot environment:

/home/chroot # cat /etc/passwd | grep smile >> /home/chroot/etc/passwd

/home/chroot # cat /etc/group | grep smile >> /home/chroot/etc/group

/home/chroot # cat /home/chroot/etc/passwd
smile:!:397:1:chroot test user:/home/chroot/./home/smile/:/usr/bin/ksh

/home/chroot # cat /home/chroot/etc/group
staff:!:1:ipsec,dasusr1,db2inst1,db2fenc1,idsldap,ldapdb2,ftp,anonymou,aroell,
ldap,ituam,ski,usrsftp,sshd,bm,smile

 

Now the chroot environment is complete and can be used with ssh, sftp, and scp, for example:

lp2:root:/root # sftp smile@lp5
Connecting to lp5...
smile@lp5's password:
sftp> ls
sftp> put smit.log
Uploading smit.log to /home/smile/smit.log
smit.log                                      100%  203KB 203.1KB/s   00:00
sftp> ls -al
drwxr-xr-x    2 smile    staff         256 Feb  1 18:32 .
drwxr-xr-x    3 0        0             256 Feb  1 18:15 ..
-rwxr-----    1 smile    staff         254 Feb  1 18:15 .profile
-rw-r--r--    1 smile    staff      207951 Feb  1 18:32 smit.log
sftp> quit

lp2:root:/root # ssh smile@lp5
smile@lp5's password:
Last login: Fri Feb  1 18:32:19 NFT 2008 on ssh from X.YYY.ZZZ.77
$ ls -al
total 424
drwxr-xr-x   2 smile    staff           256 Feb  1 18:33 .
drwxr-xr-x   3 0        0               256 Feb  1 18:15 ..
-rwxr-----   1 smile    staff           254 Feb  1 18:15 .profile
-rw-------   1 smile    staff            10 Feb  1 18:33 .sh_history
-rw-r--r--   1 smile    staff        207951 Feb  1 18:32 smit.log
$ cp smit.log test.out
$ rm smit.log
$ ls -al
total 432
drwxr-xr-x   2 smile    staff           256 Feb  1 18:33 .
drwxr-xr-x   3 0        0               256 Feb  1 18:15 ..
-rwxr-----   1 smile    staff           254 Feb  1 18:15 .profile
-rw-------   1 smile    staff            54 Feb  1 18:33 .sh_history
-rw-r--r--   1 smile    staff        207951 Feb  1 18:33 test.out
$ exit
Connection to lp5 closed.

lp2:root:/root # scp smile@lp5:/home/smile/test.out .
smile@lp5's password:
test.out                                      100%  203KB 203.1KB/s   00:00
lp2:root:/root # ls -al test.out
-rw-r--r--   1 root     system       207951 Feb 01 18:38 test.out




 

Chrooted user with different authentication methods

  • PAM Authentication: Copy the /usr/lib/security/pam_aix in the chrooted directed directory, for example:
    # cp /usr/lib/security/pam_aix    <chroot-dir>/usr/lib/security/
    

     
  • Public Key Authentication: Copy the public key file of the chrooted user in the path mentioned below:
    /home/<chroot-dir>/home/<chroot-user>/.ssh/authorized_keys
    

     

 

Shared library memory footprints on AIX 5L

Learn about shared library mechanisms and memory footprints on IBM® AIX®. This article is essential for developers writing server code or administrators managing production AIX systems. It offers developers and administrators commands and techniques, and gives the understanding necessary to analyze memory requirements of server processes on AIX. It also helps developers and administrators avoid resource shortages that can't be identified with other standard runtime analysis tools such as ps or topas. The article is intended for systems administrators or developers of native applications on AIX.

Introduction

This article examines how shared libraries occupy memory on 32-bit AIX 5L™ (5.3), demonstrating the following commands:

  • ps
  • svmon
  • slibclean
  • procldd
  • procmap
  • genkld
  • genld

The article discusses the virtual address space of processes, as well as the kernel shared-library segment, how to examine them, and how to interpret the output of the various diagnostic utilities mentioned above. The article also discusses how to diagnose situations where the kernel shared segment is full and possible approaches to resolving that situation.

In the examples throughout, we happen to use the processes from the software product Business Objects Enterprise Xir2®. This is arbitrary, as the concepts will apply to all processes running on AIX 5L.



 

Review

Just so we are all in the same mindframe, let's review a little on 32-bit architecture. In doing so, I'll resort to the employ of the most useful 'bc' command-line calculator.

In a 32-bit processor, the registers are capable of holding 2^32 possible values,

 $ bc
 2^32
 4294967296
 obase=16
 2^32
 100000000


 

That is a 4 gigabyte range. This means a program running on the system is able to access any function or data address in the range of 0 and 2^32 - 1.

 $ bc
  2^32 - 1 
 FFFFFFFF
 obase=10
 2^32 - 1 
 4294967295


 

Now, as you know, any operating system has potentially hundreds of programs running at the same time. Even though each one of them is capable of accessing a 4GB range of memory, it doesn't mean that they each get their own 4GB allotment of physical RAM. That would be impractical. Rather, the OS implements a sophisticated scheme of swapping code and data between a moderate amount of physical RAM and areas of the file system designated as swap (or paging) space. Moreover, even though each process is capable of accessing 4GB of memory space, many don't even use most of it. So the OS only loads or swaps the required amount of code and data for each particular process.


Figure 1. Conceptual diagram of virtual memory
vmm
 

This mechanism is often referred to as virtual memory and virtual address spaces.

When an executable file is run, the Virtual Memory Manager of the OS looks at the code and data that comprise the file, and decides what parts it will load into RAM, or load into swap, or reference from the file system. At the same time, it establishes some structure to map the physical locations to virtual locations in the 4GB range. This 4GB range represents the process' maximum theoretical extent and (together sometimes with the VMM's structures that represent it), is known as the virtual address space of the process.

On AIX, the 4GB virtual address space is divided into sixteen 256-megabyte segments. The segments have predetermined functions, some of which are described below:

  • Segment 0 is for kernel-related data.
  • Segment 1 is for code.
  • Segment 2 is for stack and dynamic memory allocation.
  • Segment 3 is for memory for mapped files, mmap'd memory.
  • Segment d is for shared library code.
  • Segment f is for shared library data.

On HP-UX® by comparison, the address space is divided into four quadrants. Quadrants three and four are available for shared library mappings if they are designated using the chatr command with the +q3p enable and +q4p enable options.



 

Where shared libraries are loaded

Shared libraries are, naturally, intended to be shared. More specifically, the read-only sections of the binary image, namely the code (also known as "text") and read-only data (const data, and data that can be copy-on-write) may be loaded once into physical memory, and mapped multiple times into any process that requires it.

To demonstrate this, take a running AIX machine and see which shared libraries are presently loaded:

> su 
# genkld
Text address     Size File

    d1539fe0    1a011 /usr/lib/libcurses.a[shr.o]
    d122f100    36732 /usr/lib/libptools.a[shr.o]
    d1266080    297de /usr/lib/libtrace.a[shr.o]
    d020c000     5f43 /usr/lib/nls/loc/iconv/ISO8859-1_UCS-2
    d7545000    161ff /usr/java14/jre/bin/libnet.a
    d7531000    135e2 /usr/java14/jre/bin/libzip.a
.... [ lots more libs ] ....
d1297108 3a99 /opt/rational/clearcase/shlib/libatriastats_svr.a
[atriastats_svr-shr.o]
    d1bfa100    2bcdf /opt/rational/clearcase/shlib/libatriacm.a[atriacm-shr.o]
    d1bbf100    2cf3c /opt/rational/clearcase/shlib/libatriaadm.a[atriaadm-shr.o]
.... [ lots more libs ] ....
    d01ca0f8     17b6 /usr/lib/libpthreads_compat.a[shr.o]
    d10ff000    30b78 /usr/lib/libpthreads.a[shr.o]
    d00f0100    1fd2f /usr/lib/libC.a[shr.o]
    d01293e0    25570 /usr/lib/libC.a[shrcore.o]
    d01108a0    18448 /usr/lib/libC.a[ansicore_32.o]
.... [ lots more libs ] ....
    d04a2100    fdb4b /usr/lib/libX11.a[shr4.o]
    d0049000    365c4 /usr/lib/libpthreads.a[shr_xpg5.o]
    d0045000     3c52 /usr/lib/libpthreads.a[shr_comm.o]
    d05bb100     5058 /usr/lib/libIM.a[shr.o]
    d05a7100    139c1 /usr/lib/libiconv.a[shr4.o]
    d0094100    114a2 /usr/lib/libcfg.a[shr.o]
    d0081100    125ea /usr/lib/libodm.a[shr.o]
    d00800f8      846 /usr/lib/libcrypt.a[shr.o]
    d022d660   25152d /usr/lib/libc.a[shr.o]

 

As an interesting observation, we can see on this machine right away Clearcase and Java™ are running. Let's take any one of these common libraries, say, libpthreads.a. Browse the library and see which functions it implements:

# dump -Tv /usr/lib/libpthreads.a | grep EXP
[278]   0x00002808    .data      EXP     RW SECdef        [noIMid] pthread_attr_default
[279] 0x00002a68 .data EXP RW SECdef [noIMid]
 pthread_mutexattr_default
[280]   0x00002fcc    .data      EXP     DS SECdef        [noIMid] pthread_create
[281]   0x0000308c    .data      EXP     DS SECdef        [noIMid] pthread_cond_init
[282]   0x000030a4    .data      EXP     DS SECdef        [noIMid] pthread_cond_destroy
[283]   0x000030b0    .data      EXP     DS SECdef        [noIMid] pthread_cond_wait
[284]   0x000030bc    .data      EXP     DS SECdef        [noIMid] pthread_cond_broadcast
[285]   0x000030c8    .data      EXP     DS SECdef        [noIMid] pthread_cond_signal
[286]   0x000030d4    .data      EXP     DS SECdef        [noIMid] pthread_setcancelstate
[287]   0x000030e0    .data      EXP     DS SECdef        [noIMid] pthread_join
.... [ lots more stuff ] ....

 

Hmm, that was cool. Now let's see which currently running processes have it loaded currently on the system:

# for i in $(ps -o pid -e | grep ^[0-9] ) ; do j=$(procldd $i | grep libpthreads.a); \
 if [ -n "$j" ] ; then ps -p $i -o comm | grep -v COMMAND; fi  ; done
portmap
rpc.statd
automountd
rpc.mountd
rpc.ttdbserver
dtexec
dtlogin
radiusd
radiusd
radiusd
dtexec
dtterm
procldd : no such process : 24622
dtterm
xmwlm
dtwm
dtterm
dtgreet
dtexec
ttsession
dtterm
dtexec
rdesktop
procldd : no such process : 34176
java
dtsession
dtterm
dtexec
dtexec

 

Cool! Now let's get the same thing, but eliminate the redundancies:

# cat prev.command.out.txt | sort | uniq 
       
automountd
dtexec
dtgreet
dtlogin
dtsession
dtterm
dtwm
java
portmap
radiusd
rdesktop
rpc.mountd
rpc.statd
rpc.ttdbserver
ttsession
xmwlm

 

There, now we have a nice, discrete list of binaries that are currently executing and all load libpthreads.a. Note that there are many more processes on this system than this at this time:

# ps -e | wc -l  
      85

 

Now, let's see where each process happens to load libpthreads.a :

# ps -e | grep java
 34648      -  4:13 java
#
# procmap 34648 | grep libpthreads.a
d0049000         217K  read/exec      /usr/lib/libpthreads.a[shr_xpg5.o]
f03e6000          16K  read/write     /usr/lib/libpthreads.a[shr_xpg5.o]
d0045000          15K  read/exec      /usr/lib/libpthreads.a[shr_comm.o]
f03a3000         265K  read/write     /usr/lib/libpthreads.a[shr_comm.o]
#
# ps -e | grep automountd
 15222      -  1:00 automountd
 25844      -  0:00 automountd
#
# procmap 15222 | grep libpthreads.a
d0049000         217K  read/exec      /usr/lib/libpthreads.a[shr_xpg5.o]
f03e6000          16K  read/write     /usr/lib/libpthreads.a[shr_xpg5.o]
d0045000          15K  read/exec      /usr/lib/libpthreads.a[shr_comm.o]
f03a3000         265K  read/write     /usr/lib/libpthreads.a[shr_comm.o]
d10ff000         194K  read/exec         /usr/lib/libpthreads.a[shr.o]
f0154000          20K  read/write        /usr/lib/libpthreads.a[shr.o]
#
# ps -e | grep portmap              
 12696      -  0:06 portmap
 34446      -  0:00 portmap
#
# procmap 12696 | grep libpthreads.a
d0045000          15K  read/exec      /usr/lib/libpthreads.a[shr_comm.o]
f03a3000         265K  read/write     /usr/lib/libpthreads.a[shr_comm.o]
d10ff000         194K  read/exec         /usr/lib/libpthreads.a[shr.o]
f0154000          20K  read/write        /usr/lib/libpthreads.a[shr.o]
#
# ps -e | grep dtlogin
  6208      -  0:00 dtlogin
  6478      -  2:07 dtlogin
 20428      -  0:00 dtlogin
#
# procmap 20428 | grep libpthreads.a
d0045000          15K  read/exec      /usr/lib/libpthreads.a[shr_comm.o]
f03a3000         265K  read/write     /usr/lib/libpthreads.a[shr_comm.o]
d0049000         217K  read/exec      /usr/lib/libpthreads.a[shr_xpg5.o]
f03e6000          16K  read/write     /usr/lib/libpthreads.a[shr_xpg5.o]

 

Notice that each process loads it at the same address each time. Don't be confused by the constituent listings for the .o's in the library. On AIX, you can share archive libraries (.a files, customarily) as well as dynamic shared libraries (.so files, customarily). The purpose of this is to be able to bind symbols at link time, just like traditional archive linking, yet not require the constituent object (.o file in the archive) be copied into the final binary image. No dynamic (or runtime) symbol resolution is performed, however, as is the case with dynamic shared libraries (.so/.sl files).

Also note libpthreads.a code sections, those marked read/exec, are loaded into segment 0xd. That segment, as mentioned above, is designated on AIX as the segment for shared library code. That is to say, the kernel loads the shareable segments of this shared library into an area that is shared by all processes running on the same kernel.

You might notice that the data sections are also loaded to the same segment: the shared library segment 0xf. That doesn't mean, however, that each process is also sharing the data section of libpthreads.a. Loosely defined, such an arrangement wouldn't work, as different processes would need to maintain different data values at different times. Segment 0xf is distinct for each process using libpthreads.a, even though the virtual memory address is the same.

The svmon command can show us the segment IDs in the Virtual Memory Manager (Vsid) for processes. We'll see the shared-library code segments all have the same Vsid, while the shared-library data segments all have distinct Vsids. The Esid, meaning Effective Segment ID, is the segment ID within the scope of the process's address space (just terminology; don't let it confuse you).

# svmon -P 17314

-------------------------------------------------------------------------------
     Pid Command          Inuse      Pin     Pgsp  Virtual 64-bit Mthrd  16MB
   17314 dtexec           20245     9479       12    20292      N     N     N

    Vsid      Esid Type Description              PSize  Inuse   Pin Pgsp Virtual
       0         0 work kernel segment               s  14361  9477    0 14361 
   6c01b         d work shared library text          s   5739     0    9  5786 
   19be6         f work shared library data          s     83     0    1    87 
   21068         2 work process private              s     56     2    2    58 
   18726         1 pers code,/dev/hd2:65814          s      5     0    -     - 
    40c1         - pers /dev/hd4:2                   s      1     0    -     - 
#
# svmon -P 20428

-------------------------------------------------------------------------------
     Pid Command          Inuse      Pin     Pgsp  Virtual 64-bit Mthrd  16MB
   20428 dtlogin          20248     9479       23    20278      N     N     N

    Vsid      Esid Type Description              PSize  Inuse   Pin Pgsp Virtual
       0         0 work kernel segment               s  14361  9477    0 14361 
   6c01b         d work shared library text          s   5735     0    9  5782 
   7869e         2 work process private              s     84     2   10    94 
                   parent=786be
   590b6         f work shared library data          s     37     0    4    41 
                   parent=7531d
   6c19b         1 pers code,/dev/hd2:65670          s     29     0    -     - 
   381ae         - pers /dev/hd9var:4157             s      1     0    -     - 
    40c1         - pers /dev/hd4:2                   s      1     0    -     - 
   4c1b3         - pers /dev/hd9var:4158             s      0     0    -     - 


 

Doing the math

Let's see how much is currently in this shared segment 0xd. We'll revert to our bc calculator tool again. So we know we are sane, we'll verify the size of segment 0xd:

# bc    
ibase=16
E0000000-D0000000
268435456
ibase=A
268435456/(1024^2)
256

 

That looks good. Like stated above, each segment is 256MB. Ok, now let's see how much is currently being used.

$ echo "ibase=16; $(genkld | egrep ^\ \{8\} | awk '{print $2}' | tr '[a-f]' '[A-F]' \
 |  tr '\n' '+' ) 0" | bc
39798104
$
$ bc <<EOF
> 39798104/(1024^2)
> EOF
37

 

That is saying that there is 37MB currently being used. Let's start up XIr2, and compare:

$ echo "ibase=16; $(genkld | egrep ^\ \{8\} | awk '{print $2}' | tr '[a-f]' '[A-F]' \
 |  tr '\n' '+' ) 0" | bc
266069692
$
$ bc <<EOF
> 266069692/(1024^2)
> EOF
253

 

Now there is 253MB being used. That is very close to the limit of 256MB. Let's pick a random process, like WIReportServer, and see how many shared libraries made it into shared space, and how many had to be mapped privately. Since we know the shared segment begins at address 0xd000000, we can filter that out of the output from procmap. Remember, only code sections are mapped to segment 0xd, so we'll just look for the lines that are read/exec:

$ procmap 35620 | grep read/exec | grep -v ^d
10000000       10907K  read/exec         boe_fcprocd
31ad3000       14511K  read/exec
/crystal/sj1xir2a/xir2_r/bobje/enterprise115/aix_rs6000/libEnterpriseFramework.so
3167b000        3133K  read/exec
/crystal/sj1xir2a/xir2_r/bobje/enterprise115/aix_rs6000/libcpi18nloc.so
3146c000        1848K  read/exec
/crystal/sj1xir2a/xir2_r/bobje/enterprise115/aix_rs6000/libBOCP_1252.so
31345000         226K  read/exec
/crystal/sj1xir2a/xir2_r/bobje/enterprise115/aix_rs6000/btlat300.so

 

It looks like the above four libraries couldn't be mapped into the shared segment. Consequently, they were mapped to the private segment 0x3, which is used for any general memory allocated by a call to the mmap() routine.

There are a few conditions that force a shared library to be mapped privately on 32-bit AIX:

  • It is out of space in the shared segment 0xd (as above).
  • The shared library does not have execute permissions for group or other. You can use a permission designation of rwxr-xr-x mto correct this; however, developers would want to use private permissions (eg. rwx------) so they don't have to run slibclean each time they recompile a shared library and deploy it for testing.
  • Some documentation says shared libraries are loaded over nfs.

The AIX kernel will even load the same library twice into shared memory, if it comes from a different location:

sj2e652a-chloe:~/e652_r>genkld | grep libcplib.so
        d5180000    678c6 /space2/home/sj2e652a/e652_r/lib/libcplib.so
        d1cf5000    678c6 /home/sj1e652a/xir2_r/lib/libcplib.so


 

When it goes wrong

If we run another instance of XIr2 deployed in a different directory, we see a significant difference in the process footprint:

$ ps -e -o pid,vsz,user,comm | grep WIReportServer
28166 58980   jbrown WIReportServer
46968 152408 sj1xir2a WIReportServer
48276 152716 sj1xir2a WIReportServer
49800 152788 sj1xir2a WIReportServer
50832 152708 sj1xir2a WIReportServer

 

The instance for account 'jbrown' was started first, and the instance for account 'sj1xir2a' was started second. If we were to do something obscure and risky like setting at the appropriate place in our bobje/setup/env.sh file,

    LIBPATH=~jbrown/vanpgaix40/bobje/enterprise115/aix_rs6000:$LIBPATH

 

before starting the second instance, we would see the footprints normalized, (I switch to the process boe_fcprocd, as I couldn't get WIReportServer to start for this LIBPATH test).

$ ps -e -o pid,vsz,user,comm | grep boe_fcprocd   
29432 65036   jbrown boe_fcprocd
35910 67596   jbrown boe_fcprocd
39326 82488 sj1xir2a boe_fcprocd
53470 64964 sj1xir2a boe_fcprocd

 

And we see procmap shows us the files are loaded from ~jbrown as expected:

53470 : /crystal/sj1xir2a/xir2_r/bobje/enterprise115/aix_rs6000/boe_fcprocd
-name vanpg 
10000000       10907K  read/exec         boe_fcprocd
3000079c        1399K  read/write        boe_fcprocd
d42c9000        1098K  read/exec
/home7/jbrown/vanpgaix40/bobje/enterprise115/aix_rs6000/libcrypto.so
33e34160         167K  read/write
/home7/jbrown/vanpgaix40/bobje/enterprise115/aix_rs6000/libcrypto.so
33acc000        3133K  read/exec
/home7/jbrown/vanpgaix40/bobje/enterprise115/aix_rs6000/libcpi18nloc.so
33ddc697         349K  read/write
/home7/jbrown/vanpgaix40/bobje/enterprise115/aix_rs6000/libcpi18nloc.so



 

Clean up

Once applications are shut down, shared libraries may still reside in the shared segment 0xd. In such case, you can use the utility 'slibclean' to unload any shared libraries that are no longer referenced. The utility requires no arguments:

slibclean

 

There is also the utility genld, which when passed the -l option, can show you output like procmap, but for all existing process on the system,

genld -l

 

Sometimes, after running slibclean, you may still be prohibited from copying a shared library. For example:

$ cp /build/dev/bin/release/libc3_calc.so   /runtime/app/lib/
cp: /runtime/app/lib/libc3_calc.so: Text file busy

 

You may have run slibclean already, and running 'genld -l' doesn't show any process having this library loaded. Yet the system still has this file protected. You can overcome this limitation by first deleting the shared library in the target location, and then copying the new shared library:

$ rm /runtime/app/lib/libc3_calc.so
$ cp /build/dev/bin/release/libc3_calc.so   /runtime/app/lib/

 

During shared-library development, if you are making repeated compile, link, execute, and test exercises, you can avoid having to run slibclean in each cycle by making your shared-library executable only by the owner (eg. r_xr__r__). This will cause the process that you use for testing to load and map your shared-library privately. Be sure to make it executable by all, however (e.g. r_xr_xr_x at product release time).



 

Summary

I hope you've been able to see in more detail how shared libraries occupy memory and the utilities used to examine them. With this you'll be better able to assess the sizing requirements for your applications and analyze the constituents of memory footprints for processes running on AIX systems.

Configuring Infiniband for AIX

Infiniband is an interconnect technology that breaks through the bandwidth and fanout limitations of PCI bus by switching from traditional shared bus architecture to a switched fabric architecture. It is a switched fabric I/O technology that ties together servers, storage devices, and network devices. Instead of sending data in parallel, which is what PCI does, Infiniband sends data in serial and can carry multiple channels of data at the same time in a multiplexing signal.

IBM® AIX® 610 supports Infiniband hardware and various protocols that run over Infiniband. This article shows how to configure Infiniband and set up IP over Infiniband interface (IPoIB) in AIX. Also, this article explains how to use RDS (Reliable Datagram Sockets), a protocol (similar to UDP) designed to work over Infiniband to send and receive data using sockets.

Configuring Infiniband

Internet Protocol (IP) packets can be sent over an Infiniband (IB) network interface by using IP over IB (IPoIB). IPoIB encapsulates the IP packets into IB packets and sends using the IB interface. In order to use IPoIB, you must install and configure the ICM driver and at least one IB device in the system. The following steps are required to configure an IB device and then configure IPoIB using the ICM.

  1. Before configuring Infiniband, you need to check to see if the IB device, for instance Infiniband HCA (Infiniband Host Channel Adapter), is configured and is in “Available” state on your AIX box. To check the status, do the following:
    #  lsdev -Cc adapter | grep "host channel"
    iba0 Available  InfiniBand host channel adapter
        

     

    or

    # lsdev -Cc adapter | grep "HCA"
    iba0 
                             Available 01-00 PCIE Dual Port HCA (b3157862)
    

     
  2. Configure ICM (Infiniband Communication Manager). To configure ICM, do the following:

    smit icm -> Add an Infiniband Communication Manager -> Add an Infiniband Communication Manager à select ICM (as the ‘Name of IB Communication manager to Add’) and you will see the screen as shown in Figure 1.



    Figure 1. Configure ICM
    configure

     

    Click Enter to use the default values for each of the fields. The next screen will show ‘Command: OK’ and   ‘icm Available’. ICM configuration is done.

    To check if ICM has been configured, do

    # lsdev -l icm
    icm Available  Infiniband Communication Manager
    

     
  3. Configure Infiniband Network interface. IB interface can be configured using command line interface or SMIT user interface.

    To configure IB interface using SMIT do:

    smit inet -> Change / Show Characteristics of a Network interface -> select ib0 ( IP over Infiniband Network Interface). You will see the screen as shown in Figure 2.



    Figure2. Configure IB interface
    IB

     

    Enter the values for the following fields:

    • Internet Address ( for example, 1.2.3.92)
    • Network mask (for example, 255.255.255.0)
    • HCA adapter (the one we configured in step 1, iba0)
    • Adpater’s port number . There are two ports, Port 1 and Port 2. Use the command ibstat to check which port is Active. If both are active, select that you would like to use, as per your network configuration.
    • Current state – up

     

    Use the default values for the remaining fields.

    The next screen shows ‘Command: OK’ and ‘ib0 changed’. IB interface configuration is done.

    To check the IB interface status, run the ifconfig command.

    # ifconfig ib0
    ib0: flags=e3a0063<UP,BROADCAST,NOTRAILERS,RUNNING,ALLCAST,MULTICAST,GROUPRT>
    
    inet 1.2.3.92 netmask 0xffffff00 broadcast 1.2.3.255
    
    tcp_sendspace 131072 tcp_recvspace 131072 rfc1323 1

     

Configure the IB interface using the command line interface.

Step 1 and step 2 are the same as above. For step 3, do the following:

# /usr/sbin/mkiba  -a 1.2.3.92  -i ib0 -A iba0  -p 2  -P  0xFFFF  -S up 
 -m  255.255.255.0 -M 2044 ib0 changed

 

The syntax for the mkiba command is:

 /usr/bin/mkiba {-a address -i interface -A ib_adapter -p ib_port [-P P_KEY]  
          [-m subnet_mask]
  [-S state] [ -M mtu ] [ -q queue_pair_size ] [ -Q Q_KEY ] [-k superpacket] }

 

where:

-a address IP address of the interface specified by –I (must be in dotted decimal notation)
-i interface Interface to associate with the -a IP address
-A ib_adapter IB adapter associated to the interface
-p ib_port IB port associated with the IB adapter. (Defaults to 1)
-P p_key Partition key associated with the IB port. Key ( Please note that once configured partition key cannot be changed. The user must obtain the Partition key from the network administrator before configuring).
-m subnet_mask Subnetwork mask (dotted decimal or 0x notation)
-S state down,up,detach : The state of the ib interface.
-M ib_mtu HCA MTU required
-q srq_size Send and Receive queue sizes
-Q Q_KEY Q_Key associated with the multicast group
-k superpacket Superpacket feature on or off

Keep the following in mind:

  • The –k option for superpacket is available from AIX 61B and 53N releases onwards; the lower releases do not contain the superpacket feature. Also, when this feature is enabled, it gives a good performance boost. It allows TCP/IP to send 64KB datagrams to the interface, which can increase performance. Note that this feature is supported only in AIX from an AIX host to AIX host, as long as interfaces at both the hosts are enabled with this feature.
  • The –M option for HCA MTU size. AIX supports 4K physical MTU if the switch and the adapter support it. The interface for the first time, expects the user to create the broadcast multicast group in the switch. If the group is not there, always a 2K multicast group will be created by default. So if you have a 4K physical MTU supported adapter and switch, and you don't create the broadcast group in the switch, the interface will lower the MTU to 2K by creating a multicast group of 2K.

 

Run ifconfig to check the IB interface status.

# ifconfig ib0
ib0: flags=e3a0063<UP,BROADCAST,NOTRAILERS,RUNNING,ALLCAST,MULTICAST,GROUPRT>
    inet 1.2.3.92 netmask 0xffffff00 broadcast 1.2.3.255
     tcp_sendspace 131072 tcp_recvspace 131072 rfc1323 1

 

You are done! IB interface is configured.

To verify that all is working well, configure two nodes using the steps indicated above and run ping between the two notes. If ping works, IB is configured properly.



 

Configuring RDS

RDS uses the IB network interface for communication. Thus, IpoIB and IB network interface should be configured in order to use RDS protocol.

Before loading the RDS driver on the systems that you would like to communicate with using RDS, check if the IB network interfaces on those systems are able to ping each other.

Loading RDS

Run the following command to load RDS:

# bypassctrl load rds

 

If you receive the error Exec format error .., the IB interface is not configured. See Configuring Infiniband and configure the IB interface and then try to load RDS using bypassctrl.

If RDS is already loaded, the error /usr/lib/drivers/rds already loaded displays.

To check that the RDS driver was loaded successfully, run the following:

# genkex | grep rds
         47e1000    53770 /usr/lib/drivers/rds

 

If a socket is created to use RDS protocol and returns the error socket: Addr family not supported by protocol, the RDS driver is not loaded and you need to load it. Also, note that on a reboot, the RDS driver gets unloaded and thus needs to be reloaded using the bypassctrl utility after every reboot.



 

rdsctrl utility

Once RDS is loaded, use the rdsctrl (/usr/sbin/rdsctrl) utility to get the RDS statistics for modifying the tuneable parameters and for diagnostics.

The # rdsctrl stats command displays various RDS statistics.

The statistics can be reset using

# rdsctrl stats reset .

Tuning parameters

The following RDS parameters can be tuned after RDS is loaded, but before any RDS application is run. To set any parameter, use the syntax:

# rdsctrl set <tunable parameter>=<value to be set>

 
  • The rds_sendspace parameter refers to the high-water mark of the per-flow send buffer. (There may be multiple flows per socket.)

    The default value is 524288 bytes (512KB). The value is set using the command:

      # rdsctrl set rds_sendspace=<value in bytes>

      

  • rds_recvspace refers to the per-flow high-water mark of the per-socket receive-buffer. For every additional flow to this socket, the receive high-water mark is bumped up by this value.

    The default value is 524288 bytes  (512 KB). The value is set using the command:

    # rdsctrl set rds_recvspace=<value in bytes>

      

    For good RDS streaming performance, the rds_sendspace and rds_recvspace parameters must be at least four times the largest RDS sendmsg size. RDS sends an ACK for each 4 messages received and if the rds_recvspace is not at least 4 times the message size, the throughput will be very low.

  • rds_mclustsize refers to the size of the individual memory cluster, which is also the message fragment size. The default size is 16384 bytes (16KB). The value, always a multiple of 4096, is set using the command: # rdsctrl set rds_mclustsize=<multiple of 4096, in bytes>

    The rds_mclustsize value must be the same on all machines (nodes) in the cluster. Changing this value also has performance implications.

The current values that are set for the tuneable parameters can be retrieved using the command:

   # rdsctrl get <tunable parameter>

 

If this is run without any tuneable parameter, it gives the entire list of tuneable parameters

# rdsctrl get provides the list of tuneable parameters with their current values.

      # rdsctrl get
       rds_conn_block_limit = 100
                  rds_acksz = 180
                  rds_txqsz = 1024
                  rds_rxqsz = 1024
                         rds_mclustsize = 16384
                           rds_recvspace = 524288
                           rds_sendspace = 524288

 

Data-structure dumps

Various RDS structures can be dumped for troubleshooting purposes. The command to use is # rdsctrl dump <structure>

<structure> can be any one of the following:

  • IBC (the details of the IB Reliable Connection)
  • sendcb (the flow details)
  • pcb (the RDS socket PCB details)

 



 

Conclusion

Using the information in this article you learned how to configure Infiniband over AIX and configure and use RDS over Infiniband. You learned the commands used for configuring RDS; however, to understand RDS protocol, refer the Resources section for additional information.

Tips on designing a preprocessor for C++ with Antlr

Learn how to use Antlr to create a C++ preprocessor. Using this approach to create the C++ compiler, you don't need a separate preprocessor engine. Instead, the preprocessor engine can be integrated as part of the lexer.

One of the first steps in creating a custom compiler in C++ is developing the compiler’s preprocessor engine. Typically, C++ compilers have a separate preprocessing engine that accomplishes the following four basic tasks:

  • Defines and redefines macros (#define, #undef).
  • Supports header files (the #include directive).
  • Supports conditional compilation (#ifdef, #ifndef, #else, #endif).
  • Strips all comments from the input source listing.

Typically, the output of the preprocessor engine is then fed to the C++ lexer/parser combo. This article discusses the design of a C++ preprocessor using Antlr. You should have some familiarity with Antlr and the concept of top-down recursive descent parsers. All code discussed in this article is known to work on Antlr-2.7.2 and is compiled using gcc-3.4.4.

Create a single parser for the preprocessor

A salient benefit of using Antlr as your parser-generator tool of choice for creating the C++ compiler is that a separate preprocessor engine isn't necessary. The preprocessor engine may be integrated as part of the lexer. To understand this strategy, recall how lexers and parsers typically work. The lexer processes the raw data (in this case, the .h/.cpp files), creates tokens from this data, and passes the tokens to the parser. The parser in turn processes the tokens and validates against the grammar, does semantic checking, and ultimately generates assembly code.

The key to having a single compiler + preprocessor engine lies in proper modification of the lexer. The Antlr lexer is extended to preprocess the C++ sources and only pass the relevant tokens to the parser. The parser is never aware of the preprocessing part; it's only concerned with the tokens it receives from the lexer. Consider the following snippet:

#define USE_STREAMS
   
#ifdef USE_STREAMS 
#include <iostream>
#else
# include <stdio.h>
#endif 
   

 

Assume that this snippet is declared as part of a C++ source file. The lexer needs to pass on to the parser the first token it finds by including the iostream header. The #define mapping and #ifdef conditional evaluation are the added responsibilities of the lexer.

Enhance the Antlr lexer by defining new tokens

Lexers that work on C++ language define tokens as per the C++ language standard. For example, you usually find tokens that represent variable names and language keywords defined in an average lexer file.

To combine the preprocessing engine with a lexer, you must define new token types corresponding to the preprocessor constructs. On encountering these constructs, the lexer takes the necessary action. The lexer must also strip down comments from the sources. In this case, on encountering the token for a comment, the lexer needs to ignore the token and continue looking for the next available token. It's important to note that these tokens aren't defined as part of the language standard; they're essentially implementation-defined tokens.

Define new tokens in the lexer

The lexer must contain the following tokens, along with language-mandated ones: COMMENT_TOKEN, INCLUDE_TOKEN, DEFINE_TOKEN, UNDEF_TOKEN, IFDEF_TOKEN, ELSE_TOKEN, and ENDIF_TOKEN. This article discusses a prototype lexer that supports the preprocessor macros mentioned earlier and variable declaration for integer and long data types. The barebones lexer/parser combo that processes these tokens is shown in Listing 1.


Listing 1. A bare-bones lexer/parser that supports preprocessor tokens and long/int declarations
 
                
header {
#include "Main.hpp"
#include <string>
#include <iostream>
}

options {
            language = Cpp;
}

class cppParser extends Parser;

start: ( declaration )+ ;
declaration: type a:IDENTIFIER   (COMMA b:IDENTIFIER)* SEMI;
type: INT | LONG; 

{
#include <fstream>
#include "cppParser.hpp"
}

class cppLexer extends Lexer;

options { 
            charVocabulary = '\3'..'\377';
            k=5;
}

tokens { 
  INT="int";
  LONG="long";
}
           
DEFINE_TOKEN: "#define"  WS macroText:IDENTIFIER WS  macroArgs:MACRO_TEXT;
UNDEF_TOKEN: "#undef" IDENTIFIER;  
IFDEF_TOKEN: ("ifdef" | "#ifndef") WS IDENTIFIER;
ELSE_TOKEN: ("else" | "elsif" WS IDENTIFIER);            
ENDIF_TOKEN: "endif";
INCLUDE_TOKEN:  "#include" (WS)? f:STRING;
    
COMMENT_TOKEN: 
  (
  "//" (~'\n')* '\n' { newline( ); } 
   |
  "/*" (
       {LA(2) != '/'}? '*' | '\n' { newline( ); } | ~('*'|'\n')
       )*
  "*/"
  );

IDENTIFIER: ('a'..'z'|'A'..'Z'|'_')('a'..'z'|'A'..'Z'|'_'|'0'..'9')* ;
STRING: '"'! ( ~'"' )* '"'!
SEMI: ';';
COMMA: ',';

WS :  ( ' ' | '\t' | '\f' | '\n' {newline();})+;
            
MACRO_TEXT:  ( ~'\n' )*   ;
    

 

Define the lexer/parser combo to strip comments

Comments can be written in either the C++ // style or the C style /* */ multiline comment style. The lexer is extended to include a rule for the COMMENT_TOKEN. On encountering this token, the lexer needs to be explicitly told not to pass this token to the parser, but instead to skip it and continue looking for the next available token. You do this using Antlr's $setType function. You don't need to add support at the parser end, because it will never pass the comment token. See Listing 2.


Listing 2. Defining the lexer to strip comments from the C/C++ code
 
                
COMMENT_TOKEN: 
  (
  "//" (~'\n')* '\n' { newline( ); } 
   |
 "/*" (
           {LA(2) != '/'}? '*' | '\n' { newline( ); } | ~('*'|'\n')
         )*
  "*/"
  )
  { $setType(ANTLR_USE_NAMESPACE(antlr)Token::SKIP); };
      

 

The code is simple enough for C++-style comments. For the C-style comment, note the {LA(2) != '/'}? used before matching *. It means that if there's a / after the *, then this rule isn't to be matched, and the lexer ends up matching */. This is necessary because /* this is */ an invalid comment */ is an invalid C++ comment. This also implies that the minimum look-ahead needed by the lexer is 2. LA(2) is known as a semantic predicate -- a hint to the lexer to decide what character to look for next in the input stream.



 

Include file processing

As discussed earlier, the parser needs to see a continuous stream of tokens. The lexer therefore needs to switch streams if and when it encounters an included file and switch back to the previous stream on encountering the end of the included file. The parser isn't aware of this stream switch and essentially treats included files as a continuous token stream. You can achieve this behavior several ways using Antlr; for a detailed discussion, see the Resources section.

This article uses Antlr's TokenStreamSelector class. Instead of initializing the parser with the lexer as in Listing 1, the parser is initialized with a TokenStreamSelector object. Internally, the TokenStreamSelector class maintains a stack of lexers. When the lexer encounters a new input stream, it creates a new lexer for processing of the new stream and subsequently attaches the new lexer to the tokenstreamselector object so that all future tokens (until the end of the new stream is reached) are sourced using the new lexer class. This is followed by invoking the retry method of the TokenStreamSelector class, which now sources tokens from the new input stream. The only other thing that remains to be done is to switch back to the previous input stream on encountering the EOF in an included file. To allow for user-defined actions on encountering the end of a line, Antlr provides the predefined routine uponEOF. You must modify this routine to switch to the previous input stream by calling TokenStreamSelector::pop(). Listing 3 shows a code snippet that includes file processing.


Listing 3. Initializing the parser with a TokenStreamSelector object
 
                
#include <iostream>
#include <fstream>
…
TokenStreamSelector selector;
cppParser* parser;
cppLexer* mainLexer;

int main(int argc,char** argv)
{
   try {
       std::ifstream inputstream("test.c", std::ifstream::in);
       mainLexer = new cppLexer(inputstream);
       // notify selector about starting lexer; name for convenience
       selector.addInputStream(mainLexer, "main");
       selector.select("main"); // start with main lexer

       // Create parser attached to selector
       parser = new cppParser (selector);
       parser->setFilename("test.c");
       parser->startRule();
  } 
  catch (exception& e) {
    cerr << "exception: " << e.what() << endl;
  }
  return 0;
} 
      

 

The lexer-related changes are described in Listing 4.


Listing 4. Include file processing in the lexer
 
                
class cppLexer extends Lexer;
…

{
public:
    void uponEOF()  {
        if ( selector.getCurrentStream() != mainLexer ) {
            selector.pop(); // return to old lexer/stream
            selector.retry();
        }
        else {
             ANTLR_USE_NAMESPACE(std)cout << "Hit EOF of main file\n"  ;
        }
    }
}

INCLUDE_TOKEN:  "#include" (WS)? f:STRING
    {
    ANTLR_USING_NAMESPACE(std)
    // create lexer to handle include
    string name = f->getText();
    ifstream* input = new ifstream(name.c_str());
    if (!*input) {
        cerr << "cannot find file " << name << endl;
    }
    cppLexer* sublexer = new cppLexer (*input);
    // make sure errors are reported in right file
    sublexer->setFilename(name);
    parser->setFilename(name);

    // push the previous lexer stream and make sublexer current
    selector.push(sublexer);
    // ignore this token, re-look for token in the switched stream
    selector.retry(); // throws TokenStreamRetryException
    }
    ;
    

 

Note that the TokenStreamSelector object is an intentional global variable because it needs to be shared as part of the lexer uponEOF method. Also, in the uponEOF method post, you must call the pop method retry so the TokenStreamSelector again looks into the current stream for the next token. Note that the include file processing described here isn't complete, but depends on conditional macro support . For example, if a source snippet includes a file within a #ifdef A .. #endif block where A isn't defined, then the processing for INCLUDE_TOKEN is discarded. This is further explained in the Handle macros and Conditional macro support sections.



 

Handle macros

Macro handling is primarily accomplished with the help of a hash table. This article uses the standard STL hash container. In its simplest form, support of macros implies supporting constructs like #define A and #undef A. You can easily do this by pushing the string "A" into a hash table on encountering #define and removing it from the hash on encountering #undef. The hash table is typically defined as part of the lexer. Also, note that the tokens for #define and #undef must not reach the parser -- for this reason, you add $setType(ANTLR_USE_NAMESPACE(Antlr)Token::SKIP); as part of the corresponding token processing. Listing 5 shows this behavior.


Listing 5. Supporting #define and #undef
 
                
class cppLexer extends Lexer;
…

{
bool processingMacro;
std::map<std::string, std::string> macroDefns;
public:
    void uponEOF()  {
    … // Code for include processing
    }
}
…
DEFINE_TOKEN: "#define" {processingMacro=true;} 
    WS macroText:IDENTIFIER WS macroArgs:MACRO_TEXT
  {
  macroDefns[macroText->getText()] = string(macroArgs->getText());
 $setType(ANTLR_USE_NAMESPACE(antlr)Token::SKIP);
  } ;
                           
UNDEF_TOKEN: "#undef" WS macroText:IDENTIFIER
  {
  macroDefns.erase(macroText->getText());
  $setType(ANTLR_USE_NAMESPACE(antlr)Token::SKIP);
  };

MACRO_TEXT: {processingMacro}? ( ~'\n' )* {processingMacro=false;};
      


 

Conditional macro support

Broadly speaking, supporting conditional macros means that you need to account for which tokens reach the parser, depending upon the conditional. For example, look at the following snippet:

#define A
#ifdef A
#ifdef B
int c;
#endif
int d;
#endif
      

 

On encountering the token for the first #ifdef, you must decide whether subsequent tokens that are encountered should be passed on to the parser, depending on whether A has been previously defined. At this stage, you use the hash table defined earlier.

Next, you must consider support of nested #ifdef blocks. Because you need to check for the path condition on encountering each #ifdef, it's logical to maintain a stack for the path conditions. Each #ifdef/#elsif adds the path condition to the stack head; the matching #endif removes the path condition from the stack.

Before you see the code that explains the concept in greater detail, you need to understand another important method in the Antlr lexer class: nextToken, which the parser calls continuously to retrieve the next token from the input stream. You can't modify this routine directly, so the optimal approach is to derive a class from the Antlr cppLexer class and redefine the nextToken method based on the path condition. If the path condition is true, the routine returns the next available token to the parser; otherwise, it continues until the matching #endif is found in the token stream.

Listing 6 shows the source for the derived lexer class. There should be no direct instantiation of cppLexer in the code; the parser should be initialized with a copy of the derived lexer class object.


Listing 6. Defining a new lexer class
 
                
class cppAdvancedLexer : public cppLexer 
  {
  public: 
    cppAdvancedLexer(ANTLR_USE_NAMESPACE(std)istream& in) : cppLexer(in) { }
    RefToken nextToken()
      {
      // keep looking for a token until you don't
      // get a retry exception
      for (;;) {
        try {
          RefToken _next = cppLexer::nextToken();
          if (processToken.empty() || processToken.top()) // defined in cppLexer
            return _next;
          }
        catch (TokenStreamRetryException& /*r*/) {
          // just retry "forever"
          }
        }
      }
  };
    

 

The lexer-related changes are shown in Listing 7.


Listing 7. Conditional macro support in the lexer
 
                
{
public:
    std::stack<bool> processToken;
    std::stack<bool> pathProcessed;
    std::map<std::string, std::list<std::string> > macroDefns;
    void uponEOF() {
    … // Code for include processing defined earlier
    }
}

IFDEF_TOKEN {bool negate = false; } :
  ("#ifdef" | "#ifndef" {negate = true;} ) WS macroText:IDENTIFIER
  {
  bool macroAlreadyDefined = false;
  if (macroDefns.find(macroText->getText()) != macroDefns.end())
    macroAlreadyDefined = true;
  processToken.push(negate? !macroAlreadyDefined : macroAlreadyDefined);
  pathProcessed(processToken.top());
  $setType(ANTLR_USE_NAMESPACE(antlr)Token::SKIP);
  }
  ;
  
ELSE_TOKEN:  ("#else"
  {
   bool& pathCondition = processToken.top();
   pathCondition = !processToken.top(); // no other path is true
   } 
   | 
   "#elsif" WS macroText:IDENTIFIER
    {
     if (!processToken.top() && !pathProcessed.top())  {
       if (macroDefns.find(macroText->getText()) != macroDefns.end()) {
          processToken.push(true);
          pathProcessed.push(true);
       }
     }
     else {
        bool& condition = pathProcessed.top();
        condition = false;
     }
   )
   {
   $setType(ANTLR_USE_NAMESPACE(antlr)Token::SKIP);
   }
   ;

ENDIF_TOKEN: "#endif"
   {
   processToken.pop();
   pathProcessed.pop();
   $setType(ANTLR_USE_NAMESPACE(antlr)Token::SKIP);
   };

 

Examining the code, you see that processToken is defined as a stack of booleans to store the path conditions. On encountering #ifdef, if the code is already in true path, it tests whether the path condition is valid.


 

Support expressions as part of macros

You can use a macro to represent a complex expression that involves numbers, identifiers, mathematical operators, and even function calls. To do so, you must use the macroArgs string to replace every occurrence of the macro definition in the post-processed C++ code; the context determines whether this replacement is syntactically valid. For example, consider this situation: #define A b, c. Somewhere down the line you have int A;, which is valid C++; but switch(A) is clearly invalid C++. You need to parse the stream associated with the string on a per-occurrence basis and let the grammar rules determine the syntactic validity of the context.

The code for this approach works as follows:

  1. Capture the token corresponding to the macroText identifier in the cppAdvancedLexer::nextToken.
  2. Retrieve the macroArgs text from the hash table.
  3. Create an istream for macroArgs and a new copy of the lexer that parses this stream.
  4. Attach this lexer to the TokenStreamSelector object, and call retry -- this fetches the token from the new stream that was just created.

The uponEOF method is provided in cppLexer, which takes care of switching context on encountering the end of the stream. Listing 8 illustrates this discussion.


Listing 8. Modifying the nextToken method the lexer class for macro replacement
 
                
RefToken  cppAdvancedLexer::nextToken()
  {
  // keep looking for a token until you don't
  // get a retry exception
  for (;;) {
    try {
       RefToken _next = cppLexer::nextToken();
       map<string, string>::iterator defI = macroDefns.find(_next->getText());
       if (defI != macroDefns.end() && defI->second != "")
         {
         std::stringstream macroStream;
         cppAdvancedLexer* macrolexer = new cppAdvancedLexer(macroStream);
         macrolexer->setFilename(this->getFilename());
         selector.push(macrolexer);
         selector.retry();
         }
       if (processToken.empty() || processToken.top())
         return _next;
   }
   catch (TokenStreamRetryException& /*r*/) {
          // just retry "forever"
   }
}
            

 

Conclusion

This article discussed the basic methodology you can follow to create a C++ preprocessor using Antlr. Developing a full-fledged C++ preprocessor is beyond the scope of a single article. Although this article doesn't deal with potential errors and error-handling strategies, a real-world preprocessor must account for these factors; for example, the ordering of tokens in conditional macro processing is significant, and you must take steps to ensure that it's handled correctly.

Learn 10 more good UNIX usage habits

As a follow-up to Michael Stutz's excellent article, this article provides 10 more good habits to adopt that will improve your UNIX® command-line efficiency. Learn about common errors and how to overcome them, and discover exactly why these 10 UNIX habits are worth picking up!

Let's face it: Bad habits are hard to break. But habits that you've just become comfortable with can be even more difficult to overcome. Sometimes, a fresh look at things may provide you with an "A-ha, I didn't know you could do that!" moment. Building on Michael Stutz's excellent article, "Learn 10 good UNIX usage habits," this article suggests 10 more UNIX command-line commands, tools, and techniques that may make you more productive as a UNIX command-line wizard.

The 10 additional good habits you should adopt are:

  • Use file name completion.
  • Use history expansion.
  • Reuse previous arguments.
  • Manage directory navigation with pushd and popd.
  • Find large files.
  • Create temporary files without an editor.
  • Use the curl command-line utility.
  • Make the most of regular expressions.
  • Determine the current user.
  • Process data with awk.

Use file name completion

Wouldn't it be great if you didn't have to type a long, convoluted file name at the command prompt? Well, you don't, as it turns out. You can configure the most popular UNIX shells for file name completion, instead. This functionality works a bit differently in each shell, so I show you how to use file name completion in the most popular shells. File name completion allows you to type faster and avoid errors. Lazy? Perhaps. More efficient? Definitely!

Which shell am I running?

What happens if you don’t know which shell you're currently running? Although this trick isn't officially part of the 10 more good habits, it's still pretty useful. As shown in Listing 1, you can use the echo $0 or ps –p $$ command to display the shell you're using. In my case, I'm running the Bash shell.


Listing 1. Determine your shell
 
                
$ echo $0
-bash
$ ps –p $$
PID TTY           TIME CMD
6344 ttys000    0:00.02 –bash

 

C shell

The C shell supports the most straightforward file name completion. Setting the filec variable enables the functionality. (You can use the command set filec). After you start typing the name of a file, you can click Escape, and the shell fills in the name of the file—or as much as it can. For example, say you have files named file1, file2, and file3. If you type f, then click Escape, file will be filled out, and you'll have to type the 1, 2, or 3 to complete the appropriate file name.

Bash

The Bash shell also provides file name completion but uses the Tab key instead of the Escape key. You don't need to set anything to enable file name completion in the Bash shell; it's set by default. Bash also implements an additional feature. After typing a portion of a file name, then clicking Tab, if you reach that point at which multiple files satisfy your request and you need to add text to select one of the files, you can click Tab twice more for a list of the files that match what you have typed so far. Using the earlier examples of files named file1, file2, and file3, start by typing f. When you click Tab once, Bash completes file; clicking Tab one more time expands the list of file1 file2 file3.

Korn shell

For Korn shell users, file name completion depends on the value of the EDITOR variable. If EDITOR is set to vi, you type part of name, and then click Escape followed by a backslash (\) character. If EDITOR is set to emacs, you type part of the name, and then click the Escape key twice to complete the file name.

Use history expansion

What happens if you're using the same file name for a series of commands? Well, there's a shortcut that can quickly retrieve the last file name you used. As shown in Listing 2, the !$ command returns the file name that the previous command used. The file this-is-a-long-lunch-menu-file.txt is searched for occurrences of the word pickles. After searching, the vi command is used to edit the this-is-a-long-lunch-menu-file.txt file without the need for retyping the file name. You use the bang, or exclamation point (!), to access the history, and the dollar sign ($) returns the last field of the previous command. It's a great tool if you are using long file names repeatedly.


Listing 2. Using !$ to retrieve the last file name used with a command
 
                
$ grep pickles this-is-a-long-lunch-menu-file.txt
pastrami on rye with pickles and onions
$ vi !$      

 

Reuse previous arguments

The !$ command returns the last argument used with a command. But what happens if you have a command that used arguments and you want to reuse just one of them? The !:1 operator returns the argument used in a command. The example in Listing 3 shows how you can use this operator in combination with the !$ operator. In the first command, a file is renamed to a more meaningful name, but to preserve use of the original file name, a symbolic link is created. The file kxp12.c is renamed in a more readable manner, then the link command is used to create a symbolic link back to the original file name, in case it's still used elsewhere. The !$ operator returns the file_system_access.c argument, and the !:1 operator returns the kxp12.c argument, which is the first argument of the previous command.


Listing 3. Using !$ and !:1 in combination
 
                
$ mv kxp12.c file_system_access.c
$ ln –s !$ !:1

 

Manage directory navigation with pushd and popd

UNIX supports a wide variety of directory-navigation tools. Two of my favorite productivity tools are pushd and popd. You're certainly aware that the cd command changes your current directory. What happens if you have several directories to navigate, but you want to be able to quickly return to a location? The pushd and popd commands create a virtual directory stack, with the pushd command changing your current directory and storing it on the stack, and the popd command removing the directory from the top of the stack and returning you to that location. You can use the dirs command to display the current directory stack without pushing or popping a new directory. Listing 4 shows how you can use the pushd and popd commands to quickly navigate the directory tree.


Listing 4. Using pushd and popd to navigate the directory tree
 
                
$ pushd .
~ ~
$ pushd /etc
/etc ~ ~
$ pushd /var
/var /etc ~ ~
$ pushd /usr/local/bin
/usr/local/bin /var /etc ~ ~
$ dirs
/usr/local/bin /var /etc ~ ~
$ popd
/var /etc ~ ~
$ popd
/etc ~ ~
$ popd
~ ~
$ popd

 

The pushd and popd commands also support parameters to manipulate the directory stack. Using the +n or -n parameter, where n is a number, you can rotate the stack left or right, as shown in Listing 5.


Listing 5. Rotating the directory stack
 
                
$ dirs
/usr/local/bin /var /etc ~ ~
$ pushd +1
/var /etc ~ ~ /usr/local/bin
$ pushd -1
~ /usr/local/bin /var /etc ~

 

Find large files

Need to find out where all your free disk space went? Here are a couple of tools you can use to manage your storage. As shown in Listing 6, the df command shows you the total number of blocks used on each available volume and the percentage of free space.


Listing 6. Determining volume usage
 
                
$ df
Filesystem                            512-blocks      Used  Available Capacity  Mounted on
/dev/disk0s2                           311909984 267275264   44122720    86%    /
devfs                                        224       224          0   100%    /dev
fdesc                                          2         2          0   100%    /dev
map -hosts                                     0         0          0   100%    /net
map auto_home                                  0         0          0   100%    /home

 

Want to find the largest files? Use the find command with the -size parameter. Listing 7 shows how to use the find command to find files larger than 10MB. Note that the -size parameter takes a size in kilobytes.


Listing 7. Find all files larger than 10MB
 
                    
$ find / -size +10000k –xdev –exec ls –lh {}\;

 

Create temporary files without an editor

This is a simple one: You need to quickly create a simple temporary file but don't want to fire up your editor. Use the cat command with the > file-redirection operator. As shown in Listing 8, using the cat command without a file name simply echoes anything typed to standard input; the > redirection captures that to the specified file. Note that you must provide the end-of-file character when you're finished typing—typically, Ctrl-D.


Listing 8. Quickly create a temporary file
 
                 
$ cat > my_temp_file.txt
This is my temp file text
^D
$ cat my_temp_file.txt
This is my temp file text

 

Need to do the same thing but append to an existing file instead of creating a new one? As shown in Listing 9, use the >> operator, instead. The >> file-redirection operator appends to an existing file.


Listing 9. Quickly append to a file
 
                
$ cat >> my_temp_file.txt
More text
^D
$ cat my_temp_file.txt
This is my temp file text
More text

 

Use the curl command-line utility

I can access the Web from the command line? Are you crazy? No, it's just curl! The curl command lets you retrieve data from a server using the HTTP, HTTPS, FTP, FTPS, Gopher, DICT, TELNET, LDAP, or FILE protocols. As shown in Listing 10, I can use the curl command to access the current local conditions of the National Weather Service for my location (Buffalo, NY). When combined with the grep command, I can retrieve the conditions in Buffalo. Use the -s command-line option to suppress curl processing output.


Listing 10. Retrieve the current weather conditions with curl
 
                
$ curl –s http://www.srh.noaa.gov/data/ALY/RWRALY | grep BUFFALO
BUFFALO        MOSUNNY   43  22  43 NE13      30.10R

 

As shown in Listing 11, you can also use the curl command to download HTTP-hosted files. Use the -o parameter to specify where the output is saved.


Listing 11. Use curl to download HTTP-hosted files
 
                
$ curl -o archive.tar http://www.somesite.com/archive.tar

 

This is really just a hint of what you can do with curl. You can start exploring a bit more simply by typing man curl at your command prompt to display the complete usage information for the curl command.

Make the most of regular expressions

Many UNIX commands use regular expressions as arguments. Technically speaking, a regular expression is a string (that is, a sequence of characters composed of letters, numbers, and symbols) that represents a pattern defining zero or more strings. A regular expression uses meta-characters (for example, the asterisk [*] and question mark [?] symbols) to match parts of or whole other strings. A regular expression doesn't have to contain wildcards, but wildcards can make regular expressions useful for searching for patterns and manipulating files. Table 1 shows some basic regular expression sequences.


Table 1. Regular expression sequences
 
Sequence Description
Caret (^) Matches the expression at the start of a line, as in ^A
Question mark (?) Matches the expression at the end of a line, as in A?
Backslash (\) Turns off the special meaning of the next character, as in \^
Brackets ([]) Matches any one of the enclosed characters, as in [aeiou] (Use a hyphen [-] for a range, as in [0-9].)
[^ ] Matches any one character except those enclosed in brackets, as in [^0-9]
Period (.) Matches a single character of any value except end of line
Asterisk (*) Matches zero or more of the preceding characters or expressions
\{x,y\} Matches x to y occurrences of the preceding
\{x\} Matches exactly x occurrences of the preceding
\{x,\} Matches x or more occurrences of the preceding

Listing 12 shows some of the basic regular expressions used with the grep command.


Listing 12. Using regular expressions with grep
 
                
$ # Lists your mail
$ grep '^From: ' /usr/mail/$USER   
$ # Any line with at least one letter  
$ grep '[a-zA-Z]'  search-file.txt
$ # Anything not a letter or number
$ grep '[^a-zA-Z0-9] search-file.txt
$ # Find phone numbers in the form 999-9999 
$ grep '[0-9]\{3\}-[0-9]\{4\}' search-file.txt
$ # Find lines with exactly one character
$ grep '^.$' search-file.txt
$ #  Find any line that starts with a period "."          
$ grep '^\.' search-file.txt 
$ # Find lines that  start with a "." and 2 lowercase letters
$ grep '^\.[a-z][a-z]' search-file.txt

 

Many books have been written just about regular expressions. For a more in-depth look at command-line regular expressions, I suggest the developerWorks article, "Speaking UNIX, Part 9: Regular expressions."

Determine the current user

At times, you may have an administrative script that you want to make sure a certain user has or has not executed. To find out, you can use the whoami command to return the name of the current user. Listing 13 shows the whoami command run on its own; Listing 14 shows an excerpt from a Bash script using whoami to make sure the current user isn't root.


Listing 13. Using whoami from the command line
 
                
$ whoami
John


Listing 14. Using whoami in a script
 
                
if [ $(whoami) = "root" ]
then
   echo "You cannot run this script as root."
   exit 1
fi

 

Process data with awk

The awk command always seems to live in the shadows of Perl, but it can be a quick, useful tool for simple command-line-based data manipulation. Listing 15 shows how to get started with the awk command. To get the length of each line in the file text, use the length() function. To see if the string ing is present in the file text, use the index() function, which returns the location of the first occurrence of ing so that you can use it for further string processing. To tokenize (that is, split a line into word-length pieces) a string, use the split() function.


Listing 15. Basic awk processing
 
                
$ cat text
testing the awk command
$ awk '{ i = length($0); print i }' text
23
$ awk '{ i = index($0,”ing”); print i}' text
5
$ awk 'BEGIN { i = 1 } { n = split($0,a," "); while (i <= n) {print a[i]; i++;} }' text
testing 
the
awk
command

 

Printing specified fields of text file is a simple awk task. In Listing 16, the sales file consists of each salesperson's name followed by a monthly sales figure. You can use the awk command to quickly total the sales for each month. By default, awk treats each comma-separated value as a different field. You use the $n operators to access each individual field.


Listing 16. Using awk for data summarization
 
                
$cat sales
Gene,12,23,7
Dawn,10,25,15
Renee,15,13,18
David,8,21,17
$ awk -F, '{print $1,$2+$3+$4}' sales
Gene 42
Dawn 50
Renee 46
David 46

 

The awk command can be complex and used in a wide variety of situations. To explore the awk command more fully, start with the command man awk in addition to the resources mentioned in the Resources.

Conclusion

Becoming a command-line wizard takes a bit of practice. It's easy to keep doing things the same way simply because you're used to it. Expanding your command-line resources can provide a big increase in your productivity and propel you toward becoming a UNIX command line wizard!