TweetFollow Us on Twitter

Function Logging
Volume Number:8
Issue Number:7
Column Tag:C Workshop

Function Logging in Think C

For debugging, optimizing, and better understanding a program’s execution path

By Eric Shapiro, Ann Arbor, Michigan

About the author

Eric Shapiro’s works include Business Simulator®, EzTape®, and two new QuickTime™ programs: Spectator™, a screen recorder from Baseline Publishing and VideoBeep™, a silly Control Panel from Sound Source Unlimited.

Eric has taught Mac programming seminars for Apple and developed the course materials for Apple’s Macintosh Device Driver seminar. He is best known, however, as the author of everyones favorite singing trash can, The Grouch.

Overview

This article presents a simple way to log all function calls into a text file using Think C 5.0. The technique presented here is useful for debugging, optimizing, and obtaining a better understanding of a program’s execution path.

History

In the old days before source level debugging, function logging was an important way to debug Mac programs. On one of my first Mac projects, I remember sending function names out the serial port to a machine running a terminal program in order to figure out where the program was crashing. At MacHack this year, Marshall Clow from Hewlett Packard showed how to log function calls to MacsBug in MPW. Preferring Think C to MPW, I decided that I’d do a similar project for Think C. This article presents the results.

To Use the Logger

If all you want to do is use the function logger, here’s what you need to do:

• Add the file LogToFile.c to your Think C project.

• Add the file ANSI or ANSI-Small to your project (for some string utilities).

• Turn both the profiling and stack frame options on for the project.

• Call LogInit during program initialization (with appropriate parameters).

• Call LogDInit during program de-initialization.

• Recompile your code.

That’s all there is to it! You can control logging more precisely as follows:

• The following line of code turns profiling on for an entire file (if it appears outside of all functions) or for a single routine (if it appears within a function):

/* 1 */
 #pragma options( profile, force_frame )

• Likewise, the following line of code turns profiling off:

/* 2 */
 #pragma options( !profile, !force_frame ) 

• You can determine if logging is active at compile-time using:

/* 3 */
      #if _option( profile )

• Call SetLogState( true ) or SetLogState( false ) to turn logging on or off during program execution. You can do this via a menu selection, for example.

• Call SetLogFlush( true ) or SetLogFlush( false ) to turn flushing on or off during program execution. Performance suffers considerably when flushing is active, but it can help find the location where a crash occurs.

Note: Writing to disk when debugging a program can be a dangerous activity. Be sure you have adequate backups before trying any new debugging technique.

Note: By default, the function logger handles calling depths up to 100 levels. This should be sufficient for all but the most recursive programs. You can change the constant kMaxLogDepth to control the maximum depth.

How The Logger Works

When profiling is active, Think C generates a call to the function _profile_ at the beginning of every function call. Actually, only functions that generate stack frames call _profile_, which is why we turn on the stack frame option as well (stack frames are described below). The _profile_ function has the following form:

/* 4 */
void _profile_( void *functionNamePString )

The first version of _profile_ I wrote simply logged the function name to a file. While this is adequate for some uses, I really wanted to create an indented list of function calls so a programmer can tell exactly who called each function. To do this, our code needs to be notified when a function exits as well as when it is entered. Here’s where life gets a bit complicated.

Unfortunately, Think C’s profiler doesn’t generate a function call whenever a function exits. I looked at Think C’s profiler source code and used a similar technique for my logger. The technique involves replacing return addresses on the stack with the address of our own routine. When a function exits via an rts instruction, it will unknowingly call our exit handler. Our exit handler will decrement a depth counter, write some text to the log file, and jump to the original return address. To understand how we trace back the stack to find return addresses, we need to know exactly how Think C handles stack frames.

Stack Frames

Before and during function execution, space must be allocated for the following items:

• The function’s parameters

• The caller’s return address

• The function’s local variables

• Additional information, such as register contents, that the function may want to store

The first two items, the parameters and return address, are placed on the program stack by the calling code. Space for the the local variables and register storage is also allocated on the stack, but by the function itself. For convenience, the compiler uses register A6 to reference local variables while the stack pointer itself can be lowered and raised during function execution. The previous value of A6 is also placed on the stack so the compiler can easily clean up the stack when a function exits.

