Skip to main content
Redhat Developers  Logo
  • Products

    Featured

    • Red Hat Enterprise Linux
      Red Hat Enterprise Linux Icon
    • Red Hat OpenShift AI
      Red Hat OpenShift AI
    • Red Hat Enterprise Linux AI
      Linux icon inside of a brain
    • Image mode for Red Hat Enterprise Linux
      RHEL image mode
    • Red Hat OpenShift
      Openshift icon
    • Red Hat Ansible Automation Platform
      Ansible icon
    • Red Hat Developer Hub
      Developer Hub
    • View All Red Hat Products
    • Linux

      • Red Hat Enterprise Linux
      • Image mode for Red Hat Enterprise Linux
      • Red Hat Universal Base Images (UBI)
    • Java runtimes & frameworks

      • JBoss Enterprise Application Platform
      • Red Hat build of OpenJDK
    • Kubernetes

      • Red Hat OpenShift
      • Microsoft Azure Red Hat OpenShift
      • Red Hat OpenShift Virtualization
      • Red Hat OpenShift Lightspeed
    • Integration & App Connectivity

      • Red Hat Build of Apache Camel
      • Red Hat Service Interconnect
      • Red Hat Connectivity Link
    • AI/ML

      • Red Hat OpenShift AI
      • Red Hat Enterprise Linux AI
    • Automation

      • Red Hat Ansible Automation Platform
      • Red Hat Ansible Lightspeed
    • Developer tools

      • Red Hat Trusted Software Supply Chain
      • Podman Desktop
      • Red Hat OpenShift Dev Spaces
    • Developer Sandbox

      Developer Sandbox
      Try Red Hat products and technologies without setup or configuration fees for 30 days with this shared Openshift and Kubernetes cluster.
    • Try at no cost
  • Technologies

    Featured

    • AI/ML
      AI/ML Icon
    • Linux
      Linux Icon
    • Kubernetes
      Cloud icon
    • Automation
      Automation Icon showing arrows moving in a circle around a gear
    • View All Technologies
    • Programming Languages & Frameworks

      • Java
      • Python
      • JavaScript
    • System Design & Architecture

      • Red Hat architecture and design patterns
      • Microservices
      • Event-Driven Architecture
      • Databases
    • Developer Productivity

      • Developer productivity
      • Developer Tools
      • GitOps
    • Secure Development & Architectures

      • Security
      • Secure coding
    • Platform Engineering

      • DevOps
      • DevSecOps
      • Ansible automation for applications and services
    • Automated Data Processing

      • AI/ML
      • Data Science
      • Apache Kafka on Kubernetes
      • View All Technologies
    • Start exploring in the Developer Sandbox for free

      sandbox graphic
      Try Red Hat's products and technologies without setup or configuration.
    • Try at no cost
  • Learn

    Featured

    • Kubernetes & Cloud Native
      Openshift icon
    • Linux
      Rhel icon
    • Automation
      Ansible cloud icon
    • Java
      Java icon
    • AI/ML
      AI/ML Icon
    • View All Learning Resources

    E-Books

    • GitOps Cookbook
    • Podman in Action
    • Kubernetes Operators
    • The Path to GitOps
    • View All E-books

    Cheat Sheets

    • Linux Commands
    • Bash Commands
    • Git
    • systemd Commands
    • View All Cheat Sheets

    Documentation

    • API Catalog
    • Product Documentation
    • Legacy Documentation
    • Red Hat Learning

      Learning image
      Boost your technical skills to expert-level with the help of interactive lessons offered by various Red Hat Learning programs.
    • Explore Red Hat Learning
  • Developer Sandbox

    Developer Sandbox

    • Access Red Hat’s products and technologies without setup or configuration, and start developing quicker than ever before with our new, no-cost sandbox environments.
    • Explore Developer Sandbox

    Featured Developer Sandbox activities

    • Get started with your Developer Sandbox
    • OpenShift virtualization and application modernization using the Developer Sandbox
    • Explore all Developer Sandbox activities

    Ready to start developing apps?

    • Try at no cost
  • Blog
  • Events
  • Videos

