In the previous blogs we got a brief intro to LLVM IR and the associated concepts, such as Module, BasicBlock, Function, Instruction, passes, etc. This post serves as a brief introduction to Machine IR and the related terminology. The process of how Machine IR is actually generated by the compiler will be covered in detail in upcoming posts.
MIR a human-readable serialization format used in the LLVM compiler infrastructure to represent machine-specific intermediate representation, with YAML being used as the data serialization format. The first yaml document can contain an LLVM IR module and the remaining documents contain machine functions. Please note that unlike LLVM IR, MIR doesn’t have the concept of modules. A MachineModuleInfo class exists but it’s more like a tracker rather than the parent container. A Module in the LLVM IR acts as a container and stores data like the list of functions, globals, named metadata,etc. However, MachineModuleInfo acts as a tracker, storing a pointer to the corresponding LLVM IR module, and a mapping between LLVM IR functions and Machine functions.
1 2 3 4 5 6 7
// llvm/CodeGen/MachineModuleInfo.h classMachineModuleInfo { // ... // This is the LLVM Module being worked on. const Module *TheModule = nullptr; // ... DenseMap<const Function*, std::unique_ptr<MachineFunction>> MachineFunctions;
Please note that the LLVM version used for adding references to source code in this blog is LLVM 22.1.8.
Machine Basic Block
Recall the definition of a basic block, “a basic block is a straight-line sequence of instructions with no branches, meaning that execution starts at a single entry point and proceeds sequentially to a single exit point, where it then continues to the next basic block. Basic blocks belong to functions and cannot have jumps into their middle, ensuring that once execution starts, it will proceed through all instructions in the block. A basic block must have exactly one terminator instruction. This instruction tells the possible destinations of this basic block.”
A MachineBasicBlock is similar but:
Unlike a basic block, a Machine Basic Block can have multiple terminator instructions. Let’s see some code-snippets from the LLVM source code to prove that.
// lib/CodeGen/MachineBasicBlock.cpp MachineBasicBlock::iterator MachineBasicBlock::getFirstTerminator(){ iterator B = begin(), E = end(), I = E; while (I != B && ((--I)->isTerminator() || I->isDebugInstr())) ; /*noop */ while (I != E && !I->isTerminator()) ++I; return I; }
This shows that a MachineBasicBlock can have multiple terminator instructions.
The MIR Parser
Now let’s try to load an MIR file. Since there’s nothing like MachineModule, parsing an MIR file gives an llvm::Module object.
// CodeGen/MIRParser/MIRParser.cpp std::unique_ptr<Module> MIRParserImpl::parseIRModule(DataLayoutCallbackTy DataLayoutCallback){ // ... // ... if (constauto *BSN = dyn_cast_or_null<yaml::BlockScalarNode>(In.getCurrentNode())) { SMDiagnostic Error; M = parseAssembly(MemoryBufferRef(BSN->getValue(), Filename), Error, Context, &IRSlots, DataLayoutCallback); if (!M) { reportDiagnostic(diagFromBlockStringDiag(Error, BSN->getSourceRange())); returnnullptr; } In.nextDocument(); if (!In.setCurrentDocument()) NoMIRDocuments = true; } else { // Create an new, empty module. M = std::make_unique<Module>(Filename, Context); if (auto LayoutOverride = DataLayoutCallback(M->getTargetTriple().str(), M->getDataLayoutStr())) M->setDataLayout(*LayoutOverride); NoLLVMIR = true; } return M; }
Loading an MIR file
We can use the tool LLVM-Lens to directly retrieve the MIR at a particular stage for a given C/C++ file. Let’s create a file test.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// test.c intsquare(int x) { return x * x; }
intadd(int a, int b) { return a + b; }
intsum_of_squares(int a, int b) { return add(square(a), square(b)); }
intfactorial(int n) { if (n <= 1) return1;
return n * factorial(n - 1); }
Now let’s use llvm-lens to get the MIR
1 2 3 4 5 6 7
# test.py import llvm_lens
mir = llvm_lens.machine_ir("test.c",stop_after="x86-isel",debug_info=False)
withopen("test.mir","w") as f: f.write(mir)
Here we can see that the generated MIR contains an LLVM IR module in the first document followed by MachineFunctions in the following documents. Let’s write a C++ program using the LLVM APIs that loads this file and creates an llvm::Module.
auto parser = llvm::createMIRParserFromFile(MIRPath, error, ctx); if (!parser) { error.print("learning_llvm", llvm::errs()); return1; }
auto mod = parser->parseIRModule(); if (!mod) { error.print("learning_llvm", llvm::errs()); return1; } return0; }
Target, TargetRegistry and TargetMachine
Now that we have the Module, we need to find a way to run a Machine Function pass. Note that a Module doesn’t store any MachineFunction objects so we can’t directly use it to run a Machine Function pass. What we need to do is to first create a MachineModuleInfo object and populate the required information into it. Earlier we saw that this class stores a mapping between Functions and MachineFunctions. Before creating a MachineModuleInfo object, we need to create a TargetMachine, because creating a MachineModuleInfo object without supplying a valid pointer to TargetMachine will crash the program because the constructor dereferences the ptr to TargetMachine, and it’s default initialized to nullptr.
We can get a Target (defined in llvm/MC/TargetRegistry.h) object using the TargetRegistry. A TargetRegistry is a generic interface for target specific features. It provides a method lookupTarget that gives a Target object based on a Triple. A triple is nothing but a string that stores info about the environment, such as the architecture, the vendor details, operating system, etc. For instance, x86_64-pc-linux-gnu is a triple.
To get a target triple, we can do one of the following two things.
Retrieve the triple from the module loaded for the MIR file, if it’s present there.
Get the default triple the compiler has been configured to produce code for.
Here, the MIR file already contains the target triple. The default triple can be retrieved by calling llvm::sys::getDefaultTargetTriple present in the header llvm/TargetParser/Host.h. We also need to initialize the targets by calling llvm::InitializeAllTargets, which will make all target machines that LLVM is configured to support available through the TargetRegistry. Now that we know the terminology, let’s create a TargetMachine and create a MachineModuleInfo object.
// lib/CodeGen/MachineModuleInfo.cpp MachineFunction &MachineModuleInfo::getOrCreateMachineFunction(Function &F){ // Shortcut for the common case where a sequence of MachineFunctionPasses // all query for the same Function. if (LastRequest == &F) return *LastResult;
auto I = MachineFunctions.insert( std::make_pair(&F, std::unique_ptr<MachineFunction>())); MachineFunction *MF; if (I.second) { // No pre-existing machine function, create a new one. const TargetSubtargetInfo &STI = *TM.getSubtargetImpl(F); MF = newMachineFunction(F, TM, STI, getContext(), NextFnNum++); MF->initTargetMachineFunctionInfo(STI);
// MRI callback for target specific initializations. TM.registerMachineRegisterInfoCallback(*MF);
// Update the set entry. I.first->second.reset(MF); } else { MF = I.first->second.get(); }
LastRequest = &F; LastResult = MF; return *MF; }
We can indirectly invoke it by calling parser->parseMachineFunctions
Now let’s call the function and trigger the logic to populate the MachineFunctions dense map.
1 2 3
if (parser->parseMachineFunctions(*mod, MMI)) { return1; }
Creating and running a Machine Function Pass
Now that we have all the information about MachineFunctions within the MMI, we can create a MachineFunctionPassManager and run the pass. First of all, let’s create a pass that just prints the function names.
Now let’s extract the MachineFunctions from the MMI, register and run the pass. There’s an alternate and better way to this as well, which will be covered in the upcoming posts.
Notice the line MFAM.registerPass([&]{return llvm::PassInstrumentationAnalysis();});. We cannot just ignore this otherwise the program will crash. MachineFunctionPassManager is just an alias for PassManager<MachineFunction>, and run pulls PassInstrumentationAnalysis out of the analysis manager before it runs anything.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// lib/CodeGen/MachinePassManager.cpp template <> PreservedAnalyses PassManager<MachineFunction>::run(MachineFunction &MF, AnalysisManager<MachineFunction> &MFAM) { PassInstrumentation PI = MFAM.getResult<PassInstrumentationAnalysis>(MF); PreservedAnalyses PA = PreservedAnalyses::all(); for (auto &Pass : Passes) { if (!PI.runBeforePass<MachineFunction>(*Pass, MF)) continue;
PassInstrumentationAnalysis is an analysis (defined in llvm/IR/PassInstrumentation.h) that has to be registered like any other. The getResult function called above calls getResultImpl which further calls the lookupPass function.
1 2 3 4 5 6 7
// llvm/IR/PassManager.h PassConceptT &lookUpPass(AnalysisKey *ID){ typename AnalysisPassMapT::iterator PI = AnalysisPasses.find(ID); assert(PI != AnalysisPasses.end() && "Analysis passes must be registered prior to being queried!"); return *PI->second; }
In case when assertions are disabled, such as in release builds the assertion will not be triggered, causing a segmentation fault.
1 2 3 4 5
pwndbg> bt #0 0x00007ffff4147607 in llvm::AnalysisManager<llvm::MachineFunction>::getResultImpl(llvm::AnalysisKey*, llvm::MachineFunction&) () from /usr/lib/x86_64-linux-gnu/libLLVM.so.22.1 #1 0x00007ffff4149675 in llvm::PassManager<llvm::MachineFunction, llvm::AnalysisManager<llvm::MachineFunction>>::run(llvm::MachineFunction&, llvm::AnalysisManager<llvm::MachineFunction>&) () from /usr/lib/x86_64-linux-gnu/libLLVM.so.22.1 #2 0x0000555555557a09 in main (argc=1, argv=0x7fffffffd9f8
So we need to have the PassInstrumentationAnalysis here. Doing this is simple.