Chapter 8. Debugging

This chapter covers

  • Debuggers in the Visual Studio line of products
  • WinDBG/CDB—an advanced debugger that works from the command line
  • LLDB for debugging on Linux and macOS
  • SOS—the extension that makes CDB and LLDB work with .NET Core

Debuggers are valuable tools when developing any kind of software. Most of them are fairly intuitive to use, which may make you wonder why I would dedicate a chapter to this subject.

Many developers, especially if they’re used to Visual Studio and the .NET Framework, don’t realize what options are available for debugging in other editors or on other operating systems. Also, command-line debuggers still have a place in the modern developer’s toolbox because they can do powerful things that GUI debuggers can’t. By the end of this chapter, you’ll be armed with the information you need to debug .NET Core applications almost anywhere.

8.1. Debugging applications with Visual Studio Code

I introduced Visual Studio Code (VS Code) in chapter 2. It’s Microsoft’s lightweight, cross-platform, extensible text editor (similar to the Atom text editor). If you installed the C# extension from Microsoft, then you’ve likely seen the debug capabilities show up on both the menu and the left-side bar. VS Code may have also nagged you to add “required assets to build and debug.” If not, try creating a new project and opening VS Code with the following commands:

mkdir Test1
cd Test1
dotnet new console
code .

Open the Program.cs file and click somewhere on the text in the file. You’ll see a couple of things happen: a bin folder will be created because VS Code is building the code immediately, and a warning message will be displayed at the top asking you to add assets to Test1. Clicking Yes will create a new folder under Test1 called .vscode with two files: launch.json and tasks.json (shown in listings 8.1 and 8.2).

Generating VS Code building assets

If you missed the prompt to add building assets, you can open the command palette and select .NET: Generate Assets for Build and Debug.

Listing 8.1. launch.json allows you to configure the debugger for your application
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": ".NET Core Launch (console)",
      "type": "coreclr",
      "request": "launch",
      "preLaunchTask": "build",
      "program":                                                1
        "${workspaceRoot}/bin/Debug/netcoreapp2.0/Test1.dll",
      "args": [],                                               2
      "cwd": "${workspaceRoot}",
      "console": "internalConsole",                             3
      "stopAtEntry": false,
      "internalConsoleOptions": "openOnSessionStart",
      "justMyCode": true,                                       4
      "requireExactSource": true,                               5
      "enableStepFiltering": true                               6
    },
    {
      "name": ".NET Core Attach",
      "type": "coreclr",
      "request": "attach",
      "processId": "${command:pickProcess}"
    }
  ]
}

  • 1 If you change to netstandard, change this value.
  • 2 Command-line arguments for Test1
  • 3 Where console output goes
  • 4 Turning this off lets you step into other libraries.
  • 5 Turning this off lets you use a different version of the source.
  • 6 Turning this off lets you debug into properties.
Using an external terminal

If you’re used to Visual Studio’s external command prompt, you can change the "console" setting to "externalTerminal" to get the same behavior.

Debugging through referenced code

When using a library developed by another team, you can step into their code by setting "justMyCode" to false. But you may have trouble if your local copy of their source doesn’t match the version of the package you’re using. By turning off the "requireExactSource" flag, the debugger will make a best guess as to what line you’re on. This can sometimes be good enough to figure out the cause of an issue.

Listing 8.2. tasks.json defines how to perform tasks such as build and test
{
  "version": "0.1.0",
  "command": "dotnet",                                  1
  "isShellCommand": true,
  "args": [],
  "tasks": [
    {
      "taskName": "build",                              2
      "args": [
        "${workspaceRoot}/mytests.csproj"
      ],
      "isBuildCommand": true,
      "problemMatcher": "$msCompile"
    },
    {
      "taskName": "test",                               3
      "args": [
        "${workspaceRoot}/mytests.csproj"
      ],
      "isTestCommand": true,                            4
      "problemMatcher": "$msCompile"
    }
  ]
}

  • 1 Calls the dotnet CLI command
  • 2 “build” is added to the dotnet command (for example, “dotnet build”).
  • 3 “test” is not included by default, but you can add it as shown.
  • 4 Currently unused by VS Code

