[RFC] Support for Backtraces in CTF format

Indu Bhagat indu.bhagat@oracle.com
Tue Dec 14 15:59:54 GMT 2021


Hello people,

In the GNU Tools Track at LPC 2021 this year, we talked briefly about 
our plan for CTF V4, especially support for backtraces.

https://linuxplumbersconf.org/event/11/contributions/1003/

To summarize, the motivation to add support for backtraces in CTF is to 
support the use case of online stack unwinding. A couple of large 
applications of interest (yes, one of them is the Linux Kernel) have 
adopted ad-hoc solutions because:
- eh_frame-based unwinders are complex and slow for them to manage with
- eh_frame-based unwinders are large as they need to deal with DWARF 
opcodes via a stack machine implementation

Such ad-hoc solutions are reaching their limits because the workflow is 
based on offline tools that inspect the generated binary to reconstruct 
the control-flow, and to determine the stack frame size in general. 
Other issues include needing to fix issues around the stubs included 
from system libraries from time to time. This whole effort is compounded 
with every new architecture the application supports. An interesting 
detail is that each project has come up with their own "unwind format" 
best tailored to their respective needs.

Some of the above issues were also highlighted at the Toolchain and 
Kernel MC at LPC 2021 
https://linuxplumbersconf.org/event/11/contributions/971/.

So, at a high-level, following requirements for CTF backtraces are aimed 
for:

1. Support asynchronous stack unwinding
2. Keep the format minimal, simple and compact. There should be no 
complex encodings to decipher (avoid DWARF-like expressions), no 
location lists and no stack machine to process any of this.
3. Provide means to gather the original value of the arguments of the 
functions, when possible. For some projects, which are interested in 
fast online stack tracing, this is a must have.

Stack Unwinding
---------------

First up is information for stack unwinding. The proposal is to include 
information to recover the CFA, and Return Address, given a PC. The 
other set of callee-saved registers are not included for recovery across 
frames. All this information for stack unwinding can be generated by the 
assembler using the existing .cfi_* directives (with linker doing its 
due part of merging them later). In other words, this will be based on 
the standard set of CFI directives already emitted by GCC.

This information is made available via a new section called .ctf_frame. 
It will be possible to have all of .eh_frame, .debug_frame and
.ctf_frame generated by the assembler and in the same object, if so 
desired.

Q1. The .ctf_frame section needs to be SEC_LOAD and SEC_ALLOC to support 
the use-case of fast virtual stack unwinding, correct ? What are some 
other issues that need attention here ?

At a high-level, .ctf_frame section will contain:

1. A sorted index mapping Start PC of a function to its set of CTF Frame 
Record Entries (FRE). So, something like:

  [FuncStart PC1]---FRE_section_offset11
  [FuncStart PC2]---FRE_section_offset21
  [FuncStart PC3]---FRE_section_offset31
   ...

Just specifying the offset to the first FRE of the function is enough, 
as the FREs are generated per function by the assembler and not 
rearranged later on (by the linker).

2. A set of FREs one per block of instructions with the required 
information to recover the CFA and RA.

  [FRE11] @ FRE_section_offset11
  [FRE12] @ FRE_section_offset12
  ...
  [FRE21] @ FRE_section_offset21
  [FRE22] @ FRE_section_offset22
  [FRE23] @ FRE_section_offset23
  ...
  [FRE31] @ FRE_section_offset31
  [FRE32] @ FRE_section_offset32
  [FRE33] @ FRE_section_offset33
  ...

where FRE11, FRE12 belong to function 1, FRE21, FRE22, FRE23 belong to 
function 2 and so on.

The format for FRE can be discussed in our follow up discussions. What 
do you think ? Particularly:

Q2. Is there value in keeping the index in #1 above in a separate 
section ? So something like .ctf_frame *and* and a .ctf_frame_index ?

Original value of arguments
---------------------------

[Original-value-of-arguments PHASE 1]

Next, let's see what can be done about #3, format changes to gather the 
original value of the arguments to the functions. A proposal is to view 
the challenge of specifying the argument location by splitting it into 
two separate steps:

[Step 1] Argument classification: Given a high-level language type of 
the argument, what is the machine type?
[Step 2] Argument marshaling: Given the machine type(s) for an argument, 
which storage locations are used for argument passing ?

The proposal is that the CTF format does the step #1 of assigning 
storage classes to each argument, and encodes it in the format. E.g. for 
AMD64, it identifies unambiguously a set of storage classes from INTEGER 
/ SSE / SSEUP / X87/ X87UP / MEMORY etc. per argument (for args of 
composite type, it specifies a set of storage classes as applicable). 
And next, it is the backtrace client which performs the step #2, by 
internalizing some knowledge of the ABI of the binary. In other words, 
the natural location of the original value of the argument is not 
encoded in the debug format, it is inferred by the (backtrace) client 
using the position and storage class of the argument and the knowledge 
of the associated ABI.

Thoughts on this ? Particularly:

Q3. The client is expected to know the ABI specific rules and perform 
the argument marshaling. Will this work in practice ? Most ABI docs do 
lay out the rules unambiguously.

Q4. Clearly, the CTF format will need a way to accommodate "many" 
important ABIs to be useful across the board.  At this point, we have 
started with AARCH64 and AMD64. I am curious to know what others think 
about the support for AARCH64 and AMD64 as the beginning point. Will CTF 
support for backtraces be useful enough with support for just two ABIs ? 
Do you have any other ABIs in mind that should be accommodated ?

[Original-value-of-arguments PHASE 2]

Because of the nature of our programs, it is understood that recovering 
original value of the arguments via registers may not always be 
possible, especially because the callee-saved registers will not be 
recoverable. Most importantly, the desire is to keep the format simple 
so there cannot be any new special purpose bytecodes needing 
interpretation at run time. For this, a solution where original 
arguments are saved on stack for recovery can be adopted.

Please share your thoughts/feedback, it will surely impact the shape of 
the proposal. Thanks for taking time to read through this RFC :-).

The support for CTF backtraces needs to be integrated into the toolchain 
so that the overhead and prevalence of the ad-hoc solutions for 
generating backtraces can be avoided.

Thanks


More information about the Binutils mailing list