LLDB supports many combinations of architecture, operating system and other system components. In this document we describe the considerations and requirements for porting LLDB to a combination of those things which we will refer to as a “target” within this document.
:::{note} The terms Target and Platform are used throughout LLDB, often referring to a subset of what this document calls a “target”. Unfortunately there is no more specific word to use here. :::
This document provides some hints on implementation, but because every target is unique, we expect developers to learn about implementation from existing targets.
This document starts with the end of the process, proposing your target for upstream inclusion. It is worth reading through this even if that is not one of your goals, as you will have to tackle the same topics even in a downstream implementation.
You must send an RFC to the LLDB Discourse forum before upstreaming new target support. This RFC must be accepted in some form by the community before any changes can be merged upstream that are specific to your target.
This RFC follows the normal rules of the LLVM community decision making process.
We require an RFC for target support upstreaming because:
RFCs are not required to be:
Removing target support also requires an RFC. Reasonable effort should be made to involve the original contributors and users of the target support.
:::{note} You may propose the removal of a subset of the target. For instance, removing support for an operating system on architecture A, without removing support for that operating system on architecture B. :::
The content of the RFC should cover the same topics as an upstreaming proposal, but you will be arguing for the opposite conclusion.
We reserve the right to change our standards over time, so a removal proposal is not necessarily a refutation of each point made in the upstreaming proposal. For example, what was an acceptable level of testing in the past may no longer be acceptable.
If standards change and are to be applied to existing targets, target stakeholders should be given the opportunity to meet those standards rather than going straight to removal.
Listed below are some examples of factors considered when considering accepting code upstream. These are examples and RFC authors are free to add their own, leave some out, or explain why they do not apply to their proposal.
If you do use these points, they need to come with an answer and evidence to justify the answer, rather than simply “yes this applies to my target”. In other words, your proposal must stand alone without requiring readers to read this document as well.
The first set covers your motivation for your target being upstream, rather than on a fork:
The next set is about whether you, your community, or the LLDB community, can adequately maintain the code upstream:
The factors above are part of a cost benefit analysis that the LLDB community will do, prioritising the health of the community and the project. This means that there is no set level or type of commitment required for a new target.
Each case will have unique aspects, so you need to tell the community how much impact your target will have on LLDB. Below are some questions you can start with, along with stereotypical “big” and “small” answers.
Note that these are intentionally not maximums and minimums. The impact of some targets will be bigger than “big” or smaller than “small”.
lldb, lldb-server and advanced features like shared libraries.lldb and only basic features like continue, stop, reading memory and registers.These case studies are to give you starting points for your proposals. Consider how your target compares to these.
Apple targets have possibly the most extensive support in LLDB.
FreeBSD is an example of quite self-contained support, managed by the project's community.
lldb-server, so issues with it rarely impact any other target.Linux is an example where the Linux community as a whole does not do all the work of Linux support. Some architectures have a wide contributor base and others have company-specific contributors as well.
MSP430 is handled as a bare-metal (no operating system) target in LLDB. So it is the most minimal example.
lldb-server, and lldb only required minor adjustments to be compatible with the commonly used debug server.This is a very high-level view. We recommend you combine this with reading the changes made for recently added targets, as each target is going to be slightly different.
Assuming that:
lldb and lldb-server.Then this is a list of components you will need to write. If your target is similar to others you may be able to reuse existing components and we encourage you to do so.
First the components concerned with operating systems:
Platform plugin. For example PlatformLinux.HostInfo plugin. For example HostInfoLinux.NativeProcessLinux.LinuxSignals.DynamicLoaderPOSIXDYLD.ObjectFileELF.Then the components concerned with architecture (though the distinction is often inexact):
ABISysV_arm64.ArchitectureAArch64.NativeRegisterContextLinux_arm64.EmulateInstructionARM64. Most targets need to emulate a small number of instructions for unwinding. If your target does not have hardware single-step, you will need to emulate many times more than that.The order of implementation will vary in each case. Each component does not need to be fully implemented for you to start work on the next.
If you are going to be running lldb on your target, then an obvious first step is to get lldb to build there without any changes. This lldb will only be useful for remotely debugging other targets, but you will at least know that the build system is compatible.
Next, try lldb with any existing debug servers for your target. If they are similar to lldb-server or gdbserver, this can flush out some obvious issues in lldb.
Then begin porting lldb-server to your target. We recommend that you get the test suite to run as soon as possible, however bad the results are.
As you have seen above, there are a lot of moving parts to a debugger. So having some set of results to measure progress is very important. Sometimes a change will get one test to pass, sometimes hundreds, and it is easy to regress if you are not careful.
Some hardware features can make porting easier or harder. Below is a non-exhaustive list of features and their impacts on porting LLDB.
If the target lacks this feature, you will have to implement software single stepping. This is much more complex and involves emulating any instruction that could modify the program counter.
This is any situation where to resume the program you have to replay some previous instructions. LLDB needs to know the extent of the sequence.
For example, an atomic sequence may implement an atomic operation by looping until success. When stepping through the sequence normally, this check will always fail (due to the debug exceptions) and cause it to loop forever.
You can teach LLDB to find the sequence start point, and replay the whole thing as if it were one step.
If you have instruction bundles, check how breakpoints behave and where in the bundles they can be placed.
The opposite of the previous point. If your target has single instructions for what is normally a sequence, this reduces the work needed in LLDB. Single instruction atomics are a common example.
Anything like AArch64's Scalable Vector Extension (SVE) registers. At each stop event the registers may have a different size.
Support for this is currently SVE specific as it requires lldb to know which registers scale and what to derive their size from.
If you have registers that are not present for the entire program runtime you will need to decide how to present that.
The one example we support right now is AArch64's Scalable Matrix Extension (SME) ZA register. This register can be switched off when not in use. We handle this by showing a fake zero value at these times, with a separate mode bit in another register so users can tell a real zero from a fake zero.
The more fundamental and the more numerous the registers are, the more likely you are to confuse users by showing them even when they are unusable. For instance if you have two execution modes that use separate register sets, showing both all the time may be confusing for users.
When you have banked registers or a copy of a register for each execution mode, it usually only has one name. You need to decide if it makes sense to allow users to access each one separately.
For example, AArch64‘s SME extension adds a “streaming mode”. SVE registers exist in the normal mode and the streaming mode. However programs only ever use one or the other, and the values are cleared when the mode is switched. So there is no reason to let users write to the inactive mode’s registers and we just show 1 set with the normal naming. That set always refers to the active mode.
However if you have overlapping sets that can hold their own values, you may want to make the normal register name the active set, and have a way to address the other sets.
Arm (meaning Armv7 and prior) has 2 execution modes: Arm and Thumb. If you attempt to execute Thumb mode code in Arm mode, it will not work. Programs can mix the two modes by using special mode switching branches.
The debugger has to be aware of what mode the inferior is in so that it can correctly compare addresses, place breakpoints and use the correct breakpoint instruction encoding.
In Arm's case, the information comes from markers in the program file, and the bottom bit of the program counter. This is often a source of bugs because many parts of the debugger have to know that this bit is not part of the instruction address.
LLDB already supports a wide variety of encoding strategies, so variable length encoding is not much more work than fixed length.
Of the current targets we have:
Code breakpoints can be implemented in software by replacing an instruction with a software breakpoint instruction.
However, if you want to only stop in certain situations (a single address space, a single execution mode, and so on), code breakpoints implemented in hardware will be much faster.
Doing it in software means you have to context switch between the debug stub and the debugger to filter every stop event. Which is slow even when locally debugging. Put the debug server on the end of a high latency connection and the slow down is multiplied.
In addition, hardware code breakpoints can be set in read-only memory. Which is important for code executing out of ROM, which is common on embedded targets.
Watchpoints are used to wait for a specific type of access to a specific range of memory. Doing this in software is possible but very invasive so use hardware watchpoints if you can.
For comparison, one way to implement a software watchpoint is to unmap the memory around a location and then filter the memory faults to find the access you are looking for. This requires a lot of traffic between debugger and debug server, and needs to be implemented for every supported operating system.
Whereas a hardware watchpoint is usually a few registers programmed with hardware specific values, and most operating systems expose those registers directly to userspace.
:::{note} Hardware often has many more features than LLDB makes use of. What we make use of is decided by how useful it will be to how many users and how understandable it will be when presented in the LLDB interface. :::
Ideally your break and watchpoint exceptions contain enough information to know exactly what caused them. Which seems obvious from a software level, but if you check the architecture specifications you will find that they often allow a range of behaviours.
For instance, if you watch a 4 byte chunk of memory and an instruction writes 8 bytes over it, is the hardware required to report an address in the watched 4 bytes? Or can it report an address in the second 4 bytes, because it is still part of the write that triggered the watchpoint?
LLDB will assume accuracy unless told otherwise, and if there are untraceable situations users will have to figure out which watch or breakpoints to manually disable so that they can continue.
Though many programming languages have rules that prevent the use of pointers in some integer-like ways, the reality is that a lot of hardware treats them as numbers.
This changes with capabilities (for example CHERI), mode bits (Arm‘s Thumb) and re-use of non-address bits (AArch64’s TBI, MTE and PAC). A pointer is no longer just a number that refers to a memory location, it contains extra information.
Where the size of the pointer is equal to the bit width of the architecture (64-bit pointers on AArch64 for example), LLDB can probably handle it. LLDB already has the concept of “non-address bits” that must be removed to get the memory address from a pointer.
If pointers are like capabilities where their size is greater than that of a memory address, you will need to change the type LLDB uses to store addresses which is a lot more work.