LLVM Example Practice: Compiling LLVM/Clang on ARM and Writing LLVMPass

"IT Talk" is a professional IT information and service platform under the Machinery Industry Press, dedicated to helping readers master more professional and practical knowledge and skills in the broad field of IT, quickly enhancing their competitiveness in the workplace. Click the blue WeChat name to quickly follow us!

How to Compile LLVM/Clang on ARM

1. Description of Building LLVM/Clang Environment on ARM

ARM includes various CPUs and is mainly based on ARMv architecture chips. The Ubuntu Linux operating system is used on ARM boards.

2. Compilation Plan
(1) Build LLVM/Clang in Release Mode

It is best to build LLVM/Clang in release mode, as it consumes less memory. Otherwise, the build process is likely to fail due to insufficient memory. Building only the relevant backends (ARM and AArch64) is also much faster, as it is unlikely to use ARM development boards to cross-compile to other platforms. If running compiler-rt real-time compiler tests, the x86 backend must be included; otherwise, some tests will fail.

cmake $LLVM_SRC_DIR -DCMAKE_BUILD_TYPE=Release \                    -DLLVM_TARGETS_TO_BUILD="ARM;X86;AArch64"
Other options available include:
1) Replace Make with Ninja: -G Ninja.
2) Use assertions in the build: -DLLVM_ENABLE_ASSERTIONS=True.
3) Local (non-sudo) installation path: -DCMAKE_INSTALL_PREFIX=$HOME/llvm/install.
4) CPU flags: -DCMAKE_C_FLAGS=-mcpu=cortex-a15 (corresponding to CXX_FLAGS)
5) Just type make -jN or ninja to build everything. Use make -jN check-all or ninja check-all to run all compiler tests.
(2) Build LLVM/Clang on ARM Development Boards with 1G or Less Memory

If building LLVM/Clang on ARM development boards with 1G or less memory, use gold instead of GNU ld. In any case, setting up a swap partition may be a good idea.

$ sudo ln -sf /usr/bin/ld /usr/bin/ld.gold
(3) Building on Unstable ARM Development Boards
ARM development boards may be unstable, and developers may experience kernel crashes. The cache uses ARM’s big.LITTLE architecture, and processors based on this architecture work faster and more efficiently. For example, smartphones like Samsung Galaxy S6, HTC M9, LG G4, etc., use processors based on the big.LITTLE architecture. To mitigate this impact, small scripts can be used to set the Linux scheduler to performance mode on all cores, as shown in the code below:
# The following code requires the cpufrequtils package. for ((cpu=0; cpu<`grep -c proc /proc/cpuinfo`; cpu++)); do    sudo cpufreq-set -c $cpu -g performance done
After building, disable this option to avoid damaging the CPU. Most modern kernels do not require this feature, so it should only be used when issues arise.

(4) USB Drive Assistance

Running the build on an SD card is possible, but it is more prone to failure compared to high-quality USB drives, and USB drives have much faster read and write speeds than external hard drives. Therefore, consider buying a high-speed USB drive. This is also a good choice for systems with fast eMMC.
5) Ensure Proper Power Supply

Make sure there is a decent power supply that can provide at least 4A of current, especially important when using USB devices on development boards. An external powered USB/SATA hard drive is even better than a high-performance power supply.

How to Write an LLVM Pass
1. What is a Pass

The LLVM Pass framework is an important part of the LLVM system, as LLVM Pass is where most interesting parts of the compiler are stored. Passes build the analysis results used for transformations through structured techniques in the compiler code, and then execute the transformations and optimizations that make up the compiler.

Unlike traditional pass management under pass defined by inheriting the pass interface, passes under the new pass manager rely on polymorphism without explicit interfaces. Here, all LLVM passes inherit from PassInfoMixin using CRTP mix. Each pass should have a run method that returns a PreservedAnalyses and receives some IR units along with an analysis manager. For example, a function pass will have a Preserved Analyses class example:
PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM);
This shows how to build a Pass, including setting up the build, creating the Pass, executing it, and testing the Pass. Checking existing Passes is always a good way to understand the details.
LLVM first used the traditional Pass manager for the CodeGen pipeline, then handled the new Pass manager.

2. Quick Start – Writing a HelloWorld Program

The HelloWorld program is designed to simply output the names of non-external functions present in the program being compiled. In fact, it only performs checks and does not modify the program at all. Other example code can be created based on the HelloWorld program source file, giving it a different name for the Pass.
3. Compilation Settings
First, configure and build LLVM according to the descriptions in the LLVM system.
Then, reuse the existing directory (creating a new directory requires handling more CMake files than expected). You can use the already created CPP file llvm/lib/Transforms/Utils/HelloWorld.cpp. If you want to create a custom Pass, you can add a new source file in llvm/lib/Transforms/Utils/CMakeLists.txt (assuming the Pass is in the Transforms/Utils directory). If you have built a new Pass, you need to write code for the Pass itself.
4. Basic Code to Write

Now that the build process for the new Pass is set up, you just need to write the code.

First, define the Pass in the header file by creating llvm/include/llvm/Transforms/Utils/HelloWorld.h. This file should include the following template file:

#ifndef LLVM_TRANSFORMS_HELLONEW_HELLOWORLD_H#define LLVM_TRANSFORMS_HELLONEW_HELLOWORLD_H#include "llvm/IR/PassManager.h"namespace llvm {class HelloWorldPass : public PassInfoMixin<HelloWorldPass> {public:PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM);};} // namespace llvm#endif // LLVM_TRANSFORMS_HELLONEW_HELLOWORLD_H
Then create a class for the Pass using the declaration of the run method that actually runs the Pass. Here, we inherit from PassInfoMixin<PassT> and set more templates so that we do not have to write custom implementations.
Since the class is in the LLVM namespace, it will not corrupt the global namespace.
Next, create the llvm/lib/Transforms/Utils/HelloWorld.cpp file, starting with the following code:
#include "llvm/Transforms/Utils/HelloWorld.h"

Include the header file just created.

using namespace llvm;

This step is necessary because the functions in the include file are in the LLVM namespace and can only be completed outside of header files. Then define the run method for the Pass, as shown in the following code:

PreservedAnalysesHelloWorldPass::run(Function &F,FunctionAnalysisManager &AM) {  errs() << F.getName() << "\n";  return PreservedAnalyses::all();}
This simply outputs the function name to stderr. The Pass manager ensures that the Pass will run on each function in the module. The return value of PreservedAnalyses indicates that since no functions were modified, all analyses (such as dominance trees) remain valid after this Pass.
This is the Pass itself. Now, to register the Pass, it needs to be added to several places. The implementation code is as follows:
FUNCTION_PASS content added to llvm/lib/Passes/PassRegistry.def. FUNCTION_PASS("helloworld", HelloWorldPass())

The Pass is added under the name helloworld.

For various reasons, llvm/lib/Passes/PassRegistry.def is included in llvm/lib/Passes/PassBuilder.cpp multiple times. Since the Pass is constructed, it is necessary to add the appropriate #include in llvm/lib/Passes/PassBuilder.cpp.

#include "llvm/Transforms/Utils/HelloWorld.h"
This should be all the code required for the Pass, and then it’s time to compile and run.
5. Running the Pass with opt

Now that there is a brand new Pass, you can build optimizations and run some LLVM IR in the Pass. The implementation code is as follows:

$ ninja -C build/opt# or any build system/build directory being used$ cat /tmp/a.lldefine i32 @foo() {  %a = add i32 2, 3  ret i32 %a}define void @bar() {  ret void}$ build/bin/opt -disable-output /tmp/a.ll -passes=helloworldfoobar
The Pass runs as expected and prints the function names.
6. Testing the Pass

Testing the Pass is very important to prevent regressions. A lit test will be added in llvm/test/Transforms/Utils/helloworld.ll.

The implementation code is as follows:

$ cat llvm/test/Transforms/Utils/helloworld.ll; RUN: opt -disable-output -passes=helloworld %s 2>&1 | FileCheck %s; CHECK: {{^}}foo{{$}}define i32 @foo() {  %a = add i32 2, 3  ret i32 %a}; CHECK-NEXT: {{^}}bar{{$}}define void @bar() {  ret void}$ ninja -C build check-llvm
# This new test runs along with all other llvm list tests
7. Common Questions about Passes
Defining a static isRequired method that returns true makes the Pass required. The implementation code is as follows:
class HelloWorldPass : public PassInfoMixin<HelloWorldPass> {public:PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM);  static bool isRequired() { return true; }};

A required Pass cannot be skipped, and an example of a required Pass is AlwaysInlinerPass, which must always run to preserve the alwaysinline semantics of the program. Additionally, the Pass manager is required as it may contain other required Passes.

To skip a Pass, a typical example is the optnone function attribute, which specifies that optimizations should not run on the function. However, required Passes will still run on optnone functions.

The above content is excerpted from “LLVM Compiler Principles and Practice”
Authors: Wu Jianming, Wu Yihao
Recommended Reading

LLVM Example Practice: Compiling LLVM/Clang on ARM and Writing LLVMPass

LLVM is a research project at the University of Illinois that provides a modern, SSA-based compilation strategy and can support compilation targets for any programming language, both statically and dynamically. LLVM consists of various sub-projects, many of which are used in production in commercial and open-source projects. It is also widely used in academic research.

This book aims to combine theoretical knowledge of LLVM with practical case studies, helping readers understand how LLVM works while optimizing and deploying LLVM according to application and device needs. The book includes numerous examples and code snippets to help readers master the LLVM compiler development environment.

The book consists of 11 chapters, including compiling and installing LLVM, LLVM external projects, LLVM compilers, basics of the Clang frontend, Clang architecture and practical examples, LLVM IR practice, LLVM chip compiler practical examples, LLVM compiler example code analysis, LLVM optimization examples, LLVM backend practice, and MLIR compiler.

This book is suitable for engineering and technical personnel, teachers and students in universities, researchers, and technical managers in fields such as algorithms, software, compilers, artificial intelligence, and hardware.

Author: Ji Xu

Editor: Zhang Shuqian

Reviewer: Cao Xinyu

Leave a Comment