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

BetterTouchTool 2.305 - Customize multi-...
BetterTouchTool adds many new, fully customizable gestures to the Magic Mouse, Multi-Touch MacBook trackpad, and Magic Trackpad. These gestures are customizable: Magic Mouse: Pinch in / out (zoom... Read more
calibre 3.8.0 - Complete e-book library...
Calibre is a complete e-book library manager. Organize your collection, convert your books to multiple formats, and sync with all of your devices. Let Calibre be your multi-tasking digital librarian... Read more
Carbon Copy Cloner 5.0.2 - Easy-to-use b...
Carbon Copy Cloner backups are better than ordinary backups. Suppose the unthinkable happens while you're under deadline to finish a project: your Mac is unresponsive and all you hear is an ominous,... Read more
Evernote 6.12.3 - Create searchable note...
Evernote allows you to easily capture information in any environment using whatever device or platform you find most convenient, and makes this information accessible and searchable at anytime, from... Read more
Default Folder X 5.1.6 - Enhances Open a...
Default Folder X attaches a toolbar to the right side of the Open and Save dialogs in any OS X-native application. The toolbar gives you fast access to various folders and commands. You just click on... Read more
Geekbench 4.1.2 - Measure processor and...
Geekbench provides a comprehensive set of benchmarks engineered to quickly and accurately measure processor and memory performance. Designed to make benchmarks easy to run and easy to understand,... Read more
GraphicConverter 10.5 - $39.95
GraphicConverter is an all-purpose image-editing program that can import 200 different graphic-based formats, edit the image, and export it to any of 80 available file formats. The high-end editing... Read more
Dropbox 35.4.20 - Cloud backup and synch...
Dropbox is an application that creates a special Finder folder that automatically syncs online and between your computers. It allows you to both backup files and keep them up-to-date between systems... Read more
XMind 8 Update 4 - Popular mind mapping...
XMind is the most popular mind mapping tool. Millions of people use XMind to clarify thinking, manage complex information, run brainstorming and get work organized. It couldn't be easier to get... Read more
Safari Technology Preview 11.1 - The new...
Safari Technology Preview contains the most recent additions and improvements to WebKit and the latest advances in Safari web technologies. And once installed, you will receive notifications of... Read more

Stormbound: Kingdom Wars guide - how to...
Stormbound: Kingdom Wars is an excellent new RTS turned card battler out now on iOS and Android. Lovers of strategy will get a lot of enjoyment out of Stormbound's chess-like mechanics, and it's cardbased units are perfect for anyone who loves the... | Read more »
The best AR apps and games on iOS right...
iOS 11 has officially launched, and with it comes Apple's ARKit, a helpful framework that makes it easier than ever for developers to create mobile AR experiences. To celebrate the occassion, we're featuring some of the best AR apps and games on... | Read more »
Phoenix Wright: Ace Attorney - Spirit of...
Phoenix Wright: Ace Attorney - Spirit of Justice 1.00.00 Device: iOS Universal Category: Games Price: $.99, Version: 1.00.00 (iTunes) Description: ************************************************※IMPORTANT※・Please read the “When... | Read more »
Kpressor (Utilities)
Kpressor 1.0.0 Device: iOS Universal Category: Utilities Price: $4.99, Version: 1.0.0 (iTunes) Description: The ultimate ZIP compression application for iPhone and iPad. - Full integration of iOS 11 with support for multitasking.-... | Read more »
Find out how you can save £35 and win a...
Nothing raises excitement like a good competition, and we’re thrilled to announce our latest contest. We’ll be sending one lucky reader and a friend to the Summoners War World Arena Championship at Le Comedia in Paris on October 7th. It’s the... | Read more »
Another Lost Phone: Laura's Story...
Another Lost Phone: Laura's Story 1.0 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0 (iTunes) Description: Another Lost Phone is a game about exploring the social life of a young woman whose phone you have just... | Read more »
The Witness (Games)
The Witness 1.0 Device: iOS Universal Category: Games Price: $9.99, Version: 1.0 (iTunes) Description: You wake up, alone, on a strange island full of puzzles that will challenge and surprise you. You don't remember who you are, and... | Read more »
Egg, Inc. guide - how to build your gold...
Egg, Inc.'s been around for some time now, but don't you believe for one second that this quirky clicker game has gone out of style. The game keeps popping up on Reddit and other community forums thanks to the outlandish gameplay (plus, the... | Read more »
The best deals on the App Store this wee...
Good news, everyone! Your favorite day of the week has arrived at last -- it's discount roundup day! This fine Wednesday evening we're gathering up the hottest deals on the App Store. We've got action platformers, we've got puzzle games, we've got... | Read more »
Morphite (Games)
Morphite 1.08 Device: iOS Universal Category: Games Price: $7.99, Version: 1.08 (iTunes) Description: | Read more »

Price Scanner via MacPrices.net

Apple Offering Up To $455 Credit Toward iPhon...
iPhone 8 and 8 Plus are now available at the Apple Store, and you can receive up to $375 credit toward a new iPhone purchase when you trade in your eligible smartphone. Photo Courtesy Apple Just... Read more
AnyTrans Offers iOS Users Three Ways For Movi...
iMobie Inc. today announceed AnyTrans v6.0.1, which now can help iOS users move all data to iPhone 8/8 Plus seamlessly. The software is available both on Mac and Windows and fully able to move all... Read more
Snag a 13-inch 2.3GHz MacBook Pro for $100 of...
B&H Photo has 2017 13″ 2.3GHz MacBook Pros in stock today and on sale for $100 off MSRP, each including free shipping plus NY & NJ sales tax only: – 13-inch 2.3GHz/128GB Space Gray MacBook... Read more
Verizon offers new iPhone 8 for $100-$300 off...
Verizon is offering the new iPhone 8 for up to $300 off MSRP with an eligible trade-in: • $300 off: iPhone 6S/6S Plus/7/7 Plus, Google Pixel XL, LG G6, Moto Z2 Force, Samsung Galaxy S7/S7 edge/S8/S8... Read more
Apple Refurbished 2017 13-inch MacBook Pros a...
Apple has Certified Refurbished 2017 13″ Touch Bar MacBook Pros in stock today and available for $200-$300 off MSRP. A standard Apple one-year warranty is included with each MacBook, and shipping is... Read more
OWC USB-C Travel Dock with 5 Ports Connectivi...
OWC have announced the new OWC USB-C Travel Dock, the latest addition to their line of connectivity solutions. The USB-C Travel Dock lets you connect its integrated USB-C cable to a Mac or PC laptop... Read more
Pelican Products, Inc. Unveils Cases For All...
Pelican Products, Inc. has announced the launch of its full line of cases including Voyager, Adventurer, Protector, Ambassador, Interceptor (for the Apple iPhone 8 and 8 Plus backwards compatible... Read more
$100 off new 2017 13-inch MacBook Airs
B&H Photo has 2017 13″ MacBook Airs on sale today for $100 off MSRP including free shipping. B&H charges NY & NJ sales tax only: – 13″ 1.8GHz/128GB MacBook Air (MQD32LL/A): $899, $100 off... Read more
Apple restocks Certified Refurbished 13-inch...
Apple has Certified Refurbished 2015 13″ MacBook Airs available starting at $719 and 2016 models available starting at $809. An Apple one-year warranty is included with each MacBook, and shipping is... Read more
Is iPhone X Really The Future Of The Smartpho...
Should iPhone X even be called a telephone? It does of course support telephony and texting, but its main feature set is oriented to other things. It is also debatable whether it makes any rational... Read more

Jobs Board

Specialist - Retail Customer Services and Sal...
The position listed below is not with Tennessee Interviews but with Apple , Inc. Tennessee Interviews is a private organization that works in collaboration with Read more
Specialist - Retail Customer Services and Sal...
The position listed below is not with South Carolina Interviews but with Apple , Inc. South Carolina Interviews is a private organization that works in collaboration Read more
Behavior Technician with *Apple* Consulting...
The position listed below is not with Washington Interviews but with APPLE CONSULTING Washington Interviews is a private organization that works in collaboration Read more
Infection Control RN with *Apple* Rehab - A...
The position listed below is not with Georgia Interviews but with Apple Rehab Georgia Interviews is a private organization that works in collaboration with Read more
Specialist - Retail Customer Services and Sal...
The position listed below is not with South Carolina Interviews but with Apple , Inc. South Carolina Interviews is a private organization that works in collaboration Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.