If FuncA calls FuncB, and FuncB calls FuncC, the stack looks like that shown in Figure 1.

As you can see, register A6 always points to the start of a linked list of previous A6 values. This is how debuggers such as MacsBug can back-trace function calls (using the SC command). In the _profile_ routine, we need to find the return address of the function that called the function that called us. The assembly language code finds this value on the stack at 4 + the previous A6 value. We save the old return address in a global array before modifying the stack so we know where to return to when our exit handler is called.

Note: If Think C’s force stack frame option is not active, the compiler doesn’t generate a stack frame for functions that have no local variables or parameters. Our _profile_ code is not called for these functions.

Sample Output

The code shown below produces the very simple log file also shown below. Note that the recursion in Func1 works properly. The braces in the output allow you to select an entire function using Think C’s Balance command.

/* 5 */

void DoSomething( void )
{
 Func1();
 Func2( 10 );
}

void Func1( void )
{
 static short  x = 0;
 if ( ++x < 2 )
 Func1();
}

short Func2( short x )
{
 return( 2 * x );
}

Here is the sample output from the code:

/* 6 */

DoSomething
{
 Func1
 {
 Func1 {}
 }
 Func2 {}
}

A simple object-oriented example produces the following output. Note how easy it is to follow the calling sequence of the constructors and destructors for both a base class and a child class.

/* 7 */

DoSomeOopStuff
{
 BaseClass::BaseClass {}
 ChildClass::ChildClass {}
 ChildClass::MiscFunc
 {
 BaseClass::MiscFunc {}
 }
 ChildClass::~ChildClass {}
 BaseClass::~BaseClass {}
}

Summary

A fairly simple technique was given to log function names to a text file for any Think C project, including object-oriented ones. The technique can be used to debug and optimize programs as well as to investigate a program’s runtime activity.

Possible future additions include:

• Put more information into the log file, such as the stack and register contents.

• Make the output look better, perhaps by drawing a graph of the program’s runtime activity.

• Do some validity checking on the heap and file id to make sure that the system is in a reasonably safe state for writing.

• Optionally log information to MacsBug using DebugStr() instead of to the log file.

• Support for interrupt-driven code and trap patches

This would involve setting some semaphores, restoring the global variable context, and changing the I/O calls to be asynchronous. I don’t think it would be very difficult, but as most Mac programmers know, nothing is difficult until you have to implement it.

Good luck, and let me know if you find any bugs.

Listing LogToFile.c
/*
 LogToFile.c
 © 1992 Rock Ridge Enterprises. All Rights Reserved.

 Requires Think C 5.0x

 To use this file:
 1a) Turn on the profiling and stack frame options for your    
 entire project from the Edit/Options/Debugging dialog.
 
 1b) Or, if you prefer, add the following line to your files:
 #pragma options( profile, force_frame )
 Place it within a function to profile just that function,
 within a file for just that file, or in a header file.

 2)Call LogInit at program initialization. For example:
 LogInit( NULL, true, false, true );
 
 3)Call LogDInit at program de-initialization. For example:
 LogDInit();

 4)You can call SetLogState( true ) or SetLogState( false )
 to turn logging on and off dynamically.
*/

#include <string.h>
#include <files.h>
#include <errors.h>
#include <memory.h>
#include <pascal.h>

#include “LogToFile.h”

#define kMaxLogDepth 100  // maximum 100 calls deep

 // create Think C text files as output
#define kThinkCreatorID   ‘KAHL’
#define kTextFileType‘TEXT’

 // strings written to output file
#define kTabString ((void*) “\t” )
#define kReturnString((void*) “\r” )
#define kOpenBraceString  ((void*) “{“)
#define kCloseBraceString ((void*) “}”)
// exiting a function that called nobody
#define kSimpleExitString “ {}”  

 // write this string the top of every log file
#define kStringForNewFile ((void*) “” )

 // write this string between program runs when appending to
 // the log file
#define kStringForReusedFile((void*)
 “\r\r*****************************\r”)

 // the largest buffer we’ll need (+ a little extra)
#define kMaxStringLength  ( kMaxLogDepth + 100 )

 // the default output file is on the root of the boot disk