The Tasks menu in VS Code has a Run Build Task option with a shortcut defined as Ctrl-Shift-B (the same shortcut used in Visual Studio 2017). This will execute the build task defined in tasks.json. You can also use the Run Task option item and pick the task you want to run.

If you defined test like in listing 8.2, the list of available tasks will include test.

You can add custom tasks

You may find it useful to add other tasks, such as for packaging, publishing, or running tools like the Entity Framework tools.

8.1.1. Using the .NET Core debugger

Let’s look at an example application and see how the VS Code debugger works in action. Back in chapter 6 you created a data access library using Dapper and dependency injection (you can get the code from GitHub at http://mng.bz/F146 if you don’t have it handy). The data-access library has a method that creates an order in a database based on an Order object. If a field isn’t specified in this object, CreateOrder may fail, and you can use the debugger to determine where the failure occurs.

All the chapter 6 Dapper projects are contained in a folder called DapperDi. From a terminal or command prompt, change to the DapperDi folder and run code . to start VS Code with the current folder open. Add the build and debug resources as prompted. Then find the unit test file and modify the test as follows.

Listing 8.3. An example of a test that you’ll need to debug
[Fact]
public void Test1()
{
  var orders = context.GetOrders();
  Assert.Equal(0, orders.Count());
  var supplier = context.Suppliers.First();
  var part = context.Parts.First();
  var order = new Order() {
    SupplierId = supplier.Id,
    Supplier = supplier,
    PartTypeId = part.Id,
    //Part = part,                       1
    PartCount = 10,
    PlacedDate = DateTime.Now
  };
  context.CreateOrder(order);
  Assert.NotEqual(0, order.Id);
  orders = context.GetOrders();
  Assert.Equal(1, orders.Count());
}

  • 1 This is the change from the existing test.
Use an in-memory database for this example

You may also want to change the config.json file to use the in-memory database, because you’ll likely run this test many times.

It’s easy to forget to set all the properties on an Order with the data-access layer you created in chapter 6. Let’s see how you could debug this issue with VS Code.

As you learned in chapter 4, VS Code will put links above the test method that allow you to run or debug an individual test. Click the Debug Test link. The debugger will stop when it gets a NullReferenceException. You should see something like figure 8.1.

Figure 8.1. Visual Studio Code debugger stopped on an exception

The stack trace for the NullReferenceException shows the line throw; as being the line where the exception occurred. Normally, the throw; would preserve the stack trace from the original exception. But in this case you performed some work, (transaction.Rollback();), and that resulted in the stack trace being lost. You can fix this by changing the code in this catch statement, as shown in the following listing.

Listing 8.4. Modifying catch to wrap the original exception
catch (Exception exc) {
  transaction.Rollback();
  throw new AggregateException(exc);               1
}

  • 1 AggregateException can hold many inner exceptions.
Why use an AggregateException?

The AggregateException is common to asynchronous programming because it’s possible that multiple threads can encounter exceptions and you want to capture all of them. I use an AggregateException here because it indicates to the person debugging that only the inner exceptions are important.

Now debug the test, and the exception information (shown in figure 8.2) should be slightly more helpful.

Figure 8.2. Visual Studio Code debugger stopped on a wrapped exception

As you can see, Visual Studio Code has powerful debugging capabilities. It should feel familiar to most developers who have worked with debuggers before. Also, all of these features will work regardless of the operating system you’re using.

8.2. Debugging with Visual Studio 2017

Visual Studio 2017 is the latest version of the flagship integrated development environment from Microsoft, and it has a rich set of debugging capabilities. To see the differences between VS 2017 and VS Code, try debugging the same unit test as before. Figure 8.3 shows what this might look like.

Figure 8.3. Visual Studio 2017 debugger stopped on an exception

Visual Studio needs a project to discover tests

In VS Code, you open a folder. In Visual Studio there’s an option to open a folder, but even though it may build the projects, it doesn’t see the tests. The Test Explorer will be empty. Instead, you need to create a new solution and add the projects.

By altering the exception settings to break on the NullReferenceException, you can see the exception before it’s caught in the catch statement. In VS Code, you don’t have the same granularity, but you can break on all exceptions.

IntelliTrace

The IntelliTrace feature doesn’t come with the Community edition of Visual Studio 2017. If you happen to have an Enterprise edition, this is definitely a feature worth checking out.

IntelliTrace will capture events during the debugging session. You can then select these events from the Events window (shown in figure 8.3) and use the Historical Debugging feature to see the state of your application at that time with local variables and call stack. This comes in handy when unwinding complex problems.

 

8.3. Debugging with Visual Studio for Mac

Visual Studio for Mac bears a resemblance to the other products in the Visual Studio family. One slightly different feature is the Exception Catchpoint. To try this, go to the Run menu and choose New Exception Catchpoint. You’ll see the dialog box shown in figure 8.4.

Figure 8.4. Visual Studio for Mac New Exception Catchpoint dialog box

The same Advanced Conditions functionality is available in other debuggers, including Visual Studio 2017 and Visual Studio Code, with the name “conditional breakpoints.” The slight differences in terminology stem from VS for Mac actually being a rebranded Xamarin Studio.

Visual Studio for Mac doesn’t look that different from Visual Studio 2017, at least when it comes to debugging. Figure 8.5 shows what VS for Mac looks like in action.

Figure 8.5. Visual Studio for Mac debugging a NullReferenceException

8.4. SOS

So far, we’ve only explored graphical debuggers. But some things can’t easily be expressed in a GUI. That’s why every developer should have a command-line debugger in their toolbox.

The .NET Framework comes with an extension for the Windows Debugger (WinDBG) that contains powerful commands for interpreting .NET’s managed memory, types, functions, and so on. This extension is called SOS. It works for .NET Core and on the cross-platform LLDB debugger.

What does SOS mean?

SOS isn’t a distress signal—it stands for Son Of Strike. If you’re interested in trivia, you can do a search to find out the history of the name.

8.4.1. Easier to get started with a self-contained app

One of the nice things about the Visual Studio debuggers is that they hide some of the more confusing parts of the .NET SDK. When you run dotnet test, several child processes are spawned to do things like restore and build the project. Even if you skip the build and restore steps, child processes are still created. The problem isn’t insurmountable; it’s just difficult to take on if you’re beginning.

A much easier way to get started with SOS-based debugging is to create a self-contained application. You learned about these back in chapter 2. Create a new console application by running the following command from the DapperDi folder:

dotnet new console -o ScmConsole

Modify the ScmConsole.csproj file as shown in the following listing to create a self-contained application and add the necessary references.

Listing 8.5. Contents of ScmConsole.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.0</TargetFramework>
    <RuntimeIdentifiers>win10-x64;osx.10.12-x64                         1
    </RuntimeIdentifiers>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference
      Include="Microsoft.Extensions.DependencyInjection.Abstractions"
      Version="2.0.0-*" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection"
      Version="2.0.0-*" />
    <PackageReference Include="SQLitePCLRaw.bundle_green"
      Version="1.1.8" />                                                2
    <ProjectReference Include="../SqliteScmTest/SqliteScmTest.csproj" />
  </ItemGroup>
</Project>

  • 1 Needs a runtime identifier for a self-contained app (see appendix A)
  • 2 Needed for running SQLite on Mac and Linux

This project takes an indirect dependency on SqliteDal. In order to get the self-contained application building, you need to change SqliteDal to .NET Standard 2.0 and change some of its references. Make the following changes.

Listing 8.6. Making SqliteDal.csproj a self-contained app on Windows
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Data.Sqlite.Core"
                      Version="2.0.0-*" />
    <PackageReference Include="Dapper"                                    1
                      Version="1.50.2" />
    <PackageReference Include="System.Data.SqlClient"                     2
                      Version="4.3.1" />
    <ProjectReference Include="../ScmDataAccess/ScmDataAccess.csproj" />
  </ItemGroup>
</Project>

  • 1 Dapper references old version of SqlClient
  • 2 You need a later version to create a self-contained app.

Next, add the following code to the Program.cs file of ScmConsole.

Listing 8.7. Program.cs for the ScmConsole app will create an order, like in the unit test
using System;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using ScmDataAccess;
using SqliteScmTest;

namespace ScmConsole
{
  class Program
  {
    static void Main(string[] args)
    {
      SQLitePCL.Batteries.Init();                        1
      var fixture = new SampleScmDataFixture();
      var context = fixture.Services.
        GetRequiredService<IScmContext>();
      var supplier = context.Suppliers.First();
      var part = context.Parts.First();
      var order = new Order() {
        SupplierId = supplier.Id,
        Supplier = supplier,
        PartTypeId = part.Id,
        //Part = part,
        PartCount = 10,
        PlacedDate = DateTime.Now
      };
      context.CreateOrder(order);
    }
  }
}

  • 1 Sets up SQLite before you open the connection

Now you can run dotnet restore to make sure all the packages are set up correctly. Then run the publish command to create the self-contained app:

dotnet publish -c Debug -r win10-x64               1

  • 1 You can use Release, but Debug will be easier for debugging.

The executable will be published to the ScmConsoleinDebug etcoreapp2.0win10-x64publish folder. You can run ScmConsole.exe from there, and it should crash.

8.4.2. WinDBG/CDB

WinDBG and CDB are essentially the same debugger, except CDB is command-line-based, whereas WinDBG is GUI-based. For the purposes of this chapter, it doesn’t matter which one you use.

To get these tools, you’ll need to install the Debugging Tools for Windows that come with the Windows SDK (http://mng.bz/j0xk). You can choose to install only the Debugging Tools during the installation.

Once they’re installed, go back to your console and change to the folder where the self-contained ScmConsole app was published. Launch the app with the debugger attached by using the following command:

"C:Program Files (x86)Windows Kits10Debuggersx64cdb.exe" ScmConsole.exe
Getting the debugger to stop when an exception is thrown

The debugger will pause once it’s started. That gives you a chance to set up stops and breakpoints.

In this case, you need the process to load the .NET Core CLR before you set any breakpoints. To do that, you can tell the debugger to stop when it loads a certain module or assembly. Use the following commands to tell CDB to stop when the ScmConsole assembly is loaded:

sxe ld ScmConsole
g                              1

  • 1 Tells the debugger to “go”

The program should stop shortly. You’ll see some log messages indicating which assemblies have been loaded. Among those should be coreclr.dll from your publish folder.

Now you can load SOS and set a breakpoint for when the AggregateException is thrown:

.loadby sos coreclr
!soe -create System.AggregateException               1
g

  • 1 “soe” is short for “stop on exception.”

You should see two access violations before hitting the breakpoint. You’ll need to resume (g) each time an access violation is hit. The output will look something like the following.

Listing 8.8. Output when hitting access violations and then the AggregateException
(1ec8.4c34): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
00007ffd`d54e22bf 3909            cmp     dword ptr [rcx],ecx
     ds:00000000`00000000=????????
0:000> g                                                                1
(1ec8.4c34): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
KERNELBASE!RaiseException+0x68:
00007ffe`6f8c1f28 488b8c24c0000000 mov     rcx,qword ptr [rsp+0C0h]
     ss:00000007`5d17daf0=0000ec5c4ecdfd0d
0:000> g                                                                1
(1ec8.4c34): CLR exception - code e0434352 (first chance)
'System.AggregateException hit'                                         2
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
KERNELBASE!RaiseException+0x68:
00007ffe`6f8c1f28 488b8c24c0000000 mov     rcx,qword ptr [rsp+0C0h]
     ss:00000007`5d17b900=0000ec5c4ecd9f7d

  • 1 Resume after access violation
  • 2 This is where you hit the Aggregate-Exception.
Viewing the contents of an exception

You can use the Print Exception command (!pe) to see the AggregateException. The command will output another command so you can see the NullReferenceException. The following listing shows what this might look like.

Listing 8.9. Printing an exception and an inner exception
0:000> !pe
Exception object: 00000151b0e54230
Exception type:   System.AggregateException
Message:          One or more errors occurred.
InnerException:   System.NullReferenceException,                        1
                  Use !PrintException 00000151b0e51a38 to see more.
StackTrace (generated):
<none>
StackTraceString: <none>
HResult: 80131500
0:000> !PrintException 00000151b0e51a38                                 2
Exception object: 00000151b0e51a38
Exception type:   System.NullReferenceException
Message:          Object reference not set to an instance of an object.
InnerException:   <none>
StackTrace (generated):                                                 3
    SP               IP               Function
    000000075D17E070 00007FFDD54E22BF
     SqliteDal!SqliteDal.SqliteScmContext.CreateOrder(ScmDataAccess.Order)+0x
     19f

StackTraceString: <none>
HResult: 80004003

  • 1 Gives you the command to see the inner exceptions
  • 2 Command to see inner exception
  • 3 Stacks have offsets instead of line numbers.
Viewing the stack and local variables on the current thread

Because the debugger is paused at the point when the exception was thrown, you can view the current stack. This will also show the local variables for each method.

To do this, use the command !clrstack -a. It will produce output like the following.

Listing 8.10. Selected output from the !clrstack command
0:000> !clrstack -a
OS Thread Id: 0x4c34 (0)
        Child SP               IP Call Site
000000075d17db70 00007ffe34f28b28 [FaultingExceptionFrame: 000000075d17db70]
000000075d17e070 00007ffdd54e22bf
     SqliteDal.SqliteScmContext.CreateOrder(ScmDataAccess.Order)
    PARAMETERS:
        this (0x000000075d17e200) = 0x00000151b0e15b18                 1
        order (0x000000075d17e208) =0x00000151b0e35818                 2
    LOCALS:
        0x000000075d17e1d8 = 0x00000151b0e3ac08                        3
        0x000000075d17e1d0 = 0x0000000000000000
        0x000000075d17e1c8 = 0x0000000000000000
        0x000000075d17e1c0 = 0x00000151b0e51a38                        4

000000075d17e200 00007ffdd54b0cde ScmConsole.Program.Main(System.String[])
    PARAMETERS:
        args (0x000000075d17e2c0) = 0x00000151b0df3558
    LOCALS:
        0x000000075d17e298 = 0x00000151b0df3570                        5
        0x000000075d17e290 = 0x00000151b0e15b18                        6
        0x000000075d17e288 = 0x00000151b0e35608                        7
        0x000000075d17e280 = 0x00000151b0e2cd38                        8
        0x000000075d17e278 = 0x00000151b0e35818                        9

000000075d17e4e8 00007ffe34f923f3 [GCFrame: 000000075d17e4e8]
000000075d17e9c8 00007ffe34f923f3 [GCFrame: 000000075d17e9c8]

  • 1 Pointer to the SqliteScmContext object
  • 2 Pointer to the Order object
  • 3 Pointer to the SqliteTransaction object
  • 4 Pointer to the NullReferenceException
  • 5 Pointer to the SampleScmDataFixture object
  • 6 Pointer to the SqliteScmContext object
  • 7 Pointer to the Supplier object
  • 8 Pointer to the PartType object
  • 9 Pointer to the Order object
Dumping an object’s contents

To view a .NET object, run the !do command with the object pointer. The following listing shows the output when viewing theOrder object.

Listing 8.11. Viewing the Order object (elided)
0:000> !do 0x00000151b0e35818                                             1
Name:        ScmDataAccess.Order                                          2
MethodTable: 00007ffdd5357c98
EEClass:     00007ffdd54a7ee8
Size:        72(0x48) bytes
File:        ...
etcoreapp2.0win10-x64publishScmDataAccess.dll
Fields:
                Type            Value Name
        System.Int32                1 <Id>k__BackingField
        System.Int32                1 <SupplierId>k__BackingField
...taAccess.Supplier 00000151b0e35608 <Supplier>                          3
        System.Int32                0 <PartTypeId>k__BackingField
...taAccess.PartType 0000000000000000 <Part>k__BackingField
        System.Int32               10 <PartCount>                         4
     System.DateTime 00000151b0e35840 <PlacedDate>k__BackingField
...Private.CoreLib]] 00000151b0e35848 <FulfilledDate>k__BackingField

                Type VT            Value Name
        System.Int32  1                1 <Id>k__BackingField
        System.Int32  1                1 <SupplierId>k__BackingField
