- Get a functional dev environment set up where you can mess around with things in a risk-free manner. This includes setting up any dev databases and other external dependencies so that you can add, update and delete data at will. There's nothing that gives more insight than changing a piece of code and seeing what it breaks or alters. Change a lot of things, one at a time.
- Dive deep. This is time consuming, but don't be satisfied with understanding a surface feature only. You must recursively learn the functions, modules and architecture those surface features are using as well until you get to the bottom of the stack. Once you know where the bottom is you know what everything else is based on. This knowledge will help you uncover tricky bugs later if you truly grok what's going on. It will also give you insight as to the complexity of the project (and whether it's inherent to the problem or unnecessary). This can take a lot of time, but it pays off the most.
- Read and run the tests (if any). The tests are (usually) a very clear and simple insight into otherwise complex functionality. This method should do this, this class should do that, we need to mock this other external dependency, etc.
- Read the documentation and comments (if any). This can really help you understand the how's and why's depending on the conscientiousness of the prior engineers.
- If there's something that you really can't untangle, contact the source. Tell him what you're attempting, what you tried, exactly why and how it's not working as you expect, and ask if there's a simple resolution (I don't want to waste your time if there's not). You may not get an answer, but if you've done a lot of digging already and communicate the issue clearly you might get a "Oh yeah, there's a bug with XYZ due to the interaction with the ABC library. I haven't had time to fix it but the problem is in the foo/bar file." You may be able to find a workaround or fix the bug yourself.
- When you do become comfortable enough to add features or fix issues, put forward the effort to find the right place in the code to do this. If you think it requires refactoring other things first, do this in as atomic a manner as possible and consult first with other contributors.
- Pick a simple task to attack first, even if it's imaginary. Get to more complicated stuff after you've done some legwork already.
There are other minor things but this is generally my approach.