Link LLVM Bitcode Safely with llvm-link-18

llvm-link-18 merges LLVM bitcode files into one, gives you a readable IR version, and confirms the tool did what you expected. The examples use llvm-link-18 from package llvm-18, version 18.1.3 on this machine.

Allow about fifteen minutes. You need a shell, the LLVM 18 tools, and two or more LLVM bitcode files. No command here needs elevated privileges. Work in a temporary or dedicated build directory, because choosing an existing output path can replace a file you meant to keep.

Checkpoint: This guide links modules. It does not compile source code, optimise the result, produce a native executable or resolve ordinary application libraries. Those are separate build steps.

1. Confirm the installed command

Check the executable and version before copying a command into a build script:

$ command -v llvm-link-18
/usr/bin/llvm-link-18
$ llvm-link-18 --version
Ubuntu LLVM version 18.1.3
  Optimized build.

The manpage is dated 2024-05-27 and describes the LLVM 18 command shipped here. Keep the versioned name when a host has several LLVM releases installed. Bitcode is not a promise of cross-version compatibility, so do not silently substitute an unversioned llvm-link from another installation.

2. Create two small input modules

If you already have valid bitcode, skip to the next step. Otherwise, create LLVM assembly files and assemble them. This produces deterministic test inputs without touching a project or system directory:

$ mkdir -p /tmp/llvm-link-demo
$ cat > /tmp/llvm-link-demo/left.ll <<'EOF'
define i32 @left_value() {
  ret i32 7
}
EOF
$ cat > /tmp/llvm-link-demo/right.ll <<'EOF'
define i32 @right_value() {
  ret i32 11
}
EOF
$ llvm-as-18 /tmp/llvm-link-demo/left.ll -o /tmp/llvm-link-demo/left.bc
$ llvm-as-18 /tmp/llvm-link-demo/right.ll -o /tmp/llvm-link-demo/right.bc
$ file /tmp/llvm-link-demo/left.bc /tmp/llvm-link-demo/right.bc
/tmp/llvm-link-demo/left.bc:  LLVM IR bitcode
/tmp/llvm-link-demo/right.bc: LLVM IR bitcode

The here-document is quoted so the shell writes the LLVM text literally. If your inputs come from an untrusted build directory, inspect the paths before using them. llvm-link-18 reads bitcode; it is not a general text-file concatenator.

3. Link the modules to a new bitcode file

Pass each input file as a separate argument and select a new output with -o:

$ llvm-link-18 \
    /tmp/llvm-link-demo/left.bc \
    /tmp/llvm-link-demo/right.bc \
    -o /tmp/llvm-link-demo/merged.bc
$ printf 'exit status: %s\n' "$?"
exit status: 0
$ file /tmp/llvm-link-demo/merged.bc
/tmp/llvm-link-demo/merged.bc: LLVM IR bitcode

Success returns status 0. Without -o, the linked bitcode is written to standard output. That is useful for a pipeline, but raw bitcode is not readable in a terminal and the command normally refuses to write it to a terminal. Use -o for a file, or use -S when you deliberately want text.

Safety warning: Choose an output path that is not one of the inputs. Treat an existing output as disposable only when your build system has made that choice deliberately. To recover this demonstration, remove only the generated files in its temporary directory after checking the path:

$ rm -f /tmp/llvm-link-demo/merged.bc /tmp/llvm-link-demo/merged.ll
$ test ! -e /tmp/llvm-link-demo/merged.bc
$ printf 'generated bitcode removed\n'
generated bitcode removed

4. Inspect the linked module as LLVM IR

Use -S to write LLVM intermediate language instead of bitcode. This is an inspection step, not a native compilation step:

$ llvm-link-18 -S \
    /tmp/llvm-link-demo/left.bc \
    /tmp/llvm-link-demo/right.bc \
    -o /tmp/llvm-link-demo/merged.ll
$ sed -n '1,40p' /tmp/llvm-link-demo/merged.ll
; ModuleID = 'llvm-link'
source_filename = "llvm-link"

define i32 @left_value() {
  ret i32 7
}

define i32 @right_value() {
  ret i32 11
}

The module contains both definitions. The exact header or ordering can vary with LLVM details, so verify the symbols you need rather than comparing the entire file byte for byte. If you need bitcode for the next build stage, use merged.bc, not this text file.

5. Turn on diagnostics when a link is confusing

Add -v when you need to see which modules were loaded and linked:

$ llvm-link-18 -v -S \
    /tmp/llvm-link-demo/left.bc \
    /tmp/llvm-link-demo/right.bc \
    -o /tmp/llvm-link-demo/verbose.ll
Loading '/tmp/llvm-link-demo/left.bc'
Linking in '/tmp/llvm-link-demo/left.bc'
Loading '/tmp/llvm-link-demo/right.bc'
Linking in '/tmp/llvm-link-demo/right.bc'
Writing bitcode...

Diagnostic text is written to standard error, so a script should capture it separately if standard output is part of a data pipeline. A non-zero exit status means the link failed. Common causes include a missing input, invalid bitcode, incompatible module content or duplicate definitions.

6. Handle duplicate definitions deliberately

Two inputs defining the same symbol are not an ordinary merge. Do not hide that error by suppressing warnings. First reproduce the conflict with a separate module:

$ cat > /tmp/llvm-link-demo/replacement.ll <<'EOF'
define i32 @left_value() {
  ret i32 99
}
EOF
$ llvm-as-18 /tmp/llvm-link-demo/replacement.ll -o /tmp/llvm-link-demo/replacement.bc

If replacing an earlier definition is intentional, pass the replacement with --override. The last override takes precedence when more than one file defines the symbol:

$ llvm-link-18 -S \
    /tmp/llvm-link-demo/left.bc \
    --override /tmp/llvm-link-demo/replacement.bc \
    -o /tmp/llvm-link-demo/overridden.ll
$ rg -A2 '^define i32 @left_value' /tmp/llvm-link-demo/overridden.ll
define i32 @left_value() {
  ret i32 99
}

This changes which definition enters the output. It does not patch the input file, and it is not a safe generic conflict resolver. Record why the override is expected, and review the resulting module before publishing or executing code derived from it.

7. Keep specialised options in their lane

--only-needed links only needed symbols, while --internalize internalizes linked symbols. Both can change what later stages can reference, so use them only when the surrounding build or ThinLTO design requires them. For ThinLTO imports, --import function:filename must be paired with --summary-index from the earlier link.

-d prints a human-readable form of the output bitcode to standard error. It is a diagnostic option, not a replacement for -S when you need a reusable LLVM assembly file. --help shows the options provided by this installed build.

Done means