Debugging .NET Core app from a command line on Linux

Million years ago, way before the ice age, I was preparing small C++ project for “Unix Programming” university course and at some point had to debug it via command line. That was mind blowing. And surprisingly productive. Apparently, when nothing stands in the way, especially UI, debugging can become incredibly focused.

Since .NET Framework got his cross platform twin brother .NET Core, I was looking forward to repeat the trick and debug .NET Core app on Ubuntu via command line. Few days ago it finally happened and even though it wasn’t a smooth ride, that was quite an interesting experience. So, let’s have look.

Setup

We’ll need Ubuntu, .NET Core SDK, lldb debugger and a sample app. Late April, 2018 was the month of updates, so now we have shiny new Ubuntu 18.04 and .NET Core SDK 2.1 RC1, which finally got its libsosplugin.so compiled against lldb-3.9, so v3.6 we had to use for previous .NETs finally can rest in peace. As for demo project, any .NET Core hello-world wannabe with local variables and call stacks will do.

The tools

I’ll install all of that in a VM and here’s Vagrantfile with its provision.sh file to do so:

The project

It’s very simple. Let’s have a function that returns a number of ticks passed since last measurement. It’s absolutely useless except for the fact that it will have local variables and some arguments to examine later.

Attaching debugger

OK, so let’s start the whole thing and try to connect debugger to it:

The program is producing no output and running in a background thread, so knowing the process id (6124) I can start lldb, load SOS plugin and connect to dotnet process:

Nothing difficult. We can make sure that SOS has kicked in by examining e.g. managed threads before moving on to setting up an actual breakpoint:

Setting up a breakpoint

For that we can use bpmd command introduced by SOS plugin. The only parameter it needs is a method name or its descriptor address, so in order to set up a breakpoint in a method called GetTicksElapsed, which is a member of Program class inside of console.dll assembly, here’s what I’d do:

As a side note, sometimes it might be actually easier to add a breakpoint by method descriptor address instead. Those are all over the place – in call stacks, instruction pointers, in class method tables. For instance, this is the call stack of currently selected thread:

And this how setting up a breakpoint in Main method might look like:

Easy peasy. Now, let’s resume the program with program continue  command and see how it almost immediately stops at GetLastTicks:

Good. Now it’s time to look around.

Examining call stacks

clrstack (or sos ClrStack) does exactly that:

The output is pretty straightforward. Even line numbers and file names are there. But clrstack can do more than that. For instance, it also has -p  parameter, which will include function arguments into the output, and that’s something really, really useful:

Let’s examine what we’ve got. lastTicks value is 0x8d5bacfae88e0b3, which is 636620323491995827 in decimal, which indeed corresponds to current UTC date:

UTC Date

As for args argument, most likely that’s program’s command line arguments and this is something easy to confirm:

Yup, that’s empty array of strings allright.

Examining local variables

Bad news here. clrstack -i is the command to see local variables and their values. However, in lldb-3.9 and libsosplugin.so, which comes with .NET Core 2.1 RC1, this command immediately causes segmentation fault and crash of lldb process. Haven’t checked it in earlier versions, but here it happens 100% of the time.

Stepping in/over/out

Unlike with WinDBG, it doesn’t look like there are SOS commands for stepping in, out or over the next statement or function. However, there’re still native commands, which will step over assembly instructions, but it’s better than nothing. Especially when we can call clrstack and check where in managed realm we currently are:

callq, if I’m not mistaken, is the instruction to execute procedure at given address, so step will probably jump us somewhere:

Yup, we’re in DateTime.Now‘s getter now. I actually could test where callq  leads without stepping into the method itself. Checking callq‘s argument would tell the same story:

Stepping out from current procedure could’ve been easy, if it didn’t jump 2 levels up instead of one every other time I used it. Maybe it has something to do with the fact that real call stack is actually a mixture of managed and unmanaged entries, whereas clrstack command shows only managed ones (-f argument would show all of them). Then, I couldn’t find an alias for step-out  command and had to use its full form instead: thread step-out.

Conclusion

So this is how debugging a .NET Core app from a command line on Linux feels like. It’s surprisingly hard to find any documentation for it, so probably I missed a command or to. Faulting clrstack -i also doesn’t make the debugging easier. But still it’s really cool to be able to add a breakpoint, see how execution goes and examine call stack parameters – all from a command line, on any machine and for any project. It’s also surprising to see how thin the layer between a managed code and assembly language is. If I was able to convert callq instruction argument to managed method description, maybe I’ll be able to convert CPU register values to managed objects as well. Who knows.

Leave a Reply

Your email address will not be published. Required fields are marked *