This is my understanding:
At any point, your client will know the hashes of the tips of all the chains it knows about (there is more than one as you have to cope with chain forks). When you start up, you send all those tips in a 'getblocks' command. The messages in bitcoin are all very badly named. 'getblocks' doesn't get blocks at all; it is an announcement to your peer of blocks you do have.
The peer will look at the chain tips you have and will see which of them falls on what it considers the current "best" chain. It will return an 'inv' with N block hashes that follow the best one. The hashes of the blocks it doesn't think are on the best chain will be ignored. Here's the extra bit: if it runs out of space in the 'inv' before it runs out of blocks it notes the last block hash it's sending you as a "continuation hash".
Your client then starts grabbing those 'inv' offered blocks using 'getdata' for a full block or 'getheaders' for the header only.
The peer will respond normally to most of the 'getdata' requests you send. However, when you request the hash it previously noted as a "continuation hash"; it then sends an 'inv' containing the hash of what it considers the current best chain.
Your client requests that block (since it doesn't have it) and notes that it doesn't have the parent either; therefore there are blocks missing from your chain, and you must start the process again -- sending another 'getblocks' listing it's current chain tips (probably only one at this point).
Personally, I think it's not a good way of doing it, and would prefer that the chain was downloaded backwards (since every block lists its parent there would be no need for the hoops this "continuation hash" stuff); but it is what it is.
My own woefully incomplete (but I hope clearer and better commented than the official client) client is available here:
https://github.com/andyparkins/additup