Extract One LLVM Function Safely with llvm-extract-18

A crashing test case with fifty functions in it is not a bug report, it is a puzzle, and llvm-extract-18 cuts it down to one function. You will reduce an LLVM bitcode module to a named function, write the result as readable LLVM intermediate language, and verify that the extracted module contains what you expected. Allow about ten minutes if you already have a bitcode file and know the function name.

The installed command is /usr/bin/llvm-extract-18, from Ubuntu LLVM 18.1.3. The local manual page is generated from LLVM documentation labelled version 15, so this guide treats the installed binary and its local manual as the authority for this machine. The examples use only temporary files and do not alter the input module.

1. Check the command and input

Start by confirming the executable and the input file. This needs no elevated privilege when the module is in a directory you can read:

$ command -v llvm-extract-18
/usr/bin/llvm-extract-18
$ llvm-extract-18 --version
Ubuntu LLVM version 18.1.3
  Optimized build.
$ test -r /path/to/input.bc && echo 'input is readable'
input is readable

The command accepts an LLVM bitcode filename as its final argument. Omit that argument, or pass -, and it reads bitcode from standard input instead. Its output goes to standard output unless you give -o. A common way to trip yourself up is to confuse LLVM bitcode with a native executable: llvm-extract-18 expects an LLVM module, not an ELF binary.

2. Extract one function to readable LLVM IR

Choose the function's IR name, including any spelling details that matter. Pass it with --func, select text output with -S, and write to a new destination:

$ llvm-extract-18 --func=process_record -S \
    -o /tmp/process_record.ll /path/to/input.bc
$ sed -n '1,80p' /tmp/process_record.ll
; ModuleID = '/path/to/input.bc'
source_filename = "..."

define ... @process_record(...) {
...

The exact function body depends on the input. A successful command exits with status 0 and produces LLVM intermediate language in the destination. Check the status immediately if you are diagnosing a script:

$ status=$?
$ printf 'llvm-extract status: %s\n' "$status"
llvm-extract status: 0

If the named function is absent, the command reports an error and returns non-zero. Do not treat an empty or missing destination as a useful reduced test case.

3. Keep bitcode when another LLVM tool needs it

Omit -S when the next tool expects binary bitcode. The output is still written to standard output by default, so use -o rather than letting binary data land in a terminal:

$ llvm-extract-18 --func=process_record \
    -o /tmp/process_record.bc /path/to/input.bc
$ file /tmp/process_record.bc
/tmp/process_record.bc: LLVM IR bitcode

Without -o, redirect the output to a file instead:

$ llvm-extract-18 --func=process_record /path/to/input.bc \
    > /tmp/process_record.bc

Safety boundary: Normally the program refuses to write raw bitcode to a terminal. The -f option overrides that protection. This is a safety boundary, not a display mode: raw bitcode is not readable terminal output and can make a session difficult to use. Prefer -S for inspection instead.

4. Include called functions when reducing a test

Use --recursive when the extracted function's call graph is part of the reproducer. This recursively extracts every called function, rather than leaving only the function named by --func:

$ llvm-extract-18 --func=process_record --recursive \
    -S -o /tmp/process_record-tree.ll /path/to/input.bc
$ rg '^define .*@' /tmp/process_record-tree.ll
define ... @process_record(...)
define ... @helper_called_by_process_record(...)

The second line is illustrative: the actual signatures and helper names come from your own module. If you are trying to isolate one function only, leave --recursive out. Extra reachable functions can make a reduced file larger and can hide which dependency is actually needed.

5. Select several functions or use a regular expression

Repeat --func for a known list of functions. For a family of names, use --rfunc; every function matching its regular expression is extracted:

$ llvm-extract-18 \
    --func=process_record \
    --func=validate_record \
    -S -o /tmp/record-pair.ll /path/to/input.bc
$ llvm-extract-18 --rfunc='^record_.*' \
    -S -o /tmp/record-family.ll /path/to/input.bc
$ rg '^define .*@' /tmp/record-family.ll

Quote the regular expression so the shell does not interpret characters before llvm-extract-18 receives them. Review the matches before using the result as a bug reproducer: a broad expression can pull in unrelated functions and produce a module that is technically valid but not meaningfully reduced.

6. Verify the reduced module and protect the original

Inspect the output as text first. Confirm the requested function is present, and that an intentionally excluded function is absent when the input makes that comparison meaningful:

$ rg '^define .*@process_record\b' /tmp/process_record.ll
define ... @process_record(...)
$ if rg -q '^define .*@unrelated_function\b' /tmp/process_record.ll; then
    echo 'unexpected function present' >&2
    exit 1
  fi
$ llvm-as-18 /tmp/process_record.ll -o /tmp/process_record-check.bc
$ echo 'reduced IR assembles successfully'
reduced IR assembles successfully

The final assembly check is optional but useful: it catches malformed text output before another tool consumes it, and it requires llvm-as-18 from the LLVM toolchain. A successful assembly does not prove the reduced module still reproduces your original compiler or runtime failure; it only verifies the text is accepted as LLVM bitcode.

Warning: Do not write the output over the source unless you have deliberately made a recoverable copy. Shell redirection with > truncates its destination before llvm-extract-18 starts. Use a new path, then replace the old file only after inspection:

$ cp --preserve=all /path/to/input.bc /path/to/input.bc.bak
$ llvm-extract-18 --func=process_record \
    -o /path/to/input.bc.new /path/to/input.bc
$ llvm-as-18 /tmp/process_record.ll -o /tmp/process_record-check.bc
$ mv /path/to/input.bc.new /path/to/input.bc

The backup restores the original with mv /path/to/input.bc.bak /path/to/input.bc if you need to undo the replacement. The example's final mv is state-changing and should be run only after the new file has been checked. Neither extraction nor verification needs sudo unless your chosen input or output directory is deliberately restricted; running the tool as root can leave root-owned output behind and does not fix a wrong function name.

Done means