Run LLVM 20 Bitcode Safely with lli-20
lli-20 will run a bitcode file for you, and the exit status it hands back is the whole point of the exercise. You will assemble a small LLVM IR program, run the resulting bitcode, pass it arguments, and read that exit status correctly instead of guessing at it. The examples use the locally installed LLVM 20.1.8 runtime package, llvm-20-runtime.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about fifteen minutes. You need a shell and the LLVM 20 tools lli-20 and llvm-as-20. Everything here is unprivileged.
Warning
Do not reach for sudo on this workflow. It will not repair invalid bitcode, a missing entry point or an incompatible target, it just adds risk for nothing.
1. Check the installed tools
Confirm the command names resolve before creating a test module. This is a read-only check:
$ command -v lli-20
/usr/bin/lli-20
$ lli-20 --version
Ubuntu LLVM version 20.1.8
Optimized build.
$ dpkg-query -W -f='\${Package} \${Version}\n' llvm-20-runtime
llvm-20-runtime 1:20.1.8~++20250804090239+87f0227cb601-1~exp1~20250804210352.139
The package revision can shift after an upgrade, but the installed command reports the LLVM version it actually runs. Keep that output with any bug report or reproducibility note.
2. Create a minimal bitcode program
lli-20 expects an input bitcode file, in the shape the manpage documents: lli-20 [options] <input bitcode> <program arguments>.... It will not execute an .ll source file directly, so assemble it first.
In a scratch directory, save this as exit-code.ll:
define i32 @main(i32 %argc, ptr %argv) {
entry:
ret i32 %argc
}
This program returns the argument count and prints nothing. Assemble it with the matching LLVM 20 assembler:
$ llvm-as-20 exit-code.ll -o exit-code.bc
$ file exit-code.bc
exit-code.bc: LLVM IR bitcode
Checkpoint
If llvm-as-20 reports a syntax or verification error, stop there. lli-20 cannot make malformed input executable; fix the IR and assemble a fresh bitcode file.
3. Run the bitcode and pass arguments
Put the bitcode path first and the program arguments after it:
$ lli-20 exit-code.bc alpha beta
$ printf 'exit status: %s\n' "$?"
exit status: 3
The program receives three arguments here: its own argv[0] plus alpha and beta. The return value of main becomes the shell status, hence 3. Shell statuses stay within the usual process-status range, so do not treat a negative or oversized return value as an exact diagnostic number.
Do not confuse silence with failure. A bitcode program can complete successfully without writing anything. Capture the status immediately, before you run another command:
$ lli-20 exit-code.bc one
$ status=$?
$ printf 'program status: %s\n' "$status"
program status: 2
4. Separate loading failures from program failures
The local lli-20(1) page documents status 1 for a failure to load the program. Test that with a path that does not exist:
$ lli-20 ./does-not-exist.bc
lli-20: lli: ./does-not-exist.bc: error: Could not open input file: No such file or directory
$ printf 'loader status: %s\n' "$?"
loader status: 1
The wording can vary with the file path and build, but the useful distinction holds: status 1 here belongs to lli-20, not to a program that ran and happened to return 1. Check the path, permissions and bitcode format before you start changing options.
For a repeatable shell check, give the command status its own branch:
if lli-20 exit-code.bc alpha beta; then
printf '%s\n' 'bitcode returned success'
else
status=$?
printf 'bitcode returned status %s\n' "$status" >&2
exit "$status"
fi
5. Choose the execution mode deliberately
LLVM can use a just-in-time compiler, and the installed tool also has an interpreter fallback. The normal invocation lets lli-20 pick its own path. Use --force-interpreter when you are specifically testing interpreter behaviour or isolating a JIT-related problem:
$ lli-20 --force-interpreter exit-code.bc alpha beta
$ printf 'interpreter status: %s\n' "$?"
interpreter status: 3
This is not a general portability switch. lli-20 is not an emulator: the bitcode has to suit the host architecture, and target overrides can make execution fail or crash outright if they do not match the machine. Treat --mtriple, --march, --mcpu and --mattr as target-specific troubleshooting controls, not defaults you bolt onto every command.
6. Select a non-standard entry function
The default entry function is main. If a module has a different entry point, select it explicitly. Create alternate.ll:
define i32 @main() {
entry:
ret i32 2
}
define i32 @alternate() {
entry:
ret i32 13
}
Assemble and run the alternate entry:
$ llvm-as-20 alternate.ll -o alternate.bc
$ lli-20 --entry-function=alternate alternate.bc
$ printf 'alternate status: %s\n' "$?"
alternate status: 13
Keep the option before the bitcode path so the command stays easy to read. If the selected function has an unsuitable signature, or depends on setup that main would normally do, execution can fail even though the module assembled fine. Use this for a deliberately designed entry point, not as a guess when the default launch fails.
Common traps
- Wrong input kind. Do not pass source or an object file when the command needs LLVM bitcode. Assemble LLVM IR with
llvm-as-20, and check that assembler and runner belong to the same major LLVM release. - Arguments in the wrong place. Keep program arguments separate from lli options. In
lli-20 exit-code.bc alpha beta, the last two words reach the program.--fake-argv0=NAMEchanges the value presented asargv[0]; it does not rename the bitcode file and does not change the executable on disk. - Untrusted bitcode treated as harmless. Running it can consume CPU and memory, access files, load libraries and perform network operations with your own privileges. Inspect and sandbox unfamiliar modules before execution: a failed or successful exit status is not a security review.
- Hiding a target mismatch. Do not add
--force-interpreterto paper over one. If the module was built for another architecture, rebuild it for the host or set up a tested compatible target instead.
Done means
- Version confirmed.
lli-20 --versionand the package query identify the installed LLVM 20 runtime. - Bitcode built.
llvm-as-20produced a bitcode file from valid LLVM IR. - Status captured. You passed program arguments after the bitcode path and checked the returned status immediately.
- Failure modes distinguished. You can tell a loader status of 1 from an executed program that returns 1.
- Options used deliberately.
--force-interpreter, target overrides and--entry-functiononly appear when their specific behaviour is actually required. - No unnecessary privilege. Unfamiliar bitcode is treated as executable code, and nothing here ran with elevated privileges it did not need.