lli-18 runs LLVM bitcode directly, which is exactly what you want when a .bc file lands in your lap with zero explanation. You will finish with a repeatable way to run it, pass arguments, pick a non-default entry function, and read the exit status correctly. The examples use the installed Ubuntu LLVM 18.1.3 build from package llvm-18-runtime, version 1:18.1.3-1ubuntu1.
Allow about ten minutes. You need a shell and a .bc file; the small test program below also needs llvm-as-18 to assemble LLVM IR.
Warning: lli-18 executes code. Do not use it as a way to peek at bitcode from an untrusted source: run untrusted input in a proper sandbox, well away from secrets or writable production data.
Before anything else, check which lli-18 you are actually running and what version it reports:
$ command -v lli-18
/usr/bin/lli-18
$ lli-18 --version
Ubuntu LLVM version 18.1.3
Optimized build.
The manpage describes lli as an LLVM interpreter and dynamic compiler. On this installation the command is named lli-18, and it takes an input bitcode file followed by any arguments for the program:
$ lli-18 [options] <input-bitcode> [program-arguments]
Checkpoint: if command -v comes back empty, stop there. Do not work around a missing executable by downloading some unrelated build into a system directory.
Already have a bitcode file to test? Skip ahead. Otherwise build this tiny LLVM IR module in a scratch directory. It returns status 7 and prints nothing, which makes the result trivial to check:
$ mkdir -p /tmp/lli-18-check
$ cat > /tmp/lli-18-check/status.ll <<'EOF'
define i32 @main(i32 %argc, ptr %argv) {
ret i32 7
}
define i32 @alternate(i32 %argc, ptr %argv) {
ret i32 23
}
EOF
$ llvm-as-18 /tmp/lli-18-check/status.ll -o /tmp/lli-18-check/status.bc
$ file /tmp/lli-18-check/status.bc
/tmp/lli-18-check/status.bc: LLVM IR bitcode
That temporary directory is just a scratch pad, delete it whenever. If your shell reports llvm-as-18 as unavailable, use a bitcode file from your normal LLVM toolchain instead, or install the matching development tools through your system's usual package process. Do not bolt an assembler onto a deployment box merely to run one existing bitcode file.
Checkpoint: the assembler must exit clean and the .bc file must exist before you point lli-18 at it.
Run the bitcode by giving its path as the first non-option argument:
$ lli-18 /tmp/lli-18-check/status.bc
$ status=$?
$ printf 'lli-18 exit status: %s\n' "$status"
lli-18 exit status: 7
With no entry-function option, lli-18 looks for main. A clean launch does not mean a zero exit status: the command hands back whatever the program itself returned. Grab $? straight away, because the next command you run will overwrite it.
The file name has to refer to valid bitcode. A missing input, for example, produces an error and status 1 on this installation:
$ lli-18 /tmp/lli-18-check/missing.bc
lli-18: lli: /tmp/lli-18-check/missing.bc: error: Could not open input file: No such file or directory
$ printf '%s\n' "$?"
1
Arguments after the input file belong to the executed program, not to lli-18. Keep the bitcode path in front of them:
$ lli-18 /path/to/program.bc --config /path/to/example.conf --mode check
Quote anything that might contain spaces. The classic mistake is putting an application flag before the bitcode path: the command-line parser grabs it as an lli-18 option, and the program never sees it.
Need the running program to see a different argv[0]? Use --fake-argv0=/path/to/name. It only changes what the program is told; it does not rename the bitcode file or create an executable.
The test module also defines alternate, so name it explicitly:
$ lli-18 --entry-function=alternate /tmp/lli-18-check/status.bc
$ printf 'lli-18 exit status: %s\n' "$?"
lli-18 exit status: 23
The default is always main. Whatever function you pick needs a compatible entry signature: do not aim --entry-function at some random helper just because it exists, since an incompatible one can fail during execution or produce undefined behaviour.
Checkpoint: if changing the entry function changes the exit status the way you expect, you have verified both option placement and entry-point selection.
LLVM can run the module with a just-in-time compiler or with an interpreter. The installed help lists these controls:
--force-interpreter disables JIT execution and forces interpretation.--jit-kind=mcjit, --jit-kind=orc, and --jit-kind=orc-lazy choose the underlying JIT kind.-O0 through -O3 choose the optimisation level; the installed manpage gives -O2 as the default.For a deterministic interpreter comparison, run:
$ lli-18 --force-interpreter /tmp/lli-18-check/status.bc
$ printf 'interpreter status: %s\n' "$?"
interpreter status: 7
Day to day, leave these switches at their defaults unless you are testing a specific engine or measuring behaviour. Code-generator options are not automatically meaningful to the interpreter, and changing optimisation or JIT settings can shift performance without changing what the program is meant to do.
lli-18 also exposes --extra-module, --extra-object, --extra-archive, and --dlopen for loading additional code. Reach for them only when the module's build and linking design actually requires it.
Warning: treat every extra object, archive, plugin or shared library you load as executable input, no different from the bitcode itself. --load loads an LLVM plugin, which is exactly as security-sensitive as it sounds: check every path first.
Warning: do not use sudo for normal execution. Elevated privileges just give a buggy or malicious module a bigger blast radius. If a test genuinely needs a privileged resource, build a narrowly scoped test environment first and get the privilege through your normal operational review process.
When a run fails, separate input errors from program errors:
$ test -r /path/to/program.bc && echo 'bitcode is readable'
$ lli-18 /path/to/program.bc
$ status=$?
$ printf 'exit status: %s\n' "$status"
$ lli-18 --help | less
A non-zero status can be the program's deliberate result, a loader failure, or an execution failure. The number alone does not tell you which, so read the diagnostic and reproduce with the smallest safe input. If the command runs inside a script with set -e, capture and classify expected non-zero application statuses before the shell exits out from under you.