...taAccess.Supplier  0 00000151b0e35608 <Supplier>                       5
        System.Int32  1                0 <PartTypeId>k__BackingField
...taAccess.PartType  0 0000000000000000 <Part>k__BackingField
        System.Int32  1               10 <PartCount>                      6
     System.DateTime  1 00000151b0e35840 <PlacedDate>                     7
...Private.CoreLib]]  1 00000151b0e35848 <FulfilledDate>k__BackingField

  • 1 DumpObject command with pointer to Order object
  • 2 Type name
  • 3 Pointer to Supplier object
  • 4 Value of the PartCount property
  • 5 Pointer to Supplier object
  • 6 Value of the PartCount property
  • 7 DateTime is also a value type.
Value vs. reference types

PartCount is a value type in C#, which means the value is held in memory directly. Supplier is a reference type, which is why you get a pointer value. A C# struct is also considered a value type, but in memory it’s a pointer, as you can see from the PlacedDate property. The VT column indicates 1 for a value type and 0 for a reference type. If you try !do on the PlacedDate pointer, it won’t work.

How to view an array

If you want to view an array, like the arguments passed to Program.Main, use the !da command instead.

Examining the rest of the managed memory heap

All of these commands are powerful, but they’re not providing much of an advantage over what the Visual Studio debuggers do. There’s an area where SOS really shines, though, and it’s when you care about what’s in memory besides what’s on the current thread. To see what I mean, try executing !dumpheap -stat. You’ll see every .NET object in memory grouped by type and ordered by how much memory they take up. This includes objects created by the SQLite library, the dependency-injection library, and .NET Core.

