1

Question

My app has some bugs that only occur on older macOS versions. How can I debug them?


Approaches

Below are all of the approaches I'm aware of.

Approach 1: Debug using Xcode

The idea is to just use Xcode to build and debug the project on the older macOS version.

Unfortunately the latest version of Xcode that is available on the old macOS cannot open my .xcodeproj file.

I have come accross 2 possible solutions:

1.2 Change Project Format

I have set the Project Format to be compatible with a very old version of Xcode (8.0) but that didn't help.

2.2. Change manually edit project files

You can manually edit the project file with a text editor and decrease the objectVersion to get the project to open. See this SO Answer.

This worked for me.

To get the project to compile on the old macOS version, I had to do a few more hacks:

  • Set objectVersion to 46
  • Comment out all code that uses unavailable APIs
  • Set 'minimumToolsVersion' in Interface Builder files your Xcode version
  • Set the Code Signing Identity to "Ad Hoc Code Sign" and disable hardened runtime on all targets

After these steps I could build and debug my app on the old macOS version! Some things didn't compile properly, but luckily, in my case, this was good enough to figure out all of the bugs that were occuring on older macOS versions!

Update:

I just tried to do the same thing with a Swift project (The other one was ObjC only) and it was so much work that I almost gave up on it. See the bottom of this post for more info.

Approach 2: Debugging from the command-line

The second idea is to use the lldb debugger directly from the command line on the old macOS. This works fine. But the problem here is that it will only show me assembly code. To debug in an efficient way you want to be able to step through source code line by line. This is achievable but it's complicated:

  • The following data needs to be present on the old macOS:
    • The app/executable itself
    • The source code used to build that executable
    • Some sort of 'debug data' that links machine instructions in the executable to lines in the the source code
  • You need to tell lldb how to link that data together

Here's an article on how to do this: https://medium.com/@maxraskin/background-1b4b6a9c65be

This approach is very promising but I gave up on it for now because the setup and debugging workflow sounds very slow and tedious and Approach 3 seemed to be much easier. However I can't get Approach 3 to work at all so far. I'll update this once I look into it more.

Sidenote about 'debug data'

I don't really understand some things about the 'debug data' mentioned above.

From what I gathered, this debug data normally comes in the so called DWARF data format. Debuggers usually extract this DWARF data directly from .o files. .o files are a byproduct when compiling C code (and code in other languages too?).

However for storing and transferring the DWARF data to other machines you can also store it in so called .dSym files.

Now what confuses me is what role does 'code stripping' play in all of this? Because code stripping is described on the internet to "remove debug data from the exectable". My question is - what kind of debug data is in the binaries when you don't strip them? Is it the same DWARF data from the .dSym and .o files which can be used to step through code line by line in a debugger? Or is it a subset of the DWARF data? Or is it something completely different?

Either way here's approach 3:

Approach 3: Remote debugging from the command-line

The lldb debugger has the built-in capability to connect to another machine remotely, then automatically upload the executable you want to debug to the remote machine, and then run and debug that executable.

In theory, this should also let the debugger automatically locate the source code files and the DWARF data - allowing you to step through source code line by line without any extra effort.

This would make the approach much more convenient than Approach 2! So I gave it a try, using the official tutorial.

There are many things that aren't explained in the official tutorial, and it was very hard to Google the various problems I ran into. Here are some things that I had to do which are not mentioned in the tutorial:

  • I downloaded the latest Xcode and got the debugserver and lldb command-line-tools that are buried deep inside that app bundle. I ran debugserver on the remote machine and connected to it using lldb on the local machine.
    • The official tutorial talks about using the lldb-server command-line-tool instead of debugserver. But I couldn't find lldb-server in the latest Xcode app bundle nor the latest Xcode Command Line Tools. (The ones that show up at /Library/Developer/CommandLineTools after using xcode-select --install). Older Xcode versions still contained both lldb-server and debugserver and from my testing they behave the same. Only the command line arguments they take are a little different. So I used debugserver instead of lldb-server.
  • Then I had to open a wifi hotspot on the remote machine using the Create Network... option in the menu bar, and then connect the local machine to that wifi hotspot.

On the remote machine I started the debugserver with this command debugserver 0.0.0.0:1234. 0.0.0.0 means "accept connections from any IP address" and 1234 means only accept connections on port '1234'