#define kBootVol -1
#define kRootDir 2L
#define kDefaultPName“\PLog File.txt”

 // don’t profile our profile handler
#pragma options( !profile )

/*
 Permanent storage for this file
*/
static short   gLogFileID = 0;
// vRefNum of log file’s disk
static shortgLogFileVol = -1; 
// true = call FlushVol after every write
static Boolean   gAlwaysFlush = false; 
// true = logging is active
static Boolean   gLogActive = false; 
static FSSpec  gDefaultFileLoc = { kBootVol, kRootDir,
 kDefaultPName };

/*
 Info on the calling chain
*/
// how many calls deep are we?
static long gDepth = 0;   
// the return addresses
static void *gStack[ kMaxLogDepth ]; 
// are braces needed for this function?
static Boolean   gNeedBraces[ kMaxLogDepth ];

/*
 Internal routine prototypes
*/
void    _profile_( void *pFuncName );
void    StringPtoC( void *source, void *target );
OSErr   WriteOpenBrace( short howDeep );
OSErr   WriteFunctionEntry( void *pString );
OSErr   WriteFunctionExit( void );
OSErr   WriteOpenBrace( short howDeep );

/**********************************
 LogInit - Call at program start
 fileLoc- set to NULL or a valid FSSpec for the                
 output file
 deleteOld- true => truncate old log file
 alwaysFlush- true => call FlushVol a lot (better log          
  file if you crash)
 startLogging  - true => turn logging on, false => don’t       
  log yet
**********************************/
OSErr LogInit( FSSpec *fileLoc, Boolean deleteOld,
 Boolean alwaysFlush, Boolean startLogging )
{
 OSErr  err = noErr;
 BooleancreatedFile = false;
 
 if ( !fileLoc )
 // use default if user doesn’t specify one
 fileLoc = &gDefaultFileLoc;

 // in case user calls init twice
 if ( !gLogFileID )
 {
 /*
 Create the file & open the data fork for writing.
 */
 err = FSpCreate( fileLoc, kThinkCreatorID,
 kTextFileType, 0 );
 if ( !err )
 createdFile = true;
 
 err = FSpOpenDF( fileLoc, fsRdWrPerm, &gLogFileID );
 if ( err ) goto DONE;
 }

 /*
 Clear out the file if the user requests it.
 */
 if ( deleteOld )
 {
 err = SetEOF( gLogFileID, 0L );
 if ( err ) goto DONE;
 }

 /*
 Append to the file
 */
 err = SetFPos( gLogFileID, fsFromLEOF, 0 );
 if ( err ) goto DONE;

 /*
 Setup the globals and write ‘****’ to the file.
 */
 gAlwaysFlush = alwaysFlush;
 gLogActive = startLogging;
 gLogFileVol = fileLoc->vRefNum;
 
 // write a header to the file
 if ( deleteOld || createdFile )
 err = WriteLogString( kStringForNewFile );
 else
 err = WriteLogString( kStringForReusedFile );
 
 DONE:
 if ( err )
 LogDInit();

 return( err );
}

/**********************************
 LogDInit - Call at program exit
**********************************/
OSErr LogDInit( void )
{
 OSErr  err;

 if ( !gLogFileID )
 return( openErr );
 
 /*
 Close the file and flush the data to the disk.
 */
 err = FSClose( gLogFileID );
 if ( !err )
 err = FlushVol( NULL, gLogFileVol );
 
 /*
 Clear out the globals so we know we’re not active.
 */
 gLogFileID = 0;
 gLogActive = false;

 return( err );
}

/**********************************
 SetLogState - Call to start or restart logging. 
 Returns previous state.
**********************************/
Boolean SetLogState( Boolean startLogging )
{
 Booleanresult;
 
 result = gLogActive;
 gLogActive = startLogging;

 return( result );
}

/**********************************
 SetLogFlush - Call to start or restart flushing. 
 Returns previous state.
**********************************/
Boolean SetLogFlush( Boolean startFlushing )
{
 Booleanresult;
 
 result = gAlwaysFlush;
 gAlwaysFlush = startFlushing;

 return( result );
}

