From P-Code to GNN: extract binary code semantics - Quarkslab's blog
Table of contents
Context<br>Extracting a semantic graph<br>The CISCO TALOS dataset<br>Extracting the graphs<br>Message-passing and diameter<br>GNN architecture<br>Loss function<br>Translating the graph into tensors<br>Building the batches<br>Training loop<br>Evaluation method and results<br>Conclusion
Posted<br>Thu 13 August 2026
Author<br>Samuel Hangouët
Category
Program Analysis
Tags<br>machine learning,<br>function similarity,<br>data analysis,<br>binary analysis,<br>GNN,<br>tool,<br>graph neural networks,<br>2026
pcode_graph is a Python library, published by Quarkslab, suitable to build semantic graphs from binary code. We present how to use it to detect function similarities in binaries.
Context
We introduce here the Python library pcode_graph, a tool developed at Quarkslab to abstract the semantics of binary code. It provides an API to build and visualize Control & Data flow Graphs (CDG) from a function, a basic-block or any arbitrary piece of code.
As soon as you start looking into the automated analysis of binaries, you quickly realize that many use cases require extracting a semantic representation of a piece of code. For example, such a representation could be used to:
Identify the type(s) of obfuscation applied;
Build a database of gadgets or automatically chain them to do ROP;
Find changes between two successive versions of the same binary;
Look for a function in a database of binaries;
Look for vulnerabilities;
Deobfuscate a piece of binary.
This last use case was the subject of a research paper published at ESANN 2026 using the pcode_graph library, but it was not open-sourced at the time of publication.
To present the library, here we are going to focus on the detection of similarities between functions. More precisely, we will train a neural network to recognize a function, independently of the architecture, the compiler and the options used to compile it. For that, we will use the Cisco-Talos dataset.
The library is available from Quarkslab's pcode_graph repository on GitHub. It can also be installed directly with pip:
(venv) $ pip install pcode_graph
Extracting a semantic graph
Many methods exist to teach a model to compare two binary functions.
A basic approach is to extract statistical features such as the number of instructions, the number of basic blocks, the mnemonic frequencies, etc. This yields a table of features on which the learning is performed. This can be good enough to perform some basic tasks, like malware classification.
jTrans feeds the assembly (with a bit of preprocessing) into a language-processing model.
From a production perspective, several tools were compared prior to creating Quarkslab's Sighthouse tool.
Here we are going to extract a semantic graph , a representation of what the piece of code does disregarding how it does it, for example the specific CPU instructions. Note that this is not necessarily the most relevant approach: the choice depends entirely on your use case and your data.
Let's take a very simple piece of code:
int do_it(int a, int b)<br>if (a == b)<br>return a + b;<br>return 0;
and compile it so as to get a small piece of assembly:
clang test.c -o test.o -Oz -c
On x86_64 we get:
lea ecx, [rsi + rdi*0x1]<br>xor eax, eax<br>cmp edi, esi<br>cmovz eax, ecx<br>ret
Now let's try to compile it without optimization:
push rbp<br>mov rbp, rsp<br>mov dword ptr [rbp + -0x8], edi<br>mov dword ptr [rbp + -0xc], esi<br>mov eax, dword ptr [rbp + -0x8]<br>cmp eax, dword ptr [rbp + -0xc]<br>jnz 0x1d<br>mov eax, dword ptr [rbp + -0x8]<br>add eax, dword ptr [rbp + -0xc]<br>mov dword ptr [rbp + -0x4], eax<br>jmp 0x24<br>mov dword ptr [rbp + -0x4], 0x0<br>mov eax, dword ptr [rbp + -0x4]<br>pop rbp<br>ret
These are two versions of the same function compiled with the same compiler for the same architecture, and yet:
The second version is three times longer.
The only mnemonic they have in common is CMP, and it works on different operands.
The second one has three basic blocks against a single one for the first.
These points highlight the limits of statistical feature extraction for function comparison.
Let's now look at the optimized version. If we swap the two first instructions, the semantics remain unchanged:
xor eax, eax<br>lea ecx, [rsi + rdi*0x1]<br>cmp edi, esi<br>cmovz eax, ecx<br>ret
While the order of instructions can be of interest to detect the compiler used, for our use case we are to the contrary looking for a representation that is ideally identical for identical code semantics, otherwise our model would have to learn to ignore all the semantically equivalent permutations of a same function.
To do so, we are going to extract the data flow graph of the function.
In order to abstract away the architecture, we use the pypcode library, a binding of SLEIGH which translates binary code into Ghidra's low-level internal representation. One benefit of P-Code is that it is limited to 63 distinct opcodes (at the Raw level, excluding IMARK). Compared to the thousands of x86_64 mnemonics,...