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

Chromium 119.0.6044.0 - Fast and stable...
Chromium is an open-source browser project that aims to build a safer, faster, and more stable way for all Internet users to experience the web. List of changes available here. Version for Apple... Read more
Spotify 1.2.21.1104 - Stream music, crea...
Spotify is a streaming music service that gives you on-demand access to millions of songs. Whether you like driving rock, silky R&B, or grandiose classical music, Spotify's massive catalogue puts... Read more
Tor Browser 12.5.5 - Anonymize Web brows...
Using Tor Browser you can protect yourself against tracking, surveillance, and censorship. Tor was originally designed, implemented, and deployed as a third-generation onion-routing project of the U.... Read more
Malwarebytes 4.21.9.5141 - Adware remova...
Malwarebytes (was AdwareMedic) helps you get your Mac experience back. Malwarebytes scans for and removes code that degrades system performance or attacks your system. Making your Mac once again your... Read more
TinkerTool 9.5 - Expanded preference set...
TinkerTool is an application that gives you access to additional preference settings Apple has built into Mac OS X. This allows to activate hidden features in the operating system and in some of the... Read more
Paragon NTFS 15.11.839 - Provides full r...
Paragon NTFS breaks down the barriers between Windows and macOS. Paragon NTFS effectively solves the communication problems between the Mac system and NTFS. Write, edit, copy, move, delete files on... Read more
Apple Safari 17 - Apple's Web brows...
Apple Safari is Apple's web browser that comes bundled with the most recent macOS. Safari is faster and more energy efficient than other browsers, so sites are more responsive and your notebook... Read more
Firefox 118.0 - Fast, safe Web browser.
Firefox offers a fast, safe Web browsing experience. Browse quickly, securely, and effortlessly. With its industry-leading features, Firefox is the choice of Web development professionals and casual... Read more
ClamXAV 3.6.1 - Virus checker based on C...
ClamXAV is a popular virus checker for OS X. Time to take control ClamXAV keeps threats at bay and puts you firmly in charge of your Mac’s security. Scan a specific file or your entire hard drive.... Read more
SuperDuper! 3.8 - Advanced disk cloning/...
SuperDuper! is an advanced, yet easy to use disk copying program. It can, of course, make a straight copy, or "clone" - useful when you want to move all your data from one machine to another, or do a... Read more

Latest Forum Discussions

See All

‘Monster Hunter Now’ October Events Incl...
Niantic and Capcom have just announced this month’s plans for the real world hunting action RPG Monster Hunter Now (Free) for iOS and Android. If you’ve not played it yet, read my launch week review of it here. | Read more »
Listener Emails and the iPhone 15! – The...
In this week’s episode of The TouchArcade Show we finally get to a backlog of emails that have been hanging out in our inbox for, oh, about a month or so. We love getting emails as they always lead to interesting discussion about a variety of topics... | Read more »
TouchArcade Game of the Week: ‘Cypher 00...
This doesn’t happen too often, but occasionally there will be an Apple Arcade game that I adore so much I just have to pick it as the Game of the Week. Well, here we are, and Cypher 007 is one of those games. The big key point here is that Cypher... | Read more »
SwitchArcade Round-Up: ‘EA Sports FC 24’...
Hello gentle readers, and welcome to the SwitchArcade Round-Up for September 29th, 2023. In today’s article, we’ve got a ton of news to go over. Just a lot going on today, I suppose. After that, there are quite a few new releases to look at... | Read more »
‘Storyteller’ Mobile Review – Perfect fo...
I first played Daniel Benmergui’s Storyteller (Free) through its Nintendo Switch and Steam releases. Read my original review of it here. Since then, a lot of friends who played the game enjoyed it, but thought it was overpriced given the short... | Read more »
An Interview with the Legendary Yu Suzuk...
One of the cool things about my job is that every once in a while, I get to talk to the people behind the games. It’s always a pleasure. Well, today we have a really special one for you, dear friends. Mr. Yu Suzuki of Ys Net, the force behind such... | Read more »
New ‘Marvel Snap’ Update Has Balance Adj...
As we wait for the information on the new season to drop, we shall have to content ourselves with looking at the latest update to Marvel Snap (Free). It’s just a balance update, but it makes some very big changes that combined with the arrival of... | Read more »
‘Honkai Star Rail’ Version 1.4 Update Re...
At Sony’s recently-aired presentation, HoYoverse announced the Honkai Star Rail (Free) PS5 release date. Most people speculated that the next major update would arrive alongside the PS5 release. | Read more »
‘Omniheroes’ Major Update “Tide’s Cadenc...
What secrets do the depths of the sea hold? Omniheroes is revealing the mysteries of the deep with its latest “Tide’s Cadence" update, where you can look forward to scoring a free Valkyrie and limited skin among other login rewards like the 2nd... | Read more »
Recruit yourself some run-and-gun royalt...
It is always nice to see the return of a series that has lost a bit of its global staying power, and thanks to Lilith Games' latest collaboration, Warpath will be playing host the the run-and-gun legend that is Metal Slug 3. [Read more] | Read more »