To see this in action, try executing the following command.

Listing 8.12. Using the !dumpheap command with a type filter
0:000> !dumpheap -type OutOfMemory                                   1
         Address               MT     Size
00000151b0dc10e0 00007ffe337a02f8      152                           2

Statistics:
              MT    Count    TotalSize Class Name                    3
00007ffe337a02f8        1          152 System.OutOfMemoryException
Total 1 objects

  • 1 Command to run, case-sensitive
  • 2 Address of object found
  • 3 Found a type with OutOfMemory in the name

If you’re looking at memory to find all the exceptions (!dumpheap -type Exception), you’ll always find OutOfMemoryException. .NET creates this exception in memory up front, because in the event that you do run out of memory, it won’t be able to allocate the memory to create the OutOfMemoryException. Instead, it will throw the one it created earlier. Creating a stack trace will also take memory, so you won’t get a stack trace for the OutOfMemoryException, but that’s typically not a problem.

Debugging unit tests with CDB

If you’re interested in trying to debug the unit tests, you can use a CDB command like this:

cdb.exe -o dotnet test --no-build --no-restore

That command starts CDB and launches dotnet test without restoring or building and debugging all the child processes.

You’ll have to resume a few breaks to get to the AggregateException, so you need to pay attention to the output and stack at each break to know where you are.

