Compile LLVM IR to Assembly or Object Code with llc-18
Your frontend has handed you a .ll file and now you need real x86-64 code out the other end: that's llc's entire job. This guide turns a small LLVM IR module into assembly, then into an ELF relocatable object when that's the better hand-off to a linker. Allow about fifteen minutes if you already have valid LLVM IR. Examples use Ubuntu LLVM 18.1.3 from package llvm-18 on Linux.
The route
Jump straight to the step you need, or tick off Done means at the end.
llc is a code generator, not a complete compiler driver: it accepts LLVM assembly (.ll) or bitcode (.bc) and emits assembly by default, leaving the actual assembling and linking to separate tools. Every command here is an ordinary user command; none needs elevated privileges.
1. Check the installed command
Confirm the version and the target back ends available on this machine before you rely on either:
$ dpkg-query -W -f='${Package} ${Version}\n' llvm-18
llvm-18 1:18.1.3-1ubuntu1
$ llc-18 --version
Ubuntu LLVM version 18.1.3
Default target: x86_64-pc-linux-gnu
The full --version output also lists registered targets. Don't assume a target named in a build script is installed here; if you need a target-specific check, use llc-18 -help and look at its --filetype, --march, --mcpu and --mtriple entries.
Checkpoint
You're ready once llc-18 --version reports the release you expect and your target appears in its registered-target list.
2. Compile LLVM IR from standard input
For a quick test, pipe valid LLVM assembly straight into llc-18. The function below returns zero and does nothing else, so it's safe to compile:
$ printf '%s\n' 'define i32 @main() { ret i32 0 }' | llc-18 -o -
.text
.file "<stdin>"
.globl main
.type main,@function
main:
xorl %eax, %eax
retq
The input name is - when reading standard input, and leaving the input filename out entirely means the same thing. Explicit -o - keeps output on standard output; without it, standard-input compilation writes there too by default.
Exact directives and spacing depend on the target and LLVM release, so check the exit status rather than the exact text:
$ printf '%s\n' 'define i32 @main() { ret i32 0 }' | llc-18 -o /dev/null
$ printf 'llc exit status: %s\n' "$?"
llc exit status: 0
3. Compile a file without losing the input
For a real module, swap in your own readable file and choose the output explicitly:
$ llc-18 -O=2 -o module.s /path/to/module.ll
$ test -s module.s && head -12 module.s
LLVM 18's command help reports -O2 as the default optimisation level; setting -O=2 just makes that choice visible in scripts, and you can use -O=0 through -O=3 for anything else. Optimisation changes register allocation and instruction selection, so only compare output between runs at the same level.
If you skip -o with a named input, llc derives a name by stripping a trailing .bc and adding .s. Don't rely on that rule for a .ll input you actually care about naming carefully: an explicit destination is clearer and avoids accidentally clobbering a file you meant to keep.
Safety boundary
-o module.s can replace an existing file. Before running it in a build directory, check test ! -e module.s or pick a new path. There's no undo: if you did overwrite something, recover it from version control or your normal backup. The input module itself is only read, never modified.
4. Choose the target on purpose
By default the target comes from the module's target triple or the current architecture. To make a host build explicit, pass a triple:
$ printf '%s\n' 'define i32 @main() { ret i32 0 }' \
| llc-18 -mtriple=x86_64-pc-linux-gnu -o module.s
$ head -6 module.s
.text
.file "<stdin>"
.globl main
-march=<arch> overrides the architecture and -mcpu=<cpuname> tunes for a particular chip: these aren't interchangeable, since the architecture picks the instruction set family while the CPU picks a processor inside it. Use -march=help or the architecture list from llc-18 --version rather than guessing a name.
Target attributes are equally specific. -mattr=+sse2, for example, enables a named feature only where the selected back end supports it: a feature or CPU accepted on one target isn't automatically valid on another. When portability matters, keep the triple, CPU and attributes together in the build configuration and verify them on the machine that will actually run the build.
5. Emit an object file when assembly isn't the hand-off
Ask for a native object with -filetype=obj and a new filename:
$ printf '%s\n' 'define i32 @main() { ret i32 0 }' \
| llc-18 -filetype=obj -o module.o
$ file module.o
module.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
The object format is target-dependent and not every target supports every output type. A successful llc run only means code generation succeeded: it does not mean the object links, is ABI-compatible with your libraries, or will run on the deployment CPU. Leave those checks to the linker and a real test run.
To time a code-generation path without writing anything, the manpage documents -filetype=null. It's useful for timing or pass experiments, but it can't verify an artefact for you. For a repeatable build, keep the explicit output and actually look at it.
Common traps
- Parse error. Usually means the input isn't valid LLVM assembly, or is the wrong bitcode version. Test the module with matching LLVM tools before touching target flags.
- Missing architecture, CPU or attribute. The installed LLVM build doesn't recognise that selection. Recheck
llc-18 --versionandllc-18 -helprather than guessing again. - Missing output file. Usually a shell or path problem. Check the destination directory and write permission;
sudodoes not fix an incorrect path. - Assembly that looks different from last time. Probably a different target, CPU or optimisation level. Record all three before comparing output.
If you need the available CPU names for a target, the manpage gives this pattern:
$ llvm-as-18 < /dev/null | llc-18 -march=xyz -mcpu=help
...
Swap xyz for the architecture you're investigating. It prints a list or a diagnostic and touches no files. The same pattern with -mattr=help lists target attributes.
Done means
- llc-18 --version reports the installed LLVM release and a suitable registered target.
- A known-valid LLVM IR module compiles with exit status 0.
- The output type is explicit:
.sfor assembly or.owith-filetype=obj. - The target triple, CPU and optimisation level are recorded whenever reproducibility matters.
- No input was overwritten, and any existing output was backed up or deliberately replaced.