<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged gdb</title>
<link>https://coderlegion.com/tag/gdb</link>
<description>Powered by Question2Answer</description>
<item>
<title>Automating Stack Corruption Analysis in GDB with Python</title>
<link>https://coderlegion.com/20314/automating-stack-corruption-analysis-in-gdb-with-python</link>
<description>&lt;h3 id=&quot;a-bug-in-my-operating-system&quot;&gt;A bug in my operating system&lt;/h3&gt;
&lt;p&gt;During a recent visit to my wife's family in Sarajevo, I decided to revisit my&lt;br&gt;
hobby operating system in QEMU. I discovered that the boot process consistently&lt;br&gt;
froze while printing the BIOS memory map. What initially looked like a&lt;br&gt;
protected-mode issue eventually turned into a useful exercise in automating&lt;br&gt;
debugging using GDB's Python scripting.&lt;/p&gt;
&lt;h3 id=&quot;manual-debugging-failure&quot;&gt;Manual debugging failure&lt;/h3&gt;
&lt;p&gt;After reinspecting some common pitfalls in my protected mode setup I was more&lt;br&gt;
confident that the issue was stack corruption in my &lt;code&gt;print_memory_map&lt;/code&gt; function.&lt;br&gt;
If I had an unmatched push or pop, it could have corrupted return addresses and&lt;br&gt;
eventually redirected execution flow into invalid memory. Single-stepping&lt;br&gt;
through the routine manually quickly became impractical. The function mixed BIOS&lt;br&gt;
interrupt handling, memory map parsing and multiple helper calls, making it&lt;br&gt;
difficult to reason about stack state over time.&lt;/p&gt;
&lt;h3 id=&quot;setting-up-gdb-scripting-in-python&quot;&gt;Setting up GDB scripting in Python&lt;/h3&gt;
&lt;p&gt;I needed to automate this and GDB's integration with Python was the most&lt;br&gt;
promising route I could take. The idea was to do exactly what I started&lt;br&gt;
manually: break at a specific (suspicious) function and then start single&lt;br&gt;
stepping while inspecting how the stack pointer behaved.&lt;/p&gt;
&lt;p&gt;First things first, we import GDB module in Python, connect to the remote&lt;br&gt;
target and set up a breakpoint (this is done by inheriting from&lt;br&gt;
&lt;code&gt;gdb.Breakpoint&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;import gdb

class StackTraceBreakpoint(gdb.Breakpoint):

    def __init__(self, func_name):
        super(StackTraceBreakpoint, self).__init__(func_name)
        self.active = False
    
    def stop(self):
        if self.active:
            raise Exception(&quot;Recursion detected - stopping.&quot;)
        self.active = True

        return True

if __name__ == &quot;__main__&quot;:
    gdb.execute(&quot;target remote :5555&quot;)
    gdb.execute(&quot;symbol-file ../build/arch/x86/bios-legacy/boot-stage1-5.elf&quot;)

    tracer = StackTraceBreakpoint(&quot;print_memory_map&quot;)
    gdb.execute(&quot;continue&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;stacktracebreakpoint&quot;&gt;StackTraceBreakpoint&lt;/h4&gt;
&lt;p&gt;So this is kind of bare-bones of what I wanted to do. This code will simply add&lt;br&gt;
a breakpoint with custom logic after connecting to QEMU and loading symbols.&lt;br&gt;
As soon as we do &lt;code&gt;gdb.execute(&quot;continue&quot;)&lt;/code&gt; GDB will run and, if and when it hits&lt;br&gt;
our breakpoint, it will execute whatever we wrote in the &lt;code&gt;stop()&lt;/code&gt; method. For&lt;br&gt;
now I only added a kind of assertion that we cannot analyze recursions (I didn't&lt;br&gt;
use any in my code anyway and logic would be a bit more complex).&lt;/p&gt;
&lt;h4 id=&quot;single-stepping&quot;&gt;Single-stepping&lt;/h4&gt;
&lt;p&gt;Now what we need to do is start single-stepping after we hit our breakpoint. So&lt;br&gt;
we add a while loop with &lt;code&gt;gdb.execute(&quot;stepi&quot;)&lt;/code&gt; after &lt;code&gt;gdb.execute(&quot;continue&quot;)&lt;/code&gt;.&lt;br&gt;
Note that the &lt;code&gt;stepi&lt;/code&gt; instruction steps over machine instructions, not over&lt;br&gt;
source code statements. Also note that we cannot start single-stepping in&lt;br&gt;
&lt;code&gt;stop()&lt;/code&gt; method because GDB won't be in a state which can accept these kinds of&lt;br&gt;
debugging requests.&lt;/p&gt;
&lt;h3 id=&quot;detecting-stack-imbalance&quot;&gt;Detecting stack imbalance&lt;/h3&gt;
&lt;p&gt;Furthermore I have wrapped this single-stepping logic in &lt;code&gt;trace_step()&lt;/code&gt; method&lt;br&gt;
in our breakpoint class. This method is not part of GDB breakpoint API, but&lt;br&gt;
rather just a convenience for tracking the number of pushes and pops in a&lt;br&gt;
consistent manner. To run this script we need to call:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;gdb -ex 'source debug-stack.py'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Another thing we need to track is whether we have entered another function (in&lt;br&gt;
which case we won't be counting pushes and pops) and if we have returned from&lt;br&gt;
it. If I ever wanted to inspect routines being called, I would just run the same&lt;br&gt;
script for them (my intention here is not creating a custom emulator on top of&lt;br&gt;
GDB). So I added a counter &lt;code&gt;func_count&lt;/code&gt; which will increase on &lt;code&gt;call&lt;/code&gt; and&lt;br&gt;
decrease on &lt;code&gt;ret&lt;/code&gt; instruction. Here is a rough idea:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;def trace_step(self):
insn_full = gdb.execute(&quot;x/i $pc&quot;, to_string=True).strip()
insn = re.search(r'^=&amp;gt;.*:\s*([^\s].*)$', insn_full).group(1).split(&quot; &quot;)[0]

match insn:
case &quot;push&quot;:
if self.func_count == 0:
self.pushes += 1
case &quot;pop&quot;:
if self.func_count == 0:
self.pops += 1
case &quot;call&quot;:
self.func_count += 1
case &quot;ret&quot;:
self.func_count -= 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here you can see how I'm extracting the instruction from GDB. And what is left&lt;br&gt;
is just improving logic and also fetching registers like PC, SP and CS for&lt;br&gt;
debugging. I log everything into a file as GDB can get really noisy with&lt;br&gt;
standard output (I didn't find a way to turn off all logging in GDB completely&lt;br&gt;
when single stepping). The script is available in my GitHub repository:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/StjepanPoljak/raspios/tree/master/scripts/debug-stack.py&quot;&gt;https://github.com/StjepanPoljak/raspios/tree/master/scripts/debug-stack.py&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;finding-the-root-cause&quot;&gt;Finding the root cause&lt;/h3&gt;
&lt;p&gt;Finally, you can see an example output detecting my very issue:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[START] print_memory_map SP=fffd
[0000:8183] push %ax (SP=0xfffd)
[0000:8185] push %bx (SP=0xfff9)
[0000:8187] push %cx (SP=0xfff5)
[0000:8189] push %dx (SP=0xfff1)
[0000:81a5] call 0x66e980a5 (SP=0xffed)
[0000:80a3] push %ax (SP=0xffeb)
[0000:80a7] call 0xab18056 (SP=0xffe7)
(...)
[0000:806e] ret  (SP=0xffef)
[0000:8098] call 0xf6ec8056 (SP=0xfff1)
[0000:8054] push %ax (SP=0xffef)
[0000:8056] push %bx (SP=0xffeb)
[0000:806a] pop %bx (SP=0xffe7)
[0000:806c] pop %ax (SP=0xffeb)
[0000:806e] ret  (SP=0xffef)
[0000:8098] call 0xf6ec8056 (SP=0xfff1)
[0000:8054] push %ax (SP=0xffef)
[0000:8056] push %bx (SP=0xffeb)
[0000:806a] pop %bx (SP=0xffe7)
[0000:806c] pop %ax (SP=0xffeb)
[0000:806e] ret  (SP=0xffef)
[0000:809d] pop %esi (SP=0xfff1)
[0000:809e] pop %bx (SP=0xfff3)
[0000:80a0] pop %ax (SP=0xfff7)
[0000:80a2] ret  (SP=0xfffb)
[FAIL] Extra pop detected at [0000:81fa].
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So the real culprit was an extra &lt;code&gt;pop eax&lt;/code&gt; in my &lt;code&gt;print_memory_map&lt;/code&gt; routine:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;print_memory_map:
push eax
push ebx
push ecx
push edx

; --- ommited print loop logic ---

.noprint_newline:
pop eax

cmp ecx, [memory_map_size]
jne .print_memory_map_loop

pop edx
pop ecx
pop ebx
pop eax
ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Removing this line will cause my debugging script to successfully pass.&lt;/p&gt;
&lt;h3 id=&quot;try-it-out-yourself&quot;&gt;Try it out yourself&lt;/h3&gt;
&lt;p&gt;You can try it out yourself, just check out my operating system, &lt;code&gt;raspios&lt;/code&gt;, on&lt;br&gt;
GitHub:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/StjepanPoljak/raspios&quot;&gt;https://github.com/StjepanPoljak/raspios&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Build and run it with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;mkdir build
cd build
ARCH=x86 cmake ..
make
make qemu_debug
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, in the &lt;code&gt;scripts&lt;/code&gt; folder run &lt;code&gt;gdb -ex 'source debug-stack.py'&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;This was a good reminder that low-level debugging often benefits from&lt;br&gt;
lightweight tooling tailored to the problem at hand. In this case, a small&lt;br&gt;
amount of Python automation around GDB made stack corruption analysis&lt;br&gt;
significantly more manageable than manual instruction tracing.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20314/automating-stack-corruption-analysis-in-gdb-with-python</guid>
<pubDate>Thu, 11 Jun 2026 15:55:57 +0000</pubDate>
</item>
</channel>
</rss>