Where to learn more about the CDB commands

After you’ve loaded SOS, you can get a full list of SOS commands by running the !help command. You can similarly get more detailed help on a command, like this: !help dumpheap.

Commands that don’t start with an exclamation mark (!) are part of CDB, and they can be a little archaic. There’s a good quick reference for managed code debugging called “WinDbg cheat sheet” at http://mng.bz/u7Ag. The reference is a bit old (it still refers to mscorwks, which is pre-.NET Framework 4.0) but still relevant.

8.4.3. LLDB

LLDB is a debugger that’s part of the larger LLVM project, which is a collection of modular compiler and toolchain technologies. LLVM is used by Xcode, the integrated development environment for developing Mac and iOS applications.

Using LLDB on a Mac

If you’re using a Mac, the easiest way to install LLDB is to install Xcode.

SOS on LLDB is hard to get working

Working with SOS and LLDB on a Mac is seriously complex. It requires that you build your own version of the .NET Core CLR so that you can get an SOS LLDB plugin that works with the version of LLDB you’re using. To get the .NET Core team to fix this issue, vote on GitHub on the coreclr issues page: http://mng.bz/r50R.

You can also attempt to install LLDB with Homebrew with this command:

brew install llvm --with-lldb

When attempting this command, Homebrew will first point you to a page that tells you how to install the code-signing certificate for LLDB. As mentioned in the warning, though, the next step is to build the Core CLR code, which will build the SOS plugin for LLDB. Assuming you did all these steps, you’ll have a file called libsosplugin.dylib. Use the plugin load command in LLDB, followed by the full path of the libsosplugin.dylib file to install the plugin.