/**********************************
 ClearLogFile - Call to zero out the log file
**********************************/
OSErr ClearLogFile( void )
{
 OSErr  err = noErr;
 OSErr  err2;
 
 if ( !gLogFileID )
 return( openErr );
 
 err = SetEOF( gLogFileID, 0L );

 if ( gAlwaysFlush )
 {
 err2 = FlushVol( NULL, gLogFileVol );
 if ( !err )
 err = err2;// return the first error to occur
 }

 return( err );
}

/**********************************
 WriteLogString - Write a C string to the log file
**********************************/
OSErr WriteLogString( void *cString )
{
 OSErr  err = noErr;
 OSErr  err2;
 long   numBytes;
 
 if ( !gLogFileID )
 return( openErr );

 /*
 Write the data to the file
 */
 numBytes = strlen( cString );
 err = FSWrite( gLogFileID, &numBytes, cString );
 
 /*
 Flush the volume if we always flush
 */
 if ( gAlwaysFlush )
 {
 err2 = FlushVol( NULL, gLogFileVol );
 if ( !err )
 err = err2;
 }

 return( err );
}

/**********************************
 StringPtoC - Convert a pascal string to a c string
**********************************/
static void StringPtoC( void *source, void *target )
{
 BlockMove( source, target, 1 + *( (unsigned char*)source ));
 PtoCstr( target );
}

/**********************************
 WriteFunctionEntry - called by _profile_ whenever a function  is entered

 The following string is written to the output:
 <CR> + <TABS> + FunctionName
 Where <CR> is a carriage return and <TABS> indicates 1        
 tab per depth.
**********************************/
static OSErr WriteFunctionEntry( void *pString )
{
 Str255 cString;
 unsigned char   theString[ kMaxStringLength ];
 long   count;
 OSErr  err;

 // convert func name to c string
 StringPtoC( pString, cString );   

 // start with a carriage return
 strcpy( (void*)theString, kReturnString );  
 
 // 1 tab for each level
 for ( count=0; count<gDepth; count++ )
 strcat( (void*)theString, kTabString );
 
 // the function name
 strcat( (void*)theString, (void*)cString );

 // write the string
 err = WriteLogString( theString );
 return( err );
}

/**********************************
 WriteFunctionExit - called whenever a function is exited
 by our exit handler

 If this function called another function, write the
 following string:
 <CR> + <TABS> + }
 Otherwise, write:
 {}
 Where <CR> is a carriage return and <TABS> indicates 1
 tab per depth.
**********************************/
static OSErr WriteFunctionExit( void )
{
 OSErr  err = noErr;
 long   count;
 unsigned char   theString[ kMaxStringLength ];
 
 if ( gNeedBraces[ gDepth ] )
 {
 // start with a carriage return
 strcpy( (void*)theString, kReturnString );
 // indent 1 tab for each level
 for ( count=0; count<gDepth; count++ )
 strcat( (void*)theString, kTabString );
 // then a close-brace
 strcat( (void*)theString, kCloseBraceString );
 }
 else
 {
 // just write “exit”
 strcpy( (void*)theString, kSimpleExitString );
 }

// write the string
 err = WriteLogString( theString );
 return( err );
}

/**********************************
 WriteOpenBrace - adds some tabs and an open brace to the
 output

 The following string is written to the output:
 <CR> + <TABS> + {
 Where <CR> is a carriage return and <TABS> indicates 1
 tab per depth.
**********************************/
static OSErr WriteOpenBrace( short howDeep )
{
 OSErr  err;
 unsigned char   theString[ kMaxStringLength ];
 
 // start with a return
 strcpy( (void*)theString, kReturnString );  
 // 1 tab per level deep
 while( howDeep- > 0 )
 strcat( (void*)theString, kTabString );
 // add an open-brace
 strcat( (void*)theString, kOpenBraceString );

 err = WriteLogString( theString );
 return( err );
}

