A library to make GNN work on binary code Quarkslab has released pcode_graph, a Python library for building semantic graphs from binary code, and demonstrated its use in training a neural network to detect function similarities across architectures and compilers. The library, available on GitHub and via pip, was used in research presented at ESANN 2026 and is intended for tasks such as vulnerability detection and binary diffing. Author Samuel Hangouët ./author/samuel-hangouet.html Category Program Analysis ./category/program-analysis.html Tags machine learning ./tag/machine-learning.html , function similarity ./tag/function-similarity.html , data analysis ./tag/data-analysis.html , binary analysis ./tag/binary-analysis.html , GNN ./tag/gnn.html , tool ./tag/tool.html , graph neural networks ./tag/graph-neural-networks.html , 2026 ./tag/2026.html 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 https://github.com/quarkslab/conf-presentations/blob/master/Confs/ESANN2026/ES2026-155.pdf 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 https://www.usenix.org/system/files/sec22-marcelli.pdf . The library is available from Quarkslab's pcode graph repository on GitHub https://github.com/quarkslab/pcode graph . It can also be installed directly with pip : bash 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 https://dl.acm.org/doi/pdf/10.1145/3576915.3616589 . jTrans https://dl.acm.org/doi/pdf/10.1145/3533767.3534367 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 https://blog.quarkslab.com/sighthouse-automated-function-identification.html . 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 { if a == b return a + b; 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 xor eax, eax cmp edi, esi cmovz eax, ecx ret Now let's try to compile it without optimization: push rbp mov rbp, rsp mov dword ptr rbp + -0x8 , edi mov dword ptr rbp + -0xc , esi mov eax, dword ptr rbp + -0x8 cmp eax, dword ptr rbp + -0xc jnz 0x1d mov eax, dword ptr rbp + -0x8 add eax, dword ptr rbp + -0xc mov dword ptr rbp + -0x4 , eax jmp 0x24 mov dword ptr rbp + -0x4 , 0x0 mov eax, dword ptr rbp + -0x4 pop rbp 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 lea ecx, rsi + rdi 0x1 cmp edi, esi cmovz eax, ecx 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, this limits the possibilities of expressing the same semantics in different ways. It is also a representation that makes building a data flow graph easier. We can directly use the pcode graph CLI to get the P-Code: cdg pcode test.o test.pcode We get the following file: php imark 0x0 $37632 = int mult RDI, 0x1 $38144 = int add RSI, $37632 ECX = subpiece $38144, 0x0 RCX = int zext ECX imark 0x3 CF = 0x0 OF = 0x0 EAX = int xor EAX, EAX RAX = int zext EAX SF = int sless EAX, 0x0 ZF = int equal EAX, 0x0 $361216 = int and EAX, 0xff $361472 = popcount $361216 $361728 = int and $361472, 0x1 PF = int equal $361728, 0x0 imark 0x5 $515328 = EDI CF = int less $515328, ESI OF = int sborrow $515328, ESI $515840 = int sub $515328, ESI SF = int sless $515840, 0x0 ZF = int equal $515840, 0x0 $361216 = int and $515840, 0xff $361472 = popcount $361216 $361728 = int and $361472, 0x1 PF = int equal $361728, 0x0 imark 0x7 $505344 = ECX RAX = int zext EAX $505600 = bool negate ZF cbranch 0xa , $505600 EAX = $505344 imark 0xa RIP = load 0x6b970f0, RSP RSP = int add RSP, 0x8 return RIP Ok, that's fairly verbose... To break instructions down into basic operations, pypcode introduces many temporary variables. But there is nothing to worry about: these variables will subsequently be ignored and will only make up edges of our data flow graph. Before building the graph, we start by applying several analysis passes to the P-Code: - Instruction indexing to resolve jumps ; - Unreachable code detection; - Data flow analysis. These results can be displayed as a table in markdown format: cdg table test.o | index | op | preds | succs | input defs | reachable | exit def | |---|---|---|---|---|---|---| | 0 | imark 0x0 | entry | 1 | x | || | 1 | $37632 = int mult RDI, 0x1 | 0 | 2 | RDI from entry | x | | | 2 | $38144 = int add RSI, $37632 | 1 | 3 | RSI from entry, $37632 from 1 | x | | | 3 | ECX = subpiece $38144, 0x0 | 2 | 4 | $38144 from 2 | x | | | 4 | RCX = int zext ECX | 3 | 5 | ECX from 3 | x | RCX | | 5 | imark 0x3 | 4 | 6 | x | || | 6 | CF = 0x0 | 5 | 7 | x | || | 7 | OF = 0x0 | 6 | 8 | x | || | 8 | EAX = int xor EAX, EAX | 7 | 9 | EAX from entry, EAX from entry | x | | | 9 | RAX = int zext EAX | 8 | 10 | EAX from 8 | x | | | 10 | SF = int sless EAX, 0x0 | 9 | 11 | EAX from 9 | x | | | 11 | ZF = int equal EAX, 0x0 | 10 | 12 | EAX from 9 | x | | | 12 | $361216 = int and EAX, 0xff | 11 | 13 | EAX from 9 | x | | | 13 | $361472 = popcount $361216 | 12 | 14 | $361216 from 12 | x | | | 14 | $361728 = int and $361472, 0x1 | 13 | 15 | $361472 from 13 | x | | | 15 | PF = int equal $361728, 0x0 | 14 | 16 | $361728 from 14 | x | | | 16 | imark 0x5 | 15 | 17 | x | || | 17 | $515328 = EDI | 16 | 18 | EDI from entry | x | | | 18 | CF = int less $515328, ESI | 17 | 19 | $515328 from 17, ESI from entry | x | CF | | 19 | OF = int sborrow $515328, ESI | 18 | 20 | $515328 from 17, ESI from entry | x | OF | | 20 | $515840 = int sub $515328, ESI | 19 | 21 | $515328 from 17, ESI from entry | x | | | 21 | SF = int sless $515840, 0x0 | 20 | 22 | $515840 from 20 | x | SF | | 22 | ZF = int equal $515840, 0x0 | 21 | 23 | $515840 from 20 | x | ZF | | 23 | $361216 = int and $515840, 0xff | 22 | 24 | $515840 from 20 | x | | | 24 | $361472 = popcount $361216 | 23 | 25 | $361216 from 23 | x | | | 25 | $361728 = int and $361472, 0x1 | 24 | 26 | $361472 from 24 | x | | | 26 | PF = int equal $361728, 0x0 | 25 | 27 | $361728 from 25 | x | PF | | 27 | imark 0x7 | 26 | 28 | x | || | 28 | $505344 = ECX | 27 | 29 | ECX from 4 | x | | | 29 | RAX = int zext EAX | 28 | 30 | EAX from 9 | x | EAX | | 30 | $505600 = bool negate ZF | 29 | 31 | ZF from 22 | x | | | 31 | cbranch 0xa , $505600 | 30 | 33, 32 | $505600 from 30 | x | | | 32 | EAX = $505344 | 31 | 33 | $505344 from 28 | x | EAX | | 33 | imark 0xa | 32, 31 | 34 | x | || | 34 | RIP = load 0x33fd1630, RSP | 33 | 35 | MEMORY from entry, RSP from entry | x | RIP | | 35 | RSP = int add RSP, 0x8 | 34 | 36 | RSP from entry | x | RSP | | 36 | return RIP | 35 | elsewhere | RIP from 34 | x | Since pcode graph was initially designed to work on small chunks of code, the CFG is built directly at the level of the P-Code operations instead of extracting basic-blocks. Beware that this could change in the near future. From these elements the dataflow graph is easy to build: Then, a simplification pass removes nodes that are useless or that carry information which can otherwise be found implicitly in the graph: To generate this representation as an HTML file via pyvis , you can use the cdg tool provided by the pcode graph package: cdg html --dataflow-only test.o -o test dataflow.html But one can also directly generate the graph in mermaid format: cdg md --dataflow-only test.o test dataflow.md We can notice on this graph that: - It is much simpler than the P-Code - The permutation of instructions has no impact on the graph. The Phi-node, however, leaves us puzzled: nothing tells us what makes EAX set to zero or to the sum of RDI and RSI. That's expected, since the data flow is not sufficient to express the semantics of the code. We therefore add control flow edges to the graph: In the resulting graph, in addition to the condition for writing EAX, we also see the return address loading from the stack. The EXTERNAL node expresses the jump to an address outside of the code present in the graph. If the code does not end with a branch, we will have an END node. In total, we have ten different node kinds: class NodeKinds Enum : InputRegister = 0 OutputRegister = 1 Constant = 2 Operation = 3 Phi = 4 ReadMemory = 5 WrittenMemory = 6 Begin = 7 External = 8 End = 9 There can be several memory nodes in a graph, but as no pointer aliasing analysis is performed we do not distinguish between memory addresses. You may have noticed that adding the control flow edges breaks the invariance of the graph with respect to instruction permutations. We are thinking about improving the graph format to fix this. We are saving that for a future release of pcode graph . The CISCO TALOS dataset This USENIX2022 paper https://www.usenix.org/system/files/sec22-marcelli.pdf introduces a dataset https://github.com/Cisco-Talos/binary function similarity including a large corpus of binaries compiled across: - 6 architectures x86, ARM, MIPS x 32 and 64 bits ; - 8 compiler variants 4 for gcc and 4 for clang ; - 5 optimization levels O0, O1, O2, O3, Os . The functions were extracted using IDA. In total, we have: - 256,625 functions for training; - 12,736 functions for validation; - 522,003 functions for testing. They also publish a comparison of 10 state-of-the-art approaches on this dataset. Extracting the graphs To speed up the training of our models, we start with a preprocessing step to extract the CDG graphs corresponding to the 791,364 functions of the dataset. To extract the graphs, we use the LIEF library to extract the code at the offsets given by the dataset, then the make graph from binary function to lift the code into P-Code and create the graph: python from pcode graph.lief importer import lookup chunk from pcode graph.maker import make graph from binary from pcode graph.translator import Translator for arch, binaries in dataset index.items : translator = Translator arch for binary path, functions in binaries.items : binary = parse binary binary path for name, start, end in functions: code = lookup chunk binary, start, end cdg = make graph from binary translator, code, start output path = compute graph path dataset dir, binary path, name output path.write bytes pickle.dumps cdg Lifting binaries into P-Code with pypcode is trivial in a simple case, but properly handling the presence of data in the middle of the code, or errors in function extraction heuristics, is not so simple. You can have a look at the code of the Translator class if you are interested. The make graph from binary method accepts options to guide the graph construction. In particular you can control the names of the registers whose writes should be considered as outputs of the code. If what you are extracting is a gadget, all registers can be useful, including processor flags. For our experiment, we used the default behavior which considers all the general purpose registers, but since we are interested in the semantics of a function, it would have been smarter to keep only the registers holding the return value, in order to simplify the dataflow graph while reducing the risk of bias. This step takes time but parallelizes nicely via a small helper: def run in parallel P, R function: Callable P , R , parameters: list P , num jobs: int | None = None, initializer: Callable = lambda: None, - Iterator R : with multiprocessing.Pool processes=num jobs, initializer=initializer as pool: for result in pool.imap unordered function, parameters : yield result To avoid spending too much time on some very large functions of the dataset, we set a 5-second timeout for the analysis, the graph construction and its simplification. This removes 1.4% of the functions from the dataset. Note that trying to lift a binary into P-Code while specifying the wrong architecture can give surprising results, so we had to detect and skip the 316 labeling errors of the training set https://github.com/Cisco-Talos/binary function similarity/issues/39 . Message-passing and diameter A graph neural network GNN learns a representation of each node through the so-called message passing technique, that is, by iteratively aggregating the information of its neighbors. Starting from an initial state, each node aggregates the states of its direct neighbors to update its own state. This aggregation is done via a graph convolution operator. We use the torch geometric library, which provides an implementation of a good part of the research on the topic https://pytorch-geometric.readthedocs.io/en/latest/cheatsheet/gnn cheatsheet.html : By stacking K convolution layers, a node ends up integrating the information coming from its K -hop neighborhood, while taking the topological structure of the graph into account. Then, we can combine the resulting stats of all nodes to generate a global graph representation. This last stage is called readout . To extract non-local properties of large graphs, one therefore has to stack many layers. However, unlike classical neural networks, GNNs suffer from an oversmoothing https://arxiv.org/pdf/2405.01663 problem when the number of hops increases 10 layers . This is one reason why we strive to produce graphs with the smallest possible diameter, independently of their size. To get an idea, here is the distribution of graph diameters on the validation set when considering only the control-flow edges, only the data-flow edges, or both: We notice that adding the control-flow to the graph slightly increases the diameter, but dataflow edges help limit the damage. GNN architecture GNN architecture remains a vast research topic and searching for the best hyper-parameters takes time. We simply used GINE https://arxiv.org/abs/1905.12265 to have a solid baseline: python from torch import Tensor, relu import torch from torch geometric.data import Data from torch.nn import Dropout, ReLU, Linear, Module, ModuleList, Sequential, from torch geometric.nn import GINEConv, global add pool, GraphNorm from dataclasses import dataclass @dataclass class GNNConfig: readout head outputs: int = 256 head hidden: int = 256 conv hidden: int = 64 conv layers: int = 4 feature dropout: float = 0.5 class GINE Module : def init self, config: GNNConfig, node features: int, edge features: int : super . init self.convs = ModuleList self.norms = ModuleList self.dropout = Dropout config.feature dropout for i in range config.conv layers : dim in = node features if i == 0 else config.conv hidden self.convs.append GINEConv Sequential Linear dim in, config.conv hidden , ReLU , Linear config.conv hidden, config.conv hidden , , train eps=True, edge dim=edge features, self.norms.append GraphNorm config.conv hidden self.head = Sequential Linear config.conv hidden config.conv layers, config.head hidden , ReLU , Dropout config.feature dropout , Linear config.head hidden, config.readout head outputs , def forward self, data: Data - Tensor: hs = x = data.x for conv, norm in zip self.convs, self.norms : x = conv x, data.edge index, data.edge attr x = relu norm x, data.batch x = self.dropout x hs.append global add pool x, data.batch return self.head torch.cat hs, dim=-1 Loss function Now we have a model that computes an embedding of a graph, that is a fixed-size array of numbers associated with each graph of the batch passed to its forward method. But it is not exactly what we need: to find similar functions we have to output a similarity score for a pair of functions. We can do this by measuring the distance between embeddings, provided that the model was trained to push apart the embeddings of different functions while pulling together those of functions coming from the same program. Provided that our embeddings are L2-normalized, a dot is enough: similarity = emb1 emb2 .sum .item The classical method to produce this kind of embeddings consists in using a Siamese model and a Triplet Margin Loss. We went for a technique presented at NeurIPS2020: the Supervised Contrastive loss https://proceedings.neurips.cc/paper files/paper/2020/file/d89a66c7c80a29b1bdbab0f2a1a94af8-Paper.pdf SupCon loss . Here is the loss and the final model, which wraps the former one: python from torch import Tensor, matmul, eq, eye, exp, log, clamp from torch.nn import Module from torch geometric.data import Data from torch.optim import AdamW from torch.nn.functional import normalize def supcon loss features: Tensor, labels: Tensor, temperature: float : """ Supervised Contrastive Loss Khosla et al., 2020 . """ Similarity matrix Should be already normalized features = normalize features, dim=1 logits = matmul features, features.T / temperature Hack for numeric stability logits max, = logits.max dim=1, keepdim=True logits = logits - logits max.detach Compute mask of positive pairs ie with same label labels = labels.view -1, 1 batch size = features.shape 0 positive mask = eq labels, labels.T .float self mask = eye batch size, device=features.device positive mask = positive mask - self mask Compute logprobs logits mask = 1.0 - self mask exp logits = exp logits logits mask log prob = logits - log exp logits.sum dim=1, keepdim=True + 1e-12 Mean log prob on positives for each anchor num positives = positive mask.sum dim=1 mean log prob pos = positive mask log prob .sum dim=1 / clamp num positives, min=1.0 loss = -mean log prob pos.mean return loss class SimilarityModel Module : def init self, num node features: int, num edge features: int : super . init self.gnn = GNN num node features, num edge features self.optimizer = AdamW self.gnn.parameters def forward self, graph batch: Data - Tensor: """Returns normalized embeddings of binaries with given graphs.""" z = self.gnn graph batch return normalize z, dim=1 def step self, batch : emb = self batch.graph loss = supcon loss emb, batch.func id, 0.07 self.optimizer.zero grad loss.backward self.optimizer.step Translating the graph into tensors The model consumes graphs in the torch.data.Dataset format. We still have to convert our CDG graphs to this representation. For that we use the graph to data function provided by pcode graph . This method has many parameters to adapt the node features to the task at hand. The most important one controls the way registers are encoded. Since our task only concerns the semantics of the functions, there is no point in including the registers other than those used by the calling convention: whatever the registers assigned to variables, what matters is what is the semantics of the code. The map calling convention registers function of pcode graph makes it possible to encode the registers of the various calling conventions in a way that is consistent across architectures, so as to allow the model to generalize. It outputs an array of bits where each position corresponds to a use of the register: integer argument of rank n, 32-bit return value, etc. Note that a same register can be used both to pass a parameter and to return a value. If your task only concerns a single architecture, you can simply use the map registers function to convert a set of registers into hot-encoding. The idea is to send each possible value into a different input neuron, because it is much easier to teach a neural network a relation between its inputs than between different values of the same input. Here is the code, sparing you the loading part of the dataset: python import pickle from pathlib import Path from typing import NamedTuple from torch import Tensor from torch.utils.data import Dataset from torch geometric.data import Data from pcode graph.gnn exporter import graph to data, map calling convention registers class Function NamedTuple : bin path: str func name: str func id: int graph: Data class FunctionDataset Dataset : def init self, csv path: Path : super . init Load dataset from CSV self.graphs = architectures = set ... Create register mappings of same size to have the same amount of node features for each arch self.register mappers: dict str, dict str, Tensor = {} for arch in architectures: self.register mappers arch = map calling convention registers arch def len self - int: return len self.graphs def getitem self, index - Function: bin path, arch, func name, func id, graph path = self.graphs index graph = pickle.loads graph path.read bytes data = graph to data graph, registers emb=self.register mappers arch return Function bin path, func name, func id, data def iter self : for i in range len self : yield self i Building the batches The SupCon loss needs several examples per class in order to be able to compare positive and negative pairs within a same batch. We write a sampler to build balanced batches containing several samples of several different functions: python from collections.abc import Iterator from itertools import islice from random import Random from torch.utils.data import Sampler SEED = 42 SAMPLES PER FUNCTION = 4 FUNCTIONS PER BATCH = 16 class BatchSampler Sampler : """Dataset PK sampling to produce batch suitable for use with SupCon loss.""" def init self, dataset - None: super . init self.dataset = dataset self.rng = Random SEED self.num batches = sum 1 for in self def len self - int: return self.num batches def iter self - Iterator list int : Collect shuffles samples function names = list self.dataset.by func name.keys self.rng.shuffle function names samples: dict str, list int = {} for func name in function names: indices = list self.dataset.by func name func name if len indices = SAMPLES PER FUNCTION: self.rng.shuffle indices samples func name = indices Build batches while len samples = FUNCTIONS PER BATCH: batch = to delete = for func name, indexes in islice samples.items , FUNCTIONS PER BATCH : batch += indexes -SAMPLES PER FUNCTION: if len indexes = 2 SAMPLES PER FUNCTION: del indexes -SAMPLES PER FUNCTION: else: to delete.append func name for k in to delete: del samples k yield batch Training loop In general, the crux for a fast training is to be GPU-bound, meaning that the performance of your tool is tightly correlated to the performance of your GPU. This can be achieved provided that computations and transfers are overlapped. There are plenty of libraries doing that, but a small torch function with a ping-pong buffer does the trick: python from torch.utils.data import DataLoader from typing import Callable, NamedTuple from torch import Tensor from torch geometric.data import Data class BatchedFunctions NamedTuple : bin path: list str func name: list str func id: Tensor graph: Data def apply to batches dataloader: DataLoader, func: Callable BatchedFunctions , None , device: str, : """Parallelizes computation and transfers with a ping-pong buffer, at the cost of a higher GPU memory consumption. """ previous batch: BatchedFunctions | None = None batch: BatchedFunctions for batch in dataloader: next batch = batch. replace graph=batch.graph.to device, non blocking=True , func id=batch.func id.to device, non blocking=True , if previous batch is not None: func previous batch previous batch = next batch assert previous batch, "No batch returned by dataloader" func previous batch And to put all the pieces together, the training loop: python from torch geometric.loader import DataLoader def train device, csv path : train dataset = FunctionDataset csv path train data = DataLoader dataset=train dataset, pin memory=device == "cuda", batch sampler=BatchSampler train dataset , num workers=16, persistent workers=True, model = SimilarityModel train dataset.node features, train dataset.edge features model.to device model.train for e in range config.num epochs : apply to batches train data, model.step, device Note that the conversion from Function to BatchedFunctions is handled internally by the DataLoader. Also note that to have a fully useful training pipeline you still have to incorporate: - Evaluation on the validation set after each epoch for instance ; - Logging on tensorboard ; - Best model saving. Evaluation method and results The authors provide a list of negative or positive pairs for the validation and test subsets. The algorithms to benchmark have to return a similarity score for each of these pairs. By varying the threshold beyond which the tested functions are predicted as coming from the same program, one can plot a ROC curve https://en.wikipedia.org/wiki/Receiver operating characteristic . The metric used is the area under this curve. We obtain the curves below. To compare algorithms, the benchmark provides several tasks, The hardest one, XM , consists in mixing all the possible variations: The drawing of the distributions of similarity scores obtained on each kind of pairs shows that positive and negative pairs are clearly separated: And finally, we can compare our AUC score with the best one from the benchmark: | Model | XC same arch and bitness | XC+XB same arch | XA same compiler | XM different arch, bitness, compiler & optim level | |---|---|---|---|---| | GMN | 0.86 | 0.87 | 0.86 | 0.87 | | GINE 4 layers + pcode graph features | 0.86 | 0.86 | 0.86 | 0.87 | Doing the comparison is not completely honest because 1.5% of the bigger functions where just ignored in our case, but it seems that we achieve similar results that the best algorithm compared in the benchmark: GMN, for Graph Matching Networks. This is not outstanding, but one could hardly expect more without working on the GNN architecture and optimizing hyper-parameters at least a minimum . Conclusion This article shows how to use the pcode graph library to generate binary code embeddings based on semantics. Even using an old GNN baseline, we got promising results. We could go further by exploring more recent GNN architectures like GATv2, DirGNN... and doing a decent hyper-parameter search number and size of layers, dropout probability, kind of normalization, readout operators, etc. . Do not hesitate to fork the GitHub repository https://github.com/quarkslab/pcode graph to adapt the library to your own use-case and feel free to send us feedbacks or submit issues.