Assembly - beware local label names with "-dead_strip" option!
I came across a very strange bug whilst developing an iOS application whereby the application would seg fault and whilst stepping through the code I found it was going all over the place. This lead me to run the application through otool, and I discovered that half the code for a function was missing!
Here is an example of what happened…
Consider the following C file. It’s just a very simple function that has some inline assembly (to count down from 10 to 0) and a very simple function that does absolutely nothing.
That all looks fairly normal and we could quite happily believe that was going to work just fine. So, let’s use this function in a test application. The code below is just a very simple application that calls the ‘func()’ function and then returns.
12345
voidfunc();intmain(){func();return0;}
So now let’s see what happens when we link this with the same options that you would have on by default in an iOS application (hint: -dead_strip is active).
Output from otool -vV -t on the linked application:
What?! Where’s the rest of the func() code?! Not only is it missing, but it would appear that func() just simply stops without ever returning?! That looks very suspicious… So, let’s compile it without the -dead_strip option:
Output from ‘otool -vV -t’ on the linked application:
Ah, that’s better! The loop is back and so is the return. Also, funcB() is still in there. So, what has happened you may ask. Well, -dead_strip is designed to remove symbols from a binary that are not required. So we’d expect funcB to be removed, but not .loop as it’s part of func. However, if we look closer, what has happened is that .loop has become a top level symbol rather than a symbol local to the func symbol. So -dead_strip assumed that it was a symbol not used anywhere (as it’s only accessed from within .loop itself) and so it removed it, resulting in a completely mangled application binary.
To stop this happening you must always prefix your local symbols with ‘L’ (as per the GCC documentation!). But I think this is an excellent example of what can go wrong if you just do 1 tiny thing wrong with inline assembly.
As a side note, I decided to try compiling/assembling/linking all of the above for Android as well. Interestingly the results were different. First I’ll show the outputs of the various stages and then explain the results.
You can see that the Android GCC has done a much better job at stripping out the symbols. This is because of a subtle .size attribute given to functions in the assembly for Linux (and therefore Android) which tells the assembler how big the function is. However, this doesn’t exist on Mac and the size is calculated by taking the distance between the start of a symbol and the next symbol.
This actually helps us to understand a bit more what actually went wrong. If you look back up at the assembly generated for iOS and Android you’ll see that the .loop appears as a top level symbol, which is confirmed by running ‘nm’ on the resulting object file. This sounds all wrong, since the .loop symbol should really be local to the func() function. The reason being because you need to prefix local symbols with ‘L’. If we change .loop for Lloop in the sample file, then these are the resulting outputs showing the iOS linker doing the right thing this time even with -dead_strip enabled.
This all serves to illustrate the point that it’s worth knowing about the options of your compiler, assembler & linker and understanding how everything fits together.