On the local machine I started lldb and inside the command prompt I used the following commands:

  • platform select remote-macosx
  • platform connect connect://<remote ip address>:1234
    • Replacing <Remote ip address> with the IP address of the remote machine which you can find under System Preferences > Network.
    • 1234 means 'connect on port 1234'
    • I don't know where the structure of this URL comes from. I found it by accident
    • After this, lldb on local and debugserver on remote will both say they connected successfully.
  • target create <path to appbundle>
    • Replacing <path to appbundle> with the appropriate path
    • After this, lldb looks like it's uploading the files to the remote machine, but debugserver on the remote machine won't react. Not sure if that's normal.
  • process launch
    • This should launch the app on the remote machine, but instead it just give this cryptic error: error: attach failed: invalid host:port specification: '[<Remote ip address>]'. (Where <Remote ip address> is the actual IP address of the remote machine).

I tested this many times on different macOS versions. Local was always Ventura 13.0 and remote was 10.14 or 10.13. Both lldb and debugserver had version lldb-1400.0.38.13.

I don't know what else to try to make remote debugging using lldb work.

Sidenote about 'GDB'

I haven't looked into using the classic gdb debugger yet. Should I? I heard it's less buggy than lldb and I assume my problems with remote debugging are due to bugs in lldb. If it worked as advertised, remote debugging should be by far the easiest way to solve my problem.

I'll update this if I learn more about using GDB for remote debugging.


I'll be very grateful for any tips or clarifications! I will also update this post if I find out more, so this can hopefully be a useful resource for anyone trying to debug a Mac app on an older macOS version.



Update/Conclusion

Approach 1.2 worked for me!

(Approach 1.2 is getting the project to build in an older Xcode by manually editing project files.)

If you want to further explore the other approaches I've come across, here are my thoughts on how they compare with Approach 1.

Comparison to Approach 2

(Approach 2 is debugging from the command-line)

Pros of Approach 1 Much nicer debugging workflow - You can use the Xcode GUI instead of the command-line, and you won't have to copy over 3 different files to a new machine every time you want to test a change to your code.

Pros of Approach 2 You don't rely on hacking the project files which might not work for all situations. E.g. this might introduce new bugs in your compilation target that interfere with the bugs you actually want to debug.

Comparison to Approach 3

(Approach 3 is remote debugging from the command-line.)

Approach 3 is the holy grail. The debugging workflow would be very nice, no manually transferring files between computers like Approach 2, no weird brittle hacks like Approach 1.

Sadly I couldn't get Approach 3 to to work after days of trying, so I've given up on it now. If you have tips on how to make it work please do let me know!

Update 2

Approach 1.2 worked for my orginal project which was ObjC only. I've now tried to apply it to a Swift project and it's so much more work.

I had to spend hours rewriting code which was written for Swift 5.6 to compile under Swift 5.1 - it did work in the end but it took hours. Unfortunately I haven't found a way to get a newer Swift version to compile under the older macOS, so I had to resort to this.

So if you're using Swift in your project, Approach 1.2 might be too much work to be feasible.

Update 3

Another possible solution might be using a Virtual Machine but I can't find any examples for how to do this on the internet.

Noah Nuebling
  • 219
  • 2
  • 11
  • 1
    Approach 4: Debugging in Xcode without source code. It's assembly code with Xcode breakpoints and debug area. Create an empty project, add a scheme and set the executable. – Willeke Oct 28 '22 at 00:19
  • Do not turn the question into an answer. If you have an answer to your own question, give it as an Answer. – matt Nov 03 '22 at 18:51
  • There is no clear answer. I was exploring options and documenting my findings. I could move the exploration of the answer into an "Answer" if that's better. – Noah Nuebling Nov 04 '22 at 11:59
  • 1
    You aren't using a "platform" since you are talking to a debugserver directly not an "lldb-server in platform mode"... So you want to do `process connect` not `platform connect`. There's an alias: `gdb-remote hostname:port` that will correctly invoke `process connect`. The other thing you have to be careful about is if your binary is not in the same place on host & target, you should tell it where it is on the remote by passing `--remote-file` as part of the `target create` line. Those were the only two things I saw wrong in your method 3. – Jim Ingham Mar 08 '23 at 01:49

0 Answers0