← Back to list

Understanding LLVM Passes: How Code Obfuscation Works Under the Hood

Hi devs, in this blog I’m going to explain what LLVM is, how LLVM passes work, and why LLVM passes are important in a project.

Pravs-254 · 2026-05-17 15:19 · 0 claps · 2.7 min read
#llvm-ir #llvm #android-ndk #ndk-obfuscation #omvll
Open on Medium ↗

Understanding LLVM Passes: How Code Obfuscation Works Under the Hood

Hi devs, in this blog I’m going to explain what LLVM is, how LLVM passes work, and why LLVM passes are important in a project.

In mobile app development, Apple iOS and Google Android are the two major platforms. Here, we are going to look specifically at the Android platform.

In Android, the Java/Kotlin source code is first compiled by their respective compilers (javac and kotlinc) into .class files. These .class files are then processed by the R8 tool, which performs code shrinking, optimization, and obfuscation. After this step, the bytecode is converted into Dalvik Executable (.dex) files.

Meanwhile, application resources are processed by the AAPT/AAPT2 tool, where resources are compiled and packaged into resource tables such as .arsc. Finally, all compiled code, resources, and assets are packaged together into an APK or AAB.

This is the standard Android application build process for generating an APK or AAB.

When we use a C/C++ module in our project, the respective C or C++ source files are added inside the add_library() section in the CMake configuration.

Example:

add_library( native-lib SHARED file1.cpp file2.cpp file3.cpp )

These source files are compiled using the clang compiler. During compilation, each source file is independently converted into LLVM Intermediate Representation (IR), which can be generated in .bc (bitcode) or .ll format.

Example:

file1.cpp -> file1.bc file2.cpp -> file2.bc file3.cpp -> file3.bc

At the LLVM IR stage, LLVM passes are executed. These passes can perform analysis, optimization, transformation, or obfuscation on the intermediate representation before native machine code generation.

Some common LLVM passes are:

  1. Dead Code Elimination (DCE) Removes unused or unreachable code from the binary.

  2. Constant Folding Computes constant expressions during compilation instead of runtime.

Example: int x = 5 + 10;

becomes:

int x = 15;

  1. Instruction Combining (InstCombine) Simplifies and optimizes LLVM IR instructions into more efficient forms.

  2. Control Flow Flattening (CFF) Commonly used in obfuscation projects. It transforms the normal program flow into a complex state-machine structure, making reverse engineering harder.

  3. Bogus Control Flow (BCF) Adds fake conditional branches and unnecessary control paths to confuse decompilers and reverse engineers.

  4. String Obfuscation Encrypts or transforms strings in the binary so that sensitive strings are not directly visible inside the compiled .so file.

  5. Function Inlining Replaces function calls with the actual function body to reduce call overhead and improve optimization opportunities.

LLVM passes can be used for both optimization and security hardening purposes depending on the project requirements.

After the LLVM optimization stage, the assembler converts each processed IR file into object files with the .o extension.

Example:

file1.bc -> file1.o file2.bc -> file2.o file3.bc -> file3.o

Finally, the linker combines all object files into a single shared library. The generated library name follows the format:

lib + library_name + .so

So in this example, the final output becomes:

libnative-lib.so This same workflow applies even when multiple add_library() blocks are used inside the same project. Each library is compiled independently, processed through the LLVM pipeline, optimized using LLVM passes, converted into object files, and finally linked into its own corresponding .so shared library.

Example:

add_library( security-lib SHARED security.cpp )

add_library( crypto-lib SHARED crypto.cpp )

In this case, separate shared libraries such as:

libsecurity-lib.so libcrypto-lib.so

will be generated independently.

In simple terms, LLVM is a compiler framework that performs code optimization, transformation, and other processing after the clang compiler compiles the source code and before the assembler generates the machine code.

C/C++ Source Code │ ▼ clang Compiler │ ▼ LLVM IR (.bc / .ll) │ ▼ LLVM Passes (Code Optimization / Obfuscation / Transformation) │ ▼ Assembler │ ▼ Object Files (.o) │ ▼ Linker │ ▼ Shared Library (.so)

Understanding how LLVM passes work internally is very important when building secure native Android applications. Since LLVM operates at the Intermediate Representation (IR) level, developers can implement custom optimizations, code transformations, and advanced obfuscation techniques before machine code generation happens.

This makes LLVM one of the most powerful components in modern compiler infrastructure, especially for performance optimization, binary protection, and reverse engineering resistance.

In the next part, we will look deeper into how custom LLVM passes are written and how they can be integrated into Android native builds.


메타데이터
post_id
2a4c83ce474e
slug
understanding-llvm-passes-how-code-obfuscation-works-under-the-hood-2a4c83ce474e
url
https://medium.com/@pravsgbcm/understanding-llvm-passes-how-code-obfuscation-works-under-the-hood-2a4c83ce474e
canonical_url
https://medium.com/@pravsgbcm/understanding-llvm-passes-how-code-obfuscation-works-under-the-hood-2a4c83ce474e
author_url
https://medium.com/@pravsgbcm
status
ok
fetched_at
2026-06-20 20:29:01