Given the complexities of this method, I recommend installing Xcode instead. Xcode is free and installs LLDB without much difficulty.

Using LLDB on Linux

On Linux, use the following command to install LLDB:

sudo apt install lldb-3.5

To test that LLDB is installed, run ./lldb from the terminal. It should give you a prompt like this: (lldb).

LLDB doesn’t have a command that breaks on module load, so you’ll need to add a Console.ReadLine to the test application. Make it the first line in the Main method in Program.cs. Then go to the publish folder for the ScmConsole application, and execute the application. It should wait for you to press Enter before continuing on with the program.

At this point you can start LLDB in another terminal and attach to it. But first, you’ll need the process ID. Use the following command to get the process ID:

ps -eclx | grep 'ScmConsole' | awk '{print $2}' | head -1

Now start LLDB and use the following command to attach to the process:

process attach -p [processid]                          1

  • 1 Replace processid with the process from the previous command.3

Now you’ll need to locate the libsosplugin.so file. Open a new terminal and run the following command to find it:

find /usr -name libsosplugin.so

You may see multiple versions of this file. Choose the one that matches the .NET SDK version you’re using, which is usually the latest one. Back in LLDB, enter the command plugin load followed by the full path of libsosplugin.so.

Now you can try the following sequence of commands, just like you did in section 8.4.2 on WinDBG:

!soe -create System.AggregateException
process continue
!pe
!dumpheap -type OutOfMemory

Additional resources

SOS is addictive. It’s especially useful when you’re trying to diagnose an issue on a production server. You won’t want to set breakpoints, but you can get a memory dump of the process and copy it to your workstation for analysis. Having access to all of the .NET objects in memory gives you all kinds of power. If you’re interested in learning more about debugging .NET, check out some of these resources:

Summary

In this chapter you learned about the various tools for debugging .NET Core applications. These are some key concepts from this chapter:

  • Many of the debugging tools available for .NET Core are free and powerful.
  • The Visual Studio family of debuggers has similar capabilities on Windows, Mac, and Linux.
  • .NET Core provides the SOS extension, which can be used in the command-line debuggers LLDB and WinDBG.

You also used a few techniques to debug your application:

  • Debugging tests from Visual Studio editors
  • Wrapping an exception in an AggregateException if the catch block does work that will lose the stack trace
  • Creating a self-contained application when debugging from the console, because it’s easier to debug than dotnet test or dotnet run
  • Using WinDBG’s sxe ld command to stop when the coreclr module is loaded, so you can load SOS and set a breakpoint

If you’re a .NET Framework developer, you’re probably used to Visual Studio and its powerful debugging capabilities. In this chapter, you learned that those same capabilities are available in .NET Core. We also explored some other options when developing on Mac and Linux. Command-line debuggers give you the power to work with a memory dump or via terminal or SSH, which comes in handy when a bug only happens in production.

In the next chapter, we’ll explore how to test and analyze the performance of your .NET applications.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.253.152