Price Scanner via MacPrices.net

Clearance M1 Max Mac Studio available today a...
Apple has clearance M1 Max Mac Studios available in their Certified Refurbished store for $270 off original MSRP. Each Mac Studio comes with Apple’s one-year warranty, and shipping is free: – Mac... Read more
Apple continues to offer 24-inch iMacs for up...
Apple has a full range of 24-inch M1 iMacs available today in their Certified Refurbished store. Models are available starting at only $1099 and range up to $260 off original MSRP. Each iMac is in... Read more
Final weekend for Apple’s 2023 Back to School...
This is the final weekend for Apple’s Back to School Promotion 2023. It remains active until Monday, October 2nd. Education customers receive a free $150 Apple Gift Card with the purchase of a new... Read more
Apple drops prices on refurbished 13-inch M2...
Apple has dropped prices on standard-configuration 13″ M2 MacBook Pros, Certified Refurbished, to as low as $1099 and ranging up to $230 off MSRP. These are the cheapest 13″ M2 MacBook Pros for sale... Read more
14-inch M2 Max MacBook Pro on sale for $300 o...
B&H Photo has the Space Gray 14″ 30-Core GPU M2 Max MacBook Pro in stock and on sale today for $2799 including free 1-2 day shipping. Their price is $300 off Apple’s MSRP, and it’s the lowest... Read more
Apple is now selling Certified Refurbished M2...
Apple has added a full line of standard-configuration M2 Max and M2 Ultra Mac Studios available in their Certified Refurbished section starting at only $1699 and ranging up to $600 off MSRP. Each Mac... Read more
New sale: 13-inch M2 MacBook Airs starting at...
B&H Photo has 13″ MacBook Airs with M2 CPUs in stock today and on sale for $200 off Apple’s MSRP with prices available starting at only $899. Free 1-2 day delivery is available to most US... Read more
Apple has all 15-inch M2 MacBook Airs in stoc...
Apple has Certified Refurbished 15″ M2 MacBook Airs in stock today starting at only $1099 and ranging up to $230 off MSRP. These are the cheapest M2-powered 15″ MacBook Airs for sale today at Apple.... Read more
In stock: Clearance M1 Ultra Mac Studios for...
Apple has clearance M1 Ultra Mac Studios available in their Certified Refurbished store for $540 off original MSRP. Each Mac Studio comes with Apple’s one-year warranty, and shipping is free: – Mac... Read more
Back on sale: Apple’s M2 Mac minis for $100 o...
B&H Photo has Apple’s M2-powered Mac minis back in stock and on sale today for $100 off MSRP. Free 1-2 day shipping is available for most US addresses: – Mac mini M2/256GB SSD: $499, save $100 –... Read more

Jobs Board

Licensed Dental Hygienist - *Apple* River -...
Park Dental Apple River in Somerset, WI is seeking a compassionate, professional Dental Hygienist to join our team-oriented practice. COMPETITIVE PAY AND SIGN-ON Read more
Sublease Associate Optometrist- *Apple* Val...
Sublease Associate Optometrist- Apple Valley, CA- Target Optical Date: Sep 30, 2023 Brand: Target Optical Location: Apple Valley, CA, US, 92307 **Requisition Read more
*Apple* / Mac Administrator - JAMF - Amentum...
Amentum is seeking an ** Apple / Mac Administrator - JAMF** to provide support with the Apple Ecosystem to include hardware and software to join our team and Read more
Child Care Teacher - Glenda Drive/ *Apple* V...
Child Care Teacher - Glenda Drive/ Apple ValleyTeacher Share by Email Share on LinkedIn Share on Twitter 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
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.