Summary

Clang, LLVM, and IR

As you saw in the video from HW 1.1, when you compile a C program using clang with a command like

clang myprog.c -o myprog

there are actually a few steps happening:

  1. The clang “front-end” translates myprog.c into LLVM IR code
  2. The LLVM IR code is optimized
  3. The optimized LLVM IR is translated into machine code and linked into the executable myprog

Other compilers work in similar ways. For example, gcc uses multiple intermediate representation languages on the journey from C source code to a compiled machine code binary; GIMPLE is the one that is probably most similar to LLVM IR.

What makes LLVM a bit special is that they designed LLVM IR to be language-independent, meaning that many different programming languages can all be compiled to LLVM IR. These are called “front-ends”, and it is what you will be writing in this class when you write compilers.

After the front-end converts some programming language to LLVM IR, then the “back-end” kicks in to optimize and produce machine language for whatever architecture you want.

Command-line tools

To convert from a C source program to LLVM IR, you run:

clang -S -emit-llvm myprog.c

which will create a file myprog.ll. This is the “front-end” pass, converting source code in a normal programming language to LLVM IR.

The back-end pass, compiling LLVM IR down to an executable file, is just like compiling C itself:

clang myprog.ll -o myprog

(Based on the filename extension .ll, clang knows you are passing in LLVM IR code instead of C source code.)

(Note, there are actually separate commands opt, llc, llvm-link to optimize, compile, and link LLVM IR code, but we will not usually need to get into that.)

An example program

Let’s take the following C program:

include <stdio.h>
include <stdlib.h>
include <unistd.h>

int die_roll() {
  return (rand() % 6) + 1;
}

int main() {
  srand(getpid());
  int x = die_roll();
  int y = die_roll();
  printf("%d + %d = %d\n", x, y, x+y);
  return 0;
}

If you run clang -S -emit-llvm it will produce something like the following .ll file:

; ModuleID = 'llvm-example.c'
source_filename = "llvm-example.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"

@.str = private unnamed_addr constant [14 x i8] c"%d + %d = %d\0A\00", align 1

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @die_roll() #0 {
  %1 = call i32 @rand() #3
  %2 = srem i32 %1, 6
  %3 = add nsw i32 %2, 1
  ret i32 %3
}

; Function Attrs: nounwind
declare i32 @rand() #1

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {
  %1 = alloca i32, align 4
  %2 = alloca i32, align 4
  %3 = alloca i32, align 4
  store i32 0, ptr %1, align 4
  %4 = call i32 @getpid() #3
  call void @srand(i32 noundef %4) #3
  %5 = call i32 @die_roll()
  store i32 %5, ptr %2, align 4
  %6 = call i32 @die_roll()
  store i32 %6, ptr %3, align 4
  %7 = load i32, ptr %2, align 4
  %8 = load i32, ptr %3, align 4
  %9 = load i32, ptr %2, align 4
  %10 = load i32, ptr %3, align 4
  %11 = add nsw i32 %9, %10
  %12 = call i32 (ptr, ...) @printf(ptr noundef @.str, i32 noundef %7, i32 noundef %8, i32 noundef %11)
  ret i32 0
}

; Function Attrs: nounwind
declare void @srand(i32 noundef) #1

; Function Attrs: nounwind
declare i32 @getpid() #1

declare i32 @printf(ptr noundef, ...) #2

attributes #0 = { noinline nounwind optnone uwtable "frame-pointer"="all" "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
attributes #1 = { nounwind "frame-pointer"="all" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
attributes #2 = { "frame-pointer"="all" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
attributes #3 = { nounwind }

!llvm.module.flags = !{!0, !1, !2, !3, !4}
!llvm.ident = !{!5}

!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 8, !"PIC Level", i32 2}
!2 = !{i32 7, !"PIE Level", i32 2}
!3 = !{i32 7, !"uwtable", i32 2}
!4 = !{i32 7, !"frame-pointer", i32 2}
!5 = !{!"Debian clang version 19.1.7 (3+b1)"}

There’s a lot there, but hopefully you can see what is going on:

A lot of the little decorations and attributes in the LLVM IR emitted by clang are there by default and have some meaning, but aren’t strictly necessary for the code to run. And in fact, storing the variables in memory is overkill here since we just have two variables whose values are not modified.

So here is a stripped down version of exactly the same program in LLVM IR:

target triple = "x86_64-pc-linux-gnu"

declare i32 @rand()
declare void @srand(i32)
declare i32 @getpid()
declare i32 @printf(ptr, ...)

@literal1 = constant [14 x i8] c"%d + %d = %d\0A\00"

define i32 @die_roll() {
  %temp1 = call i32 @rand()
  %temp2 = srem i32 %temp1, 6
  %roll = add nsw i32 %temp2, 1
  ret i32 %roll
}

define i32 @main() {
  %pid = call i32 @getpid()
  call void @srand(i32 %pid)
  %x = call i32 @die_roll()
  %y = call i32 @die_roll()
  %xpy = add i32 %x, %y
  %ignored = call i32 @printf(ptr @literal1, i32 %x, i32 %y, i32 %xpy)
  ret i32 0
}

Here we see the bare essentials:

You can run either this or the previous .ll version with the lli command to see that they actually work.

Single static assignment (SSA)

Let’s look again at the @die_roll() function definition. You might think to just reuse the same register three times, like this:

define i32 @die_roll() {
  %roll = call i32 @rand()
  %roll = srem i32 %roll, 6
  %roll = add nsw i32 %roll, 1
  ret i32 %roll
}

This makes sense logically, but if you try to run it with lli or compile it with clang, you will get an error like multiple definition of local value %roll.

This is because LLVM IR does not allow register values to be reassigned. Formally, this is called “single static assignment” (SSA), and it’s something which is kind of unique to compiler IR languages. It makes it easier for the compiler later to assign these to actual registers on your CPU, and allows some other kinds of optimizations.

Three address code (3AC)

You will also notice that, much like any assembly language, you can’t combine multiple steps into a single command. Formally, this is called a “three-address code” because each command in LLVM IR typically looks like

%dest = command %src1, %src2

which has a single destination register and two source registers; hence three addresses.

There are actually many different commands built-in to LLVM IR, which you can peruse on the official documentation under “Instruction reference”. In many cases, the command is actually a command name and a type name, like add i32 — this determines the types of the inputs as well as the type of the output.

But rather than looking at the reference manual, in most cases the easiest way to figure out “what is the LLVM command to do this thing I want” is just to write what you want in a small C program, compile that with clang -S -emit-llvm, and then look at what gets produced in the .ll file.