/**********************************
 _profile_ - Called every time a function is entered
 
 This more complicated version does the following:
 1) Prints the function name to the file in
    an indented list
 2) Saves the return address of the caller’s
    caller into gStack[]
 3) Modifies the stack so that the caller returns
    to our code and not to its caller.
 4) Prints exit info when the function is exited and
    then jumps to the correct return address.
**********************************/
void _profile_( void *pFuncName )
{
 OSErr  err;

 if ( !gLogFileID ) return; // output file not opened
 if ( !gLogActive ) return; // logging is off
 if ( gDepth >= kMaxLogDepth ) return; // we’re too deep

 /*
 We have to put an open brace in the output if the parent      
 function hasn’t called any other functions until now.
 */
 if ( gDepth > 0 )
 if ( !gNeedBraces[gDepth-1] )
 {
 gNeedBraces[gDepth-1] = true;
 err = WriteOpenBrace( gDepth-1 );
 if ( err ) return;
 }

 // write the function name
 err = WriteFunctionEntry( pFuncName );
 if ( err ) return;

 gNeedBraces[ gDepth ] = false;
 

 /*
 Save the return address that the caller will return to.
 Modify the stack so that the caller will return to us         
 instead.
 */
 asm
 {
 ; gStack[ gDepth ] = return address where caller
 ; will return to

 ; A1 = &gStack[ gDepth ]
 lea.l  gStack, A1
 move.l gDepth, D0
 lsl.l  #2, D0
 adda.l D0, A1
 
 move.l (A6), A0 ; A0 = A6 from previous call
 move.l 4(A0), (A1); gStack[ gDepth ] = ret Addr
 
 ; Change the return address on the stack to
 ; point to our code
 lea    @FuncExit, A1
 move.l A1, 4(A0)

 addq.l #1, gDepth ; we’re one level deeper now
 
 ; return to caller
 unlk   A6
 rts
 }

 /*
 This code is executed when a profiled function exits
 */
 FuncExit:
 asm
 {
 move.l D0, -(SP); save return value onto stack
 subq.l #1, gDepth ; we’re one level more shallow

 ; write exit info to the file
 jsr    WriteFunctionExit

 ; get the real return address from our array
 ; and jump to it
 
 ; A0 = &gStack[ gDepth ]
 lea.l  gStack, A0
 move.l gDepth, D0
 lsl.l  #2, D0
 adda.l D0, A0
 
 move.l (SP)+, D0; restore return value
 move.l (A0), A0 ; A0=real return address
 jmp    (A0); jump to real return address
 }
}
 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Whitethorn Games combines two completely...
If you have ever gone fishing then you know that it is a lesson in patience, sitting around waiting for a bite that may never come. Well, that's because you have been doing it wrong, since as Whitehorn Games now demonstrates in new release Skate... | Read more »
Call of Duty Warzone is a Waiting Simula...
It's always fun when a splashy multiplayer game comes to mobile because they are few and far between, so I was excited to see the notification about Call of Duty: Warzone Mobile (finally) launching last week and wanted to try it out. As someone who... | Read more »
Albion Online introduces some massive ne...
Sandbox Interactive has announced an upcoming update to its flagship MMORPG Albion Online, containing massive updates to its existing guild Vs guild systems. Someone clearly rewatched the Helms Deep battle in Lord of the Rings and spent the next... | Read more »
Chucklefish announces launch date of the...
Chucklefish, the indie London-based team we probably all know from developing Terraria or their stint publishing Stardew Valley, has revealed the mobile release date for roguelike deck-builder Wildfrost. Developed by Gaziter and Deadpan Games, the... | Read more »
Netmarble opens pre-registration for act...
It has been close to three years since Netmarble announced they would be adapting the smash series Solo Leveling into a video game, and at last, they have announced the opening of pre-orders for Solo Leveling: Arise. [Read more] | Read more »
PUBG Mobile celebrates sixth anniversary...
For the past six years, PUBG Mobile has been one of the most popular shooters you can play in the palm of your hand, and Krafton is celebrating this milestone and many years of ups by teaming up with hit music man JVKE to create a special song for... | Read more »
ASTRA: Knights of Veda refuse to pump th...
In perhaps the most recent example of being incredibly eager, ASTRA: Knights of Veda has dropped its second collaboration with South Korean boyband Seventeen, named so as it consists of exactly thirteen members and a video collaboration with Lee... | Read more »
Collect all your cats and caterpillars a...
If you are growing tired of trying to build a town with your phone by using it as a tiny, ineffectual shover then fear no longer, as Independent Arts Software has announced the upcoming release of Construction Simulator 4, from the critically... | Read more »
Backbone complete its lineup of 2nd Gene...
With all the ports of big AAA games that have been coming to mobile, it is becoming more convenient than ever to own a good controller, and to help with this Backbone has announced the completion of their 2nd generation product lineup with their... | Read more »
Zenless Zone Zero opens entries for its...
miHoYo, aka HoYoverse, has become such a big name in mobile gaming that it's hard to believe that arguably their flagship title, Genshin Impact, is only three and a half years old. Now, they continue the road to the next title in their world, with... | Read more »

