This is the final stage for compiling a source file to an object or an assembly file. In previous post, we got a module consisting of Machine Functions and Machine Instructions for our hello world example. In this post, we will see how it is turned into the abstractions in MC Layer then will be emitted as object file, for our example, or assembly file as needed.
The MC Layer is the abstraction of final products, such as object file, assembly file and so on. Therefore it generalizes the similarities of those final products, then it can output one of those final products with remaining one representations in LLVM. Thus we can use the classes of MC Layer to represent an assembly file, or we can embody our understanding of MC Layer by thinking it as an assembly file. As say in Code Emission of LLVM Target-Independent Code Generator.
Since the MC layer works at the level of abstraction of object files, it doesn’t have a notion of functions, global variables etc. Instead, it thinks about labels, directives, and instructions. A key class used at this time is the MCStreamer class. This is an abstract API that is implemented in different ways (e.g. to output a .s file, output an ELF .o file, etc) that is effectively an “assembler API”. MCStreamer has one method per directive, such as EmitLabel, EmitSymbolAttribute, SwitchSection, etc, which directly correspond to assembly level directives.
In a talk of LLVM MC in Practice by Grosbach and Anderson, we can see the positioning of MC layer in LLVM infrastructure.
In the post of Intro to the LLVM MC Project, we can know the primary components and their relations are depicted as following.
Now, we have some impression of MC Layer. Let us dive in the code, see how our example use it to emit object file.
First we use lldb to stop at AArch64AsmPrinter::runOnMachineFunction .
/usr/bin/lldb -- \
./bin/swift-frontend \
-frontend \
-c \
-primary-file hello.swift \
-sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \
-color-diagnostics \
-Xllvm --global-isel=false -Xllvm --fast-isel=false
(lldb) b AArch64AsmPrinter::runOnMachineFunction
(lldb) r
Here is the code of AArch64AsmPrinter::runOnMachineFunction.
bool runOnMachineFunction(MachineFunction &MF) override {
...
// Emit the rest of the function body.
emitFunctionBody();
...
}
The body of emitFunctionBody():
...
/// EmitFunctionBody - This method emits the body and trailer for a
/// function.
void AsmPrinter::emitFunctionBody() {
emitFunctionHeader();
emitFunctionBodyStart();
...
for (auto &MBB : *MF) {
// Print a label for the basic block.
emitBasicBlockStart(MBB);
...
for (auto &MI : MBB) {
...
switch (MI.getOpcode()) {
case TargetOpcode::CFI_INSTRUCTION:
emitCFIInstruction(MI);
break;
...
default:
emitInstruction(&MI);
...
break;
}
// If there is a post-instruction symbol, emit a label for it here.
if (MCSymbol *S = MI.getPostInstrSymbol())
OutStreamer->emitLabel(S);
...
}
...
emitBasicBlockEnd(MBB);
...
// Emit target-specific gunk after the function body.
emitFunctionBodyEnd();
...
// Emit section containing stack size metadata.
emitStackSizeSection(*MF);
emitPatchableFunctionEntries();
...
}
From above two code snappets of AArch64AsmPrinter::runOnMachineFunction() and emitFunctionBody(), we can see for each MachineFunction, the AsmPrinter will run on it to use one MCStreamer to emit the content inside that MachineFunction, that is each MachineInstr in each MachineBasicBlock of this MachineFunction. Moreover, we know that MCStreamer is an abstraction of object file, therefore you can see emitLabel(), emitInstruction(), etc, in the code. After all the work of AArch64AsmPrinter::runOnMachineFunction(), we get a MCStreamer which is filled with the object content of that function.
In addition, in AsmPrinter::doInitialization(Module &M) and AsmPrinter::doFinalization(Module &M), the AsmPrinter will emit to MCStreamer the object content of Module except Functions, because that is responsibility of AArch64AsmPrinter::runOnMachineFunction(). Moreover, at the end of AsmPrinter::doFinalization(Module &M), it calls OutStreamer->Finish() to product the final product. In the following stack backtracking, you can see the function calls for writing output object file (.o file).
(lldb) b llvm::raw_ostream::write
(lldb) r
(lldb) bt* thread #1, queue = 'com.apple.main-thread', stop reason = step over
* #0: `llvm::raw_ostream::write at raw_ostream.cpp:280:18
#1: `llvm::support::endian::write<unsigned int> at EndianStream.h:29:6
#2: `llvm::support::endian::Writer::write<unsigned int> at EndianStream.h:59:5
#3: `llvm::MachObjectWriter::writeHeader at MachObjectWriter.cpp:153:5
#4: `llvm::MachObjectWriter::writeObject at MachObjectWriter.cpp:842:3
#5: `llvm::MCAssembler::Finish at MCAssembler.cpp:893:37
#6: `llvm::MCObjectStreamer::finishImpl at MCObjectStreamer.cpp:780:18
#7: `(anonymous namespace)::MCMachOStreamer::finishImpl at MCMachOStreamer.cpp:509:27
#8: `llvm::MCStreamer::Finish at MCStreamer.cpp:961:3
#9: `llvm::AsmPrinter::doFinalization at AsmPrinter.cpp:1791:16
#10: `llvm::FPPassManager::doFinalization at LegacyPassManager.cpp:1570:41
#11: `(anonymous namespace)::MPPassManager::runOnModule at LegacyPassManager.cpp:1646:41
#12: `llvm::legacy::PassManagerImpl::run at LegacyPassManager.cpp:614:44
#13: `llvm::legacy::PassManager::run at LegacyPassManager.cpp:1737:14
...
Therefore, after the work of AArch64AsmPrinter, we get the complete MC layer abstraction of object file for our module. That is lowering the Machine Module, Machine Function, Machine Instruction layer to MC Layer, that is ready to emit an object file or ASM file depending on the MCStreamer. For our hello-world example. on MacOS platform, the MCStreamer is MCMachOStreamer, which will produce a MachO object file.
That is the whole procedure for producing object file from Machine Module.