Debugging in GDB: Create custom stack winders

June 19, 2023
Andrew Burgess
Related topics:
C, C#, C++LinuxPython
Related products:
Red Hat Enterprise Linux

Share:

    In this article, we will walk through the process of creating a custom stack unwinder for the GNU Project Debugger (GDB) using GDB's Python API. We'll first explore when writing such an unwinder might be necessary, then create a small example application that demonstrates a need for a custom unwinder before finally writing a custom unwinder for our application inside the debugger.

    By the end of this tutorial, you'll be able to use our custom stack unwinder to allow GDB to create a full backtrace for our application.

    What is an unwinder?

    An unwinder is how GDB figures out the call stack of an inferior, for example, GDB's backtrace command:

    Breakpoint 1, woof () at stack.c:4 4 return 0; 
    
    (gdb) backtrace
    #0 woof () at stack.c:4
    #1 0x000000000040111f in bar () at stack.c:10
    #2 0x000000000040112f in foo () at stack.c:16
    #3 0x000000000040113f in main () at stack.c:22 
    (gdb)

    Figuring out frame #0 is easy; the current program counter ($pc) value tells GDB which function the inferior is currently in. But to figure out the other frames, GDB needs to read information from the inferior's registers and memory. The unwinder is the component of GDB that performs this task.

    Having an understanding of the inferior's frames isn't just used for displaying the backtrace, though; commands like next and finish also need an accurate understanding of the stack frames in order to function properly.

    Any time GDB needs information about a frame beyond #0, an unwinder will have been used.

    What is a custom unwinder?

    GDB already has multiple built-in unwinders for all the major architectures GDB supports. By far, the most common unwinder will be the DWARF unwinder, which reads the DWARF debug information and uses it to unwind the stack for GDB.

    But not all functions are compiled with debug information. When GDB finds a function without DWARF debug information, it falls back to a built-in prologue analysis unwinder.

    The prologue analysis unwinder disassembles the instructions at the start of a function and uses this information, combined with an understanding of the architecture's ABI, to provide unwind information. For many functions, the prologue analysis unwinder will do a reasonable job. Still, there's a limit to how smart the prologue analysis unwinder can be, and GDB can never expect to handle every function this way.

    And this is where the Python unwinder API comes in. Using this API, it is possible to write Python code that will be loaded into GDB. This code can then "claim" frames for which GDB is otherwise unable to unwind correctly, and the Python code can instead be used to provide the unwind information to GDB.

    Building an example use case

    In most well-written applications, very few functions will need the support of a custom unwinder. The sort of functions that GDB will struggle with are those that do unexpected things with the underlying machine state; for example, functions that manipulate the stack in unexpected ways are likely to confuse GDB.

    The example application we're going to write does just that: it allocates a second stack and uses a small assembler function to switch to, and run a function on, the new stack.

    GDB will have no problem unwinding the standard C frames, but the assembler function, which changes the stack, is going to confuse GDB, and initially, we will be unable to obtain a backtrace through this function.

    Of course, writing in assembly language means this application will only work for one architecture, in this case, x86-64, and the unwinder we eventually write will also be tied to this one architecture. This is perfectly normal; unwinders are dealing with machine registers, so it is expected that an unwinder will only apply to a single architecture.

    The demonstration application is split into two files, first, we have demo.c:

    #include <stdio.h>
    #include <sys/mman.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    /* This function is in our assembly file.  */
    extern void run_on_new_stack (void *stack, void (*) (void));
    
    /* Return pointer to the top of a new stack.  */
    static void *
    allocate_new_stack (void)
    {
      int pagesz = getpagesize ();
      void *ptr = mmap (NULL, pagesz, PROT_READ | PROT_WRITE,
                    	MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
      if (ptr == MAP_FAILED)
    	abort ();
      return ptr + pagesz;
    }
    
    /* A function to run on the alternative stack.  */
    static void
    func (void)
    {
      printf ("Hello world\n");
    }
    
    /* Allocate a new stack.  Run a function on the new stack.  */
    int
    main (void)
    {
      void *new_stack = allocate_new_stack ();
      run_on_new_stack (new_stack, func);
      return 0;
    }

    Then we have runon.S:

        	.global run_on_new_stack
    run_on_new_stack:
        	/* Incoming arguments:
    
           	%rdi    - top of new stack pointer,
           	%rsi - function to call.
    
           	Store previous %rbp and %rsp to the new stack.
           	Set %rbp and $rsp to point to the new stack.
           	Call the function in %rsi.  */
        	mov 	%rbp, -8(%rdi)
        	mov 	%rsp, -16(%rdi)
    
        	add 	$-16, %rdi
    
        	mov 	%rdi, %rbp
        	mov 	%rdi, %rsp
    
        	callq   *%rsi
    
        	movq 	0(%rbp), %rsp
        	movq 	8(%rbp), %rbp
    
        	ret
    
        	.size run_on_new_stack, . - run_on_new_stack
        	.type run_on_new_stack, @function
    

    Finally, compile the application like this:

    gcc -g3 -O0 -Wall -Werror -o demo demo.c runon.S

    Now let's see how GDB handles stack unwinding without any additional support. For this, I'm using GDB 13.1:

    $ gdb -q demo
    Reading symbols from demo...
    (gdb) break func
    Breakpoint 1 at 0x4011b1: file demo.c, line 25.
    (gdb) run
    Starting program: /tmp/demo
    
    Breakpoint 1, func () at demo.c:25
    25      printf ("Hello world\n");
    (gdb) backtrace
    #0  func () at demo.c:25
    #1  0x00000000004011fb in run_on_new_stack () at runon.S:19
    #2  0x00007fffffffad68 in ?? ()
    #3  0x00007fffffffad80 in ?? ()
    #4  0x0000000000000000 in ?? ()
    (gdb)

    As you can see, GDB can unwind from func just fine; after all, that is a "normal" function compiled with debug information. But GDB is unable to figure out how to unwind from run_on_new_stack.

    What our application is actually doing

    Before we can write a custom unwinder in Python, we need to make sure we fully understand what the demonstration application is actually doing.

    We have three frames: main, run_on_new_stack, and func.

    In main, just before we call run_on_new_stack, the application’s stack looks like Figure 1.

    Stack in main, just before run_on_new_stack is called.
    Figure 1: Stack in main, just before run_on_new_stack is called.

    The register %rbp, sometimes known as the frame pointer, points to the top of the frame for main, while %rsp, otherwise known as the stack pointer, points to the last valid address of the frame for main.

    When we call from main to run_on_new_stack, the return address within main is pushed onto the stack and %rsp is updated. The stack now looks like Figure 2.

    State of the stack upon entry to run_on_new_stack.
    Figure 2: State of the stack upon entry to run_on_new_stack.

    In main, we also allocated a new stack, and this was passed through as the first function argument to run_on_new_stack. As such, register %rdi points at an address just above the new stack, like this (Figure 3).

    Visualisation of the new, empty stack.
    Figure 3: Visualisation of the new, empty stack.

    Within run_on_new_stack we switch over to the new stack. The return address within main is left on the original stack, and the pointers (%rbp and %rsp) to the original stack are backed up on the new stack, and then updated to point at the new stack. We then call func, which will push the return address within run_on_new_stack onto the new stack. Once we are in func, the state of the two stacks is now as shown in Figures 4 and 5.

    Layout of the original stack once run_on_new_stack has switched to the new stack.
    Figure 4: Layout of the original stack once run_on_new_stack has switched to the new stack.
    Layout of new stack.
    Figure 5: Layout of new stack.

    When unwinding, GDB doesn't understand how to use the information on the new stack to find the original stack, and so the backtrace is incomplete. This is the problem that our custom unwinder will solve for us.

    Starting our custom unwinder

    The custom unwinder will be written as a Python script in the file runon-unwind.py, which we can then source in GDB to provide the extra functionality.

    In GDB's Python API, an unwinder is an object that implements the __call__ method. GDB will call each unwinder object for every frame; the unwinder should return None if the unwinder doesn't handle the frame or return a gdb.UnwindInfo object if the unwinder wishes to take responsibility for the frame.

    Let's start by writing an empty unwinder that doesn't claim any frames:

    from gdb.unwinder import Unwinder
    
    
    class runto_unwinder(Unwinder):
        def __init__(self):
            super().__init__("runto_unwinder")
    
        def __call__(self, pending_frame):
            return None
    
    
    gdb.unwinder.register_unwinder(None, runto_unwinder(), replace=True)

    The last line of this file is responsible for registering the new unwinder with GDB. The first argument None tells GDB to register this unwinder in the global scope, but it is also possible to register an unwinder for a specific object file or a specific program space. We'll not cover these cases in this tutorial, but the GDB documentation has more details.

    The second argument to register_unwinder is our new unwinder object. We'll discuss this more below.

    The final argument replace=True indicates that this new unwinder should replace any existing unwinder with the same name. This is useful when developing the unwinder as we can adjust our script and re-source it from GDB; the updated unwinder will then replace the existing one.

    In our unwinder object runto_unwinder, the constructor just calls the parent constructor and passes in a name for our unwinder. The name can be used within GDB to disable and enable the unwinder using the disable unwinder and enable unwinder commands, respectively. There is also info unwinder which lists all the registered Python unwinders.

    Our unwinder object also implements the required __call__ method. This method is passed a gdb.PendingFrame object in pending_frame. This pending frame describes the frame that is searching for an unwinder. We must examine this object and decide whether this unwinder applies to this pending frame. By returning None, our unwinder is currently telling GDB that we don't wish to claim pending_frame, our unwinder as it currently stands will not claim any frames, but we can start to address that next.

    Identifying frames to claim

    The first task our new unwinder needs to do is to decide which frame, or frames, should be claimed and which should not be claimed. Any frames not claimed by our unwinder will be offered to any other registered unwinders and will then be offered to GDB's built-in unwinders.

    The easiest way to decide if we should claim a frame or not is to compare the program-counter address within the frame to the address range of the function we're claiming for—in this case, run_on_new_stack. We can easily find the program-counter address for the frame by reading the $pc register. This is done using the read_register method of the gdb.PendingFrame class.

    Having read $pc, we need an address range to compare against. For that, we will make use of GDB's disassembler. We will disassemble run_on_new_stack and extract the address of each instruction. We can then use the first and last addresses as the lower and upper bounds that our unwinder should claim.

    Update runon-unwinder.py like this:

    from gdb.unwinder import Unwinder
    
    _unwind_analysis = None
    
    
    def analyze():
        global _unwind_analysis
        # Disassemble the run_on_new_stack function.
        disasm = gdb.execute("disassemble run_on_new_stack", False, True)
        # Discard the first and last lines, these don't contain
        # disassembled instructions, and are of no interest.
        disasm = disasm.splitlines()[1:-1]
        # Extract the address of each instruction, and store these
        # addresses into the global _unwind_analysis list.
        disasm = [int(l.lstrip().split()[0], 16) for l in disasm]
        _unwind_analysis = disasm
    
    
    class runto_unwinder(Unwinder):
        def __init__(self):
            super().__init__("runto_unwinder")
    
        def __call__(self, pending_frame):
    
            # Analyze the function we're going to unwind.
            global _unwind_analysis
            if _unwind_analysis is None:
                analyze()
    
            # If this is not a frame we handle then return None.
            pc = pending_frame.read_register("pc")
            if pc < _unwind_analysis[0] or pc > _unwind_analysis[-1]:
                return None
    
            print(f"Found a frame we can handle at: {pc}")
            return None
    
    
    gdb.unwinder.register_unwinder(None, runto_unwinder(), replace=True)

    The new analyze function disassembles run_on_new_stack and stores the address of each instruction in the global _unwind_analysis list.

    In runto_unwinder.__call__ we initialize _unwind_analysis by calling analyze once. We read $pc by calling pending_frame.read_register, and then we compare pc to the first and last addresses in _unwind_analysis. If the frame's program-counter is outside of the accepted range, then we return None; this indicates to GDB that we don't wish to claim this frame.

    If the frame's program-counter is within the range of run_on_new_stack, then we print a message, and, for now, also return None—don't worry, though, we'll soon be doing more than returning None here, but right now, let's test our code.

    Using the same demonstration application as before, here's an example GDB session:

    $ gdb -q demo
    Reading symbols from demo...
    (gdb) break func
    Breakpoint 1 at 0x4011b1: file demo.c, line 25.
    (gdb) run
    Starting program: /tmp/demo
    
    Breakpoint 1, func () at demo.c:25
    25      printf ("Hello world\n");
    (gdb) source runto-unwind.py
    (gdb) backtrace
    Found a frame we can handle at: 0x4011fb <run_on_new_stack+20>
    #0  func () at demo.c:25
    #1  0x00000000004011fb in run_on_new_stack () at runon.S:19
    #2  0x00007fffffffad38 in ?? ()
    #3  0x00007fffffffad50 in ?? ()
    #4  0x0000000000000000 in ?? ()
    (gdb)

    Notice the line: Found a frame we can handle at: 0x4011fb <run_on_new_stack+20>. Don't worry if the addresses you see are different; what's important is that the message is printed—and printed just once. This indicates that our unwinder has identified a single frame it wishes to claim. The address from that line, 0x4011fb, matches the address from frame #1, the run_on_new_stack frame; this shows that the correct frame was claimed.

    A detour into frame-ids

    The next step is to update the __call__ method to return a value that indicates the frame has been claimed by this unwinder. However, in order to claim the frame, we must provide a frame-id for the frame.

    A frame-id is a unique identifier generated by the unwinder that must be unique for each stack frame but the same for every address within a particular invocation of a function.

    Imagine the case where GDB is stepping through a function. After each step, GDB needs to recognize if it is still in the same frame or not. After each step, the unwinder will be used to identify the frame and generate the frame-id again. So long as the generated frame-id is always the same, GDB will understand it is still in the same frame.

    Within GDB, frame-ids are a tuple of stack-pointer and code-pointer addresses. Often unwinders use the stack address at entry to the function (typically called the frame base address), and the program address for the function's first instruction.

    Within GDB's Python API, a frame-id is represented by any object that has the sp and pc attributes. These attributes should contain gdb.Value objects representing their respective addresses.

    Creating a frame-ID and UnwindInfo object

    Now that we know about frame-ids, let's dive in and update our unwinder. We'll discuss these changes afterward. Update runto-unwind.py as follows:

    from gdb.unwinder import Unwinder
    
    _unwind_analysis = None
    
    
    def analyze():
        global _unwind_analysis
        # Disassemble the run_on_new_stack function.
        disasm = gdb.execute("disassemble run_on_new_stack", False, True)
        # Discard the first and last lines, these don't contain
        # disassembled instructions, and are of no interest.
        disasm = disasm.splitlines()[1:-1]
        # Extract the address of each instruction, and store these
        # addresses into the global _unwind_analysis list.
        disasm = [int(l.lstrip().split()[0], 16) for l in disasm]
        _unwind_analysis = disasm
    
    
    class FrameID:
        def __init__(self, sp, pc):
            self.sp = sp
            self.pc = pc
    
    
    class runto_unwinder(Unwinder):
        def __init__(self):
            super().__init__("runto_unwinder")
    
        def __call__(self, pending_frame):
    
            # Analyze the function we're going to unwind.
            global _unwind_analysis
            if _unwind_analysis is None:
                analyze()
    
            # If this is not a frame we handle then return None.
            pc = pending_frame.read_register("pc")
            if pc < _unwind_analysis[0] or pc > _unwind_analysis[-1]:
                return None
    
            # Create a frame id that will remain consistent throughout
            # the frame, no matter what $pc we stop at.  We use the $sp
            # value for the previous frame (this was our $sp on frame
            # entry), and we use the $pc for the start of the function.
            #
            # For the first four and last two instructions, the previous
            # $sp value can be found in the %rsp register.
            #
            # For the fifth and sixth instructions we need to fetch the
            # previous $sp value from the original stack.
            rsp = pending_frame.read_register("rsp")
            if pc < _unwind_analysis[5] or pc > _unwind_analysis[6]:
                frame_sp = rsp
            else:
                frame_sp = gdb.parse_and_eval("*((unsigned long long *) 0x%x)" % rsp)
            func_start = gdb.Value(_unwind_analysis[0])
            frame_id = FrameID(frame_sp, func_start)
    
            # Create the unwind_info cache object which holds our unwound
            # registers.
            unwind_info = pending_frame.create_unwind_info(frame_id)
    
            print(f"Found a frame we can handle at: {pc}")
            return None
    
    
    gdb.unwinder.register_unwinder(None, runto_unwinder(), replace=True)

    Currently, GDB doesn't include any helper classes that can be used to represent a frame-id, so we need to define our own – FrameID. The only requirements are that this class has the sp and pc attributes.

    Within the __call__ method we use the first address of run_on_new_stack as the program-counter value for the frame-id. This is done with this line:

            func_start = gdb.Value(_unwind_analysis[0])

    For the stack-pointer address of the frame-id, we need to be smarter. When we first enter run_on_new_stack, the previous stack-pointer value is still present in %rsp, but within run_on_new_stack, the %rsp register is stored to the new stack and a new value loaded into %rsp.

    To handle these two cases, we use the following block of code:

            if pc < _unwind_analysis[5] or pc > _unwind_analysis[6]:
                frame_sp = rsp
            else:
                frame_sp = gdb.parse_and_eval("*((unsigned long long *) 0x%x)" 
                                              % rsp)

    We choose between the two possible paths based on the current location within the function. The addresses _unwind_analysis[5] and _unwind_analysis[6] were chosen by reviewing the instruction disassembly for run_on_new_stack. A good exercise would be to disassemble the function and convince yourself that the above choices are correct.

    We can now create an instance of our FrameID class and use this instance to create a gdb.UnwindInfo object with these lines:

            frame_id = FrameID(frame_sp, func_start)
    
            # Create the unwind_info cache object which holds our unwound
            # registers.	 
            unwind_info = pending_frame.create_unwind_info(frame_id)

    The gdb.UnwindInfo class is the last piece of the unwinder process. We will store unwound register values into our unwind_info object and return this to GDB in order to claim this frame. However, we're not there just yet—for now, we're still printing a debug message and returning None.

    Adding unwound register values

    Having decided to claim this frame, and having created a gdb.UnwindInfo object, we need to store some unwound register values in our new unwind_info object.

    The unwound value of a register is the value a register had in the previous frame.

    Locating the previous register values will involve understanding the assembler code for the function being unwound. You don't need to provide previous values for every register; in some cases, the previous value of a register will not be available at all, in which case nothing can be done.

    To keep the complexity of this example down, we are only going to provide previous values for 3 registers, the program counter, %rsp, and %rbp. These registers are enough to allow GDB to build a complete backtrace on x86-64. Once you've seen how these registers are supported, extending the example to support other registers as needed should be easy enough.

    As before, let's just update runon-unwind.py, and discuss the changes afterwards:

    from gdb.unwinder import Unwinder
    
    _unwind_analysis = None
    
    
    def analyze():
        global _unwind_analysis
        # Disassemble the run_on_new_stack function.
        disasm = gdb.execute("disassemble run_on_new_stack", False, True)
        # Discard the first and last lines, these don't contain
        # disassembled instructions, and are of no interest.
        disasm = disasm.splitlines()[1:-1]
        # Extract the address of each instruction, and store these
        # addresses into the global _unwind_analysis list.
        disasm = [int(l.lstrip().split()[0], 16) for l in disasm]
        _unwind_analysis = disasm
    
    
    class FrameID:
        def __init__(self, sp, pc):
            self.sp = sp
            self.pc = pc
    
    
    class runto_unwinder(Unwinder):
        def __init__(self):
            super().__init__("runto_unwinder")
    
        def __call__(self, pending_frame):
    
            # Analyze the function we're going to unwind.
            global _unwind_analysis
            if _unwind_analysis is None:
                analyze()
    
            # If this is not a frame we handle then return None.
            pc = pending_frame.read_register("pc")
            if pc < _unwind_analysis[0] or pc > _unwind_analysis[-1]:
                return None
    
            # Create a frame id that will remain consistent throughout the
            # frame, no matter what $pc we stop at.  We use the $sp value
            # for the previous frame (this was our $sp on frame entry),
            # and we use the $pc for the start of the function.
            #
            # For the first four and last two instructions, the previous
            # $sp value can be found in the %rsp register.
            #
            # For the fifth and sixth instructions we need to fetch the
            # previous $sp value from the original stack.
            rsp = pending_frame.read_register("rsp")
            if pc < _unwind_analysis[5] or pc > _unwind_analysis[6]:
                frame_sp = rsp
            else:
                frame_sp = gdb.parse_and_eval("*((unsigned long long *) 0x%x)" % rsp)
            func_start = gdb.Value(_unwind_analysis[0])
            frame_id = FrameID(frame_sp, func_start)
    
            # Create the unwind_info cache object which holds our unwound
            # registers.
            unwind_info = pending_frame.create_unwind_info(frame_id)
    
            # Calculate the previous register values.  Select the correct
            # previous value for $rbp based on where we are in the
            # function.
            if pc < _unwind_analysis[4] or pc > _unwind_analysis[7]:
                prev_rbp = pending_frame.read_register("rbp")
            else:
                prev_rbp = gdb.parse_and_eval("*((unsigned long long *) 0x%x)" % (rsp + 8))
    
            # We use the previous $sp value in our frame-id, which is handy!
            prev_rsp = frame_sp
    
            # The previous $pc is always on the original (incoming) stack.
            prev_pc = gdb.parse_and_eval("*((unsigned long long *) 0x%x)" % (prev_rsp))
    
            # And store the previous values into our cache.
            unwind_info.add_saved_register("rsp", prev_rsp)
            unwind_info.add_saved_register("rbp", prev_rbp)
            unwind_info.add_saved_register("pc", prev_pc)
    
            # Return the cache for GDB to use.
            return unwind_info
    
    
    gdb.unwinder.register_unwinder(None, runto_unwinder(), replace=True)

    The most important part of this new version are the three calls to unwind_info.add_saved_register; this is how we record the unwound register values. The first argument to these calls is the name of the register we are recording, and the second argument is the value that register had in the previous frame.

    The three registers we record are pc, rsp, and rbp. Figuring out the previous value for the first two registers is pretty easy. We already have the previous rsp value, remember, this is what we used for our frame-id so that we can reuse that value here.

    Recall from our earlier stack diagrams; the return address in main was the last thing stored on the original stack, this is what the previous stack-pointer points at, so we can load the return address with this line:

            prev_pc = gdb.parse_and_eval("*((unsigned long long *) 0x%x)" 
                                         % (prev_rsp))

    And this just leaves the previous rbp value. Just like we found the previous rsp value earlier, the current instruction within run_on_new_stack will determine the location of the previous rbp value. Initially the previous value is in the rbp register, but we store this previous value to the new stack before calling func. And so, to find the correct previous value, we need to switch based on the program-counter value, which we do with these lines:

            if pc < _unwind_analysis[4] or pc > _unwind_analysis[7]:
                prev_rbp = pending_frame.read_register("rbp")
            else:
                prev_rbp = gdb.parse_and_eval("*((unsigned long long *) 0x%x)" 
                                              % (rsp + 8))

    The last update is to remove the debug message that we have been printing until now, and instead of returning None, return our gdb.UnwindInfo object unwind_info. This tells GDB that our unwinder has claimed this frame. GDB will use the previous register values stored within unwind_info when it needs to unwind through this frame.

    So, for the last time, let's try our unwinder in GDB:

    $ gdb -q demo
    Reading symbols from demo...
    (gdb) break func
    Breakpoint 1 at 0x4011b1: file demo.c, line 25.
    (gdb) run
    Starting program: /tmp/demo
    
    Breakpoint 1, func () at demo.c:25
    25      printf ("Hello world\n");
    (gdb) source runon-unwind.py
    (gdb) backtrace
    #0  func () at demo.c:25
    #1  0x00000000004011fb in run_on_new_stack () at runon.S:19
    #2  0x00000000004011e0 in main () at demo.c:33
    (gdb)

    And success! We can now unwind through run_on_new_stack back to main.

    Summary and conclusions

    Writing custom stack unwinders is not trivial; it requires a good understanding of the function being unwound and the architecture the unwinder is being written for. There is more to GDB's unwinder API than has been discussed in this brief introduction. The full details can all be found in the documentation.

    Last updated: August 14, 2023

    Related Posts

    • How to debug stack frames and recursion in GDB

    • How to debug C++ lambda expressions with GDB

    • Add custom windows to GDB: Programming the TUI in Python

    • Debugging binaries invoked from scripts with GDB

    • Printf-style debugging using GDB, Part 1

    Recent Posts

    • Supercharging AI isolation: microVMs with RamaLama & libkrun

    • Simplify multi-VPC connectivity with amazon.aws 9.0.0

    • How HaProxy router settings affect middleware applications

    • Fly Eagle(3) fly: Faster inference with vLLM & speculative decoding

    • Kafka Monthly Digest: June 2025

    What’s up next?

    Users and administrators query and control systemd behavior through the systemctl command. The systemd Commands Cheat Sheet presents the most common uses of systemctl, along with journalctl for displaying information about systemd activities from its logs.

    Get the cheat sheet
    Red Hat Developers logo LinkedIn YouTube Twitter Facebook

    Products

    • Red Hat Enterprise Linux
    • Red Hat OpenShift
    • Red Hat Ansible Automation Platform

    Build

    • Developer Sandbox
    • Developer Tools
    • Interactive Tutorials
    • API Catalog

    Quicklinks

    • Learning Resources
    • E-books
    • Cheat Sheets
    • Blog
    • Events
    • Newsletter

    Communicate

    • About us
    • Contact sales
    • Find a partner
    • Report a website issue
    • Site Status Dashboard
    • Report a security problem

    RED HAT DEVELOPER

    Build here. Go anywhere.

    We serve the builders. The problem solvers who create careers with code.

    Join us if you’re a developer, software engineer, web designer, front-end designer, UX designer, computer scientist, architect, tester, product manager, project manager or team lead.

    Sign me up

    Red Hat legal and privacy links

    • About Red Hat
    • Jobs
    • Events
    • Locations
    • Contact Red Hat
    • Red Hat Blog
    • Inclusion at Red Hat
    • Cool Stuff Store
    • Red Hat Summit
    © 2025 Red Hat

    Red Hat legal and privacy links

    • Privacy statement
    • Terms of use
    • All policies and guidelines
    • Digital accessibility

    Report a website issue