Price Scanner via MacPrices.net

B&H has Apple’s 13-inch M2 MacBook Airs o...
B&H Photo has 13″ MacBook Airs with M2 CPUs and 256GB of storage in stock and on sale for up to $150 off Apple’s new MSRP, starting at only $849. Free 1-2 day delivery is available to most US... Read more
M2 Mac minis on sale for $100-$200 off MSRP,...
B&H Photo has Apple’s M2-powered Mac minis back in stock and on sale today for $100-$200 off MSRP. Free 1-2 day shipping is available for most US addresses: – Mac mini M2/256GB SSD: $499, save $... Read more
Mac Studios with M2 Max and M2 Ultra CPUs on...
B&H Photo has standard-configuration Mac Studios with Apple’s M2 Max & Ultra CPUs in stock today and on Easter sale for $200 off MSRP. Their prices are the lowest available for these models... Read more
Deal Alert! B&H Photo has Apple’s 14-inch...
B&H Photo has new Gray and Black 14″ M3, M3 Pro, and M3 Max MacBook Pros on sale for $200-$300 off MSRP, starting at only $1399. B&H offers free 1-2 day delivery to most US addresses: – 14″ 8... Read more
Department Of Justice Sets Sights On Apple In...
NEWS – The ball has finally dropped on the big Apple. The ball (metaphorically speaking) — an antitrust lawsuit filed in the U.S. on March 21 by the Department of Justice (DOJ) — came down following... Read more
New 13-inch M3 MacBook Air on sale for $999,...
Amazon has Apple’s new 13″ M3 MacBook Air on sale for $100 off MSRP for the first time, now just $999 shipped. Shipping is free: – 13″ MacBook Air (8GB RAM/256GB SSD/Space Gray): $999 $100 off MSRP... Read more
Amazon has Apple’s 9th-generation WiFi iPads...
Amazon has Apple’s 9th generation 10.2″ WiFi iPads on sale for $80-$100 off MSRP, starting only $249. Their prices are the lowest available for new iPads anywhere: – 10″ 64GB WiFi iPad (Space Gray or... Read more
Discounted 14-inch M3 MacBook Pros with 16GB...
Apple retailer Expercom has 14″ MacBook Pros with M3 CPUs and 16GB of standard memory discounted by up to $120 off Apple’s MSRP: – 14″ M3 MacBook Pro (16GB RAM/256GB SSD): $1691.06 $108 off MSRP – 14... Read more
Clearance 15-inch M2 MacBook Airs on sale for...
B&H Photo has Apple’s 15″ MacBook Airs with M2 CPUs (8GB RAM/256GB SSD) in stock today and on clearance sale for $999 in all four colors. Free 1-2 delivery is available to most US addresses.... Read more
Clearance 13-inch M1 MacBook Airs drop to onl...
B&H has Apple’s base 13″ M1 MacBook Air (Space Gray, Silver, & Gold) in stock and on clearance sale today for $300 off MSRP, only $699. Free 1-2 day shipping is available to most addresses in... Read more

Jobs Board

Medical Assistant - Surgical Oncology- *Apple...
Medical Assistant - Surgical Oncology- Apple Hill Location: WellSpan Medical Group, York, PA Schedule: Full Time Sign-On Bonus Eligible Remote/Hybrid Regular Apply Read more
Omnichannel Associate - *Apple* Blossom Mal...
Omnichannel Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Cashier - *Apple* Blossom Mall - JCPenney (...
Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Mall Read more
Operations Associate - *Apple* Blossom Mall...
Operations Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Business Analyst | *Apple* Pay - Banco Popu...
Business Analyst | Apple PayApply now " Apply now + Apply Now + Start applying with LinkedIn Start + Please wait Date:Mar 19, 2024 Location: San Juan-Cupey, PR Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.